@ambushsoftworks/nestjs-payments-graphql 0.8.0 → 0.10.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/README.md +131 -2
  3. package/dist/constants.d.ts +2 -0
  4. package/dist/constants.js +3 -1
  5. package/dist/constants.js.map +1 -1
  6. package/dist/exceptions/index.d.ts +3 -0
  7. package/dist/exceptions/index.js +7 -1
  8. package/dist/exceptions/index.js.map +1 -1
  9. package/dist/gateways/payment-gateway.interface.d.ts +4 -0
  10. package/dist/gateways/recurring-payment-gateway.interface.d.ts +14 -0
  11. package/dist/gateways/stripe/stripe-webhook.controller.d.ts +5 -3
  12. package/dist/gateways/stripe/stripe-webhook.controller.js +40 -21
  13. package/dist/gateways/stripe/stripe-webhook.controller.js.map +1 -1
  14. package/dist/gateways/stripe/stripe.gateway.d.ts +20 -0
  15. package/dist/gateways/stripe/stripe.gateway.js +127 -9
  16. package/dist/gateways/stripe/stripe.gateway.js.map +1 -1
  17. package/dist/gateways/stripe/types.d.ts +1 -0
  18. package/dist/gateways/stripe/types.js +2 -1
  19. package/dist/gateways/stripe/types.js.map +1 -1
  20. package/dist/index.d.ts +4 -3
  21. package/dist/index.js +7 -3
  22. package/dist/index.js.map +1 -1
  23. package/dist/interfaces/payment-event-listener.interface.d.ts +2 -0
  24. package/dist/interfaces/payment-method-policy.interface.d.ts +11 -0
  25. package/dist/interfaces/payment-method-policy.interface.js +3 -0
  26. package/dist/interfaces/payment-method-policy.interface.js.map +1 -0
  27. package/dist/payments.module.d.ts +3 -0
  28. package/dist/payments.module.js +10 -0
  29. package/dist/payments.module.js.map +1 -1
  30. package/dist/services/customer.service.d.ts +3 -1
  31. package/dist/services/customer.service.js +2 -1
  32. package/dist/services/customer.service.js.map +1 -1
  33. package/dist/services/payment-method.service.d.ts +19 -2
  34. package/dist/services/payment-method.service.js +151 -12
  35. package/dist/services/payment-method.service.js.map +1 -1
  36. package/dist/services/payment.service.js +19 -3
  37. package/dist/services/payment.service.js.map +1 -1
  38. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -28,6 +28,160 @@ prose for buried obligations. The marker was introduced in v0.4.0 and
28
28
 
29
29
  ## [Unreleased]
30
30
 
