billing: Nebenläufigkeit & Idempotenz härten (H4, Stripe, Boost-Slots)
- CreditTopup-Gutschrift, MAN-Rechnung, Proof-PDF und Stripe-Webhook- Spiegelung sperren die betroffene Zeile innerhalb der Transaktion und prüfen die Idempotenz erst danach (verhindert Doppelgutschrift bei at-least-once-Webhooks, H4). - Gemeinsamer LocksModelForUpdate-Trait für die häufige Lock-by-Key-Form (CreditTopup, ManualInvoice). - Unique-Index auf invoices.stripe_invoice_id; Boost-Inventar über boost_slot_locks serialisiert, verwaiste Kategorie-Locks werden beim Löschen der Rubrik entfernt. Deployment: vor migrate prüfen, ob invoices.stripe_invoice_id bereits Duplikate enthält (die Unique-Migration bricht sonst bewusst ab). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b3fdd6f522
commit
7043ea8ad5
14 changed files with 418 additions and 106 deletions
|
|
@ -59,6 +59,27 @@ test('fulfilling a topup credits the wallet exactly once', function () {
|
|||
expect($user->creditTransactions()->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('two concurrent webhook deliveries credit the wallet only once', function () {
|
||||
/**
|
||||
* Regression zu Befund H4 (Audit 14.07.2026): Stripe stellt Webhooks
|
||||
* at-least-once zu. Beide Zustellungen laden die Topup-Zeile, BEVOR eine
|
||||
* committet (credited_at je noch null). Nur eine darf gutschreiben.
|
||||
*/
|
||||
$user = User::factory()->create();
|
||||
$topup = $this->topups->startTopup($user, 'p50');
|
||||
|
||||
// Zwei separate Instanzen derselben Zeile – wie zwei parallele Deliveries,
|
||||
// die die Zeile vor der ersten Gutschrift geladen haben.
|
||||
$deliveryA = $topup->fresh();
|
||||
$deliveryB = $topup->fresh();
|
||||
|
||||
$this->topups->fulfill($deliveryA, 'cs_test_1', 'pi_test_1');
|
||||
$this->topups->fulfill($deliveryB, 'cs_test_1', 'pi_test_1');
|
||||
|
||||
expect($this->wallet->balance($user))->toBe(55);
|
||||
expect($user->creditTransactions()->count())->toBe(1);
|
||||
});
|
||||
|
||||
test('the checkout webhook credits the wallet from session metadata', function () {
|
||||
$user = User::factory()->create();
|
||||
$topup = $this->topups->startTopup($user, 'p100');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue