Files
api/services/nginx/app/tests/Unit/Orders/StripePaymentIntentsPersistenceWiringTest.php
T
Jeppe BandJeppe Bundgaard 42ddce84bc Serialize VAT collection mutations with payment operations (#326)
## Summary

- Makes Stripe Terminal card payment intents always use 25% moms in the
API, independent of any client-supplied `tax_percentage`.
- Updates amount calculation, metadata persistence, stored-intent reuse
matching, the authoritative OpenAPI contracts, and operation-specific
Writerside outputs.
- Prevents double charging and false order closure across stale,
concurrently succeeded, partially recorded, or mismatched intents.
- Serializes payment create/capture/closure with order-item changes and
every order-to-invoice-collection reassignment through shared database
locks.
- Converts expected lock contention and reconciliation cases into
deliberate 409 responses.

## Exact-head evidence

Current head: `3a0f70d315a94d2efe586a2188d2c54f8ff11cd4`

- PHP syntax passed for all changed runtime files.
- Focused Orders suite: **42 tests / 293 assertions passed**.
- `git diff --check` passed.
- Fresh exact-head Tests and Qodana are running.
- Every Codex finding has a concrete reply; a fresh exact-head review is
requested below.

## Safety behavior

- Caller-controlled VAT is absent from request contracts; fixed 25% moms
is server-owned.
- A succeeded payment is preserved, requires the full expected
`amount_received`, and cannot close a changed/mismatched or
already-claimed collection.
- A compatible partially recorded Stripe closure is completed
idempotently; conflicting partial state fails closed for manual
reconciliation.
- Every cancellation/delete caller honors a concurrent-success result
and never falsely reports a completed payment as cleared.
- Price changes and invoice-collection reassignment share the payment
lock through validation, capture, post-capture reload, and closure.
- Reader changes are persisted only for reusable matching intents, so
stale intent cancellation targets the original terminal.
- Accepted legacy succeeded intents normalize stored tax to 25% before
response construction.

---------

Co-authored-by: Jeppe Bundgaard <jb@truckwash.dk>
2026-07-28 22:00:59 +02:00

26 lines
1.7 KiB
PHP

<?php
it('wires stripe payment intent persistence to prune duplicates and clear reader state safely', function (): void {
$objectFile = dirname(__DIR__, 3) . '/objects/stripe_payment_intents_o.php';
$content = file_get_contents($objectFile);
expect($content)->not->toBeFalse();
expect($content)->toContain("new object_property(\$this->table, \$this->id, 'reader_id', 'string', false);");
expect($content)->toContain('clearOrderPaymentIntents($order_id);');
expect($content)->toContain('public function getOrderPaymentIntentRows(int $order_id): array');
expect($content)->toContain('usort($rows');
expect($content)->toContain('public function clearOrderPaymentIntents(int $order_id, ?int $keepId = null): void');
expect($content)->toContain("self::delete_object(\$this->getTable(), \$rowId);");
expect($content)->toContain('public function updateStoredPaymentIntent(mixed $paymentIntent): void');
expect($content)->toContain('public function setReaderId(?string $readerId): void');
expect($content)->toContain('sendCancelPaymentIntent($readerId);');
expect($content)->toContain('payment_intents->cancel($paymentIntentId);');
expect($content)->toContain('public function delete(): bool');
expect($content)->toContain('if (!$this->cancelPaymentIntent())');
expect($content)->toContain('public function cancelPaymentIntent(): bool');
expect($content)->toContain("if (\$status === 'succeeded')");
expect($content)->toContain('return false;');
expect(substr_count($content, 'payment_intents->get($paymentIntentId)'))->toBeGreaterThanOrEqual(2);
expect($content)->toContain('deleteDuplicateOrderPaymentIntents(int $order_id, int $keepId): void');
});