@ambushsoftworks/nestjs-payments-graphql 0.5.0-rc.2 → 0.6.0
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.
- package/CHANGELOG.md +394 -10
- package/README.md +389 -10
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -1
- package/dist/constants.js.map +1 -1
- package/dist/exceptions/index.d.ts +9 -0
- package/dist/exceptions/index.js +19 -1
- package/dist/exceptions/index.js.map +1 -1
- package/dist/gateways/gateway-registry.service.d.ts +7 -0
- package/dist/gateways/gateway-registry.service.js +14 -0
- package/dist/gateways/gateway-registry.service.js.map +1 -1
- package/dist/gateways/payment-gateway.interface.d.ts +2 -1
- package/dist/gateways/recurring-payment-gateway.interface.d.ts +25 -0
- package/dist/gateways/stripe/stripe-webhook.controller.d.ts +1 -0
- package/dist/gateways/stripe/stripe-webhook.controller.js +34 -1
- package/dist/gateways/stripe/stripe-webhook.controller.js.map +1 -1
- package/dist/gateways/stripe/stripe.gateway.d.ts +29 -0
- package/dist/gateways/stripe/stripe.gateway.js +195 -11
- package/dist/gateways/stripe/stripe.gateway.js.map +1 -1
- package/dist/gateways/stripe/types.d.ts +1 -1
- package/dist/gateways/stripe/types.js +2 -0
- package/dist/gateways/stripe/types.js.map +1 -1
- package/dist/graphql/index.d.ts +18 -0
- package/dist/graphql/index.js +40 -0
- package/dist/graphql/index.js.map +1 -0
- package/dist/index.d.ts +14 -21
- package/dist/index.js +16 -40
- package/dist/index.js.map +1 -1
- package/dist/interfaces/payment-customer-repository.interface.d.ts +1 -0
- package/dist/interfaces/payment-event-listener.interface.d.ts +12 -0
- package/dist/models/payment-method-setup.model.js +3 -1
- package/dist/models/payment-method-setup.model.js.map +1 -1
- package/dist/models/saved-payment-method.model.d.ts +10 -0
- package/dist/models/saved-payment-method.model.js +65 -0
- package/dist/models/saved-payment-method.model.js.map +1 -0
- package/dist/payments.module.d.ts +1 -0
- package/dist/payments.module.js +20 -2
- package/dist/payments.module.js.map +1 -1
- package/dist/prisma/assert-payments-schema.d.ts +1 -0
- package/dist/prisma/assert-payments-schema.js +10 -3
- package/dist/prisma/assert-payments-schema.js.map +1 -1
- package/dist/prisma/columns.d.ts +3 -1
- package/dist/prisma/columns.js +9 -2
- package/dist/prisma/columns.js.map +1 -1
- package/dist/prisma/prisma-payment-customer.repository.d.ts +1 -0
- package/dist/prisma/prisma-payment-customer.repository.js +7 -0
- package/dist/prisma/prisma-payment-customer.repository.js.map +1 -1
- package/dist/prisma/prisma-repository-classes.js.map +1 -1
- package/dist/services/customer.service.d.ts +7 -2
- package/dist/services/customer.service.js +47 -11
- package/dist/services/customer.service.js.map +1 -1
- package/dist/services/invoice.service.d.ts +2 -0
- package/dist/services/invoice.service.js +93 -48
- package/dist/services/invoice.service.js.map +1 -1
- package/dist/services/money.util.d.ts +7 -0
- package/dist/services/money.util.js +22 -0
- package/dist/services/money.util.js.map +1 -0
- package/dist/services/payment-method.service.d.ts +21 -0
- package/dist/services/payment-method.service.js +150 -0
- package/dist/services/payment-method.service.js.map +1 -0
- package/dist/services/payment-plan.service.d.ts +2 -1
- package/dist/services/payment-plan.service.js +54 -32
- package/dist/services/payment-plan.service.js.map +1 -1
- package/dist/services/payment.service.d.ts +9 -0
- package/dist/services/payment.service.js +198 -29
- package/dist/services/payment.service.js.map +1 -1
- package/dist/services/recurring-interval.util.d.ts +1 -0
- package/dist/services/recurring-interval.util.js +12 -0
- package/dist/services/recurring-interval.util.js.map +1 -1
- package/dist/services/recurring-invoice.service.d.ts +2 -1
- package/dist/services/recurring-invoice.service.js +57 -34
- package/dist/services/recurring-invoice.service.js.map +1 -1
- package/dist/services/refund.service.js +6 -3
- package/dist/services/refund.service.js.map +1 -1
- package/dist/testing/in-memory-repositories.d.ts +21 -0
- package/dist/testing/in-memory-repositories.js +198 -0
- package/dist/testing/in-memory-repositories.js.map +1 -0
- package/dist/testing/in-memory-store.d.ts +21 -0
- package/dist/testing/in-memory-store.js +68 -0
- package/dist/testing/in-memory-store.js.map +1 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +11 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +1 -1
- package/dist/types/payment-customer.types.d.ts +1 -0
- package/dist/types/payment-method.types.d.ts +10 -0
- package/dist/types/payment-method.types.js +3 -0
- package/dist/types/payment-method.types.js.map +1 -0
- package/package.json +15 -1
- package/prisma/payment-models.prisma +33 -4
package/CHANGELOG.md
CHANGED
|
@@ -28,12 +28,261 @@ prose for buried obligations. The marker was introduced in v0.4.0 and
|
|
|
28
28
|
|
|
29
29
|
## [Unreleased]
|
|
30
30
|
|
|
31
|
+
## [0.6.0] - 2026-09-19
|
|
32
|
+
|
|
33
|
+
> **Upgrade notes.** One optional database column; nothing else to migrate.
|
|
34
|
+
>
|
|
35
|
+
> 1. **`CreateRefundInput`, `RecordManualPaymentInput`, `CreateInvoiceInput`,
|
|
36
|
+
> `CreatePaymentPlanInput`, `CreateRecurringInvoiceInput` and
|
|
37
|
+
> `UpdateRecurringTemplateInput` now mean the *service* interfaces.** The
|
|
38
|
+
> GraphQL `@InputType` classes of the same names moved to the `/graphql`
|
|
39
|
+
> subpath. If you use them for `@Args()`, change the import; if you were
|
|
40
|
+
> fighting the types when calling a service, that is what this fixes — the
|
|
41
|
+
> type you needed was not exported at all.
|
|
42
|
+
> 2. **If you want saved-card management**, add
|
|
43
|
+
> `defaultPaymentMethodId String?` to `PaymentCustomer`, implement
|
|
44
|
+
> `IPaymentCustomerRepository.setDefaultPaymentMethod`, and set
|
|
45
|
+
> `features.cardManagement`. The module refuses to boot without both. Leave
|
|
46
|
+
> the flag off and nothing changes.
|
|
47
|
+
> 3. **A Stripe gateway that cannot be constructed now fails the boot.** It
|
|
48
|
+
> used to log and continue, leaving every payment to fail at request time.
|
|
49
|
+
> If your deployment has been starting with a bad key, it will now stop.
|
|
50
|
+
> 4. **Recurring templates no longer backfill missed cycles.** A template whose
|
|
51
|
+
> schedule has stalled generates one invoice on the next run, not one per
|
|
52
|
+
> missed cycle. If you were relying on the old behaviour to catch up, you
|
|
53
|
+
> will need to trigger those invoices explicitly.
|
|
54
|
+
> 5. **Delayed-notification payments no longer close an invoice on session
|
|
55
|
+
> completion.** ACH, SEPA and Bacs now record as processing and close when
|
|
56
|
+
> they settle. If you report on invoices closing, expect the lag to appear.
|
|
57
|
+
> 6. Services now reject non-integer and negative money. Anything that was
|
|
58
|
+
> silently storing fractional cents will start throwing.
|
|
59
|
+
> 7. **If you took Checkout payments before 0.3.1, back them up to the
|
|
60
|
+
> PaymentIntent id before upgrading, or refunds for them are silently
|
|
61
|
+
> skipped.** 0.3.1 fixed how new payments are keyed; it did not migrate
|
|
62
|
+
> existing rows. A pre-0.3.1 Checkout payment can be keyed on the Checkout
|
|
63
|
+
> Session id while refund webhooks quote the PaymentIntent id, and the lookup
|
|
64
|
+
> is an exact match with no fallback — so `RefundService` logs an error and
|
|
65
|
+
> skips, the customer's money goes back, and the invoice stays PAID. The
|
|
66
|
+
> README's "Backfilling Checkout payments taken before 0.3.1" has the SQL.
|
|
67
|
+
> Found by the jobsites team while upgrading from 0.2.0; our own answer to
|
|
68
|
+
> their question about this had said 0.3.1 covered it, which was true only
|
|
69
|
+
> for rows written after it.
|
|
70
|
+
> 8. **Subscribe to `charge.dispute.created` and `charge.dispute.closed`** — see
|
|
71
|
+
> the external-configuration section below. Build the list from
|
|
72
|
+
> `STRIPE_WEBHOOK_EVENTS` rather than prose, or set `stripe.verifyOnBoot`.
|
|
73
|
+
> 9. **The package no longer searches the provider for a customer to adopt.**
|
|
74
|
+
> If you lose a `PaymentCustomer` row while the provider customer survives,
|
|
75
|
+
> a new one is created and the client re-saves their card. In exchange,
|
|
76
|
+
> whether two divisions share a wallet is now entirely your repository's
|
|
77
|
+
> decision — see "Provider customers and the scope key" in the README. No
|
|
78
|
+
> action needed if you use the bundled adapter or scope per division; that
|
|
79
|
+
> behaviour is unchanged.
|
|
80
|
+
|
|
81
|
+
### ⚠ External configuration required
|
|
82
|
+
|
|
83
|
+
- **Stripe Dashboard:** subscribe your webhook endpoint to
|
|
84
|
+
`charge.dispute.created` and `charge.dispute.closed`. Without them a
|
|
85
|
+
chargeback pulls the funds with no local record at all — the payment stays
|
|
86
|
+
`SUCCEEDED`, the invoice stays `PAID`, and nothing in your system knows the
|
|
87
|
+
money has gone. `STRIPE_WEBHOOK_EVENTS.always` carries both, so a preflight
|
|
88
|
+
built from that constant will tell you if the subscription is missing.
|
|
89
|
+
|
|
90
|
+
### Added
|
|
91
|
+
- **Dispute handling.** `charge.dispute.created` and `charge.dispute.closed`
|
|
92
|
+
are routed; the package annotates the payment, sets `needsReconciliation`,
|
|
93
|
+
and fires the new optional `IPaymentEventListener.onPaymentDisputed`. It
|
|
94
|
+
deliberately does **not** adjust the invoice — writing it off, re-invoicing,
|
|
95
|
+
or pursuing the client is a business decision, and a payments library
|
|
96
|
+
guessing at it would be worse than saying nothing. A dispute that cannot be
|
|
97
|
+
matched to a payment is still announced, with `paymentId: null` and an error
|
|
98
|
+
log: money has left the account, and silence is the worst outcome.
|
|
99
|
+
- **`PaymentMethodService`** — saved-card management, gated behind
|
|
100
|
+
`features.cardManagement`. `listPaymentMethods`, `setDefaultPaymentMethod`,
|
|
101
|
+
`removePaymentMethod`, `confirmSetup`, `resolveChargeablePaymentMethod` and
|
|
102
|
+
`requireChargeablePaymentMethod`. Keyed by `(divisionId, clientDetailsId)`
|
|
103
|
+
rather than a customer id, so tenant scope is structural; every method naming
|
|
104
|
+
a `paymentMethodId` verifies ownership and throws
|
|
105
|
+
`PaymentMethodNotOwnedException` otherwise. Requested by the jobsites team.
|
|
106
|
+
- **`PaymentCustomer.defaultPaymentMethodId`** — optional, a read-through cache
|
|
107
|
+
of the provider's own default. The provider stays authoritative for what gets
|
|
108
|
+
charged; this exists so a consumer can render the current card without a
|
|
109
|
+
round trip. Requires `features.cardManagement`.
|
|
110
|
+
- **Four optional members on `RecurringPaymentGateway`** —
|
|
111
|
+
`listPaymentMethods`, `detachPaymentMethod`, `setDefaultPaymentMethod`,
|
|
112
|
+
`getDefaultPaymentMethod` — reached through
|
|
113
|
+
`GatewayRegistryService.getPaymentMethodCapable()`.
|
|
114
|
+
- **`@ambushsoftworks/nestjs-payments-graphql/testing`** — in-memory
|
|
115
|
+
repositories and a transaction manager, so a consumer can test an integration
|
|
116
|
+
without a database. They enforce the unique constraints the reference schema
|
|
117
|
+
declares, because the package's recovery paths depend on them. They do not
|
|
118
|
+
model row locking; for that, run against PostgreSQL.
|
|
119
|
+
- **`@ambushsoftworks/nestjs-payments-graphql/graphql`** — the `@InputType`
|
|
120
|
+
classes, moved off the main entry point.
|
|
121
|
+
- **New exceptions:** `PaymentMethodNotOwnedException`
|
|
122
|
+
(`PAYMENT_METHOD_NOT_OWNED`), `PaymentMethodOperationNotSupportedException`
|
|
123
|
+
(`PAYMENT_METHOD_OPERATION_NOT_SUPPORTED`),
|
|
124
|
+
`NoChargeablePaymentMethodException` (`NO_CHARGEABLE_PAYMENT_METHOD`).
|
|
125
|
+
- **`SavedPaymentMethod`** GraphQL object type and TypeScript interface.
|
|
126
|
+
- **`computeNextInvoiceDateAfter`** and **`assertMoneyAmount`**, both exported.
|
|
127
|
+
- **Five README sections that did not exist:** Services, Tenancy and scoping,
|
|
128
|
+
Error handling, Operations, and Upgrading. Three exported services had never
|
|
129
|
+
been mentioned in the README at all, and neither had the manual-payment
|
|
130
|
+
confirmation flow the Quick Start promises.
|
|
131
|
+
|
|
132
|
+
### Fixed
|
|
133
|
+
- **Two writers of `amountPaid`/`amountDue` disagreed on the invariant.**
|
|
134
|
+
`RefundService` derived `amountDue` from the clamped `amountPaid` and
|
|
135
|
+
documented that the two must sum to the total; `applyPaymentToInvoice` wrote
|
|
136
|
+
`amountPaid` unclamped and `amountDue` clamped, so an overpayment persisted
|
|
137
|
+
exactly the state the other side's comment forbade. Both now share
|
|
138
|
+
`deriveInvoiceAmounts`: `amountPaid` records reality and never goes negative,
|
|
139
|
+
`amountDue` is always derived from it, and an overpayment is kept and logged
|
|
140
|
+
rather than silently absorbed — it is money somebody has to decide about.
|
|
141
|
+
- **A lost refund response could refund twice.** `stripe.refunds.create` carried
|
|
142
|
+
no idempotency key, so a refund whose HTTP response was lost was
|
|
143
|
+
indistinguishable from one that failed. The operator retries,
|
|
144
|
+
`resolveRefundAmount` reads the local table — which never recorded the first —
|
|
145
|
+
and a second real refund goes out. The `(paymentId, providerRefundId)`
|
|
146
|
+
constraint cannot catch it: two genuinely different provider refunds. Now
|
|
147
|
+
keyed on the payment and amount, so a retry collapses while a deliberate
|
|
148
|
+
second partial refund still goes through.
|
|
149
|
+
- **Who was affected.** Anyone issuing online refunds. The 0.4.0 post-mortem
|
|
150
|
+
measured a refund call at 7.8 seconds; a timeout anywhere in that window is
|
|
151
|
+
all it takes.
|
|
152
|
+
- **A stalled recurring template charged once per missed cycle.** The claim
|
|
153
|
+
advanced `nextInvoiceAt` by one cycle from its own stale value, leaving the
|
|
154
|
+
template still due, so the batch loop re-claimed it and went round again. A
|
|
155
|
+
daily template six weeks behind generated ~42 invoices in one run — and under
|
|
156
|
+
`autoCharge`, 42 distinct invoice ids meant 42 distinct idempotency keys and
|
|
157
|
+
**42 real card charges**. Missed cycles are now skipped, matching `resume()`.
|
|
158
|
+
- `computeNextInvoiceDateAfter` also corrects an overshoot in `resume()`
|
|
159
|
+
itself: rounding the elapsed cycles could step over a date that had not
|
|
160
|
+
happened yet, so a Jan 15 anchor resumed on May 3 moved to June 15 rather
|
|
161
|
+
than May 15.
|
|
162
|
+
- **ACH and other delayed payments closed the invoice before settling.**
|
|
163
|
+
`checkout.session.completed` was treated as success regardless of
|
|
164
|
+
`payment_status`, so a session completing `unpaid` with the intent still
|
|
165
|
+
`processing` marked the payment SUCCEEDED, closed the invoice and emailed a
|
|
166
|
+
receipt. There was no way back — `handlePaymentFailed` only transitions from
|
|
167
|
+
PENDING/PROCESSING, so a later ACH return could not reopen it. The webhook now
|
|
168
|
+
emits `payment.processing` unless the session is genuinely paid.
|
|
169
|
+
- **Voiding an invoice left its payment session live.** `voidInvoice`'s comment
|
|
170
|
+
said cancellation was "handled separately by the caller"; no such call
|
|
171
|
+
existed, and `PaymentGateway.cancelPayment` was implemented and invoked
|
|
172
|
+
nowhere. The client's open checkout tab stayed payable: they pay, the webhook
|
|
173
|
+
declines to apply it, and the money sits at the provider with no refund path.
|
|
174
|
+
Voiding now cancels pending provider sessions, outside any transaction, and
|
|
175
|
+
flags `needsReconciliation` when the provider cannot be reached.
|
|
176
|
+
- **A declined Checkout card was never recorded as failed.**
|
|
177
|
+
`handlePaymentFailed` and `handlePaymentProcessing` looked up the event's own
|
|
178
|
+
id, but a Checkout row is keyed by the session id while
|
|
179
|
+
`payment_intent.payment_failed` carries the intent id. The lookup never
|
|
180
|
+
matched, the idempotency row was written so Stripe stopped retrying, and no
|
|
181
|
+
listener heard anything. Both now resolve canonically and fall back to the
|
|
182
|
+
invoice the event names.
|
|
183
|
+
- **`sendInvoice` could send twice and store a mismatched e-transfer pair.**
|
|
184
|
+
Read-then-write with no lock let two callers both observe DRAFT and proceed:
|
|
185
|
+
two "your invoice" emails, and both ran `assignCode`, so the stored
|
|
186
|
+
`eTransferCode` and `eTransferAnswer` could come from different writers. The
|
|
187
|
+
client was emailed a pair that did not match the row and their transfer
|
|
188
|
+
bounced. The transition is now claimed under a row lock. `ensureSent` is
|
|
189
|
+
documented as the call to make before opening a payment session, so a
|
|
190
|
+
double-clicked Pay button was the ordinary way in.
|
|
191
|
+
- **A rolled-back payment could leave an instalment permanently PAID.**
|
|
192
|
+
`applyPaymentToInvoice` ran inside the caller's transaction but notified
|
|
193
|
+
`PaymentPlanService` through the module-scoped repository, which committed on
|
|
194
|
+
a separate connection. The instalment update now happens after the
|
|
195
|
+
transaction commits.
|
|
196
|
+
- **Cancelling a payment plan voided invoices outside its own transaction**, and
|
|
197
|
+
since voiding now calls the provider, would have made a provider call inside
|
|
198
|
+
one. The cancellation is transactional; the invoice voids follow it.
|
|
199
|
+
- **A failed line item restarted invoice creation and orphaned the first
|
|
200
|
+
invoice.** The retry caught `UniqueConstraintViolationException` from a block
|
|
201
|
+
spanning creation, every line item, tax components and the totals update, then
|
|
202
|
+
retried the whole thing with a fresh number — leaving a numbered row with
|
|
203
|
+
partial line items and `total: 0`, indistinguishable from a real invoice. Only
|
|
204
|
+
the `create` is retried now.
|
|
205
|
+
- **Refunding a payment on a VOID invoice made it payable again.** Status was
|
|
206
|
+
derived from amounts alone, so a partial refund moved a voided invoice to
|
|
207
|
+
`PARTIALLY_PAID` — which is in `PAYABLE_STATUSES`. VOID is now terminal.
|
|
208
|
+
- **The services accepted money that was not money.** `amount: -5000` passed
|
|
209
|
+
`recordManualPayment`'s only check and left the invoice owing more than its
|
|
210
|
+
total; `amount: 10.5` stored fractional cents that propagated into every later
|
|
211
|
+
sum. The only guards were in the GraphQL DTOs, which the documented
|
|
212
|
+
architecture does not require anyone to use.
|
|
213
|
+
- **A Stripe gateway that failed to construct was swallowed.** The consumer
|
|
214
|
+
booted cleanly, every payment threw at request time, and the webhook answered
|
|
215
|
+
400 — so Stripe retried every real payment event for three days and then
|
|
216
|
+
disabled the endpoint. Supplying `stripe` config now means a failure to build
|
|
217
|
+
the gateway fails the boot.
|
|
218
|
+
- **`checkout.session.completed` cast `payment_intent` to a string.** Stripe
|
|
219
|
+
sends null for a zero-amount or fully-discounted session; the cast fed null to
|
|
220
|
+
`paymentIntents.retrieve`, whose failure was logged as a receipt-URL problem.
|
|
221
|
+
- **Documentation corrections.** The auth-exemption instructions told readers to
|
|
222
|
+
compare a route path against `PAYMENTS_WEBHOOK`, which is a metadata key — a
|
|
223
|
+
guard written as documented never matches and every webhook 401s. Invoice
|
|
224
|
+
numbering was described as per-division and is global. The e-transfer prefix
|
|
225
|
+
sample produced `ARD--A1B2-C3D4` because the service adds its own separator,
|
|
226
|
+
and the documented code format named an invoice component that does not
|
|
227
|
+
exist. The webhook controller was described as mounting conditionally; it
|
|
228
|
+
mounts always. Design Principles contradicted the Email Options table on which
|
|
229
|
+
pieces are required.
|
|
230
|
+
|
|
231
|
+
### Changed
|
|
232
|
+
- **The scope key for provider customers belongs to your repository.**
|
|
233
|
+
`StripeGateway.createOrRetrieveCustomer` no longer searches the provider for
|
|
234
|
+
a customer to adopt; it creates. `IPaymentCustomerRepository.findByClientAndProvider`
|
|
235
|
+
is the only index, so filtering on `divisionId` gives each division its own
|
|
236
|
+
wallet and ignoring it gives one client one wallet across all of them —
|
|
237
|
+
whichever is right for what a division means in your product.
|
|
238
|
+
|
|
239
|
+
This **supersedes the 0.5.0 fix below rather than relaxing it.** 0.5.0
|
|
240
|
+
narrowed *which* provider customers could be adopted, from "anyone with this
|
|
241
|
+
email" to "one this package created for this division and client". That made
|
|
242
|
+
it safe and also nearly pointless: if the package created it, the package
|
|
243
|
+
wrote a row, and the repository lookup one step earlier would have found it.
|
|
244
|
+
Removing adoption entirely closes the same hole harder, and hands the
|
|
245
|
+
decision to the layer that can actually make it. Email was never identity —
|
|
246
|
+
a company billing address is shared by several clients, and clients change
|
|
247
|
+
email.
|
|
248
|
+
|
|
249
|
+
Requested by the jobsites team, whose `divisionId` is a job site: one
|
|
250
|
+
recruiter, one card, three sites. Note that a shared wallet means every
|
|
251
|
+
division can change the default card the others charge, and is only safe when
|
|
252
|
+
`clientDetailsId` is globally unique. Both are documented.
|
|
253
|
+
- **`createInMemoryRepositories` takes `customerKey: 'division' | 'client'`**
|
|
254
|
+
(default `'division'`). The in-memory double enforced the reference schema's
|
|
255
|
+
three-column constraint unconditionally, which made it *more permissive than
|
|
256
|
+
the database* of a consumer scoped per client — the one failure mode a fake
|
|
257
|
+
must not have.
|
|
258
|
+
- **`InvoiceService` and `PaymentService` gained a shared money validator**, and
|
|
259
|
+
`PaymentService.cancelPendingPaymentsForInvoice` is new and public.
|
|
260
|
+
- **`PaymentGateway.createRefund` takes an optional `idempotencyKey`**, and
|
|
261
|
+
`RecurringPaymentGateway` gained the four optional card members above. All
|
|
262
|
+
optional, so a gateway written against v0.5.x still satisfies both interfaces.
|
|
263
|
+
- **`assertPaymentsSchema` takes `cardManagement`.** The `PaymentCustomer`
|
|
264
|
+
requirement is now driven by either `recurringInvoices` or `cardManagement`,
|
|
265
|
+
and the new column is only required by the latter — so an existing consumer's
|
|
266
|
+
schema still passes unchanged.
|
|
267
|
+
|
|
268
|
+
### Testing
|
|
269
|
+
- **+87 specs, 551 across 24 suites.** New: `payment-method.service.spec.ts`,
|
|
270
|
+
`recurring-invoice-scheduling.spec.ts`, `money-validation.spec.ts`, plus
|
|
271
|
+
saved-card coverage on the Stripe gateway and the catch-up storm, the
|
|
272
|
+
session-cancellation path, the auto-charge convergence race and the money
|
|
273
|
+
validators. Five new documentation guards fail the build when an exported
|
|
274
|
+
service, a module option or an optional interface member goes undocumented —
|
|
275
|
+
each one encoding drift that had already happened.
|
|
276
|
+
|
|
31
277
|
## [0.5.0] - 2026-09-15
|
|
32
278
|
|
|
33
279
|
> **Release candidates.** `0.5.0-rc.1` carries the tax-input changes, the
|
|
34
280
|
> billing jobs, and the webhook retry and duplicate-delivery fixes. The shared
|
|
35
281
|
> Prisma repositories (`@ambushsoftworks/nestjs-payments-graphql/prisma`) were
|
|
36
282
|
> not in rc.1. `0.5.0-rc.2` adds them, with no change to the main entry point.
|
|
283
|
+
> `0.5.0-rc.3` adds the saved-card and auto-charge fixes reported by the
|
|
284
|
+
> jobsites team — retest auto-charge on a recurring template, and saving a card
|
|
285
|
+
> end to end, against Stripe test mode.
|
|
37
286
|
|
|
38
287
|
> **Upgrade notes.** No database migration.
|
|
39
288
|
>
|
|
@@ -53,6 +302,23 @@ prose for buried obligations. The marker was introduced in v0.4.0 and
|
|
|
53
302
|
> so two concurrent `invoiceInstallment` calls cannot both link an invoice.
|
|
54
303
|
> 5. If you construct `InvoiceService` or `RecurringInvoiceService` by hand, pass
|
|
55
304
|
> `{ requireExplicit: false }` as the new last argument.
|
|
305
|
+
> 6. **If you use auto-charge or saved cards, they did not work before this
|
|
306
|
+
> release** — see the entries under Fixed. Nothing to migrate, but the
|
|
307
|
+
> first successful auto-charge on an existing recurring template may be its
|
|
308
|
+
> first ever. Check `consecutiveFailures` on your templates before upgrading:
|
|
309
|
+
> a template at 3 was auto-paused and stays paused until you resume it.
|
|
310
|
+
> 7. **Provider customers are now scoped to the division that created them.**
|
|
311
|
+
> Existing shared customers are left alone, so nothing breaks on upgrade, but
|
|
312
|
+
> a client billed by two divisions will get a second provider customer the
|
|
313
|
+
> next time `createOrLinkCustomer` runs for the division that did not create
|
|
314
|
+
> the first one — and cards saved against the original are not visible to it.
|
|
315
|
+
> If you bill one client from more than one division and rely on a shared
|
|
316
|
+
> saved card, have them re-save it under the second division.
|
|
317
|
+
> 8. If you construct `CustomerService` by hand, it takes two new constructor
|
|
318
|
+
> arguments: `PaymentConfigService` and the `DEFAULT_CURRENCY` value.
|
|
319
|
+
> 9. **`createSetupSession().providerSetupId` is now the Checkout Session id**
|
|
320
|
+
> (`cs_…`), not the SetupIntent id. It previously returned `null` cast to a
|
|
321
|
+
> string on every call, so nothing can have depended on the old value.
|
|
56
322
|
|
|
57
323
|
### Added
|
|
58
324
|
- **`tax.requireExplicit` module option.** With it set,
|
|
@@ -122,6 +388,16 @@ prose for buried obligations. The marker was introduced in v0.4.0 and
|
|
|
122
388
|
schema, and copies drift.
|
|
123
389
|
|
|
124
390
|
### Changed
|
|
391
|
+
- **`RecurringPaymentGateway` gained one optional method and three optional
|
|
392
|
+
params.** `adoptSetupPaymentMethod?()` records the card a completed setup
|
|
393
|
+
session collected; `createOffSessionPayment` takes `paymentMethodId?` and
|
|
394
|
+
`idempotencyKey?`; `createSetupSession` takes `currency?`. All optional, so a
|
|
395
|
+
custom gateway written against v0.2.x still satisfies the interface — but one
|
|
396
|
+
that does not implement `adoptSetupPaymentMethod` cannot support saved cards,
|
|
397
|
+
and the webhook logs a warning saying so.
|
|
398
|
+
- **`CustomerService` takes two new constructor arguments**, `PaymentConfigService`
|
|
399
|
+
and the `DEFAULT_CURRENCY` value. This only matters if you construct it by
|
|
400
|
+
hand, in tests for example.
|
|
125
401
|
- **An empty `taxComponents` list means untaxed.** `createInvoice` and
|
|
126
402
|
`RecurringInvoiceService.create` treat `taxComponents: []` exactly like
|
|
127
403
|
`taxRate: 0`, with no component rows and `taxAmount` 0, where it used to
|
|
@@ -164,6 +440,114 @@ prose for buried obligations. The marker was introduced in v0.4.0 and
|
|
|
164
440
|
unrenamed. A hand-written adapter can still rename anything.
|
|
165
441
|
|
|
166
442
|
### Fixed
|
|
443
|
+
|
|
444
|
+
Five of these were reported by the jobsites team against `0.2.0`, from running a
|
|
445
|
+
prototype on live cards; the rest were found auditing the same paths before this
|
|
446
|
+
release. They share one property: every one of them is silent. The API returns
|
|
447
|
+
200, the webhook returns 200, and the logs look healthy.
|
|
448
|
+
|
|
449
|
+
- **Auto-charge took the money and then refused to record it.**
|
|
450
|
+
`generateInvoiceFromConfig` creates a DRAFT invoice, optionally sends it, then
|
|
451
|
+
charges the saved card — and charged *before* checking the invoice could
|
|
452
|
+
accept a payment. `recordAutoChargePayment` then rejected the DRAFT, so the
|
|
453
|
+
charge stood at Stripe while the consumer's listener was told the renewal had
|
|
454
|
+
FAILED. One client was emailed a failure notice while the package held their
|
|
455
|
+
money. `attemptAutoCharge` now re-reads the invoice and refuses to call the
|
|
456
|
+
provider unless it is in `PAYABLE_STATUSES`, recording an
|
|
457
|
+
`invoice_not_payable` failure instead.
|
|
458
|
+
- **Who was affected.** Any consumer using recurring auto-charge. Two
|
|
459
|
+
configurations reach it: `autoCharge: true` with `autoSend: false`, which
|
|
460
|
+
jobsites found, and `autoSend: true` where `sendInvoice` throws — an email
|
|
461
|
+
transport outage, say — which `generateInvoiceFromConfig` swallows as
|
|
462
|
+
non-fatal before charging anyway. The second was not reported and is the
|
|
463
|
+
reason the fix is a status check at the charge site rather than a validation
|
|
464
|
+
on the template.
|
|
465
|
+
- **Detecting affected charges.** Look for provider charges whose invoice is
|
|
466
|
+
still DRAFT, or for `onRecurringPaymentFailed` events whose invoice has no
|
|
467
|
+
corresponding `Payment` row. The money is real; the invoice never moved.
|
|
468
|
+
- **Auto-charge could be charged, paid, and reported as failed at the same time.**
|
|
469
|
+
Stripe fires `payment_intent.succeeded` the moment an off-session charge
|
|
470
|
+
settles, so the webhook races `recordAutoChargePayment` for the same
|
|
471
|
+
`(provider, providerPaymentId)`. The webhook usually won: its writer applied
|
|
472
|
+
the amount and closed the invoice, then `recordAutoChargePayment` hit the
|
|
473
|
+
unique constraint, its transaction rolled back, and the throw reached
|
|
474
|
+
`attemptAutoCharge`'s catch — which recorded a failure and fired
|
|
475
|
+
`onRecurringPaymentFailed`. **The client was charged, the invoice was paid,
|
|
476
|
+
and the consumer was told the renewal had failed.** Three consecutive
|
|
477
|
+
"failures" auto-pause the schedule, so a working subscription could pause
|
|
478
|
+
itself. Both writers now converge on one row, covering either ordering: a
|
|
479
|
+
pre-check for a webhook that already committed (needed because
|
|
480
|
+
`validatePayable` would otherwise throw on the now-PAID invoice before the
|
|
481
|
+
constraint could fire), and unique-violation adoption for one that commits
|
|
482
|
+
mid-transaction. The adopting call announces nothing — the writer that caused
|
|
483
|
+
the transition already did.
|
|
484
|
+
- **Auto-charge billed the invoice total, not the balance.** The payable-status
|
|
485
|
+
check re-read the invoice, but the amount still came from the snapshot taken
|
|
486
|
+
before the invoice was sent. Anything that landed in between — an operator
|
|
487
|
+
recording an advance, or the client paying the link in the auto-send email —
|
|
488
|
+
leaves the invoice `PARTIALLY_PAID`, which *is* payable, so the check passed
|
|
489
|
+
and the full total was charged on top. `recordAutoChargePayment` validates
|
|
490
|
+
payability but never the amount, so nothing downstream caught it, and
|
|
491
|
+
`applyPaymentToInvoice` clamped `amountDue` to zero and marked the invoice
|
|
492
|
+
PAID while the client had been billed twice. It now charges `amountDue`, and
|
|
493
|
+
skips entirely when nothing is outstanding.
|
|
494
|
+
- **`RecurringInvoiceService` threw a `TypeError` when the feature was off.**
|
|
495
|
+
The module provides `RECURRING_INVOICE_REPOSITORY` as `?? null` while the
|
|
496
|
+
service injected it as non-null — the same defect fixed in `PaymentPlanService`
|
|
497
|
+
and `CustomerService` above, and the last of the three. A consumer whose cron
|
|
498
|
+
called `processDueRecurringInvoices` without `features.recurringInvoices` got
|
|
499
|
+
`Cannot read properties of null` inside a scheduler. It now names the missing
|
|
500
|
+
option.
|
|
501
|
+
- **Auto-charge could never charge anyone.** `createOffSessionPayment` built a
|
|
502
|
+
PaymentIntent with a `customer` and no `payment_method`. Stripe applies
|
|
503
|
+
`invoice_settings.default_payment_method` to Invoices and Subscriptions, not
|
|
504
|
+
to a bare PaymentIntent, so confirming one failed every time with *"You cannot
|
|
505
|
+
confirm this PaymentIntent because it's missing a payment method."* The
|
|
506
|
+
gateway now resolves the customer's default method and attaches it explicitly,
|
|
507
|
+
accepts an optional `paymentMethodId` override, and returns
|
|
508
|
+
`failureReason: 'no_payment_method'` without calling Stripe when nothing
|
|
509
|
+
resolves — rather than falling back to whichever card was added most recently.
|
|
510
|
+
- **"Add a card" failed for every client.** `createSetupSession` omitted
|
|
511
|
+
`currency`, which Stripe requires in `mode: 'setup'` when `payment_method_types`
|
|
512
|
+
is not given, so every call raised *"Missing required param: currency."* The
|
|
513
|
+
currency now comes from the division's `PaymentConfig.defaultCurrency`, then
|
|
514
|
+
the module's `defaultCurrency`, and can be overridden per call. This is the
|
|
515
|
+
first thing in the package to read `PaymentConfig.defaultCurrency`.
|
|
516
|
+
- **A saved card was invisible to auto-charge.** Nothing in the package ever
|
|
517
|
+
wrote `invoice_settings.default_payment_method` — the gateway's own docblock
|
|
518
|
+
asserted that Stripe set it on setup-session completion, which Stripe does
|
|
519
|
+
not do. The webhook now records the collected method as the customer's
|
|
520
|
+
default when they have none, leaving an existing default alone so adding a
|
|
521
|
+
second card does not silently change which one gets charged. Together with
|
|
522
|
+
the two entries above, this is the first release in which saving a card and
|
|
523
|
+
charging it later works end to end.
|
|
524
|
+
- **`PaymentPlanService` threw on every successful payment when payment plans
|
|
525
|
+
were off.** `PaymentService` calls `onInvoicePaid` after every invoice reaches
|
|
526
|
+
PAID, and the service dereferenced a repository the module provides as `null`,
|
|
527
|
+
logging `Cannot read properties of null` each time. Cosmetic, but it trains
|
|
528
|
+
people to ignore payment warnings in a module where the other four bugs here
|
|
529
|
+
are silent. `onInvoicePaid` and `markInstallmentOverdue` now return early, and
|
|
530
|
+
consumer-facing methods name the missing option instead of throwing a
|
|
531
|
+
`TypeError`. `CustomerService` had the same latent fault and got the same
|
|
532
|
+
treatment.
|
|
533
|
+
- **Two divisions billing the same person shared one provider customer, and one
|
|
534
|
+
set of saved cards.** `createOrRetrieveCustomer` matched on email across the
|
|
535
|
+
whole Stripe account, ignoring the `divisionId`/`clientDetailsId` metadata it
|
|
536
|
+
then wrote — so it also adopted customers this package never created. It now
|
|
537
|
+
reuses a customer only when both metadata values match, and creates one
|
|
538
|
+
otherwise. Found while fixing the reported bugs, not reported. See upgrade
|
|
539
|
+
note 7.
|
|
540
|
+
- **A retried generation job could charge twice.** The off-session
|
|
541
|
+
`paymentIntents.create` carried no `idempotencyKey`, unlike
|
|
542
|
+
`createPaymentIntent`. It now derives one from the invoice id.
|
|
543
|
+
- **Auto-charge payments never recorded a receipt URL.**
|
|
544
|
+
`createOffSessionPayment` read `latest_charge.receipt_url` without expanding
|
|
545
|
+
`latest_charge`, so the field was an id string and the check silently fell
|
|
546
|
+
through to `undefined` on every charge.
|
|
547
|
+
- **`createSetupSession` returned null as a string.** `providerSetupId` was
|
|
548
|
+
`session.setup_intent`, which Stripe leaves null in setup mode until the
|
|
549
|
+
customer completes the session, feeding a non-nullable GraphQL field. It now
|
|
550
|
+
returns the Checkout Session id.
|
|
167
551
|
- **`invoiceInstallment` recorded a tax rate it did not charge.** Its single
|
|
168
552
|
line was non-taxable and it passed no tax, so `PaymentConfig.defaultTaxRate`
|
|
169
553
|
was stamped on the invoice. With a 13% default, the client received a
|
|
@@ -566,16 +950,6 @@ API wrote — and delete the rest. Then recalculate the affected invoices;
|
|
|
566
950
|
### Fixed
|
|
567
951
|
- **`payment_intent.succeeded` Stripe webhook event is now handled.** Previously the event fell through to the `default → ignored` branch in `StripeGateway.normalizeWebhookEvent`, meaning any PaymentIntent created outside the Checkout flow — chiefly the inline Stripe Elements surface arriving in 0.3.0, but also any consumer calling `stripe.paymentIntents.create` directly — succeeded at Stripe with no corresponding invoice or payment-record update. The new handler mirrors `handleCheckoutSessionCompleted`'s `latest_charge` → `receipt_url` fetch pattern (including the inflated-object vs string-id fork) and emits `type: 'payment.succeeded'` with `providerPaymentId: intent.id`, so `PaymentService.handlePaymentSucceeded` matches the row stored at `createPendingPayment` time without an id-swap step. Consumers using direct-PaymentIntent flows MUST subscribe to `payment_intent.succeeded` in their Stripe Dashboard webhook configuration — see "Stripe Gateway & Webhooks" in the README. This is also a precondition for the inline-PaymentIntent surface arriving in 0.3.0.
|
|
568
952
|
|
|
569
|
-
## [0.1.3] - 2026-05-15
|
|
570
|
-
|
|
571
|
-
### Added
|
|
572
|
-
- **`NormalizedWebhookEvent` / `IgnoredWebhookEvent` types** exported from the package root. `PaymentGateway.handleWebhook` now returns the discriminated union; the `'ignored'` variant signals "received but no action needed" so the webhook controller responds 200 instead of 400 + Stripe retry loop. See "Stripe Gateway & Webhooks" in the README.
|
|
573
|
-
|
|
574
|
-
### Changed
|
|
575
|
-
- **Stripe webhook controller now responds 200 for unrecognized Stripe event types** (e.g. `payment_intent.created`, `charge.updated`, `charge.succeeded`). Previously it threw inside the gateway's `normalizeWebhookEvent` and the controller's signature-verification try/catch re-threw as `BadRequestException('Webhook signature verification failed')`, causing Stripe to retry the delivery for ~3 days and filling logs with misleading "signature verification failed" messages. The new behavior matches Stripe's documented contract: log unrecognized events at `debug`, acknowledge with 200. Real signature failures are unchanged (still 400 with the correct error message).
|
|
576
|
-
- **`PaymentService` now logs a WARN when a SUCCEEDED payment lands on an invoice in a non-payable status** (DRAFT, VOID, PAID, REFUNDED, …). Previously this was a silent skip — the consumer's card was charged but the invoice never updated. Affects both `handlePaymentSucceeded` (webhook path) and `reconcilePayment` (reconciliation scheduler path). Most commonly hit when a consumer creates a DRAFT invoice and starts checkout without first calling `InvoiceService.sendInvoice` to transition it to SENT.
|
|
577
|
-
- **`PaymentGateway.handleWebhook` return type widened from `Promise<PaymentWebhookEvent>` to `Promise<NormalizedWebhookEvent>`** to express the new `'ignored'` variant. TypeScript covariance: existing custom-gateway implementations whose `handleWebhook` is declared as `Promise<PaymentWebhookEvent>` continue to satisfy the interface without modification (no TS error). Callers who destructure the return value should add a guard on the discriminator (`if (event.type === 'ignored') return;`) before accessing `PaymentWebhookEvent`-only fields.
|
|
578
|
-
|
|
579
953
|
## [0.2.0] - 2026-05-15
|
|
580
954
|
|
|
581
955
|
### Added
|
|
@@ -587,6 +961,16 @@ API wrote — and delete the rest. Then recalculate the affected invoices;
|
|
|
587
961
|
### Changed
|
|
588
962
|
- **`InvoiceModel.payments` and `InvoiceModel.lineItems` are now nullable in the GraphQL schema** (`@Field(() => [...], { nullable: 'itemsAndList' })`, TS properties optional). The package ships no `@ResolveField` for these fields per its "no resolvers in the package" rule; the previous non-nullable declaration caused Apollo to throw `Cannot return null for non-nullable field` for any consumer that hadn't wired up their own resolver. Both fields now carry JSDoc explaining the consumer-population responsibility. **This is a GraphQL schema-shape change**: consumer clients that selected `payments` or `lineItems` and assumed non-null arrays must now handle `null` (defensive `?.` / `?? []`).
|
|
589
963
|
|
|
964
|
+
## [0.1.3] - 2026-05-15
|
|
965
|
+
|
|
966
|
+
### Added
|
|
967
|
+
- **`NormalizedWebhookEvent` / `IgnoredWebhookEvent` types** exported from the package root. `PaymentGateway.handleWebhook` now returns the discriminated union; the `'ignored'` variant signals "received but no action needed" so the webhook controller responds 200 instead of 400 + Stripe retry loop. See "Stripe Gateway & Webhooks" in the README.
|
|
968
|
+
|
|
969
|
+
### Changed
|
|
970
|
+
- **Stripe webhook controller now responds 200 for unrecognized Stripe event types** (e.g. `payment_intent.created`, `charge.updated`, `charge.succeeded`). Previously it threw inside the gateway's `normalizeWebhookEvent` and the controller's signature-verification try/catch re-threw as `BadRequestException('Webhook signature verification failed')`, causing Stripe to retry the delivery for ~3 days and filling logs with misleading "signature verification failed" messages. The new behavior matches Stripe's documented contract: log unrecognized events at `debug`, acknowledge with 200. Real signature failures are unchanged (still 400 with the correct error message).
|
|
971
|
+
- **`PaymentService` now logs a WARN when a SUCCEEDED payment lands on an invoice in a non-payable status** (DRAFT, VOID, PAID, REFUNDED, …). Previously this was a silent skip — the consumer's card was charged but the invoice never updated. Affects both `handlePaymentSucceeded` (webhook path) and `reconcilePayment` (reconciliation scheduler path). Most commonly hit when a consumer creates a DRAFT invoice and starts checkout without first calling `InvoiceService.sendInvoice` to transition it to SENT.
|
|
972
|
+
- **`PaymentGateway.handleWebhook` return type widened from `Promise<PaymentWebhookEvent>` to `Promise<NormalizedWebhookEvent>`** to express the new `'ignored'` variant. TypeScript covariance: existing custom-gateway implementations whose `handleWebhook` is declared as `Promise<PaymentWebhookEvent>` continue to satisfy the interface without modification (no TS error). Callers who destructure the return value should add a guard on the discriminator (`if (event.type === 'ignored') return;`) before accessing `PaymentWebhookEvent`-only fields.
|
|
973
|
+
|
|
590
974
|
## [0.1.2] - 2026-04-19
|
|
591
975
|
|
|
592
976
|
### Fixed
|