Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
40.00% covered (danger)
40.00%
12 / 30
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
StripeWebhookRepository
40.00% covered (danger)
40.00%
12 / 30
25.00% covered (danger)
25.00%
1 / 4
17.58
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 recordReceived
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
2.00
 markProcessed
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 markFailed
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3declare(strict_types=1);
4
5namespace App\Domain\Stripe\Repository;
6
7use PDO;
8use RuntimeException;
9
10/**
11 * Persists inbound Stripe webhook events for idempotency + audit.
12 *
13 * The PRIMARY KEY on event_id makes idempotency trivial: a duplicate
14 * delivery hits a UNIQUE violation, which {@see recordReceived()}
15 * translates into a `false` return so the caller knows to short-circuit.
16 */
17final readonly class StripeWebhookRepository
18{
19    public function __construct(
20        private PDO $pdo,
21    ) {}
22
23    /**
24     * Insert a newly received event. Returns true on first insertion,
25     * false when the event_id already exists (duplicate delivery).
26     *
27     * @param array<mixed> $payload
28     * @param string $eventId
29     * @param string $eventType
30     */
31    public function recordReceived(string $eventId, string $eventType, array $payload): bool
32    {
33        $stmt = $this->pdo->prepare(
34            'INSERT INTO stripe_webhook_events (event_id, event_type, payload)
35                VALUES (:event_id, :event_type, :payload::jsonb)
36                ON CONFLICT (event_id) DO NOTHING',
37        );
38        if ($stmt === false) {
39            throw new RuntimeException('Failed to prepare statement');
40        }
41        $stmt->execute([
42            'event_id' => $eventId,
43            'event_type' => $eventType,
44            'payload' => json_encode($payload, JSON_THROW_ON_ERROR),
45        ]);
46
47        return $stmt->rowCount() === 1;
48    }
49
50    public function markProcessed(string $eventId): void
51    {
52        $stmt = $this->pdo->prepare(
53            'UPDATE stripe_webhook_events
54                SET processed_at = CURRENT_TIMESTAMP, error = NULL
55                WHERE event_id = :event_id',
56        );
57        if ($stmt === false) {
58            throw new RuntimeException('Failed to prepare statement');
59        }
60        $stmt->execute(['event_id' => $eventId]);
61    }
62
63    public function markFailed(string $eventId, string $error): void
64    {
65        $stmt = $this->pdo->prepare(
66            'UPDATE stripe_webhook_events
67                SET processed_at = CURRENT_TIMESTAMP, error = :error
68                WHERE event_id = :event_id',
69        );
70        if ($stmt === false) {
71            throw new RuntimeException('Failed to prepare statement');
72        }
73        $stmt->execute([
74            'event_id' => $eventId,
75            'error' => $error,
76        ]);
77    }
78}