mirror->available()) { throw new RuntimeException('product_purchase_price_mirror_schema_missing'); } $state = $this->mirror->state(); if (! $full && ( ($state['initial_completed_at'] ?? null) === null || (int) ($state['projection_version'] ?? 1) < ProductPurchasePriceMirrorRepository::CURRENT_PROJECTION_VERSION )) { return ['status' => 'requires_full_sync', 'mode' => 'incremental', 'batches' => 0, 'rows' => 0]; } $mode = $full ? 'full' : 'incremental'; $resume = is_array($state) && ($state['run_mode'] ?? null) === $mode && in_array($state['status'] ?? null, ['running', 'failed'], true) && (int) ($state['projection_version'] ?? 1) >= ProductPurchasePriceMirrorRepository::CURRENT_PROJECTION_VERSION && ($state['window_started_at'] ?? null) !== null && ($state['window_ends_at'] ?? null) !== null; $generation = $resume && is_string($state['generation'] ?? null) ? (string) $state['generation'] : (string) Str::uuid(); if ($resume) { $since = Carbon::parse((string) $state['window_started_at']); $until = Carbon::parse((string) $state['window_ends_at']); } elseif ($full) { $since = Carbon::create(1900, 1, 1, 0, 0, 0, config('app.timezone')); $until = now()->addSecond(); } else { $overlap = max((int) config('upstream_mirrors.product_purchase_prices.overlap_hours', 48), 1); $since = Carbon::parse((string) $state['last_succeeded_at'])->subHours($overlap); $until = now()->addSecond(); } $state = $this->mirror->startRun($mode, $generation, $since, $until, $resume); $cursor = ($state['cursor_updated_at'] ?? null) === null ? null : [ 'updated_at' => (string) $state['cursor_updated_at'], 'item_id' => (int) ($state['cursor_item_id'] ?? 0), ]; $batchSize = min(max( $batchSize ?? (int) config('upstream_mirrors.product_purchase_prices.batch_size', 1000), 1, ), 5000); $maxBatches = max( $maxBatches ?? (int) config('upstream_mirrors.product_purchase_prices.max_batches_per_run', 5), 0, ); $batches = 0; $rows = (int) ($state['last_upserted_rows'] ?? 0); try { while (true) { $batch = $this->upstream->mirrorBatch( $cursor, $since->format('Y-m-d\TH:i:s.v'), $until->format('Y-m-d\TH:i:s.v'), $batchSize, ); if ($batch === []) { $this->mirror->complete($mode, $generation, $until, $rows); return ['status' => 'succeeded', 'mode' => $mode, 'batches' => $batches, 'rows' => $rows]; } $last = $batch[array_key_last($batch)]; $cursor = [ 'updated_at' => (string) $last['purchase_updated_at'], 'item_id' => (int) $last['item_id'], ]; $rows += count($batch); $batches++; $this->mirror->storeBatch($batch, $cursor, $generation, $rows); if (count($batch) < $batchSize) { $this->mirror->complete($mode, $generation, $until, $rows); return ['status' => 'succeeded', 'mode' => $mode, 'batches' => $batches, 'rows' => $rows]; } if ($maxBatches > 0 && $batches >= $maxBatches) { return ['status' => 'running', 'mode' => $mode, 'batches' => $batches, 'rows' => $rows]; } } } catch (Throwable $exception) { $this->mirror->fail($exception::class); throw $exception; } } }