feat: phase 4 — orders with status workflow, idempotent complete_order RPC, delivery note PDF

This commit is contained in:
sean
2026-08-25 19:10:11 +07:00
parent 002fdce854
commit 029a4a21f8
15 changed files with 1749 additions and 4 deletions
+81 -1
View File
@@ -10,11 +10,15 @@ class InsForgeFake
/** @var array<string, array<string, array<string, mixed>>> table => id => row */
public static array $store = [];
/** @var list<array{function: string, args: array<string, mixed>}> */
public static array $rpcCalls = [];
public static string $accessToken = '';
public static function reset(): void
{
self::$store = [];
self::$rpcCalls = [];
}
public static function seed(string $table, array $row): void
@@ -67,6 +71,18 @@ class InsForgeFake
{
Http::fake([
'*/api/auth/refresh' => Http::response(['accessToken' => self::$accessToken], 200),
'*/api/database/rpc/*' => function (Request $request) {
$function = str_replace('/api/database/rpc/', '', (string) parse_url($request->url(), PHP_URL_PATH));
$args = $request->data();
self::$rpcCalls[] = ['function' => $function, 'args' => $args];
if ($function === 'complete_order') {
return self::simulateCompleteOrder($args);
}
return Http::response(['ok' => true], 200);
},
'*/api/database/records/*' => fn (Request $request) => self::handleRecordsRequest($request),
]);
}
@@ -89,7 +105,12 @@ class InsForgeFake
if (str_starts_with((string) $value, 'eq.')) {
$column = $key;
$expected = substr((string) $value, 3);
$rows = array_values(array_filter($rows, fn (array $row): bool => (string) ($row[$column] ?? '') === $expected));
if ($expected === 'null') {
$rows = array_values(array_filter($rows, fn (array $row): bool => ($row[$column] ?? null) === null));
} else {
$rows = array_values(array_filter($rows, fn (array $row): bool => (string) ($row[$column] ?? '') === $expected));
}
}
}
@@ -150,4 +171,63 @@ class InsForgeFake
return Http::response([], 200);
}
/**
* Tiruan semantik server: idempotent via gerbang unik (reference_type, reference_id),
* decrement stok sekali, set status completed + history.
*/
private static function simulateCompleteOrder(array $args)
{
$orderId = $args['p_order_id'] ?? null;
if ($orderId === null || ! isset(self::$store['orders'][$orderId])) {
return Http::response(['error' => 'ORDER_NOT_FOUND'], 500);
}
$order = self::$store['orders'][$orderId];
if (($order['order_status'] ?? '') === 'completed') {
return Http::response(['ok' => true], 200);
}
if (($order['order_status'] ?? '') !== 'delivering') {
return Http::response(['error' => 'INVALID_TRANSITION'], 500);
}
foreach (self::$store['order_items'] ?? [] as $item) {
if (($item['order_id'] ?? null) !== $orderId) {
continue;
}
$alreadyOut = collect(self::$store['stock_movements'] ?? [])->contains(
fn (array $m): bool => $m['type'] === 'order_out'
&& ($m['reference_id'] ?? null) === $orderId
&& ($m['product_id'] ?? null) === $item['product_id'],
);
if ($alreadyOut) {
continue;
}
self::seed('stock_movements', [
'id' => fake()->uuid(),
'product_id' => $item['product_id'],
'type' => 'order_out',
'quantity' => -1 * (int) $item['quantity'],
'reference_type' => 'order',
'reference_id' => $orderId,
]);
foreach (self::$store['stocks'] as &$stock) {
if (($stock['product_id'] ?? null) === $item['product_id']) {
$stock['quantity'] -= (int) $item['quantity'];
}
}
unset($stock);
}
self::$store['orders'][$orderId]['order_status'] = 'completed';
return Http::response(['ok' => true], 200);
}
}