363 lines
14 KiB
PHP
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;
|
|
}
|
|
}
|
|
}
|