Fix: Adding cart id and order id to stripe payload

This commit is contained in:
2026-09-16 01:39:23 +03:00
parent 68233f43ef
commit 3ad3a1b4d6
2 changed files with 61 additions and 16 deletions
+42 -16
View File
@@ -145,23 +145,20 @@ produced had it resolved synchronously.
with no memory of the request that started the payment. Something has to persist enough to with no memory of the request that started the payment. Something has to persist enough to
answer "which order/cart does gateway reference X belong to?" between the two calls. answer "which order/cart does gateway reference X belong to?" between the two calls.
**Read directly from `lunarphp/stripe`'s own source** (`StripePaymentType::authorize()`, The precedent for this originally came from reading `lunarphp/stripe`'s own source
`ProcessStripeWebhook`, `WebhookController`) to see how Lunar itself solves this — confirmed (`StripePaymentType::authorize()`, `ProcessStripeWebhook`, `WebhookController`) — that package
it does **not** stash a generic opaque blob. It writes the correlating ids as real, typed solved this the same way, writing the correlating ids as real, typed columns on its own
columns on `Lunar\Stripe\Models\StripePaymentIntent` (`cart_id`, `order_id`) at the moment the `StripePaymentIntent` model rather than a generic opaque blob. **`lunarphp/stripe` has since
intent is created/first seen, then reads them back the same way when the webhook arrives: been removed from this project** in favour of depending on `stripe/stripe-php` directly (see
CHANGELOG.md) — `Modules\Core\Payment\Models\StripePaymentIntent` is now a first-party model
over the same table shape, kept for exactly the same reason.
```php **`StripePaymentDriver` follows this pattern**: it reads `cart_id`/`order_id` out of `$context`
// ProcessStripeWebhook::handle() — falls back through two real lookups, at `pay()`/`authorize()` time and writes them onto its own `StripePaymentIntent` row (`src/
// neither of them a generic context blob: Payment/Models/StripePaymentIntent.php`, table `stripe_payment_intents`), then reads them back
$cart = StripePaymentIntent::where('intent_id', $this->paymentIntentId)->first()?->cart the same way in `handleCallback()`. No generic `context` json column beyond what that table
?: Cart::where('meta->payment_intent', '=', $this->paymentIntentId)->first(); already carries (`context`, added for a different purpose — see that migration's own
``` docblock), no new table.
**`StripePaymentDriver` follows this exact precedent**: it reads `cart_id`/`order_id` out of
`$context` at `pay()`/`authorize()` time and writes them onto its own `StripePaymentIntent`
row (a table already owned by `lunarphp/stripe`, already shaped for exactly this), then reads
them back the same way in `handleCallback()`. No generic `context` json column, no new table.
### This pattern is per-driver, not a shared table ### This pattern is per-driver, not a shared table
@@ -176,6 +173,35 @@ a shared generic one.
--- ---
## Reconciliation — a charge that succeeds on Stripe but is never written locally
This app never creates or reuses a Stripe **Customer** object — every PaymentIntent is a
one-off (`StripePaymentDriver::createAndConfirm()`'s own `$params` never includes a `customer`
key), and nothing calls Stripe's Customer API anywhere in this codebase. That's a deliberate
choice, not an oversight: a Customer object only earns its keep if something actually needs it
(saved/reusable payment methods, subscriptions, Stripe-side lifetime-value grouping across
orders) — none of which exist in this checkout flow today. Creating one anyway would just be
more PII sitting on a third party's servers for no functional benefit, and it would become
another cross-reference a future Payment privacy provider has to account for (detaching/
deleting the Customer on erasure, not just the local PaymentIntent row). If a real feature
needs it later (e.g. "save my card"), add it then, scoped to that feature.
The gap this creates: with no Customer object and no other identifying field previously sent
to Stripe, a PaymentIntent that succeeds on Stripe's side but is never written to our own DB
(e.g. a database outage at exactly the wrong moment, between Stripe confirming the charge and
`rememberIntent()`'s insert) would be **untraceable** back to a cart or order — nothing to
search Stripe's dashboard by except amount, timestamp, and card last-4.
**Fix**: `createAndConfirm()` now sets `metadata: ['cart_id' => ..., 'order_id' => ...]`
(`array_filter()`-ed, since `order_id` isn't known yet at initial `pay()`/`authorize()` time —
same null-coalesce `rememberIntent()` already does) on every PaymentIntent. This is metadata
only, visible on Stripe's own dashboard/API for manual reconciliation — it does not create a
Customer object and does not change anything about how `handleCallback()`/webhook correlation
works (that still goes through `stripe_payment_intents`, per "Async resolution" above). It's
purely a recovery aid for the case where our own write never happened at all.
---
## Explicitly out of scope for this pass ## Explicitly out of scope for this pass
- **`Checkout`/`Order` wiring** — how `Checkout` calls into `Payment`, how `Order`/`Checkout` - **`Checkout`/`Order` wiring** — how `Checkout` calls into `Payment`, how `Order`/`Checkout`
@@ -115,6 +115,25 @@ class StripePaymentDriver implements
$params['payment_method'] = $data['payment_method']; $params['payment_method'] = $data['payment_method'];
} }
// Reconciliation safety net: this app never creates a Stripe Customer
// object and attaches no other identifying info to the PaymentIntent
// (see docs/payments.md "Reconciliation" for the full reasoning), so
// without this, a charge that succeeds on Stripe's side but is never
// written to our own DB (e.g. a DB outage at exactly the wrong
// moment) would be untraceable back to a cart/order — nothing to
// search Stripe's dashboard by except amount/time/card last-4.
// array_filter() drops order_id when it's not yet known (still null
// in $context at initial pay()/authorize() time — see
// rememberIntent()'s own null-coalesce for the same case).
$metadata = array_filter([
'cart_id' => $context['cart_id'] ?? null,
'order_id' => $context['order_id'] ?? null,
]);
if ($metadata !== []) {
$params['metadata'] = $metadata;
}
try { try {
$paymentIntent = $this->stripe->getClient()->paymentIntents->create($params); $paymentIntent = $this->stripe->getClient()->paymentIntents->create($params);
} catch (ApiErrorException $e) { } catch (ApiErrorException $e) {