Files
wb-api-test/app/Services/Wb/WbSyncService.php
T
2026-07-01 03:28:17 +03:00

363 lines
14 KiB
PHP

<?php
namespace App\Services\Wb;
use DateTimeImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Throwable;
class WbSyncService
{
private const ENDPOINTS = ['sales', 'orders', 'incomes', 'stocks'];
public function __construct(private readonly WbApiClient $client)
{
}
public function syncAll(string $from, string $to, string $stocksDate, ?string $endpoint = null, ?callable $progress = null): array
{
$endpoints = $endpoint ? [$endpoint] : self::ENDPOINTS;
$result = [
'sales' => 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 ? '<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;
}
}
}