31
+ ## [0.10.0] - 2026-09-29
32
+
33
+ > **Upgrade note.** `PaymentMethodService` takes `ModuleRef` as a third
34
+ > constructor argument. It is optional, so a consumer constructing the service
35
+ > by hand — in tests, mostly — still compiles and behaves exactly as before;
36
+ > without it no policy is resolved. Pass it if you want the policy honoured in a
37
+ > hand-built container. Nothing else changes for an existing consumer, and
38
+ > **absent a registered policy every path behaves exactly as it did in 0.9.0.**
39
+
40
+ 0.9.0 let a session say whether the card it kept should become the customer's
41
+ default. This closes the class that switch guards case by case: **the package
42
+ now asks before every default it writes, on every path, including ones not
43
+ written yet.**
44
+
45
+ ### Added
46
+ - **`IPaymentMethodPolicy`**, with the `PAYMENT_METHOD_POLICY` token and the
47
+ `paymentMethodPolicyInstance` module option. `mayBecomeDefault` is asked
48
+ before the provider is called, with the `trigger` that caused the write —
49
+ `explicit`, `setup` or `payment` — so a consumer can answer differently by
50
+ origin.
51
+
52
+ It exists because the knowledge and the write live in different places. A
53
+ consumer holding cards belonging to third parties knows which card is whose
54
+ and had already written guards on its own mutations; the package writes the
55
+ default inside webhook handling, where those guards never run. Rather than
56
+ teach the package what a third party is, it asks.
57
+
58
+ - **`PaymentMethodDefaultRefusedException`** (`PAYMENT_METHOD_DEFAULT_REFUSED`),
59
+ thrown when a policy refuses an `explicit` request. Someone clicked a button
60
+ and is waiting; a silent no-op reads as success while the card does not stick.
61
+ The automatic paths log and leave the default alone instead — they run inside
62
+ a webhook, where throwing answers 500 and the provider redelivers for days.
63
+
64
+ - **`paymentMethodPolicyTimeoutMs`** (default `5000`) and the
65
+ `PAYMENT_METHOD_POLICY_TIMEOUT` token.
66
+
67
+ - **`resolveSetupPaymentMethod` on `RecurringPaymentGateway`** (optional):
68
+ reports the card a completed setup session collected, and whether the session
69
+ asked for it to be adopted, **without writing anything**. A setup session only
70
+ reveals its card on completion, so `adoptSetupPaymentMethod` resolved and
71
+ adopted in one call and left no point at which a policy could be consulted.
72
+ That method now builds on this one, so there is a single reading of the
73
+ session's answer.
74
+
75
+ ### Changed
76
+ - **A policy that throws, rejects or times out is treated as a refusal**, logged
77
+ at error level. This is the opposite of every other consumer hook in the
78
+ package — a branding resolver or a charge-options resolver that fails lets the
79
+ payment proceed, because the cost of proceeding is cosmetic. The cost of
80
+ proceeding here is charging the wrong person. **Fail open for cosmetic, fail
81
+ closed for authority.**
82
+
83
+ The timeout is the only one in the package, and the inconsistency is
84
+ deliberate: everywhere else the safe answer to an unanswerable question is
85
+ "proceed", so a timeout could only cause harm.
86
+
87
+ - **A gateway that cannot be gated refuses rather than proceeds.** With a policy
88
+ registered, a setup-session adoption on a gateway without
89
+ `resolveSetupPaymentMethod` is refused and logged with what to implement.
90
+ Without a policy, the legacy path is used unchanged.
91
+
92
+ ### Docs
93
+ - New **Deciding which cards may become the default**, answering the question
94
+ **What "ownership" means** (v0.9.0) ends on: ownership means only that a card
95
+ is attached to that provider customer, and this is how you tell the package
96
+ which of those cards are not the payer's own.
97
+ - `payment-method-policy-gate.spec.ts` fails the build if a new default-writing
98
+ call site skips the gate — the guarantee that makes a hook better than a
99
+ per-path switch, and the reason it is worth having at all.
100
+
101
+ ## [0.9.0] - 2026-09-29
102
+
103
+ > **Not published separately — released as part of `0.10.0`.** Verifying this
104
+ > release on its own would have verified code `0.10.0` replaces: the setup-card
105
+ > path documented below was rewired a day later so a policy could be consulted
106
+ > before anything is written. The two were verified together instead. Pin
107
+ > `0.10.0`; everything here is in it.
108
+
109
+ A card kept while paying is now usable, and **the caller decides whether it
110
+ becomes the card everything else gets charged**. That second half is the
111
+ release: the first draft of this work adopted automatically, which is wrong for
112
+ any customer who pays on behalf of others, and the reporting consumer caught it
113
+ on production before we shipped.
114
+
115
+ ### Added
116
+ - **`savePaymentMethod` on `createPaymentSession`** — `{ providerCustomerId,
117
+ adoptAsDefault? }`. One object, so asking to keep a card without naming a
118
+ customer to attach it to cannot be expressed; that combination left a
119
+ consumer's card on a provider-invented customer nothing in their database
120
+ pointed at. For Stripe it becomes `customer` plus
121
+ `payment_intent_data.setup_future_usage: 'off_session'`.
122
+
123
+ `customer` and `setup_future_usage` remain **unreserved**, so passthrough code
124
+ written before this is unaffected. Only a contradiction is refused — a
125
+ different customer, or `on_session` alongside `savePaymentMethod` — on the
126
+ same precedent as `paymentIntentData.metadata.invoiceId`.
127
+
128
+ - **`adoptAsDefault` on `createSetupSession`** and
129
+ `CustomerService.createSetupSession(..., { adoptAsDefault })`, **defaulting to
130
+ `true`** — what that path has always done. Pass `false` when collecting a card
131
+ for somebody the customer pays on behalf of.
132
+
133
+ - **`adoptPaymentMethod` on `RecurringPaymentGateway`** (optional), for a card
134
+ whose id is already resolved. `adoptSetupPaymentMethod` now delegates to the
135
+ same private tail, so "default only when the customer has none" exists once.
136
+
137
+ - **`customerId` in `providerData`** on payment-mode Checkout completions, and
138
+ **`providerCustomerId` / `paymentMethodId` on `PaidInvoiceEvent`** — both
139
+ nullable and "when present". A listener receives the event, not the normalised
140
+ webhook, so these ids were previously unreachable without re-reading the
141
+ payment and calling the provider.
142
+
143
+ - **`STRIPE_ADOPT_AS_DEFAULT_METADATA_KEY`**, the metadata key carrying the
144
+ adoption decision from session creation to the webhook that acts on it.
145
+
146
+ ### Changed
147
+ - **Adoption is opt-in for payments, and silence means no.** A card kept during
148
+ a payment becomes the default only when the session asked for it.
149
+
150
+ A customer who pays on behalf of others is one provider customer holding
151
+ several people's cards, and the default is what everyone *without* their own
152
+ card is charged. Adopting automatically makes one third party's card the payer
153
+ for all the rest, invisibly to the cardholder. A recruiting agency with 53
154
+ client companies hit exactly this: one client's card became the account
155
+ default and was on course to pay for the other 52. Nothing was charged.
156
+
157
+ - **Only an `off_session` mandate is adopted.** A card kept `on_session` is
158
+ attached and logged and never becomes the off-session default: that consent is
159
+ for charges the customer is present for, and spending it on a renewal produces
160
+ an authentication-required decline nobody is there to answer.
161
+
162
+ - **Adoption runs only for a settled payment**, after the payment is recorded,
163
+ and can never fail the webhook. A throw would answer 500 and the provider
164
+ would redeliver for about three days, re-running the payment handler each
165
+ time. Failures are reported through `webhook.errorReporter` instead of being
166
+ swallowed silently, as they were before.
167
+
168
+ ### Fixed
169
+ - **The webhook never wrote `PaymentCustomer.defaultPaymentMethodId`.** Only
170
+ `PaymentMethodService` mirrored it, and the webhook called the gateway
171
+ directly — so a card saved through the *documented* setup flow set the
172
+ provider's default and left the mirrored column null, making this README's
173
+ promise that you can render the current card without a provider round trip
174
+ untrue for the main path. Both adoption paths now go through
175
+ `PaymentMethodService` and mirror what they set. Found while verifying a
176
+ consumer's report, not reported.
177
+
178
+ ### Docs
179
+ - New **Saving a card while paying**, **Choosing whether a saved card becomes
180
+ the default**, and **What "ownership" means** sections. The last one says
181
+ plainly that ownership means "attached to this provider customer" and nothing
182
+ more, which is weaker than the word suggests and matters to anyone building a
183
+ card picker on `listPaymentMethods`.
184
+
31
185
  ## [0.8.0] - 2026-09-28
32
186
 
33
187
  Off-session charges can finally say **which brand is charging**. A consumer
