|null $items */ public function createJob( AdminUser $admin, Request $request, string $reconcileType, ?Carbon $periodStart, ?Carbon $periodEnd, ?array $items, ?int $playerId = null, ): ReconcileJob { if ($reconcileType === 'wallet_transfer' && $items === null) { return $this->createWalletTransferScanJob($admin, $request, $periodStart, $periodEnd, $playerId); } return DB::transaction(function () use ($admin, $request, $reconcileType, $periodStart, $periodEnd, $items): ReconcileJob { $jobNo = 'REC'.now()->format('YmdHis').strtoupper(Str::random(4)); $job = ReconcileJob::query()->create([ 'job_no' => $jobNo, 'admin_user_id' => (int) $admin->getKey(), 'reconcile_type' => $reconcileType, 'status' => 'completed', 'period_start' => $periodStart, 'period_end' => $periodEnd, 'summary_json' => null, 'finished_at' => now(), ]); $mismatch = 0; foreach ($items ?? [] as $row) { ReconcileItem::query()->create([ 'reconcile_job_id' => (int) $job->getKey(), 'side_a_ref' => $row['side_a_ref'] ?? null, 'side_b_ref' => $row['side_b_ref'] ?? null, 'difference_amount' => (int) ($row['difference_amount'] ?? 0), 'status' => (string) ($row['status'] ?? 'mismatch'), 'resolved_at' => null, ]); if (($row['status'] ?? 'mismatch') === 'mismatch') { $mismatch++; } } $job->forceFill([ 'summary_json' => [ 'item_count' => count($items ?? []), 'mismatch_count' => $mismatch, ], ])->save(); AuditLogger::recordForAdmin( $admin, $request, 'reconcile_jobs', 'create', 'reconcile_job', (string) $job->getKey(), null, [ 'job_no' => $jobNo, 'reconcile_type' => $reconcileType, 'item_count' => count($items ?? []), ], ); return $job->fresh(); }); } private function createWalletTransferScanJob( AdminUser $admin, Request $request, ?Carbon $periodStart, ?Carbon $periodEnd, ?int $playerId, ): ReconcileJob { $periodEnd ??= now()->endOfDay(); $periodStart ??= $periodEnd->copy()->startOfDay(); $player = null; if ($playerId !== null) { $player = Player::query()->find($playerId); if ($player === null || ! AdminSiteScope::playerAccessible($admin, $player)) { abort(ApiMessage::errorResponse( $request, 'admin.site_player_access_denied', ErrorCode::AdminForbidden->value, null, 403, )); } } $scope = AdminScopePolicy::resolveContext($request, $admin); $adminSiteId = AdminReconcileScope::resolveAdminSiteIdForJob($admin, $player); [$items] = $this->detector->scanItems( $periodStart, $periodEnd, limit: 1000, staleMinutes: 15, playerId: $playerId, includeMainSiteCheck: true, scope: $scope, ); $job = $this->detector->persistJob( $items, $periodStart, $periodEnd, (int) $admin->getKey(), $adminSiteId, $playerId, ); AuditLogger::recordForAdmin( $admin, $request, 'reconcile_jobs', 'create', 'reconcile_job', (string) $job->getKey(), null, [ 'job_no' => $job->job_no, 'reconcile_type' => 'wallet_transfer', 'item_count' => count($items), 'player_id' => $playerId, ], ); return $job->fresh(); } }