first commit
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\WbService\SyncServices;
|
||||
|
||||
use App\Contracts\WbClientInterface;
|
||||
use App\Contracts\WbSyncInterface;
|
||||
use App\DTO\WbRequestDTO;
|
||||
use App\Exceptions\WbServiceException;
|
||||
use App\Models\Income;
|
||||
|
||||
final readonly class IncomesSyncService implements WbSyncInterface
|
||||
{
|
||||
public function __construct(
|
||||
private WbClientInterface $client,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WbServiceException
|
||||
*/
|
||||
public function sync(WbRequestDTO $dto): array
|
||||
{
|
||||
return $this->client->fetch($dto);
|
||||
}
|
||||
|
||||
public function save(array $data): void
|
||||
{
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
collect($data)
|
||||
->chunk(500)
|
||||
->each(function ($chunk) {
|
||||
$rows = $chunk->map(function (array $row) {
|
||||
return [
|
||||
'income_id' => $row['income_id'],
|
||||
'number' => $row['number'],
|
||||
'date' => $row['date'],
|
||||
'last_change_date' => $row['last_change_date'],
|
||||
'supplier_article' => $row['supplier_article'],
|
||||
'tech_size' => $row['tech_size'],
|
||||
'barcode' => (string)$row['barcode'],
|
||||
'quantity' => $row['quantity'],
|
||||
'total_price' => $row['total_price'],
|
||||
'date_close' => $row['date_close'],
|
||||
'warehouse_name' => $row['warehouse_name'],
|
||||
'nm_id' => $row['nm_id'],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
Income::insert($rows);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\WbService\SyncServices;
|
||||
|
||||
use App\Contracts\WbClientInterface;
|
||||
use App\Contracts\WbSyncInterface;
|
||||
use App\DTO\WbRequestDTO;
|
||||
use App\Exceptions\WbServiceException;
|
||||
use App\Models\Order;
|
||||
|
||||
final readonly class OrdersSyncService implements WbSyncInterface
|
||||
{
|
||||
public function __construct(
|
||||
private WbClientInterface $client,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WbServiceException
|
||||
*/
|
||||
public function sync(WbRequestDTO $dto): array
|
||||
{
|
||||
return $this->client->fetch($dto);
|
||||
}
|
||||
|
||||
public function save(array $data): void
|
||||
{
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
collect($data)
|
||||
->chunk(500)
|
||||
->each(function ($chunk) {
|
||||
$rows = $chunk->map(function (array $row) {
|
||||
return [
|
||||
'g_number' => $row['g_number'],
|
||||
'date' => !empty($row['date']) ? $row['date'] : now(),
|
||||
'last_change_date' => $row['last_change_date'],
|
||||
'supplier_article' => $row['supplier_article'],
|
||||
'tech_size' => $row['tech_size'],
|
||||
'barcode' => (int)$row['barcode'],
|
||||
'total_price' => $row['total_price'],
|
||||
'discount_percent' => $row['discount_percent'],
|
||||
'warehouse_name' => $row['warehouse_name'],
|
||||
'oblast' => $row['oblast'],
|
||||
'income_id' => $row['income_id'],
|
||||
'odid' => (int)$row['odid'],
|
||||
'nm_id' => $row['nm_id'],
|
||||
'subject' => $row['subject'],
|
||||
'category' => $row['category'],
|
||||
'brand' => $row['brand'],
|
||||
'is_cancel' => $row['is_cancel'],
|
||||
'cancel_dt' => $row['cancel_dt'],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
try {
|
||||
Order::insert($rows);
|
||||
} catch (\Throwable $e) {
|
||||
\Log::error('Insert failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'row_sample' => $rows[0] ?? null,
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\WbService\SyncServices;
|
||||
|
||||
use App\Contracts\WbClientInterface;
|
||||
use App\Contracts\WbSyncInterface;
|
||||
use App\DTO\WbRequestDTO;
|
||||
use App\Exceptions\WbServiceException;
|
||||
use App\Models\Sale;
|
||||
|
||||
final readonly class SalesSyncService implements WbSyncInterface
|
||||
{
|
||||
public function __construct(
|
||||
private WbClientInterface $client,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WbServiceException
|
||||
*/
|
||||
public function sync(WbRequestDTO $dto): array
|
||||
{
|
||||
return $this->client->fetch($dto);
|
||||
}
|
||||
|
||||
public function save(array $data): void
|
||||
{
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
collect($data)
|
||||
->chunk(500)
|
||||
->each(function ($chunk) {
|
||||
$rows = $chunk->map(function (array $row) {
|
||||
return [
|
||||
'g_number' => $row['g_number'],
|
||||
'date' => $row['date'],
|
||||
'last_change_date' => $row['last_change_date'],
|
||||
'supplier_article' => $row['supplier_article'],
|
||||
'tech_size' => $row['tech_size'],
|
||||
'barcode' => (string)$row['barcode'],
|
||||
'total_price' => $row['total_price'],
|
||||
'discount_percent' => $row['discount_percent'],
|
||||
'is_supply' => $row['is_supply'],
|
||||
'is_realization' => $row['is_realization'],
|
||||
'promo_code_discount' => $row['promo_code_discount'],
|
||||
'warehouse_name' => $row['warehouse_name'],
|
||||
'country_name' => $row['country_name'],
|
||||
'oblast_okrug_name' => $row['oblast_okrug_name'],
|
||||
'region_name' => $row['region_name'],
|
||||
'income_id' => $row['income_id'],
|
||||
'sale_id' => $row['sale_id'],
|
||||
'odid' => $row['odid'],
|
||||
'spp' => $row['spp'],
|
||||
'for_pay' => $row['for_pay'],
|
||||
'finished_price' => $row['finished_price'],
|
||||
'price_with_disc' => $row['price_with_disc'],
|
||||
'nm_id' => $row['nm_id'],
|
||||
'subject' => $row['subject'],
|
||||
'category' => $row['category'],
|
||||
'brand' => $row['brand'],
|
||||
'is_storno' => $row['is_storno'],
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
Sale::insert($rows);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\WbService\SyncServices;
|
||||
|
||||
use App\Contracts\WbClientInterface;
|
||||
use App\Contracts\WbSyncInterface;
|
||||
use App\DTO\WbRequestDTO;
|
||||
use App\Exceptions\WbServiceException;
|
||||
use App\Models\Stock;
|
||||
|
||||
final readonly class StocksSyncService implements WbSyncInterface
|
||||
{
|
||||
public function __construct(
|
||||
private WbClientInterface $client,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WbServiceException
|
||||
*/
|
||||
public function sync(WbRequestDTO $dto): array
|
||||
{
|
||||
return $this->client->fetch($dto);
|
||||
}
|
||||
|
||||
public function save(array $data): void
|
||||
{
|
||||
if (empty($data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
collect($data)
|
||||
->chunk(500)
|
||||
->each(function ($chunk) {
|
||||
$rows = $chunk->map(function (array $row) {
|
||||
return [
|
||||
'date' => $row['date'],
|
||||
'last_change_date' => $row['last_change_date'],
|
||||
'supplier_article' => $row['supplier_article'],
|
||||
'tech_size' => $row['tech_size'],
|
||||
'barcode' => (string)$row['barcode'],
|
||||
'quantity' => $row['quantity'],
|
||||
'is_supply' => $row['is_supply'],
|
||||
'is_realization' => $row['is_realization'],
|
||||
'quantity_full' => $row['quantity_full'],
|
||||
'warehouse_name' => $row['warehouse_name'],
|
||||
'in_way_to_client' => $row['in_way_to_client'],
|
||||
'in_way_from_client' => $row['in_way_from_client'],
|
||||
'nm_id' => $row['nm_id'],
|
||||
'subject' => $row['subject'],
|
||||
'category' => $row['category'],
|
||||
'brand' => $row['brand'],
|
||||
'sc_code' => (string)$row['sc_code'],
|
||||
'price' => $row['price'],
|
||||
'discount' => $row['discount'],
|
||||
'updated_at' => now(),
|
||||
'created_at' => now(),
|
||||
];
|
||||
})->toArray();
|
||||
|
||||
Stock::insert($rows);// todo upsert
|
||||
});
|
||||
}
|
||||
}
|
||||
50
laravel/app/Services/WbService/WbClient.php
Normal file
50
laravel/app/Services/WbService/WbClient.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\WbService;
|
||||
|
||||
use App\Contracts\WbClientInterface;
|
||||
use App\DTO\WbRequestDTO;
|
||||
use App\Exceptions\WbServiceException;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Throwable;
|
||||
|
||||
final class WbClient implements WbClientInterface
|
||||
{
|
||||
/**
|
||||
* @throws WbServiceException
|
||||
*/
|
||||
public function fetch(WbRequestDTO $requestDTO): array
|
||||
{
|
||||
$query = [
|
||||
'dateFrom' => $requestDTO->dateFrom,
|
||||
'page' => $requestDTO->page,
|
||||
'limit' => $requestDTO->limit,
|
||||
'key' => $requestDTO->endpoint->key(),
|
||||
];
|
||||
|
||||
if ($requestDTO->dateTo !== null) {
|
||||
$query['dateTo'] = $requestDTO->dateTo;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::timeout(30)
|
||||
->retry(3, 500)
|
||||
->acceptJson()
|
||||
->get(
|
||||
$requestDTO->endpoint->url(),
|
||||
$query
|
||||
);
|
||||
|
||||
$response->throw();
|
||||
} catch (Throwable $e) {
|
||||
throw new WbServiceException(
|
||||
'WB API request failed: ' . $e->getMessage(),
|
||||
previous: $e
|
||||
);
|
||||
}
|
||||
|
||||
return $response->json();
|
||||
}
|
||||
}
|
||||
28
laravel/app/Services/WbService/WbSyncFactory.php
Normal file
28
laravel/app/Services/WbService/WbSyncFactory.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\WbService;
|
||||
|
||||
use App\Contracts\WbSyncFactoryInterface;
|
||||
use App\Contracts\WbSyncInterface;
|
||||
use App\Enums\WbEndpoint;
|
||||
use App\Exceptions\WbServiceException;
|
||||
|
||||
final readonly class WbSyncFactory implements WbSyncFactoryInterface
|
||||
{
|
||||
public function __construct(
|
||||
/** @var array<string, WbSyncInterface> */
|
||||
private array $services
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws WbServiceException
|
||||
*/
|
||||
public function make(WbEndpoint $type): WbSyncInterface
|
||||
{
|
||||
return $this->services[$type->name]
|
||||
?? throw new WbServiceException("unknown sync type $type->name");
|
||||
}
|
||||
}
|
||||
213
laravel/app/Services/WbService/WbSyncManager.php
Normal file
213
laravel/app/Services/WbService/WbSyncManager.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\WbService;
|
||||
|
||||
use App\Contracts\WbSyncManagerInterface;
|
||||
use App\Enums\SyncStatus;
|
||||
use App\Enums\WbEndpoint;
|
||||
use App\Jobs\WbSyncJob;
|
||||
use App\Models\SyncState;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
final readonly class WbSyncManager implements WbSyncManagerInterface
|
||||
{
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function start(
|
||||
WbEndpoint $endpoint,
|
||||
string $dateFrom,
|
||||
?string $dateTo = null
|
||||
): void {
|
||||
$now = now();
|
||||
|
||||
DB::transaction(function () use ($endpoint, $dateFrom, $dateTo, $now) {
|
||||
$sync = SyncState::query()
|
||||
->where('entity', $endpoint->name)
|
||||
->where('date_from', $dateFrom)
|
||||
->when(
|
||||
$dateTo === null,
|
||||
fn($q) => $q->whereNull('date_to'),
|
||||
fn($q) => $q->where('date_to', $dateTo)
|
||||
)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
|
||||
if (!$sync) {
|
||||
$sync = SyncState::create([
|
||||
'entity' => $endpoint->name,
|
||||
'date_from' => $dateFrom,
|
||||
'date_to' => $dateTo,
|
||||
'status' => SyncStatus::PENDING,
|
||||
'started_at' => $now,
|
||||
]);
|
||||
|
||||
Log::info('WB sync created', [
|
||||
'id' => $sync->id,
|
||||
'entity' => $endpoint->name,
|
||||
'date_from' => $dateFrom,
|
||||
]);
|
||||
|
||||
WbSyncJob::dispatch($endpoint, $dateFrom, $dateTo, 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($sync->status === SyncStatus::SUCCESS) {
|
||||
Log::info('WB sync skipped, already SUCCESS', [
|
||||
'id' => $sync->id,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($sync->status === SyncStatus::PENDING) {
|
||||
if ($sync->started_at && $sync->started_at->diffInMinutes($now) < config('wb-sync.timeout')) {
|
||||
Log::info('WB sync already PENDING, skipping', [
|
||||
'id' => $sync->id,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
Log::warning('WB sync stuck > wb-sync.timeout, restart', [
|
||||
'id' => $sync->id,
|
||||
'started_at' => $sync->started_at,
|
||||
]);
|
||||
}
|
||||
|
||||
$sync->update([
|
||||
'status' => SyncStatus::PENDING,
|
||||
'started_at' => $now,
|
||||
'updated_at' => $now,
|
||||
]);
|
||||
|
||||
Log::info('WB sync started', ['id' => $sync->id]);
|
||||
|
||||
WbSyncJob::dispatch($endpoint, $dateFrom, $dateTo, 1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function handleFirstPage(
|
||||
WbEndpoint $endpoint,
|
||||
string $dateFrom,
|
||||
?string $dateTo,
|
||||
int $lastPage
|
||||
): void {
|
||||
DB::transaction(function () use ($endpoint, $dateFrom, $dateTo, $lastPage) {
|
||||
$sync = $this->getSyncForUpdate($endpoint, $dateFrom, $dateTo);
|
||||
|
||||
if ($sync->batch_id) {
|
||||
Log::info('WB sync batch already created', [
|
||||
'id' => $sync->id,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$sync->update([
|
||||
'total_pages' => $lastPage,
|
||||
'processed_pages' => 1,
|
||||
]);
|
||||
|
||||
if ($lastPage <= 1) {
|
||||
$this->markSuccess($endpoint, $dateFrom, $dateTo);
|
||||
return;
|
||||
}
|
||||
|
||||
$jobs = [];
|
||||
|
||||
for ($page = 2; $page <= $lastPage; $page++) {
|
||||
$jobs[] = new WbSyncJob(
|
||||
$endpoint,
|
||||
$dateFrom,
|
||||
$dateTo,
|
||||
$page
|
||||
);
|
||||
}
|
||||
|
||||
$batch = Bus::batch($jobs)
|
||||
->then(fn() => $this->markSuccess($endpoint, $dateFrom, $dateTo))
|
||||
->catch(fn() => $this->markFailed($endpoint, $dateFrom, $dateTo))
|
||||
->name("sync-$endpoint->name-$dateFrom")
|
||||
->dispatch();
|
||||
|
||||
$sync->update([
|
||||
'batch_id' => $batch->id,
|
||||
]);
|
||||
|
||||
Log::info('WB batch dispatched', [
|
||||
'batch_id' => $batch->id,
|
||||
'pages' => $lastPage,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function incrementProgress(
|
||||
WbEndpoint $endpoint,
|
||||
string $dateFrom,
|
||||
?string $dateTo
|
||||
): void {
|
||||
$sync = $this->getSyncForUpdate($endpoint, $dateFrom, $dateTo);
|
||||
|
||||
$sync->incrementProcessedPages();
|
||||
|
||||
Log::info('WB progress incremented', [
|
||||
'id' => $sync->id,
|
||||
'processed_pages' => $sync->processed_pages + 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function markSuccess(
|
||||
WbEndpoint $endpoint,
|
||||
string $dateFrom,
|
||||
?string $dateTo
|
||||
): void {
|
||||
$sync = $this->getSyncForUpdate($endpoint, $dateFrom, $dateTo);
|
||||
|
||||
$sync->update([
|
||||
'status' => SyncStatus::SUCCESS,
|
||||
]);
|
||||
|
||||
Log::info('WB sync success', [
|
||||
'id' => $sync->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function markFailed(
|
||||
WbEndpoint $endpoint,
|
||||
string $dateFrom,
|
||||
?string $dateTo
|
||||
): void {
|
||||
$sync = $this->getSyncForUpdate($endpoint, $dateFrom, $dateTo);
|
||||
|
||||
$sync->update([
|
||||
'status' => SyncStatus::FAILED,
|
||||
]);
|
||||
|
||||
Log::error('WB sync failed', [
|
||||
'id' => $sync->id,
|
||||
]);
|
||||
}
|
||||
|
||||
private function getSyncForUpdate(
|
||||
WbEndpoint $endpoint,
|
||||
string $dateFrom,
|
||||
?string $dateTo
|
||||
): SyncState {
|
||||
return SyncState::query()
|
||||
->where('entity', $endpoint->name)
|
||||
->where('date_from', $dateFrom)
|
||||
->when(
|
||||
$dateTo === null,
|
||||
fn($q) => $q->whereNull('date_to'),
|
||||
fn($q) => $q->where('date_to', $dateTo)
|
||||
)
|
||||
->lockForUpdate()
|
||||
->firstOrFail();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user