package/README.md CHANGED
@@ -24,6 +24,10 @@ Production-grade payments module for NestJS with GraphQL support. Invoicing (inc
24
24
  - [Pinning a schedule to one card](#pinning-a-schedule-to-one-card)
25
25
  - [Per-charge gateway options for renewals](#per-charge-gateway-options-for-renewals)
26
26
  - [Saved Cards](#saved-cards)
27
+ - [What "ownership" means](#what-ownership-means)
28
+ - [Deciding which cards may become the default](#deciding-which-cards-may-become-the-default)
29
+ - [Saving a card while paying](#saving-a-card-while-paying)
30
+ - [Choosing whether a saved card becomes the default](#choosing-whether-a-saved-card-becomes-the-default)
27
31
  - [Managing saved cards](#managing-saved-cards)
28
32
  - [Stripe-specific off-session options](#stripe-specific-off-session-options-gatewayoptions)
29
33
  - [E-Transfer](#e-transfer)
@@ -1062,7 +1066,125 @@ const { setupUrl, providerSetupId } = await customerService.createSetupSession(
1062
1066
 
1063
1067
  **Completing the setup is a webhook, not a redirect.** Your success URL fires in the customer's browser; the card is not usable until `checkout.session.completed` arrives and the package records it. Do not tell the client their card is saved based on the redirect alone.
1064
1068
 
1065
- **Which card gets charged.** The provider is the source of truth — for Stripe, `invoice_settings.default_payment_method`. `PaymentCustomer.defaultPaymentMethodId` mirrors it so you can render the current card without a provider round trip, but the package never consults that column to decide what to charge. One authority is what stops the two disagreeing. On the completion webhook the collected card becomes the default **only when the customer has none**, so a client adding a second card does not silently change what gets billed.
1069
+ **Which card gets charged.** The provider is the source of truth — for Stripe, `invoice_settings.default_payment_method`. `PaymentCustomer.defaultPaymentMethodId` mirrors it so you can render the current card without a provider round trip, but the package never consults that column to decide what to charge. One authority is what stops the two disagreeing. A collected card becomes the default **only when the customer has none**, so a client adding a second card does not silently change what gets billed.
1070
+
1071
+ > **The mirror was not written on the webhook path before v0.9.0.** Only `PaymentMethodService` ever updated `defaultPaymentMethodId`, and the webhook called the gateway directly — so a card saved through the ordinary setup flow set the provider's default and left the column null. If you have been rendering from that column, it has been blank for cards saved that way; it is maintained from v0.9.0 on, and a `confirmSetup` call or any card-management write refreshes it for existing rows.
1072
+
1073
+ #### What "ownership" means
1074
+
1075
+ `ownsPaymentMethod`, `assertOwned` and every method taking a `paymentMethodId` mean one thing by ownership: **the card is attached to that client's provider customer.** Nothing more.
1076
+
1077
+ That is worth stating plainly, because a provider customer can end up holding cards that are not the payer's own. Anything paid with `savePaymentMethod` attaches, and so does any card saved through a setup session — so a customer who pays on behalf of others accumulates their cards in one wallet, and the package cannot tell them apart.
1078
+
1079
+ The consequences are yours to manage:
1080
+
1081
+ - `listPaymentMethods` returns **every** attached card, so a picker built straight onto it will offer a third party's card for somebody else's purchase.
1082
+ - `setDefaultPaymentMethod` accepts any attached card, because by this definition the client owns it.
1083
+
1084
+ What the package guarantees is narrower and worth having: it never *chooses* a card. Adoption is opt-in per session (below), an existing default is never overridden, and `createOffSessionPayment` refuses rather than falling back to whatever is on file. The package still does not know *which* cards are not the payer's own — but as of v0.10.0 you can tell it, per decision, with a policy: see [Deciding which cards may become the default](#deciding-which-cards-may-become-the-default).
1085
+
1086
+
1087
+
1088
+ #### Deciding which cards may become the default
1089
+
1090
+ *Added in v0.10.0.*
1091
+
1092
+ The section above ends by saying the package cannot tell a third party's card from the payer's own. This is how you tell it.
1093
+
1094
+ ```typescript
1095
+ import type { IPaymentMethodPolicy } from '@ambushsoftworks/nestjs-payments-graphql';
1096
+
1097
+ class WalletPolicy implements IPaymentMethodPolicy {
1098
+ constructor(private readonly cards: CardOwnershipService) {}
1099
+
1100
+ async mayBecomeDefault({ clientDetailsId, paymentMethodId, trigger }) {
1101
+ // An account that pays for other people never gets an automatic default.
1102
+ if (trigger !== 'explicit') {
1103
+ return !(await this.cards.paysOnBehalfOfOthers(clientDetailsId));
1104
+ }
1105
+ // A deliberate click is allowed, unless the card is somebody else's.
1106
+ return !(await this.cards.belongsToThirdParty(paymentMethodId));
1107
+ }
1108
+ }
1109
+
1110
+ // In forRootAsync's useFactory:
1111
+ paymentMethodPolicyInstance: new WalletPolicy(cards),
1112
+ ```
1113
+
1114
+ **It is asked at the only two places the package writes a default**, before the provider is called — so a refusal costs no network request. `trigger` says what caused the write:
1115
+
1116
+ | Trigger | Cause | A refusal |
1117
+ |---|---|---|
1118
+ | `explicit` | `setDefaultPaymentMethod` — a person chose this card | **throws** `PaymentMethodDefaultRefusedException` |
1119
+ | `setup` | a setup session completed, via the webhook or `confirmSetup` | logs, leaves the default alone |
1120
+ | `payment` | a buyer kept a card while paying and the session asked to adopt it | logs, leaves the default alone |
1121
+
1122
+ The explicit path throws because somebody is waiting for an answer, and a silent no-op reads as success while the card quietly does not stick. The automatic paths do not, because they run inside a webhook: throwing there answers 500 and Stripe redelivers for about three days.
1123
+
1124
+ In every case the card stays attached and chargeable by id. Only the default is untouched.
1125
+
1126
+ **A policy that fails is a refusal.** If `mayBecomeDefault` throws, rejects, or takes longer than `paymentMethodPolicyTimeoutMs` (default 5000), the package logs at error level and treats the answer as `false`.
1127
+
1128
+ > This is the opposite of every other consumer hook here, and deliberately so. A branding resolver that fails lets the payment go through, because the cost of proceeding is a missing brand name on a statement. The cost of proceeding *here* is charging the wrong person's card; the cost of refusing is a customer with no default and a loud `no_payment_method` on their next renewal. **Fail open for cosmetic, fail closed for authority.** It is also the only timeout in the package — everywhere else a slow callback is left to finish, because there the safe answer is "proceed" and a timeout could only cause harm.
1129
+
1130
+ **Why a hook and not a flag on each path.** [Saving a card while paying](#saving-a-card-while-paying) already lets a session say whether its card should be adopted, and that stays — it is the first line, and it is the right place to answer when you know at checkout time. The policy is the backstop: it guards the *two functions* rather than the paths anyone thought to list, so a path added in a later version inherits it. A spec fails the build if a new default-writing call site skips the gate.
1131
+
1132
+ Use both. A session flag cannot help when the decision depends on something you only learn from the webhook that triggers the adoption.
1133
+
1134
+ **Your gateway needs `resolveSetupPaymentMethod`** if you wrote your own and you register a policy. A setup session only reveals its card when it completes, so the package has to be able to ask what was collected before anything is written. Without it a setup-session adoption is **refused** rather than performed ungated, and the refusal is logged with what to implement. The bundled Stripe gateway has it.
1135
+
1136
+ #### Saving a card while paying
1137
+
1138
+ *Added in v0.9.0.*
1139
+
1140
+ A buyer paying through hosted Checkout can keep the card for later. Pass `savePaymentMethod` and the gateway attaches it to the customer you name:
1141
+
1142
+ ```typescript
1143
+ const customer = await customerService.createOrLinkCustomer(
1144
+ divisionId, clientDetailsId, email, name,
1145
+ );
1146
+
1147
+ await gateway.createPaymentSession({
1148
+ invoiceId, amount, currency: 'CAD', successUrl, cancelUrl,
1149
+ savePaymentMethod: {
1150
+ providerCustomerId: customer.providerCustomerId,
1151
+ adoptAsDefault: true, // see below — defaults to false
1152
+ },
1153
+ });
1154
+ ```
1155
+
1156
+ **One object, not a flag and a customer id.** A card needs a customer to attach to; without one the provider either refuses or invents a customer nothing in your database points at, and the card is lost with no error anywhere. A consumer hit exactly that and spent a day on it. This shape makes it unrepresentable.
1157
+
1158
+ **`adoptAsDefault` decides whether it becomes the card auto-renew uses, and it defaults to `false`.**
1159
+
1160
+ | `adoptAsDefault` | customer has no default | customer has a default |
1161
+ |---|---|---|
1162
+ | `true` | the saved card becomes the default | unchanged — never overridden |
1163
+ | `false` or absent | unchanged: still no default | unchanged |
1164
+
1165
+ The card is attached in every row, and chargeable by id via `createOffSessionPayment`'s `paymentMethodId`. Only the default differs.
1166
+
1167
+ > **Why silence means no.** Keeping a card and choosing what everything *else* gets charged are different decisions, and only you know whether the payer owns the card they just typed. A customer who pays on behalf of others — an agency, a parent company, a bookkeeper — is one provider customer holding several people's cards, and the default is what everyone *without* their own card is charged. A consumer hit this on production: a recruiting agency with 53 client companies paid for one client's posting, that client's card became the account default, and it was on course to pay for the other 52. Nothing was charged; they caught it within the hour. Pass `true` when the payer is buying for themselves, and leave it out when they are not.
1168
+
1169
+ The card id reaches your listener on `PaidInvoiceEvent` as `paymentMethodId`, with `providerCustomerId` beside it, so you can file it against whoever it belongs to without a provider round trip.
1170
+
1171
+ **Only `off_session` counts.** The package adopts a card only when the mandate says the customer will not be present for future charges. A card kept `on_session` is attached and logged, and never becomes the off-session default — that consent is for charges the customer is there for, and using it for a renewal is how you get an authentication-required decline at 02:00.
1172
+
1173
+ **Equivalent through `gatewayOptions`.** `customer` and `setup_future_usage` are not reserved, so code written before v0.9.0 keeps working. What is refused is a *contradiction* — naming a different customer, or asking for `on_session` alongside `savePaymentMethod` — because resolving it either way would be a guess. The package writes its decision into the PaymentIntent's metadata under `adoptAsDefault` (exported as `STRIPE_ADOPT_AS_DEFAULT_METADATA_KEY`); keep that key out of your own metadata.
1174
+
1175
+ #### Choosing whether a saved card becomes the default
1176
+
1177
+ The same choice applies to setup sessions, where it **defaults to `true`** — that is what `createSetupSession` has always done, so nothing changes for existing consumers:
1178
+
1179
+ ```typescript
1180
+ await customerService.createSetupSession(
1181
+ divisionId, clientDetailsId, email, name, successUrl, cancelUrl,
1182
+ 'stripe', undefined,
1183
+ { adoptAsDefault: false }, // a card you are collecting for a third party
1184
+ );
1185
+ ```
1186
+
1187
+ **The answer travels with the session.** Adoption happens in two places that cannot see each other — the `checkout.session.completed` webhook, and `confirmSetup` when you confirm on the redirect — and either may land first. The package writes the decision onto the SetupIntent at session creation, so both read the same answer and `confirmSetup` needs no extra argument. Clearing a default yourself afterwards does not work: the webhook can land after your clear and set it again.
1066
1188
 
1067
1189
  #### Managing saved cards
1068
1190
 
@@ -1137,7 +1259,7 @@ Declare `@@unique([clientDetailsId, provider])` on `PaymentCustomer` to match, a
1137
1259
 
1138
1260
  **Keep the `divisionId` column** even though you no longer key on it. The bundled adapter still writes it on create and `assertPaymentsSchema` still requires it, so dropping it because "we do not scope by division" fails at boot — or on the first card save if you skip the schema assertion. It stays useful as a record of which division first created the customer.
1139
1261
 
1140
- A custom gateway must implement `adoptSetupPaymentMethod` to support saved cards. It is optional on `RecurringPaymentGateway` so v0.2.x gateways still compile; without it the webhook logs a warning and the card stays unusable for auto-charge.
1262
+ A custom gateway must implement `adoptSetupPaymentMethod` to support saved cards, `adoptPaymentMethod` (v0.9.0) to support a card kept while paying, and `resolveSetupPaymentMethod` (v0.10.0) so a setup session's card can be reported before it is adopted — the second takes a payment method id the gateway has already resolved, rather than a setup intent. Both are optional on `RecurringPaymentGateway` so older gateways still compile; without them the webhook logs a warning and the card stays unusable for auto-charge. Implement them over one shared code path: "default only when the customer has none, never override" stops being true the moment there are two copies of it.
1141
1263
 
1142
1264
  #### Stripe-specific off-session options (`gatewayOptions`)
1143
1265
 
@@ -1250,6 +1372,8 @@ paymentEventListenerInstance: {
1250
1372
 
1251
1373
  Event payloads include `metadata` from the invoice plus the full `lineItems` snapshot so consumers don't need to re-query.
1252
1374
 
1375
+ `PaidInvoiceEvent` also carries `providerCustomerId` and `paymentMethodId` (v0.9.0), so a listener can act on a card the payment kept — filing it against the party it belongs to, for instance — without re-reading the payment and calling the provider. Both are **nullable and "when present", not a guarantee**: five paths fire `onInvoicePaid`, and a manually recorded payment or an e-transfer confirmation has no card at all. The recurring auto-charge path knows the card but does not yet pass it here.
1376
+
1253
1377
  ### Transactions
1254
1378
 
1255
1379
  `ITransactionManager.runInTransaction(fn)` gives the callback a `TransactionRepositories` bag with all repositories bound to the same ORM transaction. The package uses this internally when recording payments and mutating totals.
@@ -1523,6 +1647,8 @@ Two database constraints are load-bearing wherever you are coming from, because
1523
1647
  |--------|------|-------------|
1524
1648
  | `paymentEventListenerInstance` | `IPaymentEventListener` | Domain callbacks — see [Event Listeners](#event-listeners). Only `onInvoicePaid` is required. |
1525
1649
  | `recurringChargeOptionsResolverInstance` | `IRecurringChargeOptionsResolver` | Supplies `gatewayOptions` for each renewal charge — see [Per-charge gateway options for renewals](#per-charge-gateway-options-for-renewals). Absent, renewals charge as before; a resolver that throws degrades to charging without options rather than failing the renewal. |
1650
+ | `paymentMethodPolicyInstance` | `IPaymentMethodPolicy` | Decides whether a card may become a customer's default — see [Deciding which cards may become the default](#deciding-which-cards-may-become-the-default). Absent, every path behaves as before. A policy that throws, rejects or times out is treated as a **refusal**. |
1651
+ | `paymentMethodPolicyTimeoutMs` | `number` | How long `mayBecomeDefault` may take before the answer is treated as a refusal. Default `5000`. The only timeout in the package. |
1526
1652
  | `webhook` | `{ errorReporter? }` | `webhook.errorReporter` is called with anything a webhook handler throws. Route it somewhere you will see it: since v0.5.0 a failed handler answers 500 and Stripe retries for ~3 days. |
1527
1653
  | `tax` | `{ requireExplicit? }` | See [Tax](#tax). Set `tax.requireExplicit` if you bill in more than one jurisdiction. |
1528
1654
  | `features` | see below | Feature flags. Enabling one without its backing repository throws at boot. |
@@ -1600,6 +1726,8 @@ import {
1600
1726
  TRANSACTION_MANAGER,
1601
1727
  PAYMENT_EVENT_LISTENER,
1602
1728
  RECURRING_CHARGE_OPTIONS_RESOLVER,
1729
+ PAYMENT_METHOD_POLICY,
1730
+ PAYMENT_METHOD_POLICY_TIMEOUT,
1603
1731
  PAYMENT_EMAIL_SENDER,
1604
1732
  PAYMENT_CLIENT_RESOLVER,
1605
1733
  PAYMENT_EMAIL_BRANDING_RESOLVER,
@@ -1648,6 +1776,7 @@ All exceptions extend `PaymentException` (a plain `Error` subclass) with a stabl
1648
1776
  | `PaymentIntentOperationNotSupportedException` | `PAYMENT_INTENT_OPERATION_NOT_SUPPORTED` |
1649
1777
  | `InvalidTaxConfigurationException` | `INVALID_TAX_CONFIGURATION` |
1650
1778
  | `PaymentMethodNotOwnedException` | `PAYMENT_METHOD_NOT_OWNED` |
1779
+ | `PaymentMethodDefaultRefusedException` | `PAYMENT_METHOD_DEFAULT_REFUSED` |
1651
1780
  | `PaymentMethodOperationNotSupportedException` | `PAYMENT_METHOD_OPERATION_NOT_SUPPORTED` |
1652
1781
  | `NoChargeablePaymentMethodException` | `NO_CHARGEABLE_PAYMENT_METHOD` |
1653
1782
 
@@ -9,6 +9,8 @@ export declare const PAYMENT_EVENT_LISTENER: unique symbol;
9
9
  export declare const RECURRING_INVOICE_REPOSITORY: unique symbol;
10
10
  export declare const PAYMENT_CUSTOMER_REPOSITORY: unique symbol;
11
11
  export declare const RECURRING_CHARGE_OPTIONS_RESOLVER: unique symbol;
12
+ export declare const PAYMENT_METHOD_POLICY: unique symbol;
13
+ export declare const PAYMENT_METHOD_POLICY_TIMEOUT: unique symbol;
12
14
  export declare const PAYMENT_EMAIL_SENDER: unique symbol;
13
15
  export declare const PAYMENT_CLIENT_RESOLVER: unique symbol;
14
16
  export declare const PAYMENT_EMAIL_BRANDING_RESOLVER: unique symbol;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PAYMENT_SERVICE = exports.TAX_OPTIONS = exports.E_TRANSFER_CODE_PREFIX = exports.DEFAULT_CURRENCY = exports.WEBHOOK_ERROR_REPORTER = exports.PAYMENT_EMAIL_TEMPLATE_RENDERER = exports.PAYMENT_EMAIL_BRANDING_RESOLVER = exports.PAYMENT_CLIENT_RESOLVER = exports.PAYMENT_EMAIL_SENDER = exports.RECURRING_CHARGE_OPTIONS_RESOLVER = exports.PAYMENT_CUSTOMER_REPOSITORY = exports.RECURRING_INVOICE_REPOSITORY = exports.PAYMENT_EVENT_LISTENER = exports.TRANSACTION_MANAGER = exports.WEBHOOK_IDEMPOTENCY_REPOSITORY = exports.PAYMENT_CONFIG_REPOSITORY = exports.PAYMENT_PLAN_REPOSITORY = exports.REFUND_REPOSITORY = exports.PAYMENT_REPOSITORY = exports.INVOICE_REPOSITORY = void 0;
3
+ exports.PAYMENT_SERVICE = exports.TAX_OPTIONS = exports.E_TRANSFER_CODE_PREFIX = exports.DEFAULT_CURRENCY = exports.WEBHOOK_ERROR_REPORTER = exports.PAYMENT_EMAIL_TEMPLATE_RENDERER = exports.PAYMENT_EMAIL_BRANDING_RESOLVER = exports.PAYMENT_CLIENT_RESOLVER = exports.PAYMENT_EMAIL_SENDER = exports.PAYMENT_METHOD_POLICY_TIMEOUT = exports.PAYMENT_METHOD_POLICY = exports.RECURRING_CHARGE_OPTIONS_RESOLVER = exports.PAYMENT_CUSTOMER_REPOSITORY = exports.RECURRING_INVOICE_REPOSITORY = exports.PAYMENT_EVENT_LISTENER = exports.TRANSACTION_MANAGER = exports.WEBHOOK_IDEMPOTENCY_REPOSITORY = exports.PAYMENT_CONFIG_REPOSITORY = exports.PAYMENT_PLAN_REPOSITORY = exports.REFUND_REPOSITORY = exports.PAYMENT_REPOSITORY = exports.INVOICE_REPOSITORY = void 0;
4
4
  exports.INVOICE_REPOSITORY = Symbol('IInvoiceRepository');
5
5
  exports.PAYMENT_REPOSITORY = Symbol('IPaymentRepository');
6
6
  exports.REFUND_REPOSITORY = Symbol('IRefundRepository');
@@ -12,6 +12,8 @@ exports.PAYMENT_EVENT_LISTENER = Symbol('IPaymentEventListener');
12
12
  exports.RECURRING_INVOICE_REPOSITORY = Symbol('IRecurringInvoiceRepository');
13
13
  exports.PAYMENT_CUSTOMER_REPOSITORY = Symbol('IPaymentCustomerRepository');
14
14
  exports.RECURRING_CHARGE_OPTIONS_RESOLVER = Symbol('IRecurringChargeOptionsResolver');
15
+ exports.PAYMENT_METHOD_POLICY = Symbol('IPaymentMethodPolicy');
16
+ exports.PAYMENT_METHOD_POLICY_TIMEOUT = Symbol('PaymentMethodPolicyTimeout');
15
17
  exports.PAYMENT_EMAIL_SENDER = Symbol('IPaymentEmailSender');
16
18
  exports.PAYMENT_CLIENT_RESOLVER = Symbol('IPaymentClientResolver');
17
19
  exports.PAYMENT_EMAIL_BRANDING_RESOLVER = Symbol('IPaymentEmailBrandingResolver');
@@ -1 +1 @@
1
- {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAClD,QAAA,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAClD,QAAA,iBAAiB,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAChD,QAAA,uBAAuB,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAC3D,QAAA,yBAAyB,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAC/D,QAAA,8BAA8B,GAAG,MAAM,CAClD,+BAA+B,CAChC,CAAC;AACW,QAAA,mBAAmB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AACpD,QAAA,sBAAsB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC;AACzD,QAAA,4BAA4B,GAAG,MAAM,CAChD,6BAA6B,CAC9B,CAAC;AACW,QAAA,2BAA2B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAOnE,QAAA,iCAAiC,GAAG,MAAM,CACrD,iCAAiC,CAClC,CAAC;AAGW,QAAA,oBAAoB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AACrD,QAAA,uBAAuB,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAC3D,QAAA,+BAA+B,GAAG,MAAM,CACnD,+BAA+B,CAChC,CAAC;AACW,QAAA,+BAA+B,GAAG,MAAM,CACnD,+BAA+B,CAChC,CAAC;AAGW,QAAA,sBAAsB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AAGxD,QAAA,gBAAgB,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAC7C,QAAA,sBAAsB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AAGvD,QAAA,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAYnC,QAAA,eAAe,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":";;;AAAa,QAAA,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAClD,QAAA,kBAAkB,GAAG,MAAM,CAAC,oBAAoB,CAAC,CAAC;AAClD,QAAA,iBAAiB,GAAG,MAAM,CAAC,mBAAmB,CAAC,CAAC;AAChD,QAAA,uBAAuB,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAC3D,QAAA,yBAAyB,GAAG,MAAM,CAAC,0BAA0B,CAAC,CAAC;AAC/D,QAAA,8BAA8B,GAAG,MAAM,CAClD,+BAA+B,CAChC,CAAC;AACW,QAAA,mBAAmB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AACpD,QAAA,sBAAsB,GAAG,MAAM,CAAC,uBAAuB,CAAC,CAAC;AACzD,QAAA,4BAA4B,GAAG,MAAM,CAChD,6BAA6B,CAC9B,CAAC;AACW,QAAA,2BAA2B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAOnE,QAAA,iCAAiC,GAAG,MAAM,CACrD,iCAAiC,CAClC,CAAC;AASW,QAAA,qBAAqB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AAOvD,QAAA,6BAA6B,GAAG,MAAM,CAAC,4BAA4B,CAAC,CAAC;AAGrE,QAAA,oBAAoB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AACrD,QAAA,uBAAuB,GAAG,MAAM,CAAC,wBAAwB,CAAC,CAAC;AAC3D,QAAA,+BAA+B,GAAG,MAAM,CACnD,+BAA+B,CAChC,CAAC;AACW,QAAA,+BAA+B,GAAG,MAAM,CACnD,+BAA+B,CAChC,CAAC;AAGW,QAAA,sBAAsB,GAAG,MAAM,CAAC,sBAAsB,CAAC,CAAC;AAGxD,QAAA,gBAAgB,GAAG,MAAM,CAAC,iBAAiB,CAAC,CAAC;AAC7C,QAAA,sBAAsB,GAAG,MAAM,CAAC,qBAAqB,CAAC,CAAC;AAGvD,QAAA,WAAW,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;AAYnC,QAAA,eAAe,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC"}
@@ -50,6 +50,9 @@ export declare class InvalidTaxConfigurationException extends PaymentException {
50
50
  export declare class PaymentMethodNotOwnedException extends PaymentException {
51
51
  constructor(message: string);
52
52
  }
53
+ export declare class PaymentMethodDefaultRefusedException extends PaymentException {
54
+ constructor(message: string);
55
+ }
53
56
  export declare class PaymentMethodOperationNotSupportedException extends PaymentException {
54
57
  constructor(message: string);
55
58
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NoChargeablePaymentMethodException = exports.PaymentMethodOperationNotSupportedException = exports.PaymentMethodNotOwnedException = exports.InvalidTaxConfigurationException = exports.PaymentIntentOperationNotSupportedException = exports.PaymentIntentNotCancellableException = exports.PaymentIntentNotReusableException = exports.PublishableKeyNotConfiguredException = exports.UniqueConstraintViolationException = exports.InvalidRecurringInvoiceStateException = exports.InvalidPaymentStateException = exports.RefundNotAllowedException = exports.InvoiceNumberExhaustedException = exports.InvoiceNotPayableException = exports.DuplicatePaymentException = exports.PaymentGatewayException = exports.PaymentAmountExceededException = exports.InvalidInvoiceStateException = exports.PaymentException = void 0;
3
+ exports.NoChargeablePaymentMethodException = exports.PaymentMethodOperationNotSupportedException = exports.PaymentMethodDefaultRefusedException = exports.PaymentMethodNotOwnedException = exports.InvalidTaxConfigurationException = exports.PaymentIntentOperationNotSupportedException = exports.PaymentIntentNotCancellableException = exports.PaymentIntentNotReusableException = exports.PublishableKeyNotConfiguredException = exports.UniqueConstraintViolationException = exports.InvalidRecurringInvoiceStateException = exports.InvalidPaymentStateException = exports.RefundNotAllowedException = exports.InvoiceNumberExhaustedException = exports.InvoiceNotPayableException = exports.DuplicatePaymentException = exports.PaymentGatewayException = exports.PaymentAmountExceededException = exports.InvalidInvoiceStateException = exports.PaymentException = void 0;
4
4
  class PaymentException extends Error {
5
5
  constructor(message, code) {
6
6
  super(message);
@@ -108,6 +108,12 @@ class PaymentMethodNotOwnedException extends PaymentException {
108
108
  }
109
109
  }
110
110
  exports.PaymentMethodNotOwnedException = PaymentMethodNotOwnedException;
111
+ class PaymentMethodDefaultRefusedException extends PaymentException {
112
+ constructor(message) {
113
+ super(message, 'PAYMENT_METHOD_DEFAULT_REFUSED');
114
+ }
115
+ }
116
+ exports.PaymentMethodDefaultRefusedException = PaymentMethodDefaultRefusedException;
111
117
  class PaymentMethodOperationNotSupportedException extends PaymentException {
112
118
  constructor(message) {
113
119
  super(message, 'PAYMENT_METHOD_OPERATION_NOT_SUPPORTED');
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/exceptions/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/exceptions/index.ts"],"names":[],"mappings":"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"}
@@ -11,6 +11,10 @@ export interface PaymentGateway {
11
11
  metadata?: Record<string, string>;
12
12
  expiresAt?: Date;
13
13
  gatewayOptions?: Record<string, unknown>;
14
+ savePaymentMethod?: {
15
+ providerCustomerId: string;
16
+ adoptAsDefault?: boolean;
17
+ };
14
18
  idempotencyKey?: string;
15
19
  }): Promise<{
16
20
  checkoutUrl: string;
@@ -30,6 +30,7 @@ export interface RecurringPaymentGateway {
30
30
  cancelUrl: string;
31
31
  metadata?: Record<string, string>;
32
32
  currency?: string;
33
+ adoptAsDefault?: boolean;
33
34
  }): Promise<{
34
35
  setupUrl: string;
35
36
  providerSetupId: string;
@@ -41,6 +42,19 @@ export interface RecurringPaymentGateway {
41
42
  paymentMethodId: string | null;
42
43
  madeDefault: boolean;
43
44
  }>;
45
+ resolveSetupPaymentMethod?(params: {
46
+ setupIntentId: string;
47
+ }): Promise<{
48
+ paymentMethodId: string | null;
49
+ adoptAsDefault: boolean;
50
+ }>;
51
+ adoptPaymentMethod?(params: {
52
+ providerCustomerId: string;
53
+ paymentMethodId: string;
54
+ }): Promise<{
55
+ paymentMethodId: string | null;
56
+ madeDefault: boolean;
57
+ }>;
44
58
  listPaymentMethods?(params: {
45
59
  providerCustomerId: string;
46
60
  }): Promise<SavedPaymentMethod[]>;
@@ -5,7 +5,7 @@ import type { Request } from 'express';
5
5
  import { GatewayRegistryService } from '../gateway-registry.service';
6
6
  import { PaymentService } from '../../services/payment.service';
7
7
  import { RefundService } from '../../services/refund.service';
8
- import { CustomerService } from '../../services/customer.service';
8
+ import { PaymentMethodService } from '../../services/payment-method.service';
9
9
  export declare const PAYMENTS_WEBHOOK = "payments_webhook";
10
10
  export declare const PaymentsWebhook: () => <TFunction extends Function, Y>(target: TFunction | object, propertyKey?: string | symbol, descriptor?: TypedPropertyDescriptor<Y>) => void;
11
11
  export declare class StripeWebhookController implements OnModuleInit {
@@ -13,15 +13,17 @@ export declare class StripeWebhookController implements OnModuleInit {
13
13
  private readonly gatewayRegistry;
14
14
  private readonly paymentService;
15
15
  private readonly refundService;
16
- private readonly customerService;
16
+ private readonly paymentMethodService;
17
17
  private readonly logger;
18
18
  private errorReporter;
19
- constructor(moduleRef: ModuleRef, gatewayRegistry: GatewayRegistryService, paymentService: PaymentService, refundService: RefundService, customerService: CustomerService);
19
+ constructor(moduleRef: ModuleRef, gatewayRegistry: GatewayRegistryService, paymentService: PaymentService, refundService: RefundService, paymentMethodService: PaymentMethodService);
20
20
  onModuleInit(): void;
21
21
  handleStripeWebhook(req: RawBodyRequest<Request>): Promise<{
22
22
  received: boolean;
23
23
  }>;
24
24
  private routeEvent;
25
25
  private adoptSetupPaymentMethod;
26
+ private adoptPaymentMethod;
27
+ private adopt;
26
28
  private handleWithRetry;
27
29
  }
@@ -19,7 +19,7 @@ const core_1 = require("@nestjs/core");
19
19
  const gateway_registry_service_1 = require("../gateway-registry.service");
20
20
  const payment_service_1 = require("../../services/payment.service");
21
21
  const refund_service_1 = require("../../services/refund.service");
22
- const customer_service_1 = require("../../services/customer.service");
22
+ const payment_method_service_1 = require("../../services/payment-method.service");
23
23
  const constants_1 = require("../../constants");
24
24
  const MAX_RETRIES = 3;
25
25
  const RETRY_DELAY_MS = 2000;
@@ -27,12 +27,12 @@ exports.PAYMENTS_WEBHOOK = 'payments_webhook';
27
27
  const PaymentsWebhook = () => (0, common_1.applyDecorators)((0, common_1.SetMetadata)(exports.PAYMENTS_WEBHOOK, true), (0, common_1.SetMetadata)('isPublic', true), (0, common_1.SetMetadata)('skipTenant', true));
28
28
  exports.PaymentsWebhook = PaymentsWebhook;
29
29
  let StripeWebhookController = StripeWebhookController_1 = class StripeWebhookController {
30
- constructor(moduleRef, gatewayRegistry, paymentService, refundService, customerService) {
30
+ constructor(moduleRef, gatewayRegistry, paymentService, refundService, paymentMethodService) {
31
31
  this.moduleRef = moduleRef;
32
32
  this.gatewayRegistry = gatewayRegistry;
33
33
  this.paymentService = paymentService;
34
34
  this.refundService = refundService;
35
- this.customerService = customerService;
35
+ this.paymentMethodService = paymentMethodService;
36
36
  this.logger = new common_1.Logger(StripeWebhookController_1.name);
37
37
  this.errorReporter = null;
38
38
  }
@@ -99,6 +99,7 @@ let StripeWebhookController = StripeWebhookController_1 = class StripeWebhookCon
99
99
  switch (event.type) {
100
100
  case 'payment.succeeded':
101
101
  await this.handleWithRetry(() => this.paymentService.handlePaymentSucceeded(event), event);
102
+ await this.adoptPaymentMethod(event);
102
103
  break;
103
104
  case 'payment.failed':
104
105
  await this.paymentService.handlePaymentFailed(event);
@@ -136,31 +137,49 @@ let StripeWebhookController = StripeWebhookController_1 = class StripeWebhookCon
136
137
  `nothing to record.`);
137
138
  return;
138
139
  }
139
- if (!(await this.customerService.knowsProviderCustomer(providerCustomerId))) {
140
- this.logger.log(`Setup succeeded (event ${event.eventId}) for provider customer ` +
141
- `${providerCustomerId}, which this deployment has no record of. ` +
142
- `Most likely another environment sharing this provider account. ` +
143
- `Skipped.`);
140
+ await this.adopt(event, { providerCustomerId, setupIntentId }, 'setup');
141
+ }
142
+ async adoptPaymentMethod(event) {
143
+ const providerCustomerId = event.providerData?.customerId;
144
+ const paymentMethodId = event.providerData?.savedPaymentMethodId;
145
+ if (typeof providerCustomerId !== 'string' ||
146
+ typeof paymentMethodId !== 'string') {
144
147
  return;
145
148
  }
149
+ if (event.providerData?.adoptSavedPaymentMethod !== true) {
150
+ this.logger.log(`A card was kept during payment ${event.providerPaymentId} ` +
151
+ `(event ${event.eventId}) without asking for it to become the ` +
152
+ `default. Attached and chargeable by id; the default is unchanged.`);
153
+ return;
154
+ }
155
+ await this.adopt(event, { providerCustomerId, paymentMethodId }, 'payment');
156
+ }
157
+ async adopt(event, params, kind) {
146
158
  try {
147
- const gateway = this.gatewayRegistry.getRecurring('stripe');
148
- if (!gateway.adoptSetupPaymentMethod) {
149
- this.logger.warn(`Gateway "stripe" does not implement adoptSetupPaymentMethod — the saved ` +
150
- `card will not be usable for auto-charge.`);
159
+ const result = await this.paymentMethodService.adoptWebhookPaymentMethod({
160
+ ...params,
161
+ provider: 'stripe',
162
+ });
163
+ if (!result) {
164
+ this.logger.log(`A card was saved for provider customer ${params.providerCustomerId} ` +
165
+ `(event ${event.eventId}), which this deployment has no record of, ` +
166
+ `or which no gateway here can adopt. Skipped.`);
151
167
  return;
152
168
  }
153
- const { paymentMethodId, madeDefault } = await gateway.adoptSetupPaymentMethod({
154
- providerCustomerId,
155
- setupIntentId,
156
- });
157
- this.logger.log(`Payment method setup succeeded (event ${event.eventId}): ` +
158
- `method=${paymentMethodId ?? 'none'}, ` +
159
- `${madeDefault ? 'set as default' : 'existing default kept'}`);
169
+ this.logger.log(`Payment method recorded from a ${kind} event (${event.eventId}): ` +
170
+ `method=${result.paymentMethodId ?? 'none'}, ` +
171
+ `${result.madeDefault ? 'set as default' : 'existing default kept'}`);
160
172
  }
161
173
  catch (error) {
162
- this.logger.error(`Failed to record the payment method from setup event ${event.eventId}: ` +
174
+ this.logger.error(`Failed to record the payment method from ${kind} event ${event.eventId}: ` +
163
175
  `${error instanceof Error ? error.message : String(error)}`, error instanceof Error ? error.stack : undefined);
176
+ this.errorReporter?.(error, {
177
+ tags: {
178
+ webhookEventType: event.type,
179
+ webhookEventId: event.eventId,
180
+ providerCustomerId: params.providerCustomerId,
181
+ },
182
+ });
164
183
  }
165
184
  }
166
185
  async handleWithRetry(handler, event) {
@@ -197,6 +216,6 @@ exports.StripeWebhookController = StripeWebhookController = StripeWebhookControl
197
216
  gateway_registry_service_1.GatewayRegistryService,
198
217
  payment_service_1.PaymentService,
199
218
  refund_service_1.RefundService,
200
- customer_service_1.CustomerService])
219
+ payment_method_service_1.PaymentMethodService])
201
220
  ], StripeWebhookController);
202
221
  //# sourceMappingURL=stripe-webhook.controller.js.map