0, 'orders' => 0, 'incomes' => 0, 'stocks' => 0, ]; foreach ($endpoints as $currentEndpoint) { if (!in_array($currentEndpoint, self::ENDPOINTS, true)) { throw new \InvalidArgumentException("Unknown endpoint: {$currentEndpoint}"); } $params = match ($currentEndpoint) { 'stocks' => ['dateFrom' => $stocksDate], default => ['dateFrom' => $from, 'dateTo' => $to], }; $result[$currentEndpoint] += $this->syncEndpoint($currentEndpoint, $params, $progress); } return $result; } public function syncEndpoint(string $endpoint, array $params, ?callable $progress = null): int { $page = 1; $total = 0; while (true) { try { $items = $this->client->fetchPage($endpoint, $params, $page); $count = count($items); if ($count === 0) { $this->log($endpoint, $page, 0, 'ok', 'Empty page, sync finished'); $progress && $progress($endpoint, $page, 0, $total, true); break; } $rows = array_map(fn (array $item) => $this->mapItem($endpoint, $item), $items); $this->upsertRows($this->tableName($endpoint), $rows); $total += $count; $this->log($endpoint, $page, $count, 'ok', 'Page synced'); $progress && $progress($endpoint, $page, $count, $total, false); if ($count < $this->client->limit()) { break; } $page++; } catch (Throwable $e) { $this->log($endpoint, $page, 0, 'error', Str::limit($e->getMessage(), 1500)); throw $e; } } return $total; } private function tableName(string $endpoint): string { return match ($endpoint) { 'sales' => 'wb_sales', 'orders' => 'wb_orders', 'incomes' => 'wb_incomes', 'stocks' => 'wb_stocks', default => throw new \InvalidArgumentException("Unknown endpoint: {$endpoint}"), }; } private function mapItem(string $endpoint, array $item): array { return match ($endpoint) { 'sales' => $this->mapSale($item), 'orders' => $this->mapOrder($item), 'incomes' => $this->mapIncome($item), 'stocks' => $this->mapStock($item), default => throw new \InvalidArgumentException("Unknown endpoint: {$endpoint}"), }; } private function mapIncome(array $item): array { $now = now(); $identity = [ $this->intOrNull($item['income_id'] ?? null), $this->dateOrNull($item['date'] ?? null), $this->strOrNull($item['number'] ?? null), $this->intOrNull($item['barcode'] ?? null), $this->intOrNull($item['nm_id'] ?? null), $this->strOrNull($item['warehouse_name'] ?? null), $this->strOrNull($item['supplier_article'] ?? null), ]; return [ 'source_uid' => $this->uid('income', $identity), 'income_id' => $this->intOrNull($item['income_id'] ?? null), 'number' => $this->strOrNull($item['number'] ?? null, 128), 'income_date' => $this->dateOrNull($item['date'] ?? null), 'date_close' => $this->dateOrNull($item['date_close'] ?? null), 'last_change_date' => $this->dateTimeOrNull($item['last_change_date'] ?? null), 'warehouse_name' => $this->strOrNull($item['warehouse_name'] ?? null), 'supplier_article' => $this->strOrNull($item['supplier_article'] ?? null), 'tech_size' => $this->strOrNull($item['tech_size'] ?? null, 128), 'barcode' => $this->intOrNull($item['barcode'] ?? null), 'nm_id' => $this->intOrNull($item['nm_id'] ?? null), 'quantity' => $this->intOrNull($item['quantity'] ?? null), 'total_price' => $this->decimalOrNull($item['total_price'] ?? null), 'created_at' => $now, 'updated_at' => $now, ]; } private function mapOrder(array $item): array { $now = now(); $identity = [ $this->strOrNull($item['g_number'] ?? null), $this->strOrNull($item['odid'] ?? null), $this->dateTimeOrNull($item['date'] ?? null), $this->intOrNull($item['barcode'] ?? null), $this->intOrNull($item['nm_id'] ?? null), $this->strOrNull($item['warehouse_name'] ?? null), ]; return [ 'source_uid' => $this->uid('order', $identity), 'order_date' => $this->dateTimeOrNull($item['date'] ?? null), 'last_change_date' => $this->dateTimeOrNull($item['last_change_date'] ?? null), 'cancel_dt' => $this->dateTimeOrNull($item['cancel_dt'] ?? null), 'odid' => $this->strOrNull($item['odid'] ?? null, 128), 'g_number' => $this->strOrNull($item['g_number'] ?? null, 128), 'income_id' => $this->intOrNull($item['income_id'] ?? null), 'warehouse_name' => $this->strOrNull($item['warehouse_name'] ?? null), 'supplier_article' => $this->strOrNull($item['supplier_article'] ?? null), 'tech_size' => $this->strOrNull($item['tech_size'] ?? null, 128), 'barcode' => $this->intOrNull($item['barcode'] ?? null), 'nm_id' => $this->intOrNull($item['nm_id'] ?? null), 'subject' => $this->strOrNull($item['subject'] ?? null), 'category' => $this->strOrNull($item['category'] ?? null), 'brand' => $this->strOrNull($item['brand'] ?? null), 'oblast' => $this->strOrNull($item['oblast'] ?? null), 'total_price' => $this->decimalOrNull($item['total_price'] ?? null), 'discount_percent' => $this->intOrNull($item['discount_percent'] ?? null), 'is_cancel' => $this->boolOrNull($item['is_cancel'] ?? null), 'created_at' => $now, 'updated_at' => $now, ]; } private function mapSale(array $item): array { $now = now(); $identity = $this->strOrNull($item['sale_id'] ?? null) ?: $this->uid('sale-fallback', [ $this->strOrNull($item['g_number'] ?? null), $this->strOrNull($item['odid'] ?? null), $this->dateTimeOrNull($item['date'] ?? null), $this->intOrNull($item['barcode'] ?? null), $this->intOrNull($item['nm_id'] ?? null), $this->strOrNull($item['warehouse_name'] ?? null), $this->decimalOrNull($item['for_pay'] ?? null), ]); return [ 'source_uid' => $this->uid('sale', [$identity]), 'sale_date' => $this->dateTimeOrNull($item['date'] ?? null), 'last_change_date' => $this->dateTimeOrNull($item['last_change_date'] ?? null), 'sale_id' => $this->strOrNull($item['sale_id'] ?? null, 128), 'odid' => $this->strOrNull($item['odid'] ?? null, 128), 'g_number' => $this->strOrNull($item['g_number'] ?? null, 128), 'income_id' => $this->intOrNull($item['income_id'] ?? null), 'warehouse_name' => $this->strOrNull($item['warehouse_name'] ?? null), 'country_name' => $this->strOrNull($item['country_name'] ?? null), 'oblast_okrug_name' => $this->strOrNull($item['oblast_okrug_name'] ?? null), 'region_name' => $this->strOrNull($item['region_name'] ?? null), 'supplier_article' => $this->strOrNull($item['supplier_article'] ?? null), 'tech_size' => $this->strOrNull($item['tech_size'] ?? null, 128), 'barcode' => $this->intOrNull($item['barcode'] ?? null), 'nm_id' => $this->intOrNull($item['nm_id'] ?? null), 'subject' => $this->strOrNull($item['subject'] ?? null), 'category' => $this->strOrNull($item['category'] ?? null), 'brand' => $this->strOrNull($item['brand'] ?? null), 'total_price' => $this->decimalOrNull($item['total_price'] ?? null), 'discount_percent' => $this->intOrNull($item['discount_percent'] ?? null), 'spp' => $this->intOrNull($item['spp'] ?? null), 'for_pay' => $this->decimalOrNull($item['for_pay'] ?? null), 'finished_price' => $this->decimalOrNull($item['finished_price'] ?? null), 'price_with_disc' => $this->decimalOrNull($item['price_with_disc'] ?? null), 'promo_code_discount' => $this->decimalOrNull($item['promo_code_discount'] ?? null), 'is_supply' => $this->boolOrNull($item['is_supply'] ?? null), 'is_realization' => $this->boolOrNull($item['is_realization'] ?? null), 'is_storno' => $this->boolOrNull($item['is_storno'] ?? null), 'created_at' => $now, 'updated_at' => $now, ]; } private function mapStock(array $item): array { $now = now(); $identity = [ $this->dateOrNull($item['date'] ?? null), $this->strOrNull($item['warehouse_name'] ?? null), $this->intOrNull($item['barcode'] ?? null), $this->intOrNull($item['nm_id'] ?? null), $this->strOrNull($item['supplier_article'] ?? null), $this->strOrNull($item['tech_size'] ?? null), $this->intOrNull($item['sc_code'] ?? null), $this->boolOrNull($item['is_supply'] ?? null), $this->boolOrNull($item['is_realization'] ?? null), $this->intOrNull($item['in_way_to_client'] ?? null), $this->intOrNull($item['in_way_from_client'] ?? null), ]; return [ 'source_uid' => $this->uid('stock', $identity), 'stock_date' => $this->dateOrNull($item['date'] ?? null), 'last_change_date' => $this->dateTimeOrNull($item['last_change_date'] ?? null), 'warehouse_name' => $this->strOrNull($item['warehouse_name'] ?? null), 'supplier_article' => $this->strOrNull($item['supplier_article'] ?? null), 'tech_size' => $this->strOrNull($item['tech_size'] ?? null, 128), 'barcode' => $this->intOrNull($item['barcode'] ?? null), 'nm_id' => $this->intOrNull($item['nm_id'] ?? null), 'subject' => $this->strOrNull($item['subject'] ?? null), 'category' => $this->strOrNull($item['category'] ?? null), 'brand' => $this->strOrNull($item['brand'] ?? null), 'quantity' => $this->intOrNull($item['quantity'] ?? null), 'quantity_full' => $this->intOrNull($item['quantity_full'] ?? null), 'in_way_to_client' => $this->intOrNull($item['in_way_to_client'] ?? null), 'in_way_from_client' => $this->intOrNull($item['in_way_from_client'] ?? null), 'price' => $this->decimalOrNull($item['price'] ?? null), 'discount' => $this->intOrNull($item['discount'] ?? null), 'sc_code' => $this->intOrNull($item['sc_code'] ?? null), 'is_supply' => $this->boolOrNull($item['is_supply'] ?? null), 'is_realization' => $this->boolOrNull($item['is_realization'] ?? null), 'created_at' => $now, 'updated_at' => $now, ]; } private function upsertRows(string $table, array $rows): void { if ($rows === []) { return; } $updateColumns = array_values(array_diff(array_keys($rows[0]), ['source_uid', 'created_at'])); foreach (array_chunk($rows, 100) as $chunk) { DB::table($table)->upsert($chunk, ['source_uid'], $updateColumns); } } private function log(string $endpoint, ?int $page, int $itemsCount, string $status, ?string $message = null): void { DB::table('wb_sync_logs')->insert([ 'endpoint' => $endpoint, 'page' => $page, 'items_count' => $itemsCount, 'status' => $status, 'message' => $message ? Str::limit($message, 1500) : null, 'created_at' => now(), 'updated_at' => now(), ]); } private function uid(string $prefix, array $parts): string { $normalized = array_map(static fn ($value) => $value === null ? '' : (string) $value, $parts); return hash('sha256', $prefix . '|' . implode('|', $normalized)); } private function strOrNull(mixed $value, int $max = 255): ?string { if ($value === null || $value === '') { return null; } return mb_substr((string) $value, 0, $max); } private function intOrNull(mixed $value): ?int { if ($value === null || $value === '') { return null; } return (int) $value; } private function decimalOrNull(mixed $value): ?string { if ($value === null || $value === '') { return null; } return str_replace(',', '.', (string) $value); } private function boolOrNull(mixed $value): ?int { if ($value === null || $value === '') { return null; } if (is_bool($value)) { return $value ? 1 : 0; } return ((int) $value) ? 1 : 0; } private function dateOrNull(mixed $value): ?string { if ($value === null || $value === '') { return null; } try { return (new DateTimeImmutable((string) $value))->format('Y-m-d'); } catch (Throwable) { return null; } } private function dateTimeOrNull(mixed $value): ?string { if ($value === null || $value === '') { return null; } try { return (new DateTimeImmutable((string) $value))->format('Y-m-d H:i:s'); } catch (Throwable) { return null; } } }