diff --git a/app/Billing/Webhooks/BillingWebhookEventRecorder.php b/app/Billing/Webhooks/BillingWebhookEventRecorder.php new file mode 100644 index 0000000..0c47c83 --- /dev/null +++ b/app/Billing/Webhooks/BillingWebhookEventRecorder.php @@ -0,0 +1,34 @@ + $eventId], + [ + 'provider' => $provider, + 'type' => $type, + 'payload' => $payload, + ], + ); + } + + public function markProcessed(BillingWebhookEvent $event): void + { + $event->update(['processed_at' => Carbon::now()]); + } + + public function wasProcessed(string $provider, string $eventId): bool + { + return BillingWebhookEvent::query() + ->where('provider_event_id', $eventId) + ->whereNotNull('processed_at') + ->exists(); + } +} diff --git a/tests/Feature/Billing/BillingWebhookEventRecorderTest.php b/tests/Feature/Billing/BillingWebhookEventRecorderTest.php new file mode 100644 index 0000000..cc9e446 --- /dev/null +++ b/tests/Feature/Billing/BillingWebhookEventRecorderTest.php @@ -0,0 +1,34 @@ +recordIfNew('stripe', 'evt_123', 'invoice.paid', ['id' => 'evt_123']); + $second = $recorder->recordIfNew('stripe', 'evt_123', 'invoice.paid', ['id' => 'evt_123']); + + expect($first->wasRecentlyCreated)->toBeTrue(); + expect($second->wasRecentlyCreated)->toBeFalse(); + expect(BillingWebhookEvent::query()->where('provider_event_id', 'evt_123')->count())->toBe(1); +}); + +it('marks webhook event as processed', function () { + $recorder = app(BillingWebhookEventRecorder::class); + + $event = $recorder->recordIfNew('stripe', 'evt_456', 'customer.subscription.updated', []); + $recorder->markProcessed($event); + + expect($event->fresh()->processed_at)->not->toBeNull(); +}); + +it('detects already processed webhook event', function () { + $recorder = app(BillingWebhookEventRecorder::class); + + $event = $recorder->recordIfNew('stripe', 'evt_789', 'invoice.paid', []); + expect($recorder->wasProcessed('stripe', 'evt_789'))->toBeFalse(); + + $recorder->markProcessed($event); + expect($recorder->wasProcessed('stripe', 'evt_789'))->toBeTrue(); +});