356 lines
13 KiB
PHP
356 lines
13 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Http\Requests\LambdaRequest;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class NeblogisticPushWorkflowStatus extends Command
|
|
{
|
|
protected $signature = 'neblogistic:push-workflow-status
|
|
{--days= : Optional created_at lookback in days}
|
|
{--limit=200 : Max MySQL rows this run}
|
|
{--submission-id= : Check one submission only}';
|
|
|
|
protected $description = 'Compare MySQL neblogistic approved-like statuses to Dynamo form workflow_status (read-only; no approvereject)';
|
|
|
|
private const APPROVED_LIKE = [
|
|
'L1 Approved',
|
|
'L2 Approved',
|
|
'L3 Approved',
|
|
'L4 Approved',
|
|
'Approved',
|
|
'Dispatched',
|
|
'Delivered',
|
|
];
|
|
|
|
public function handle(): int
|
|
{
|
|
$partition = config('neblogistic.partition', 'rapport-team3');
|
|
$formId = config('neblogistic.form_id', '1750914801-153443b9-94d0-4dcb-b3f3-4fa11588a0a9');
|
|
$timeout = (int) config('neblogistic.http_timeout', 120);
|
|
$limit = max(1, (int) $this->option('limit'));
|
|
$submissionId = trim((string) $this->option('submission-id'));
|
|
$daysOption = $this->option('days');
|
|
|
|
if (empty($partition) || empty($formId)) {
|
|
$this->error('neblogistic.partition and neblogistic.form_id must be configured.');
|
|
return 1;
|
|
}
|
|
|
|
$query = DB::table('neblogistic')
|
|
->select('id', 'submission_id', 'status')
|
|
->whereNotNull('submission_id')
|
|
->where('submission_id', '!=', '');
|
|
|
|
if ($submissionId !== '') {
|
|
$query->where('submission_id', $submissionId);
|
|
} else {
|
|
$query->whereIn('status', self::APPROVED_LIKE);
|
|
if ($daysOption !== null && $daysOption !== '') {
|
|
$days = max(1, (int) $daysOption);
|
|
$query->where('created_at', '>=', now()->subDays($days)->startOfDay());
|
|
}
|
|
}
|
|
|
|
$records = $query->orderByDesc('id')->limit($limit)->get();
|
|
|
|
if ($records->isEmpty()) {
|
|
$this->info('No neblogistic rows to compare.');
|
|
Log::info('Neblogistic push-workflow-status compare: no rows');
|
|
return 0;
|
|
}
|
|
|
|
$this->info(sprintf('Comparing %d row(s), latest first. partition=%s form_id=%s', $records->count(), $partition, $formId));
|
|
Log::info('Neblogistic push-workflow-status compare started', [
|
|
'rows' => $records->count(),
|
|
'limit' => $limit,
|
|
'days' => $daysOption,
|
|
'submission_id' => $submissionId !== '' ? $submissionId : null,
|
|
'partition' => $partition,
|
|
'form_id' => $formId,
|
|
]);
|
|
|
|
$checked = 0;
|
|
$inSync = 0;
|
|
$mismatch = 0;
|
|
$failed = 0;
|
|
$reportRows = [];
|
|
|
|
foreach ($records as $row) {
|
|
$mysqlStatus = trim((string) ($row->status ?? ''));
|
|
if (!in_array($mysqlStatus, self::APPROVED_LIKE, true)) {
|
|
$this->warn(sprintf(
|
|
'Skipping neblogistic id=%s submission_id=%s status=%s (not approved-like).',
|
|
$row->id,
|
|
$row->submission_id,
|
|
$mysqlStatus === '' ? '(empty)' : $mysqlStatus
|
|
));
|
|
continue;
|
|
}
|
|
|
|
$checked++;
|
|
$this->line(sprintf(
|
|
'[%d/%d] Checking id=%s submission_id=%s mysql=%s ...',
|
|
$checked,
|
|
$records->count(),
|
|
$row->id,
|
|
$row->submission_id,
|
|
$mysqlStatus
|
|
));
|
|
$result = $this->compareRow($row, $partition, $formId, $timeout);
|
|
|
|
$this->line(sprintf(
|
|
' -> %s reason=%s dynamo=%s history=%s next=%s',
|
|
$result['outcome'],
|
|
$result['reason'],
|
|
$result['dynamo_workflow_status'] !== '' ? $result['dynamo_workflow_status'] : '(empty)',
|
|
$result['dynamo_history_status'] !== '' ? $result['dynamo_history_status'] : '(empty)',
|
|
$result['workflow_next_approver'] !== '' ? $result['workflow_next_approver'] : '(empty)'
|
|
));
|
|
|
|
Log::info('Neblogistic push-workflow-status row', [
|
|
'neblogistic_id' => $row->id,
|
|
'submission_id' => $row->submission_id,
|
|
'mysql_status' => $mysqlStatus,
|
|
'outcome' => $result['outcome'],
|
|
'reason' => $result['reason'],
|
|
'dynamo_workflow_status' => $result['dynamo_workflow_status'],
|
|
'dynamo_history_status' => $result['dynamo_history_status'],
|
|
'workflow_next_approver' => $result['workflow_next_approver'],
|
|
]);
|
|
|
|
if ($result['outcome'] === 'in_sync') {
|
|
$inSync++;
|
|
continue;
|
|
}
|
|
|
|
if ($result['outcome'] === 'failed') {
|
|
$failed++;
|
|
} else {
|
|
$mismatch++;
|
|
}
|
|
|
|
$reportRows[] = [
|
|
$row->id,
|
|
$row->submission_id,
|
|
(string) ($row->status ?? ''),
|
|
$result['dynamo_workflow_status'],
|
|
$result['dynamo_history_status'],
|
|
$result['workflow_next_approver'],
|
|
$result['reason'],
|
|
];
|
|
}
|
|
|
|
if ($reportRows !== []) {
|
|
$this->table(
|
|
['mysql_id', 'submission_id', 'mysql_status', 'dynamo_workflow_status', 'dynamo_history_status', 'workflow_next_approver', 'reason'],
|
|
$reportRows
|
|
);
|
|
} else {
|
|
$this->info('No mismatches or failures.');
|
|
}
|
|
|
|
$summary = sprintf(
|
|
'Done. checked: %d, in_sync: %d, mismatch: %d, failed: %d.',
|
|
$checked,
|
|
$inSync,
|
|
$mismatch,
|
|
$failed
|
|
);
|
|
$this->info($summary);
|
|
|
|
Log::info('Neblogistic push-workflow-status compare', [
|
|
'checked' => $checked,
|
|
'in_sync' => $inSync,
|
|
'mismatch' => $mismatch,
|
|
'failed' => $failed,
|
|
'limit' => $limit,
|
|
'days' => $daysOption,
|
|
'submission_id' => $submissionId !== '' ? $submissionId : null,
|
|
]);
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @param object $row
|
|
* @return array{outcome: string, reason: string, dynamo_workflow_status: string, dynamo_history_status: string, workflow_next_approver: string}
|
|
*/
|
|
private function compareRow($row, string $partition, string $formId, int $timeout): array
|
|
{
|
|
$empty = [
|
|
'dynamo_workflow_status' => '',
|
|
'dynamo_history_status' => '',
|
|
'workflow_next_approver' => '',
|
|
];
|
|
|
|
try {
|
|
$response = LambdaRequest::getrequest(
|
|
'form',
|
|
'formentity/getsubmissiondetails',
|
|
[
|
|
'partition' => $partition,
|
|
'form_id' => $formId,
|
|
'id' => $row->submission_id,
|
|
],
|
|
'',
|
|
$timeout > 0 ? $timeout : null
|
|
);
|
|
|
|
if (!is_object($response) || ($response->status ?? '') !== 'success') {
|
|
Log::warning('Neblogistic push-workflow-status: form API did not return success', [
|
|
'neblogistic_id' => $row->id,
|
|
'submission_id' => $row->submission_id,
|
|
]);
|
|
return $this->result('failed', 'api_failed', $empty);
|
|
}
|
|
|
|
$submission = $response->data->submission ?? null;
|
|
if (!is_object($submission)) {
|
|
Log::warning('Neblogistic push-workflow-status: missing submission', [
|
|
'neblogistic_id' => $row->id,
|
|
'submission_id' => $row->submission_id,
|
|
]);
|
|
return $this->result('failed', 'missing_submission', $empty);
|
|
}
|
|
|
|
$workflowStatus = strtolower(trim((string) ($submission->workflow_status ?? '')));
|
|
$historyStatus = $this->statusFromApprovalHistory($submission->approval_history ?? null) ?? '';
|
|
$nextApprover = (string) ($submission->workflow_next_approver ?? '');
|
|
$meta = [
|
|
'dynamo_workflow_status' => $workflowStatus,
|
|
'dynamo_history_status' => $historyStatus,
|
|
'workflow_next_approver' => $nextApprover,
|
|
];
|
|
|
|
$mysqlStatus = trim((string) ($row->status ?? ''));
|
|
|
|
if ($workflowStatus === 'rejected') {
|
|
return $this->result('mismatch', 'dynamo_rejected', $meta);
|
|
}
|
|
|
|
if ($workflowStatus === 'approved') {
|
|
return $this->result('in_sync', 'workflow_approved', $meta);
|
|
}
|
|
|
|
if ($this->historyMeetsMysqlLevel($historyStatus, $mysqlStatus)) {
|
|
return $this->result('in_sync', 'sequential_pending', $meta);
|
|
}
|
|
|
|
if ($historyStatus === '') {
|
|
return $this->result('mismatch', 'empty_history', $meta);
|
|
}
|
|
|
|
return $this->result('mismatch', 'behind_mysql', $meta);
|
|
} catch (\Throwable $e) {
|
|
Log::error('Neblogistic push-workflow-status: exception', [
|
|
'neblogistic_id' => $row->id ?? null,
|
|
'submission_id' => $row->submission_id ?? null,
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
return $this->result('failed', 'api_failed', $empty);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array{dynamo_workflow_status: string, dynamo_history_status: string, workflow_next_approver: string} $meta
|
|
* @return array{outcome: string, reason: string, dynamo_workflow_status: string, dynamo_history_status: string, workflow_next_approver: string}
|
|
*/
|
|
private function result(string $outcome, string $reason, array $meta): array
|
|
{
|
|
return [
|
|
'outcome' => $outcome,
|
|
'reason' => $reason,
|
|
'dynamo_workflow_status' => $meta['dynamo_workflow_status'],
|
|
'dynamo_history_status' => $meta['dynamo_history_status'],
|
|
'workflow_next_approver' => $meta['workflow_next_approver'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* True when Dynamo history already reflects the MySQL approval level
|
|
* (including real sequential L2/L3 still pending on workflow_status).
|
|
*/
|
|
private function historyMeetsMysqlLevel(string $historyStatus, string $mysqlStatus): bool
|
|
{
|
|
if ($historyStatus === '' || $mysqlStatus === '') {
|
|
return false;
|
|
}
|
|
|
|
$historyLevel = $this->approvalLevel($historyStatus);
|
|
$mysqlLevel = $this->approvalLevel($mysqlStatus);
|
|
|
|
if ($historyLevel === null || $mysqlLevel === null) {
|
|
return strcasecmp($historyStatus, $mysqlStatus) === 0;
|
|
}
|
|
|
|
return $historyLevel >= $mysqlLevel;
|
|
}
|
|
|
|
private function approvalLevel(string $status): ?int
|
|
{
|
|
if (preg_match('/^L([1-4])\s+Approved$/i', $status, $m)) {
|
|
return (int) $m[1];
|
|
}
|
|
|
|
if (in_array($status, ['Approved', 'Dispatched', 'Delivered'], true)) {
|
|
return 5;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Mirror metric.js neblogisticStatusFromApprovalHistory (newest first).
|
|
*
|
|
* @param mixed $approvalHistory
|
|
*/
|
|
private function statusFromApprovalHistory($approvalHistory): ?string
|
|
{
|
|
if (!is_array($approvalHistory) || count($approvalHistory) === 0) {
|
|
return null;
|
|
}
|
|
|
|
$known = [
|
|
'L1 Approved' => true,
|
|
'L2 Approved' => true,
|
|
'L3 Approved' => true,
|
|
'L4 Approved' => true,
|
|
'Rejected' => true,
|
|
];
|
|
|
|
$latest = $approvalHistory[0];
|
|
$latest = is_object($latest) ? get_object_vars($latest) : (array) $latest;
|
|
$action = (string) ($latest['action'] ?? '');
|
|
$actionStatus = (string) ($latest['action_status'] ?? '');
|
|
|
|
if ($action === 'reject' || preg_match('/rejected/i', $actionStatus)) {
|
|
return 'Rejected';
|
|
}
|
|
if (isset($known[$actionStatus])) {
|
|
return $actionStatus;
|
|
}
|
|
if (preg_match('/^L([1-4])\s+Approved$/i', $actionStatus, $m)) {
|
|
return 'L' . $m[1] . ' Approved';
|
|
}
|
|
|
|
$maxLevel = 0;
|
|
foreach ($approvalHistory as $entry) {
|
|
$entry = is_object($entry) ? get_object_vars($entry) : (array) $entry;
|
|
$entryAction = (string) ($entry['action'] ?? '');
|
|
$entryStatus = (string) ($entry['action_status'] ?? '');
|
|
if ($entryAction === 'reject' || preg_match('/rejected/i', $entryStatus)) {
|
|
return 'Rejected';
|
|
}
|
|
if (preg_match('/^L([1-4])\s+Approved$/i', $entryStatus, $m)) {
|
|
$maxLevel = max($maxLevel, (int) $m[1]);
|
|
}
|
|
}
|
|
|
|
return $maxLevel > 0 ? ('L' . $maxLevel . ' Approved') : null;
|
|
}
|
|
}
|