feat(129): erli status mapping sync
Phase 129 complete: - Add Erli pull/push status mapping tables, seeds and repositories - Wire Erli status sync cron for inbox pull and manual-only push - Add tabbed Erli settings UI, tests and documentation Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ namespace Tests\Unit;
|
||||
|
||||
use App\Core\Constants\IntegrationSources;
|
||||
use App\Modules\Settings\ErliOrderMapper;
|
||||
use App\Modules\Settings\ErliPullStatusMappingRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use RuntimeException;
|
||||
|
||||
@@ -51,6 +52,30 @@ final class ErliOrderMapperTest extends TestCase
|
||||
self::assertTrue($aggregate['order']['is_canceled_by_buyer']);
|
||||
}
|
||||
|
||||
public function testConfiguredPullMappingOverridesDefaultStatus(): void
|
||||
{
|
||||
$pullMappings = $this->createMock(ErliPullStatusMappingRepository::class);
|
||||
$pullMappings
|
||||
->method('findMappedStatusCode')
|
||||
->with('purchased')
|
||||
->willReturn('w_realizacji');
|
||||
|
||||
$mapper = new ErliOrderMapper($pullMappings);
|
||||
$aggregate = $mapper->mapInboxMessage(7, $this->message('purchased'));
|
||||
|
||||
self::assertIsArray($aggregate);
|
||||
self::assertSame('w_realizacji', $aggregate['order']['status_code']);
|
||||
}
|
||||
|
||||
public function testUnknownStatusFallsBackToRawCodeForDiscoveryMappingLater(): void
|
||||
{
|
||||
$aggregate = $this->mapper->mapInboxMessage(7, $this->message('readyToProcess'));
|
||||
|
||||
self::assertIsArray($aggregate);
|
||||
self::assertSame('readytoprocess', $aggregate['order']['status_code']);
|
||||
self::assertSame('readytoprocess', $aggregate['order']['preferences_json']['erli_status_raw']);
|
||||
}
|
||||
|
||||
public function testCompanyInvoiceDataDetectsInvoiceRequest(): void
|
||||
{
|
||||
$message = $this->message('purchased');
|
||||
|
||||
200
tests/Unit/ErliStatusSyncServiceTest.php
Normal file
200
tests/Unit/ErliStatusSyncServiceTest.php
Normal file
@@ -0,0 +1,200 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Modules\Cron\CronRepository;
|
||||
use App\Modules\Settings\ErliApiClient;
|
||||
use App\Modules\Settings\ErliIntegrationRepository;
|
||||
use App\Modules\Settings\ErliOrdersSyncService;
|
||||
use App\Modules\Settings\ErliOrderSyncStateRepository;
|
||||
use App\Modules\Settings\ErliStatusMappingRepository;
|
||||
use App\Modules\Settings\ErliStatusSyncService;
|
||||
use PDO;
|
||||
use PDOStatement;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class ErliStatusSyncServiceTest extends TestCase
|
||||
{
|
||||
private CronRepository&MockObject $cronRepository;
|
||||
private ErliIntegrationRepository&MockObject $integrationRepository;
|
||||
private ErliOrdersSyncService&MockObject $ordersSyncService;
|
||||
private ErliApiClient&MockObject $apiClient;
|
||||
private ErliOrderSyncStateRepository&MockObject $syncStateRepository;
|
||||
private ErliStatusMappingRepository&MockObject $statusMappings;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->cronRepository = $this->createMock(CronRepository::class);
|
||||
$this->integrationRepository = $this->createMock(ErliIntegrationRepository::class);
|
||||
$this->ordersSyncService = $this->createMock(ErliOrdersSyncService::class);
|
||||
$this->apiClient = $this->createMock(ErliApiClient::class);
|
||||
$this->syncStateRepository = $this->createMock(ErliOrderSyncStateRepository::class);
|
||||
$this->statusMappings = $this->createMock(ErliStatusMappingRepository::class);
|
||||
}
|
||||
|
||||
public function testPullDirectionDelegatesToOrdersInboxImport(): void
|
||||
{
|
||||
$this->cronRepository
|
||||
->method('getStringSetting')
|
||||
->willReturn('erli_to_orderpro');
|
||||
|
||||
$this->ordersSyncService
|
||||
->expects($this->once())
|
||||
->method('sync')
|
||||
->with([
|
||||
'ignore_orders_fetch_enabled' => true,
|
||||
'max_messages' => 200,
|
||||
])
|
||||
->willReturn([
|
||||
'failed' => 0,
|
||||
'processed' => 2,
|
||||
]);
|
||||
|
||||
$result = $this->createServiceWithRows([])->sync();
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame('erli_to_orderpro', $result['direction']);
|
||||
self::assertSame(2, $result['processed']);
|
||||
}
|
||||
|
||||
public function testPushDirectionProcessesMappedManualOrdersAndSkipsUnmapped(): void
|
||||
{
|
||||
$this->preparePushDefaults([
|
||||
'w_realizacji' => 'inProgress',
|
||||
]);
|
||||
|
||||
$this->apiClient
|
||||
->expects($this->exactly(2))
|
||||
->method('updateOrderStatus')
|
||||
->willReturn(['ok' => true, 'http_code' => 204, 'message' => 'OK']);
|
||||
|
||||
$this->syncStateRepository
|
||||
->expects($this->once())
|
||||
->method('updateLastStatusPushedAt')
|
||||
->with(12, '2026-05-16 10:10:00');
|
||||
|
||||
$service = $this->createServiceWithRows([
|
||||
['source_order_id' => 'E1', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:00:00'],
|
||||
['source_order_id' => 'E2', 'orderpro_status_code' => 'nowe', 'latest_change' => '2026-05-16 10:05:00'],
|
||||
['source_order_id' => 'E3', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:10:00'],
|
||||
]);
|
||||
|
||||
$result = $service->sync();
|
||||
|
||||
self::assertTrue($result['ok']);
|
||||
self::assertSame('orderpro_to_erli', $result['direction']);
|
||||
self::assertSame(2, $result['pushed']);
|
||||
self::assertSame(1, $result['skipped']);
|
||||
self::assertSame(0, $result['failed']);
|
||||
}
|
||||
|
||||
public function testPushFailureDoesNotAdvanceCursorPastFailedOrder(): void
|
||||
{
|
||||
$this->preparePushDefaults([
|
||||
'w_realizacji' => 'inProgress',
|
||||
]);
|
||||
|
||||
$this->apiClient
|
||||
->expects($this->exactly(2))
|
||||
->method('updateOrderStatus')
|
||||
->willReturnOnConsecutiveCalls(
|
||||
['ok' => true, 'http_code' => 204, 'message' => 'OK'],
|
||||
['ok' => false, 'http_code' => 500, 'message' => 'Erli error']
|
||||
);
|
||||
|
||||
$this->syncStateRepository
|
||||
->expects($this->once())
|
||||
->method('updateLastStatusPushedAt')
|
||||
->with(12, '2026-05-16 10:00:00');
|
||||
|
||||
$service = $this->createServiceWithRows([
|
||||
['source_order_id' => 'E1', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:00:00'],
|
||||
['source_order_id' => 'E2', 'orderpro_status_code' => 'w_realizacji', 'latest_change' => '2026-05-16 10:10:00'],
|
||||
]);
|
||||
|
||||
$result = $service->sync();
|
||||
|
||||
self::assertFalse($result['ok']);
|
||||
self::assertSame(1, $result['pushed']);
|
||||
self::assertSame(1, $result['failed']);
|
||||
self::assertCount(1, $result['errors']);
|
||||
}
|
||||
|
||||
public function testPushDirectionReturnsEarlyWithoutCredentials(): void
|
||||
{
|
||||
$this->cronRepository
|
||||
->method('getStringSetting')
|
||||
->willReturn('orderpro_to_erli');
|
||||
|
||||
$this->integrationRepository
|
||||
->method('getCredentials')
|
||||
->willReturn(null);
|
||||
|
||||
$this->apiClient
|
||||
->expects($this->never())
|
||||
->method('updateOrderStatus');
|
||||
|
||||
$result = $this->createServiceWithRows([])->sync();
|
||||
|
||||
self::assertFalse($result['ok']);
|
||||
self::assertSame(0, $result['pushed']);
|
||||
self::assertSame('orderpro_to_erli', $result['direction']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $map
|
||||
*/
|
||||
private function preparePushDefaults(array $map): void
|
||||
{
|
||||
$this->cronRepository
|
||||
->method('getStringSetting')
|
||||
->willReturn('orderpro_to_erli');
|
||||
$this->integrationRepository
|
||||
->method('getCredentials')
|
||||
->willReturn([
|
||||
'integration_id' => 12,
|
||||
'base_url' => 'https://erli.test',
|
||||
'api_key' => 'token',
|
||||
'timeout_seconds' => 15,
|
||||
'orders_fetch_enabled' => true,
|
||||
'orders_fetch_start_date' => null,
|
||||
]);
|
||||
$this->syncStateRepository
|
||||
->method('getLastStatusPushedAt')
|
||||
->willReturn(null);
|
||||
$this->statusMappings
|
||||
->method('buildOrderproToErliMap')
|
||||
->willReturn($map);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $rows
|
||||
*/
|
||||
private function createServiceWithRows(array $rows): ErliStatusSyncService
|
||||
{
|
||||
$statement = $this->createMock(PDOStatement::class);
|
||||
$statement
|
||||
->method('execute')
|
||||
->willReturn(true);
|
||||
$statement
|
||||
->method('fetchAll')
|
||||
->willReturn($rows);
|
||||
|
||||
$pdo = $this->createMock(PDO::class);
|
||||
$pdo
|
||||
->method('prepare')
|
||||
->willReturn($statement);
|
||||
|
||||
return new ErliStatusSyncService(
|
||||
$this->cronRepository,
|
||||
$this->integrationRepository,
|
||||
$this->ordersSyncService,
|
||||
$this->apiClient,
|
||||
$this->syncStateRepository,
|
||||
$this->statusMappings,
|
||||
$pdo
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user