OrderImportRepository::updateOrderDelta() przechodzi na dynamic SET builder. total_paid jest dolaczane do UPDATE tylko gdy payment_status realnie sie zmienia; is_canceled_by_buyer analogicznie, ale z override przez cancelledBySource (cancel ze zrodla nadal propaguje sie do bazy). Chroni reczne korekty operatora (zwroty czesciowe) przed cichym nadpisaniem z payloadu zrodla przy kolejnym sync. Incydent #976: operator zwrocil klientowi 28,00 PLN obnizajac total_paid 119->91, co bez tej zmiany byloby cofniete przez kolejny re-import shoppro. Boundaries: identical-payload guard, paymentTransition, statusOverwriteAllowed, cancel propagation (status_code='anulowane') - bez zmian. Tests: tests/Unit/OrderImportRepositoryTest.php - 3 scenariusze (preserve / transition / cancel propagation) via Reflection + sqlite in-memory. PHPUnit run odroczony (vendor/ gitignored). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
167 lines
5.6 KiB
PHP
167 lines
5.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Modules\Orders\OrderImportRepository;
|
|
use PDO;
|
|
use PHPUnit\Framework\TestCase;
|
|
use ReflectionMethod;
|
|
|
|
final class OrderImportRepositoryTest extends TestCase
|
|
{
|
|
private PDO $pdo;
|
|
private OrderImportRepository $repository;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->pdo = new PDO('sqlite::memory:');
|
|
$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$this->pdo->sqliteCreateFunction('NOW', static fn(): string => date('Y-m-d H:i:s'));
|
|
|
|
$this->pdo->exec(
|
|
'CREATE TABLE orders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
status_code TEXT,
|
|
payment_status INTEGER,
|
|
total_paid REAL,
|
|
is_canceled_by_buyer INTEGER DEFAULT 0,
|
|
source_updated_at TEXT,
|
|
payload_json TEXT,
|
|
fetched_at TEXT,
|
|
updated_at TEXT
|
|
)'
|
|
);
|
|
|
|
$this->repository = new OrderImportRepository($this->pdo);
|
|
}
|
|
|
|
public function testTotalPaidPreservedWhenPaymentStatusUnchanged(): void
|
|
{
|
|
$this->seedOrder([
|
|
'status_code' => 'wyslane',
|
|
'payment_status' => 2,
|
|
'total_paid' => 91.00,
|
|
'is_canceled_by_buyer' => 0,
|
|
'payload_json' => '{"a":1}',
|
|
'source_updated_at' => '2026-05-01 10:00:00',
|
|
'fetched_at' => '2026-05-01 10:00:00',
|
|
]);
|
|
|
|
$orderData = [
|
|
'status_code' => 'wyslane',
|
|
'payment_status' => 2,
|
|
'total_paid' => 119.00,
|
|
'is_canceled_by_buyer' => 0,
|
|
'source_updated_at' => '2026-05-10 12:00:00',
|
|
'payload_json' => ['a' => 2],
|
|
'fetched_at' => '2026-05-10 12:00:00',
|
|
];
|
|
|
|
$this->invokeUpdateOrderDelta(1, $orderData, true, false);
|
|
|
|
$row = $this->fetchOrder(1);
|
|
self::assertSame('91', $this->normalizeAmount($row['total_paid']), 'total_paid must remain at manually corrected value');
|
|
self::assertSame(2, (int) $row['payment_status']);
|
|
self::assertSame('2026-05-10 12:00:00', $row['source_updated_at']);
|
|
}
|
|
|
|
public function testTotalPaidUpdatedOnPaymentTransition(): void
|
|
{
|
|
$this->seedOrder([
|
|
'status_code' => 'nieoplacone',
|
|
'payment_status' => 0,
|
|
'total_paid' => 0.00,
|
|
'is_canceled_by_buyer' => 0,
|
|
'payload_json' => '{}',
|
|
'source_updated_at' => '2026-05-01 10:00:00',
|
|
'fetched_at' => '2026-05-01 10:00:00',
|
|
]);
|
|
|
|
$orderData = [
|
|
'status_code' => 'w_realizacji',
|
|
'payment_status' => 2,
|
|
'total_paid' => 119.00,
|
|
'is_canceled_by_buyer' => 0,
|
|
'source_updated_at' => '2026-05-10 12:00:00',
|
|
'payload_json' => ['a' => 1],
|
|
'fetched_at' => '2026-05-10 12:00:00',
|
|
];
|
|
|
|
$this->invokeUpdateOrderDelta(1, $orderData, false, false);
|
|
|
|
$row = $this->fetchOrder(1);
|
|
self::assertSame('119', $this->normalizeAmount($row['total_paid']));
|
|
self::assertSame(2, (int) $row['payment_status']);
|
|
}
|
|
|
|
public function testIsCanceledByBuyerPropagatedOnSourceCancelEvenWhenPaymentStable(): void
|
|
{
|
|
$this->seedOrder([
|
|
'status_code' => 'wyslane',
|
|
'payment_status' => 2,
|
|
'total_paid' => 91.00,
|
|
'is_canceled_by_buyer' => 0,
|
|
'payload_json' => '{}',
|
|
'source_updated_at' => '2026-05-01 10:00:00',
|
|
'fetched_at' => '2026-05-01 10:00:00',
|
|
]);
|
|
|
|
$orderData = [
|
|
'status_code' => 'anulowane',
|
|
'payment_status' => 2,
|
|
'total_paid' => 119.00,
|
|
'is_canceled_by_buyer' => 1,
|
|
'source_updated_at' => '2026-05-10 12:00:00',
|
|
'payload_json' => ['a' => 9],
|
|
'fetched_at' => '2026-05-10 12:00:00',
|
|
];
|
|
|
|
$this->invokeUpdateOrderDelta(1, $orderData, true, true);
|
|
|
|
$row = $this->fetchOrder(1);
|
|
self::assertSame(1, (int) $row['is_canceled_by_buyer'], 'source cancellation must propagate');
|
|
self::assertSame('anulowane', $row['status_code']);
|
|
self::assertSame('91', $this->normalizeAmount($row['total_paid']), 'total_paid still protected because payment_status unchanged');
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $orderData
|
|
*/
|
|
private function invokeUpdateOrderDelta(int $orderId, array $orderData, bool $paymentStatusUnchanged, bool $cancelledBySource): void
|
|
{
|
|
$method = new ReflectionMethod(OrderImportRepository::class, 'updateOrderDelta');
|
|
$method->setAccessible(true);
|
|
$method->invoke($this->repository, $orderId, $orderData, $paymentStatusUnchanged, $cancelledBySource);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $values
|
|
*/
|
|
private function seedOrder(array $values): void
|
|
{
|
|
$columns = array_keys($values);
|
|
$placeholders = array_map(static fn(string $c): string => ':' . $c, $columns);
|
|
$sql = 'INSERT INTO orders (' . implode(', ', $columns) . ', updated_at) VALUES (' . implode(', ', $placeholders) . ", '2026-05-01 10:00:00')";
|
|
$stmt = $this->pdo->prepare($sql);
|
|
$stmt->execute($values);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function fetchOrder(int $id): array
|
|
{
|
|
$stmt = $this->pdo->prepare('SELECT * FROM orders WHERE id = :id');
|
|
$stmt->execute(['id' => $id]);
|
|
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
self::assertIsArray($row);
|
|
return $row;
|
|
}
|
|
|
|
private function normalizeAmount(mixed $value): string
|
|
{
|
|
return (string) (float) $value;
|
|
}
|
|
}
|