@ambushsoftworks/nestjs-payments-graphql 0.3.0 → 0.4.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 (67) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/README.md +234 -6
  3. package/dist/dto/create-invoice.input.d.ts +2 -0
  4. package/dist/dto/create-invoice.input.js +8 -0
  5. package/dist/dto/create-invoice.input.js.map +1 -1
  6. package/dist/dto/create-tax-component.input.d.ts +7 -0
  7. package/dist/dto/create-tax-component.input.js +56 -0
  8. package/dist/dto/create-tax-component.input.js.map +1 -0
  9. package/dist/dto/index.d.ts +1 -0
  10. package/dist/dto/index.js +3 -1
  11. package/dist/dto/index.js.map +1 -1
  12. package/dist/dto/update-payment-config.input.d.ts +1 -0
  13. package/dist/dto/update-payment-config.input.js +7 -0
  14. package/dist/dto/update-payment-config.input.js.map +1 -1
  15. package/dist/exceptions/index.d.ts +3 -0
  16. package/dist/exceptions/index.js +7 -1
  17. package/dist/exceptions/index.js.map +1 -1
  18. package/dist/gateways/stripe/stripe-config-verifier.service.d.ts +19 -0
  19. package/dist/gateways/stripe/stripe-config-verifier.service.js +107 -0
  20. package/dist/gateways/stripe/stripe-config-verifier.service.js.map +1 -0
  21. package/dist/gateways/stripe/stripe.gateway.js +1 -1
  22. package/dist/gateways/stripe/stripe.gateway.js.map +1 -1
  23. package/dist/gateways/stripe/types.d.ts +6 -0
  24. package/dist/gateways/stripe/types.js +15 -1
  25. package/dist/gateways/stripe/types.js.map +1 -1
  26. package/dist/gateways/stripe/verify-stripe-configuration.d.ts +41 -0
  27. package/dist/gateways/stripe/verify-stripe-configuration.js +281 -0
  28. package/dist/gateways/stripe/verify-stripe-configuration.js.map +1 -0
  29. package/dist/index.d.ts +8 -3
  30. package/dist/index.js +13 -2
  31. package/dist/index.js.map +1 -1
  32. package/dist/interfaces/invoice-repository.interface.d.ts +3 -1
  33. package/dist/interfaces/recurring-invoice-repository.interface.d.ts +3 -1
  34. package/dist/models/index.d.ts +1 -0
  35. package/dist/models/index.js +3 -1
  36. package/dist/models/index.js.map +1 -1
  37. package/dist/models/invoice-tax-component.model.d.ts +12 -0
  38. package/dist/models/invoice-tax-component.model.js +60 -0
  39. package/dist/models/invoice-tax-component.model.js.map +1 -0
  40. package/dist/models/invoice.model.d.ts +2 -0
  41. package/dist/models/invoice.model.js +5 -0
  42. package/dist/models/invoice.model.js.map +1 -1
  43. package/dist/models/payment-config.model.d.ts +1 -0
  44. package/dist/models/payment-config.model.js +4 -0
  45. package/dist/models/payment-config.model.js.map +1 -1
  46. package/dist/payments.module.d.ts +2 -0
  47. package/dist/payments.module.js +6 -0
  48. package/dist/payments.module.js.map +1 -1
  49. package/dist/services/default-payment-email-templates.js +20 -0
  50. package/dist/services/default-payment-email-templates.js.map +1 -1
  51. package/dist/services/invoice.service.d.ts +17 -0
  52. package/dist/services/invoice.service.js +123 -14
  53. package/dist/services/invoice.service.js.map +1 -1
  54. package/dist/services/payment-config.service.d.ts +1 -0
  55. package/dist/services/payment-config.service.js +1 -0
  56. package/dist/services/payment-config.service.js.map +1 -1
  57. package/dist/services/payment.service.d.ts +3 -0
  58. package/dist/services/payment.service.js +109 -16
  59. package/dist/services/payment.service.js.map +1 -1
  60. package/dist/services/recurring-invoice.service.d.ts +4 -1
  61. package/dist/services/recurring-invoice.service.js +66 -2
  62. package/dist/services/recurring-invoice.service.js.map +1 -1
  63. package/dist/types/index.d.ts +24 -0
  64. package/dist/types/index.js.map +1 -1
  65. package/dist/types/recurring-invoice.types.d.ts +20 -0
  66. package/package.json +1 -1
  67. package/prisma/payment-models.prisma +105 -5
package/CHANGELOG.md CHANGED
@@ -5,10 +5,125 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## Reading this file
9
+
10
+ Alongside the standard Keep-a-Changelog sections, releases may carry:
11
+
12
+ ### ⚠ External configuration required
13
+
14
+ Steps that must be performed **outside your repository** — a Stripe Dashboard
15
+ setting, DNS, a third-party console — to make the release work.
16
+
17
+ These get their own heading because they are a different species from API
18
+ changes. A breaking export, a new required field, a changed signature: your
19
+ compiler, your tests or a code review will catch all of them. A webhook
20
+ endpoint that was never subscribed to a required event is invisible to every
21
+ one of those, and the failure shows up as money moving at the provider while
22
+ your invoices quietly stay open.
23
+
24
+ Grep for `⚠ External configuration required` across the versions you are
25
+ skipping to build an upgrade checklist mechanically, rather than reading
26
+ prose for buried obligations. The marker was introduced in v0.4.0 and
27
+ **retro-applied** to earlier releases, so it is reliable for the whole file.
28
+
8
29
  ## [Unreleased]
9
30
 
31
+ ## [0.4.0] - 2026-08-20
32
+
33
+ > **Upgrade notes.** Two database changes, both in your own schema:
34
+ >
35
+ > 1. **If you copied the reference schema**, widen three columns —
36
+ > `Invoice.taxRate`, `RecurringInvoice.taxRate`, `PaymentConfig.defaultTaxRate`
37
+ > — from `numeric(5,4)` to `numeric(6,5)`. Non-destructive; see the first
38
+ > entry under Fixed. Skip if you chose your own column types.
39
+ > 2. **Only if you want invoices carrying more than one tax**, add the
40
+ > `InvoiceTaxComponent` table (and `RecurringInvoiceTaxComponent` if you use
41
+ > recurring billing) and implement the matching optional repository methods.
42
+ > Single-rate invoices need neither, and behave exactly as they do today.
43
+ >
44
+ > Includes everything in 0.3.1, which was never published separately. If you
45
+ > are on 0.3.0 and running Checkout alongside inline PaymentIntent flows, read
46
+ > the 0.3.1 notes below before moving traffic onto this — the double-application
47
+ > fix has an audit step attached to it.
48
+
49
+ ### Fixed
50
+ - **Tax rates lost precision at 4 decimal places, systematically over-charging.** `Invoice.taxRate`, `RecurringInvoice.taxRate` and `PaymentConfig.defaultTaxRate` were declared `Decimal(5, 4)` in `prisma/payment-models.prisma`, which cannot hold Quebec's 9.975% QST: `0.09975` stores as `0.0998`. The error is not random — it rounds up, so every affected invoice over-charges by the same margin (5¢ per $1,000). All three are now `Decimal(6, 5)`.
51
+ - **This is a reference-schema fix, not a package-code fix.** `DecimalLike` always handled five decimal places, and `recalculateTotals` has carried a comment about *"rates like 9.975%"* since before the column existed — we anticipated the rate and then shipped a column that could not hold it. Consumers who chose their own column types were never affected.
52
+ - **Migration required if you copied the reference schema.** Widening a numeric scale is non-destructive and no package behaviour changes, but values already truncated on write stay truncated: re-save any tax rate with more than four decimal places after migrating. `ALTER TABLE "Invoice" ALTER COLUMN "taxRate" TYPE numeric(6,5);` and likewise for `RecurringInvoice.taxRate` and `PaymentConfig.defaultTaxRate`.
53
+ - A spec now asserts the precision of every tax-rate column in the reference schema, and that every money column stays `Int`, so a future re-sync cannot narrow them back silently.
54
+
55
+ ### Added
56
+ - **Invoices can carry more than one tax.** `Invoice.taxRate` models a sale attracting exactly one tax, which is all most sales are — and cannot express one attracting two. A Quebec sale carries 5% GST *and* 9.975% QST: two taxes, levied by two governments, remitted separately. Blending them to 14.975% produces an invoice the client cannot claim input tax credits from and the supplier cannot reconcile two remittances against. US state + county + city sales tax stacks the same way. `CreateInvoiceInput.taxComponents` now accepts a list of `{ name, rate, authority?, registrationNumber?, sortOrder? }`, stored as `InvoiceTaxComponent` rows.
57
+ - **Nothing changes for single-rate invoices**, which remain the default and the entire behaviour when `taxComponents` is omitted. `taxRate` keeps working exactly as before, including against repository adapters that never implement the new optional methods.
58
+ - **The consumer owns the rate table; the package owns the arithmetic.** You decide which taxes apply and at what rate — that is tax law and it changes without warning. The package computes each component's `amount` from the invoice's taxable subtotal and re-prices them whenever line items change, so a component's amount cannot go stale against the invoice it belongs to.
59
+ - **Components are authoritative.** `taxAmount` is their sum, not a blended rate applied to the subtotal — each component is remitted to a different authority, so the invoice total is derived from them rather than the other way round. `taxRate` becomes the sum of the component rates, kept as a rollup for callers reading one number; re-deriving `taxAmount` from it can differ by a cent, and when it does, the components are right.
60
+ - **`IInvoiceRepository.findTaxComponentsByInvoice` / `replaceTaxComponents`** — new OPTIONAL methods, so v0.3.x adapters keep compiling untouched. Implement both or neither. Supplying components to a repository missing them throws `InvalidTaxConfigurationException` rather than silently dropping them, which would write an invoice whose stated tax covers taxes it cannot list.
61
+ - **`InvalidTaxConfigurationException`** (`INVALID_TAX_CONFIGURATION`) — thrown for `taxRate` and `taxComponents` together, an empty component list, an unnamed or duplicately-named component, or a negative/non-finite rate. Each of those would otherwise resolve to a silently wrong figure on a financial document; failing to issue the invoice is the better outcome.
62
+ - **`InvoiceTaxComponent` GraphQL type** and **`CreateTaxComponentInput`** are registered by the package — see "Reserved GraphQL Type Names". `InvoiceModel.taxComponents` exposes them, consumer-populated like `lineItems` (the package ships no `@ResolveField`).
63
+ - **Recurring templates carry tax components too.** `RecurringInvoiceService.create` accepts `taxComponents`, stored as `RecurringInvoiceTaxComponent` rows and copied onto every invoice the template generates, priced against that invoice's own subtotal. Without this, a template billing a client in a multi-tax province could only hold the blended rollup rate, and every invoice it generated would misstate which taxes were collected — silently, on a schedule, for as long as the template ran. That is a worse failure than the single-invoice case, not a lesser one.
64
+ - The template rows deliberately have **no `amount` column**: a template has no subtotal to price against. Amounts exist only on the generated invoices.
65
+ - **`IRecurringInvoiceRepository.findTaxComponentsByRecurringInvoice` / `replaceTaxComponents`** — new OPTIONAL methods, same contract as the invoice-level pair.
66
+ - **`PaymentConfig.taxRegistrationNumber`** — the supplier's tax registration number, rendered on invoices so the recipient can claim input tax credits; a Canadian invoice without the supplier's GST/HST number is not fully usable by the client. Stored and displayed verbatim, not validated — the format differs by jurisdiction. Per-tax overrides live on `InvoiceTaxComponent.registrationNumber`, because a supplier's GST and QST numbers are different. Optional on the interface so existing `PaymentConfig` adapters keep compiling. Settable through `UpdatePaymentConfigInput` and readable on `PaymentConfigModel`.
67
+ - **Refunds do not apportion tax across components**, and now say so. A refund reduces `amountPaid` and recalculates invoice status; it does not write back a reduced `taxAmount` or adjust component amounts. The components continue to state the tax charged on the original sale, which is what a credit note references. Whether a partial refund releases tax pro-rata or tax-last is a tax-law question, so the package does not answer it — documented under "Tax" in the README rather than left for someone to discover.
68
+ - **The default invoice email now shows a totals block** — subtotal, each tax on its own line with its name and registration number, discount, total. It previously showed only the amount due, so no tax was visible at all. Invoices without components print a single `Tax` line as they always effectively did. Consumers using their own `IPaymentEmailTemplateRenderer` receive `taxComponents` on the invoice and can render them however they like; the package still ships no PDF renderer.
69
+ - **`STRIPE_WEBHOOK_EVENTS`** — the Stripe webhook events this package routes, exported so consumers can assert a live Dashboard subscription list against the version they actually run. Split into `always` (every Stripe deployment) and `inlinePaymentIntent` (`payment_intent.succeeded`, needed only when calling `createPaymentIntent`), so a Checkout-only consumer is not told to subscribe to an event it never receives. Compute your own required set from your own configuration; see "Stripe Gateway & Webhooks" in the README for a preflight example, including handling `enabled_events: ['*']`.
70
+ - **`ALL_STRIPE_WEBHOOK_EVENTS`** — both groups flattened, for consumers using Checkout and inline flows together.
71
+ - **`webhook-events.spec.ts`** — asserts the constant against the gateway in **both** directions: every named event is really routed (driving `handleWebhook` end to end), and every event the routing switch handles is really named. The second is the one that matters — adding a `case` to `normalizeWebhookEvent` without updating the constant would ship consumers an incomplete list and quietly recreate the failure the constant exists to prevent.
72
+
73
+ - **`verifyStripeConfiguration(opts)`** — checks a Stripe deployment against what this version of the package needs and returns `StripeConfigFinding[]`. Verifies that the secret key authenticates (reporting the account id and display name), that the account has `charges_enabled`, that the publishable key is a `pk_` key belonging to the same account and mode as the secret, that a webhook endpoint exists for `webhookUrl` and is enabled, that it is subscribed to everything in `STRIPE_WEBHOOK_EVENTS`, and that its `api_version` matches the version the package pins. **Returns findings and never throws** — including for bad keys, permission errors and network failures — so consumers choose the policy: fail CI, fail boot, print a report, expose a health check.
74
+ - **Supplying `publishableKey` is treated as a declaration of intent.** It means the deployment uses inline PaymentIntent flows, which is what makes `payment_intent.succeeded` mandatory. A missing subscription for it is an `error` when the key is set and `info` when it is not, because a Checkout-only deployment genuinely never receives that event.
75
+ - **A check that could not run is reported, never passed.** A restricted key without webhook read permission yields `webhook_endpoints_unreadable` rather than an empty result, and an endpoint returned without its event list yields `webhook_events_unreadable` rather than being read as "subscribed to nothing" — absent is not empty. Keys that do not embed an account id yield `publishable_key_account_undetermined` rather than a guess, since a false mismatch would fail a correct deployment's CI. `webhook_endpoint_not_found` is a `warning` rather than an `error` because Stripe's v2 event destinations and CLI listeners are not returned by the endpoints API and would otherwise look missing. `enabled_events: ['*']` is treated as satisfying everything, and where several endpoints share a URL the enabled one is reported on.
76
+ - **The same-account check calibrates instead of assuming a key layout.** The secret key has just authenticated as the account, so if it embeds that account's id the property holds for these keys and the publishable key not embedding it is real evidence of a different account; if it does not, no conclusion is drawn. An earlier draft compared a fixed-width prefix of each key, which bled past a shorter account body into the random tail and produced a false `error` on a correct pair.
77
+ - Match on `code` (typed as `StripeConfigFindingCode`), not on `message`. Codes are public API; messages are written for humans and may be reworded.
78
+ - `client` accepts a `StripeVerificationClient` — a narrow structural slice of the SDK — so the function is testable without a live account and usable with a client configured for Connect or a proxy.
79
+ - **`STRIPE_API_VERSION`** — the Stripe API version the SDK client is pinned to, now a shared constant. `StripeGateway` and `verifyStripeConfiguration` both read it, so the version the package parses payloads as and the version it checks endpoints against cannot drift apart.
80
+
81
+ - **`stripe.verifyOnBoot?: boolean`** — runs `verifyStripeConfiguration` at application bootstrap and logs each finding at its severity. Defaults to `false` so nobody pays a network call at boot without asking. **Never throws and never blocks startup**: a check that fails or exceeds a 10-second timeout is logged as `UNVERIFIED` — explicitly not as a pass — so an unreachable Stripe API cannot stall a deploy. Consumers wanting a hard gate call `verifyStripeConfiguration` in a preflight or CI job, where failing is free; refusing to boot a running service because Stripe was briefly unreachable trades a configuration problem for an outage.
82
+ - **`stripe.webhookUrl?: string`** — the public URL of this app's webhook endpoint, used *only* by `verifyOnBoot`. The package does not route on it; the controller still mounts at the fixed path `POST /webhooks/stripe`. Without it, boot verification checks the keys and the account but cannot check the webhook subscription.
83
+ - **Setting `publishableKey` is now treated as a declaration of intent.** Previously the package held that signal at module init and did nothing with it: the key gates `createPaymentIntent`, so configuring it means the deployment uses inline PaymentIntent flows, which is exactly what makes `payment_intent.succeeded` mandatory. With `verifyOnBoot` on, a webhook endpoint missing that subscription now logs at **error** naming the consequence in operator terms — *inline payments will succeed at Stripe and the invoice will never close* — instead of at `info`. Without a publishable key the same gap stays informational, because a Checkout-only deployment genuinely never receives that event.
84
+
85
+ ### Changed
86
+ - **The CHANGELOG now marks external configuration with its own `### ⚠ External configuration required` heading**, and the marker has been **retro-applied to 0.2.1 and 0.3.0** so it is reliable across the whole file — anyone upgrading from 0.1.x reads those entries, and a convention that only exists going forward does not help them. Requirements satisfied outside the consumer's repository are a different species from API changes: a compiler, a test and a code review all catch a changed signature, and none of them can see an unsubscribed webhook endpoint. The 0.2.1 requirement was previously the second-to-last sentence of a seven-line paragraph under `Fixed`, which read as a description of what was fixed rather than as a task. Consumers can now grep the versions they are skipping and build an upgrade checklist mechanically. Convention documented for readers at the top of this file and for maintainers in `CONTRIBUTING.md`.
87
+
88
+ ### Testing
89
+ - **293 specs across 14 suites** (up from 100 at 0.3.0). Beyond the per-feature coverage above, three of those suites exist to stop a class of silent drift rather than to test a behaviour, and a fourth exercises the module as a consumer would get it:
90
+ - `webhook-events.spec.ts` — the exported webhook-event list against the gateway's routing switch, in both directions.
91
+ - `prisma-schema.spec.ts` — tax-rate precision and integer money columns in the reference schema, which ships in the tarball and is what consumers copy from.
92
+ - `published-docs.spec.ts` — the README's exception table and reserved GraphQL type names against what the package actually exports, and that the packaged version has a CHANGELOG entry. Both lists are load-bearing: a missing exception code leaves consumers unable to map an error, and a missing reserved type name means a consumer registers the same name and their application crashes at boot.
93
+ - `payments.module.spec.ts` boots the real module against stub repositories, because unit tests cannot see a provider registered in the wrong place or a lifecycle hook that never fires — those surface at a consumer's boot rather than ours.
94
+
95
+ ## [0.3.1] - 2026-08-20
96
+
97
+ > **Upgrade note.** This release repairs invoice totals going forward but does
98
+ > not touch rows already written. If you run Checkout alongside inline
99
+ > PaymentIntent flows, or use recurring auto-charge, audit your invoices for
100
+ > the double-application described below before upgrading traffic onto it —
101
+ > the corruption is confined to invoice totals, and the `Payment` ledger is
102
+ > intact, so affected invoices can be recomputed from it.
103
+
104
+ ### Fixed
105
+ - **A payment could be applied to its invoice twice, inflating `amountPaid`.** One Stripe payment emits two success events — `checkout.session.completed` (keyed by the Checkout Session id, `cs_…`) and `payment_intent.succeeded` (keyed by the PaymentIntent id, `pi_…`). They carry different event ids, so the `IWebhookIdempotencyRepository` dedup did not catch them, and `handlePaymentSucceeded` had no guard on the payment's current status: it transitioned an already-SUCCEEDED payment to SUCCEEDED again and re-ran `applyPaymentToInvoice`, adding the amount a second time. `handlePaymentFailed` and `handlePaymentProcessing` already gated on status; only the success path did not.
106
+ - **Who was affected.** Consumers subscribed to **both** events — which is every consumer running inline PaymentIntent flows alongside Checkout, since 0.2.1 requires `payment_intent.succeeded` for the inline surface. Full payments were saved by accident: the invoice reached `PAID`, which is not in `PAYABLE_STATUSES`, so the second application hit the non-payable warn branch. **Partial payments were not** — `PARTIALLY_PAID` is payable, so deposits and payment-plan installments double-counted, and an invoice could flip to `PAID` on half the money.
107
+ - **Detecting corrupted invoices.** Only the invoice totals drift — the `Payment` ledger stays correct, because the double application updated one existing row rather than inserting a second. So the corruption is detectable, and repairable, from the ledger: `amountPaid` should equal `SUM(payments WHERE status IN ('SUCCEEDED','PARTIALLY_REFUNDED','REFUNDED')) - SUM(succeeded refunds)`, and any invoice whose `amountPaid` **exceeds** that sum has drifted. Recompute `amountPaid`/`amountDue`/`status` from it; `RefundService.recalculateInvoiceAmounts` implements exactly this formula. Re-check the invoice `status` afterwards — an invoice pushed to `PAID` on a partial payment needs to go back to `PARTIALLY_PAID` and, if its due date has passed, `OVERDUE`.
108
+ - **A flagged invoice is not proof this defect reached you.** `amountPaid` is net of refunds, so *any* cause of drift between an invoice and its ledger shows up identically — most commonly a refund recorded without recalculating the invoice it belonged to. Before concluding it was double-applied, compare the gap: a double-application overshoots by **exactly one SUCCEEDED card payment's amount** (and that payment carries a `provider`), whereas refund drift overshoots by the **sum of that invoice's SUCCEEDED refunds**. The repair is the same either way — recompute from the ledger — but the cause, and whether you need to audit anything else, is not. (Reported by a consumer whose only flagged row turned out to be a seeded fixture.)
109
+ - **Duplicate confirmation emails.** The second application also re-sent the payment confirmation email to the customer. Fixed by the same change.
110
+ - **Recurring auto-charges were affected too.** `recordAutoChargePayment` creates its `Payment` row already `SUCCEEDED` and keyed by the PaymentIntent id, then applies the amount. Stripe fires `payment_intent.succeeded` for that same off-session charge, which the old handler treated as a fresh success and applied again. Any consumer using recurring billing *and* subscribed to `payment_intent.succeeded` double-counted every auto-charged partial payment — this was not limited to the Checkout + inline combination.
111
+ - **`checkout.session.completed` could fail permanently when `payment_intent.succeeded` arrived first.** The intent event could not find the row stored under the session id, so it took the create-from-metadata path and inserted its own record under the PaymentIntent id. The checkout event then tried to swap the original row to that same id and hit `@@unique([provider, providerPaymentId])` — throwing through all three controller retries and leaving the original row stranded in `PENDING` forever, where the reconciliation sweep and `findReusablePendingByInvoiceId` would both keep picking it up. (The invoice itself was credited once and correctly, by the webhook-created row — this path stuck the webhook rather than corrupting totals. It requires the consumer to have put `invoiceId` in the PaymentIntent metadata via `gatewayOptions.paymentIntentData`; without it the intent event simply logged and skipped.) `handlePaymentSucceeded` now resolves the target row **canonically** — by the PaymentIntent id, which both events carry in `providerData` — so the outcome no longer depends on delivery order, and the leftover pre-swap row is marked `CANCELLED` with an explanatory note instead of stranded.
112
+ - **Concurrent delivery of both events no longer double-applies.** The payment row is now re-read under `findByIdForUpdate` *inside* the transaction before its status is checked, so two simultaneous deliveries cannot both observe the pre-transition status. Lock order is payment-then-invoice, matching `confirmPayment`, so the two cannot deadlock. When both transactions race past the lookup, the `@@unique([provider, providerPaymentId])` constraint on `Payment` makes the loser roll back — un-recording its idempotency row with it — and Stripe's retry re-runs it against the settled record. That constraint (already relied on by `createPendingPayment`) is load-bearing; adapters must not drop it.
113
+
114
+ ### Changed
115
+ - **`handlePaymentSucceeded` now gates on payment status.** The amount is applied only from `PENDING`, `PROCESSING`, or `FAILED`. `FAILED` is included deliberately: Stripe reuses one PaymentIntent across retries, so a declined attempt followed by a successful retry on the same intent is a real money-moving flow, and `handlePaymentFailed` never touched invoice amounts. `SUCCEEDED` (already applied), `REFUNDED`/`PARTIALLY_REFUNDED` (would silently undo a refund on the invoice), `PENDING_CONFIRMATION`/`REJECTED` (the e-transfer lifecycle, owned by `confirmPayment`/`rejectPayment`), and `CANCELLED` are skipped. Skips still record the event id so Stripe stops retrying, and log at a severity matching how surprising they are — the already-applied case is routine for dual-subscribed endpoints and logs at `log`; the rest log at `warn`.
116
+ - **Payments created from a webhook are now keyed by the canonical PaymentIntent id** rather than the event's own `providerPaymentId`, so a later duplicate event resolves to the same row. Affects only the create-from-metadata path (provider-dashboard charges); rows created by `createPendingPayment` are unchanged until the existing session-id → intent-id swap.
117
+ - **`handlePaymentSucceeded` now records and credits the amount the provider actually charged**, rather than the amount the pending `Payment` row was created with. A pending row holds an *intention*; the success event holds what was taken. Previously the invoice was credited with the intention, so an under-charged payment could still mark the invoice `PAID` on money that never arrived. The row's `amount` is re-stated at the same time, so the payment ledger and the invoice agree and `RefundService.recalculateInvoiceAmounts` recomputes from the real figure. The divergence is reachable via `createPaymentSession`, whose `gatewayOptions` are spread at the top level of the Checkout Session call — a consumer enabling `allow_promotion_codes` or `automatic_tax` lets Stripe settle on a total the pending row never saw. Any drift is logged at `warn`.
118
+ - Two cases deliberately keep the recorded amount. **A currency mismatch** logs at `error` and credits the recorded amount — the package will not invent an exchange rate, and a human has to resolve it. **A non-positive event amount** logs at `warn` and keeps the recorded amount: `handleCheckoutSessionCompleted` falls back to `session.amount_total ?? 0` when its PaymentIntent retrieve fails, so zero means "unknown", not "free".
119
+ - **+28 specs** covering `PaymentService.handlePaymentSucceeded`, total now 128 across 7 suites. They run against an in-memory store that enforces the `Payment` unique constraint and rolls back on throw, so the ordering, concurrency and rollback behaviour above is exercised rather than asserted in prose. The suite also pins the *opposite* failure mode — that legitimate repeat payments (a second PaymentIntent on the same invoice, a card payment on top of a manual one) still apply in full.
120
+
10
121
  ## [0.3.0] - 2026-05-28
11
122
 
123
+ ### ⚠ External configuration required
124
+
125
+ - **Stripe Dashboard:** subscribe your webhook endpoint to `payment_intent.succeeded` before enabling the inline PaymentIntent surface. Carried forward from 0.2.1 and load-bearing here: setting `stripe.publishableKey` and calling `createPaymentIntent` moves you onto a flow whose success signal is this event, not `checkout.session.completed`. Miss it and inline payments succeed at Stripe while the invoice stays open with a live "Pay now" button. *(Marker retro-applied in 0.4.0.)*
126
+
12
127
  ### Added
13
128
  - **`PaymentGateway.createPaymentIntent(params)`** — new (optional) interface method. Returns `{ providerPaymentId, clientSecret, status, reused, providerStatus? }` for an inline payment flow (Stripe Elements / `flutter_stripe` / equivalents). The Stripe implementation reuses an existing PaymentIntent when the consumer's payment repository has a still-confirmable PENDING row for the same invoice (via the new optional `IPaymentRepository.findReusablePendingByInvoiceId`). Idempotent on the Stripe side via a stable `idempotencyKey` per `(invoiceId, attemptId)`; pass a request-scoped UUID as `attemptId` for the most robust idempotency, or omit and the gateway falls back to invoice-only. Non-Stripe gateways without an implementation throw `PaymentIntentOperationNotSupportedException`. Use `GatewayRegistryService.getInlineCapable('stripe')` to obtain a type-narrowed gateway where the methods are non-optional.
14
129
  - **`PaymentGateway.cancelPaymentIntent(providerPaymentId)`** — new (optional) interface method. Cancels a still-cancellable PaymentIntent and returns `{ success, alreadyTerminal }`. Distinct from `cancelPayment` (which is the refund-adjacent variant used by `voidInvoice` cleanup). Throws `PaymentIntentNotCancellableException` when the intent is mid-3DS — callers should retry after the challenge resolves or expires.
@@ -37,6 +152,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
152
 
38
153
  ## [0.2.1] - 2026-05-28
39
154
 
155
+ ### ⚠ External configuration required
156
+
157
+ - **Stripe Dashboard:** subscribe your webhook endpoint to `payment_intent.succeeded`. Required for direct-PaymentIntent flows — anything not going through Checkout, including the inline Stripe Elements surface added in 0.3.0 and any consumer calling `paymentIntents.create` directly. Without it those payments succeed at Stripe and the invoice never closes. *(Marker retro-applied in 0.4.0; the requirement itself shipped with this release, stated inside the `Fixed` entry below.)*
158
+
40
159
  ### Fixed
41
160
  - **`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.
42
161
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @ambushsoftworks/nestjs-payments-graphql
2
2
 
3
- Production-grade payments module for NestJS with GraphQL support. Invoicing, Stripe payment processing, refunds, payment plans, recurring invoicing with auto-charge, e-transfer, and composable email notifications — with zero database coupling.
3
+ Production-grade payments module for NestJS with GraphQL support. Invoicing (including multi-component tax), Stripe payment processing, refunds, payment plans, recurring invoicing with auto-charge, e-transfer, and composable email notifications — with zero database coupling.
4
4
 
5
5
  ## Table of Contents
6
6
 
@@ -10,8 +10,12 @@ Production-grade payments module for NestJS with GraphQL support. Invoicing, Str
10
10
  - [Architecture](#architecture)
11
11
  - [Features](#features)
12
12
  - [Invoicing](#invoicing)
13
+ - [Tax](#tax)
13
14
  - [Stripe Gateway & Webhooks](#stripe-gateway--webhooks)
14
15
  - [Stripe-specific session options](#stripe-specific-session-options-gatewayoptions)
16
+ - [Inline PaymentIntent (Stripe Elements)](#inline-paymentintent-stripe-elements)
17
+ - [Verifying your Stripe configuration](#verifying-your-stripe-configuration)
18
+ - [Reconciliation (webhook backstop)](#reconciliation-webhook-backstop)
15
19
  - [Refunds](#refunds)
16
20
  - [Payment Plans](#payment-plans)
17
21
  - [Recurring Invoices](#recurring-invoices)
@@ -110,7 +114,7 @@ Repositories you must implement:
110
114
 
111
115
  | Interface | Purpose |
112
116
  |-----------|---------|
113
- | `IInvoiceRepository` | Invoice + line item persistence |
117
+ | `IInvoiceRepository` | Invoice + line item persistence (plus optional tax components — see [Tax](#tax)) |
114
118
  | `IPaymentRepository` | Payment records |
115
119
  | `IRefundRepository` | Refund records |
116
120
  | `IPaymentConfigRepository` | Per-division payment config + providers |
@@ -125,6 +129,14 @@ Opt-in interfaces:
125
129
  | `IRecurringInvoiceRepository` | `features.recurringInvoices = true` |
126
130
  | `IPaymentCustomerRepository` | `features.recurringInvoices = true` |
127
131
 
132
+ Some interfaces also carry **optional methods** that unlock a capability without breaking existing adapters — implement them only if you need what they enable:
133
+
134
+ | Optional method | On | Enables |
135
+ |---|---|---|
136
+ | `findReusablePendingByInvoiceId` | `IPaymentRepository` | Durable PaymentIntent reuse for inline flows (v0.3.0) |
137
+ | `findTaxComponentsByInvoice` + `replaceTaxComponents` | `IInvoiceRepository` | Invoices carrying more than one tax (v0.4.0) |
138
+ | `findTaxComponentsByRecurringInvoice` + `replaceTaxComponents` | `IRecurringInvoiceRepository` | The same, on recurring templates (v0.4.0) |
139
+
128
140
  ### 2. Register the module
129
141
 
130
142
  ```typescript
@@ -250,15 +262,63 @@ The dynamic module is registered as `global: true` — any module in your app ca
250
262
  - Create DRAFT invoices with optional line items.
251
263
  - Line item edits allowed only in DRAFT; totals auto-recalculate on mutation.
252
264
  - `InvoiceNumberService` generates globally-unique numbers against a per-division prefix, retrying on unique-constraint collisions up to 5 times.
253
- - Tax rate resolution: use the value passed on `createInvoice` if provided, otherwise `PaymentConfig.defaultTaxRate` for the division, otherwise `0`.
265
+ - Tax rate resolution: use the value passed on `createInvoice` if provided, otherwise `PaymentConfig.defaultTaxRate` for the division, otherwise `0`. Invoices attracting more than one tax pass `taxComponents` instead — see [Tax](#tax).
254
266
  - Status transitions: `DRAFT → SENT → {PARTIALLY_PAID, PAID, OVERDUE, VOID, REFUNDED}`.
255
267
  - Invoice `metadata` is a Stripe-style `Record<string, string>` for consumer-specific context.
256
268
 
269
+ #### Tax
270
+
271
+ Most sales attract exactly one tax, and `taxRate` on its own covers them:
272
+
273
+ ```typescript
274
+ await invoiceService.createInvoice(divisionId, {
275
+ clientDetailsId,
276
+ taxRate: 0.13, // 13% HST
277
+ lineItems: [...],
278
+ }, userId);
279
+ ```
280
+
281
+ Some attract two. A Quebec sale carries **5% GST and 9.975% QST** — two taxes, levied by two governments, remitted separately. Blending them to 14.975% produces an invoice the client cannot claim input tax credits from and the supplier cannot reconcile two remittances against. US state + county + city sales tax stacks the same way. Pass `taxComponents` instead:
282
+
283
+ ```typescript
284
+ await invoiceService.createInvoice(divisionId, {
285
+ clientDetailsId,
286
+ taxComponents: [
287
+ { name: 'GST', rate: 0.05, authority: 'CRA', registrationNumber: '123456789 RT0001' },
288
+ { name: 'QST', rate: 0.09975, authority: 'Revenu Québec', registrationNumber: '1234567890 TQ0001' },
289
+ ],
290
+ lineItems: [...],
291
+ }, userId);
292
+ ```
293
+
294
+ **The consumer owns the rate table; the package owns the arithmetic.** Which taxes apply to a given sale, at what rate, and whether a supplier charges tax at all is tax law — it varies by jurisdiction and changes without notice, and the package does not model it. You decide the components; the package computes each `amount` from the invoice's taxable subtotal and re-prices them whenever line items change, so a component's amount can never go stale against the invoice it belongs to.
295
+
296
+ **Components are authoritative.** `taxAmount` is their sum, not a blended rate applied to the subtotal — each component is remitted to a different authority. `taxRate` becomes the sum of the component rates, kept as a rollup for surfaces that read one number; re-deriving `taxAmount` from it can differ by a cent, and when it does the components are right.
297
+
298
+ `taxRate` and `taxComponents` are **mutually exclusive** — supplying both throws `InvalidTaxConfigurationException`, as do an empty list, an unnamed or duplicately-named component, and a negative or non-finite rate. Each of those would otherwise put a silently wrong number on a financial document.
299
+
300
+ To store components, implement two OPTIONAL methods on your `IInvoiceRepository`:
301
+
302
+ ```typescript
303
+ findTaxComponentsByInvoice(invoiceId: string): Promise<InvoiceTaxComponent[]>;
304
+ replaceTaxComponents(invoiceId: string, components: CreateInvoiceTaxComponentData[]): Promise<InvoiceTaxComponent[]>;
305
+ ```
306
+
307
+ Implement both or neither. `replaceTaxComponents` must be atomic — the package calls it immediately after writing `taxAmount`, and the two must agree. Supplying components to a repository without them throws rather than dropping them silently. Copy `InvoiceTaxComponent` from [`prisma/payment-models.prisma`](./prisma/payment-models.prisma); single-rate consumers need no new table.
308
+
309
+ **Recurring templates** take `taxComponents` the same way, stored without amounts (a template has no subtotal) and priced onto each generated invoice. `IRecurringInvoiceRepository` has the matching optional pair.
310
+
311
+ **Supplier registration number.** Set `PaymentConfig.taxRegistrationNumber` for the number that appears on every invoice — a Canadian invoice without the supplier's GST/HST number is not fully usable by the recipient. Where two authorities are involved and the supplier has a different number for each, set `registrationNumber` per component; it takes precedence. The package stores and renders these verbatim and does not validate the format, which differs by jurisdiction.
312
+
313
+ **Rendering.** The default invoice email prints a totals block: subtotal, each tax on its own line with its name and registration number, discount, total. Supply your own `IPaymentEmailTemplateRenderer` to change it — `params.invoice.taxComponents` carries the list. The package ships no PDF renderer; invoice PDFs stay with the consumer.
314
+
315
+ **Refunds do not apportion tax across components.** A refund reduces the invoice's `amountPaid` and recalculates its status; it does not write back a reduced `taxAmount`, and it does not adjust component amounts. The components continue to state the tax that was charged on the original sale, which is what a credit note references. If your jurisdiction requires a tax-adjusted credit note, generate it in your own layer — deciding whether a partial refund releases tax pro-rata or tax-last is a tax-law question, and the package does not answer those.
316
+
257
317
  ### Stripe Gateway & Webhooks
258
318
 
259
319
  When `stripe` config is provided, the module registers `StripeGateway` and mounts `POST /webhooks/stripe`. Webhook security relies entirely on Stripe signature verification. Behaviour:
260
320
 
261
- - **Idempotency** — every event ID is recorded via `IWebhookIdempotencyRepository` before the handler runs; replays no-op.
321
+ - **Idempotency** — two levels, both required. *Event level*: each event ID is recorded via `IWebhookIdempotencyRepository` (inside the handler's transaction, so a failed attempt rolls the record back and Stripe's retry can re-run it); replays no-op. *Payment level*: one payment emits more than one success event — a Checkout payment fires both `checkout.session.completed` and `payment_intent.succeeded`, under two different event IDs and two different provider payment IDs. `handlePaymentSucceeded` resolves both to a single `Payment` row by the PaymentIntent ID and applies the amount to the invoice only once, whichever event arrives first. Subscribing to both events is safe and expected. (Before v0.3.1 it was not: the second event re-applied the amount, inflating `amountPaid` on partially-paid invoices.)
262
322
  - **Retries** — `payment_intent.succeeded` events retry up to 3 times with a 2-second delay if the corresponding `Payment` row has not yet been created.
263
323
  - **Unrecognized event types** — Stripe fires many events per checkout (`payment_intent.created`, `charge.updated`, `charge.succeeded`, …) that the package does not route. These return 200 with a `debug`-level log and Stripe does not retry. The gateway surfaces them as an `IgnoredWebhookEvent` (`{ type: 'ignored', eventId, eventType }`) — if you implement a custom `PaymentGateway`, your `handleWebhook` must return this sentinel for unrouted events instead of throwing.
264
324
  - **Error reporting** — configure `webhook.errorReporter` to pipe exceptions into Sentry/GlitchTip/etc.
@@ -271,6 +331,43 @@ When `stripe` config is provided, the module registers `StripeGateway` and mount
271
331
  - `payment_intent.payment_failed` — surfaces declines back into the payment record.
272
332
  - `charge.refunded` — closes the loop on refunds.
273
333
 
334
+ **Do not transcribe that list by hand.** As of v0.4.0 the package exports it, so your deployment can assert its live Stripe configuration against the events this version actually routes instead of against a README you read once:
335
+
336
+ ```typescript
337
+ import {
338
+ STRIPE_WEBHOOK_EVENTS,
339
+ ALL_STRIPE_WEBHOOK_EVENTS,
340
+ } from '@ambushsoftworks/nestjs-payments-graphql';
341
+
342
+ // Compute your own required set from your own configuration.
343
+ const required = [
344
+ ...STRIPE_WEBHOOK_EVENTS.always,
345
+ ...(usingInlinePaymentIntent ? STRIPE_WEBHOOK_EVENTS.inlinePaymentIntent : []),
346
+ ];
347
+
348
+ // Fail a preflight / CI job / health check when the endpoint falls behind.
349
+ const endpoint = await stripe.webhookEndpoints.retrieve(endpointId);
350
+ const subscribed = new Set(endpoint.enabled_events);
351
+ const missing = subscribed.has('*')
352
+ ? []
353
+ : required.filter((e) => !subscribed.has(e));
354
+
355
+ if (missing.length) {
356
+ throw new Error(
357
+ `Stripe endpoint ${endpointId} is missing ${missing.join(', ')}. ` +
358
+ `Payments will succeed at Stripe and invoices will not close.`,
359
+ );
360
+ }
361
+ ```
362
+
363
+ The split is deliberate: `always` is what every Stripe deployment needs, and `inlinePaymentIntent` is required only if you call `createPaymentIntent`, because that flow's success signal is the PaymentIntent event rather than the Checkout Session one. A Checkout-only consumer genuinely never receives `payment_intent.succeeded`. `ALL_STRIPE_WEBHOOK_EVENTS` is both groups flattened, for consumers using both flows.
364
+
365
+ Handle `enabled_events: ['*']` as shown — Stripe allows a wildcard subscription, and a naive superset check reports it as missing everything.
366
+
367
+ Subscribing to more events than you need is safe. A Checkout payment emits both `checkout.session.completed` and `payment_intent.succeeded`; since v0.3.1 the package applies that payment to the invoice exactly once no matter how many success events describe it.
368
+
369
+ A spec asserts this constant against the gateway's routing switch, so the exported list cannot drift from the events the code handles.
370
+
274
371
  > **Payable-status requirement.** `PaymentService.handlePaymentSucceeded` only applies an incoming payment if the invoice is in `SENT`, `PARTIALLY_PAID`, or `OVERDUE`. If a consumer creates a DRAFT invoice and starts checkout without first transitioning it to SENT, the SUCCEEDED payment lands in the `Payment` table but the invoice stays in DRAFT with `amountPaid=0`.
275
372
  >
276
373
  > As of v0.1.3 this fires a WARN log identifying the payment, provider, invoice, and status. As of v0.2.0, call `InvoiceService.ensureSent(invoiceId, eTransferEnabled, eTransferAutoDeposit, eTransferConfig?)` from your checkout flow immediately before `gateway.createPaymentSession(...)`. `ensureSent` is idempotent: DRAFT → SENT (delegates to `sendInvoice`), already-SENT/PARTIALLY_PAID/OVERDUE → returns the invoice unchanged with no re-emailed notification, PAID/VOID/REFUNDED → throws `InvalidInvoiceStateException`. `sendInvoice` itself remains strict (DRAFT-only) for callers that want the strictness.
@@ -350,6 +447,8 @@ If you forget, the first call to `createPaymentIntent` throws `PublishableKeyNot
350
447
 
351
448
  **Subscribe to `payment_intent.succeeded`.** Inline payments succeed at Stripe via a direct PaymentIntent confirmation (no Checkout session). The package handles `payment_intent.succeeded` as of v0.2.1 — your Stripe Dashboard webhook config must list this event or invoices will never close. See the bullet list at the top of "Stripe Gateway & Webhooks".
352
449
 
450
+ Do not take that on trust: as of v0.4.0, [`verifyStripeConfiguration`](#verifying-your-stripe-configuration) checks the live subscription for you, and reports a missing `payment_intent.succeeded` as an `error` precisely when a publishable key is configured. This is the one requirement in this package that lives entirely outside your repository, where no diff, type error or review can catch it.
451
+
353
452
  **Reuse-via-pending-row.** `StripeGateway.createPaymentIntent` looks up an existing PENDING payment for the same invoice via `IPaymentRepository.findReusablePendingByInvoiceId(invoiceId, provider)` and, when one is found, retrieves the intent from Stripe. The retrieved intent is reused **only when all of the following hold**:
354
453
 
355
454
  - its status is still confirmable (`requires_payment_method`, `requires_confirmation`, or `requires_action`),
@@ -477,6 +576,124 @@ export class ClientInvoicesResolver {
477
576
  }
478
577
  ```
479
578
 
579
+ #### Verifying your Stripe configuration
580
+
581
+ The subscription list above, the keys, and the account's ability to charge all live in the Stripe Dashboard — outside your repository, where no diff, type error, code review or CI job can see them. `verifyStripeConfiguration` checks them against what this version of the package actually needs and reports what's wrong.
582
+
583
+ ```typescript
584
+ import { verifyStripeConfiguration } from '@ambushsoftworks/nestjs-payments-graphql';
585
+
586
+ const findings = await verifyStripeConfiguration({
587
+ secretKey: process.env.STRIPE_SECRET_KEY!,
588
+ publishableKey: process.env.STRIPE_PUBLISHABLE_KEY, // omit for Checkout-only
589
+ webhookUrl: `${process.env.API_URL}/webhooks/stripe`,
590
+ });
591
+
592
+ for (const f of findings) {
593
+ console.log(`[${f.severity}] ${f.code}: ${f.message}`);
594
+ if (f.remediation) console.log(` → ${f.remediation}`);
595
+ }
596
+
597
+ if (findings.some((f) => f.severity === 'error')) process.exit(1);
598
+ ```
599
+
600
+ **It returns findings and never throws**, including for bad keys, permission errors and network failures. That leaves the policy to you — fail a CI job, fail a boot, print a setup report, expose a health check. Throwing would impose one of those on everyone.
601
+
602
+ What it checks:
603
+
604
+ | Check | Severity when wrong |
605
+ |---|---|
606
+ | Secret key authenticates; reports account id and display name | `error` if it doesn't |
607
+ | Account has `charges_enabled` | `error` — test payments still succeed, so a dead account looks healthy |
608
+ | Publishable key is a `pk_` key | `error` — a secret key here gets served to browsers |
609
+ | Publishable and secret keys are the same mode (test/live) | `error` — presents to customers as a card decline |
610
+ | Publishable and secret keys are the same account | `error` — same misdiagnosis |
611
+ | Webhook endpoint exists for `webhookUrl` and is enabled | `warning` / `error` |
612
+ | Endpoint is subscribed to everything in `STRIPE_WEBHOOK_EVENTS.always` | `error` |
613
+ | Endpoint is subscribed to `payment_intent.succeeded` | `error` when `publishableKey` is set, `info` otherwise |
614
+ | Endpoint's `api_version` matches the version the package pins | `warning` |
615
+
616
+ Two behaviours worth knowing:
617
+
618
+ - **Supplying `publishableKey` is a declaration of intent.** It means "we use inline PaymentIntent flows", which is what makes `payment_intent.succeeded` mandatory rather than optional. Omit it and a missing subscription for that event is reported at `info`, because a Checkout-only deployment genuinely never receives it.
619
+ - **A check that couldn't run is reported, never passed.** A restricted key without webhook read permission yields `webhook_endpoints_unreadable`, not an empty result; an endpoint returned without its event list yields `webhook_events_unreadable` rather than "subscribed to nothing". Keys that don't embed an account id yield `publishable_key_account_undetermined` rather than a guess — a false mismatch would fail a correct deployment's CI. Similarly, `webhook_endpoint_not_found` is a `warning` rather than an `error`, because Stripe's v2 event destinations and CLI listeners aren't returned by the endpoints API and will look missing.
620
+
621
+ The same-account check calibrates rather than assuming a key layout: the secret key has just authenticated *as* the account, so if it embeds that account's id the property holds for your keys and the publishable key not embedding it is real evidence. If it doesn't, no conclusion is drawn.
622
+
623
+ Match on `code` (typed as `StripeConfigFindingCode`), not on `message` — messages are written for humans and may be reworded; codes are public API.
624
+
625
+ For tests, or to use a client configured your own way, pass `client`:
626
+
627
+ ```typescript
628
+ await verifyStripeConfiguration({
629
+ secretKey: 'sk_test_…',
630
+ client: myStubOrConfiguredStripeInstance,
631
+ });
632
+ ```
633
+
634
+ ##### Checking at boot
635
+
636
+ Set `stripe.verifyOnBoot` to run the same check at application bootstrap and log each finding at its severity:
637
+
638
+ ```typescript
639
+ stripe: {
640
+ secretKey: cfg.get('STRIPE_SECRET_KEY'),
641
+ webhookSecret: cfg.get('STRIPE_WEBHOOK_SECRET'),
642
+ publishableKey: cfg.get('STRIPE_PUBLISHABLE_KEY'),
643
+ webhookUrl: `${cfg.get('API_URL')}/webhooks/stripe`, // needed to check the subscription
644
+ verifyOnBoot: cfg.get('NODE_ENV') !== 'test',
645
+ }
646
+ ```
647
+
648
+ Defaults to `false`, so nobody pays a network call at boot without asking for it. `webhookUrl` is used *only* by this check — the controller still mounts at the fixed path `POST /webhooks/stripe`. Without it, boot verification checks the keys and the account but not the subscription, which is the setting most likely to be wrong.
649
+
650
+ **It never throws and never blocks startup.** Findings are logged; a check that fails or times out is logged as `UNVERIFIED` — explicitly not as a pass — and abandoned after 10 seconds so an unreachable Stripe API cannot stall a deploy. If you want a hard gate, call `verifyStripeConfiguration` in a preflight or CI job instead, where failing costs nothing. Refusing to boot a running service because Stripe was briefly unreachable trades a configuration problem for an outage.
651
+
652
+ Setting `publishableKey` raises the stakes: it declares the deployment uses inline PaymentIntent flows, so an endpoint not subscribed to `payment_intent.succeeded` logs at **error** naming the consequence, rather than as information.
653
+
654
+ ```
655
+ ERROR [StripeConfigVerifier] [webhook_inline_events_missing] A publishable key is
656
+ configured, so this deployment uses inline PaymentIntent flows — but the endpoint is
657
+ not subscribed to payment_intent.succeeded. Inline payments will succeed at Stripe and
658
+ the invoice will never close, leaving a live "Pay now" button on an invoice the
659
+ customer already paid. → Add payment_intent.succeeded to the endpoint's events in the
660
+ Stripe Dashboard.
661
+ ERROR [StripeConfigVerifier] Stripe configuration has 1 error(s) and 0 warning(s).
662
+ Payments will not work correctly until these are resolved.
663
+ ```
664
+
665
+ #### Reconciliation (webhook backstop)
666
+
667
+ Webhooks are best-effort. An endpoint can be down, misconfigured, unsubscribed from a required event, or exhaust Stripe's ~3 days of retries — and in every one of those cases money moves at Stripe while the local invoice stays open. Reconciliation closes that gap after the fact, independent of the cause.
668
+
669
+ `PaymentService` ships the pieces; the schedule is yours (the package deliberately ships no schedulers):
670
+
671
+ ```typescript
672
+ // Consumer-side, e.g. @nestjs/schedule
673
+ @Cron(CronExpression.EVERY_15_MINUTES)
674
+ async reconcileStalePayments() {
675
+ const cutoff = new Date(Date.now() - 15 * 60 * 1000);
676
+ const stale = await this.paymentService.findStalePayments(
677
+ ['PENDING', 'PROCESSING'],
678
+ cutoff,
679
+ );
680
+
681
+ for (const payment of stale) {
682
+ try {
683
+ await this.paymentService.reconcilePayment(payment.id);
684
+ } catch (err) {
685
+ this.logger.error(`Reconcile failed for ${payment.id}`, err);
686
+ }
687
+ }
688
+ }
689
+ ```
690
+
691
+ `reconcilePayment` fetches the payment's current status from the gateway and, when the provider says `succeeded` and the local row is still `PENDING`/`PROCESSING`, applies the amount to the invoice inside a transaction, fires `onInvoicePaid`, and sends the confirmation email — the same effects the webhook would have had. It is safe to run against payments that are already settled: a status that already matches the provider is a no-op, and a payment that is already `SUCCEEDED` is never re-applied.
692
+
693
+ Uses `IPaymentRepository.findStaleByStatuses`, already part of the repository interface. The reference schema indexes `Payment` on `(invoiceId, status)`; if your sweep interval is short or the table is large, consider adding `(status, createdAt)` to match the query shape.
694
+
695
+ > Strongly recommended for any production deployment. Had this been running, the class of incident that motivated the v0.3.1 fixes — an endpoint not subscribed to `payment_intent.succeeded`, so inline payments succeeded at Stripe and invoices never closed — would have self-healed within one sweep interval instead of being found by hand.
696
+
480
697
  ### Refunds
481
698
 
482
699
  Full and partial refunds through Stripe (when the originating payment went through Stripe) or manual refund records. All amounts in cents.
@@ -574,6 +791,8 @@ You can override:
574
791
 
575
792
  Covered emails: invoice sent, e-transfer instructions, payment confirmation, overdue reminder, refund confirmation.
576
793
 
794
+ The invoice email prints a totals block — subtotal, each tax on its own line with its name and registration number, discount, total. Invoices without tax components print a single `Tax` line. `InvoiceEmailParams.invoice` is an `InvoiceWithItems`, so a custom renderer receives `lineItems` and `taxComponents` and can lay them out however it likes. The package ships no PDF renderer; invoice PDFs stay with the consumer.
795
+
577
796
  ### Event Listeners
578
797
 
579
798
  Register a single `IPaymentEventListener` to react to domain events without touching services. All handlers except `onInvoicePaid` are optional.
@@ -632,6 +851,8 @@ stripe: {
632
851
  secretKey: string;
633
852
  webhookSecret: string;
634
853
  publishableKey?: string; // v0.3.0+ — required only for inline PaymentIntent flows
854
+ webhookUrl?: string; // v0.4.0+ — used only by verifyOnBoot
855
+ verifyOnBoot?: boolean; // v0.4.0+ — default false
635
856
  }
636
857
  ```
637
858
 
@@ -639,6 +860,12 @@ Omit the entire `stripe` key for non-Stripe deployments. A warning logs at start
639
860
 
640
861
  `publishableKey` is **optional**. It is required only if your consumer uses `StripeGateway.createPaymentIntent` (inline Stripe Elements) or exposes a `stripePublicConfig` GraphQL query. Validation happens at first call rather than at module init — consumers using only Checkout-redirect never need to set this. Per Stripe's docs, the publishable key is safe to ship to any client; it cannot create charges on its own.
641
862
 
863
+ Setting it also declares intent: it means this deployment uses inline PaymentIntent flows, which is what makes `payment_intent.succeeded` a required webhook subscription rather than an optional one. `verifyOnBoot` acts on that — see [Verifying your Stripe configuration](#verifying-your-stripe-configuration).
864
+
865
+ `verifyOnBoot` runs `verifyStripeConfiguration` at application bootstrap and logs each finding at its severity. Defaults to `false` so nobody pays a network call at boot without asking. It never throws and never blocks startup — a check that fails or exceeds a 10-second timeout is logged as `UNVERIFIED`, explicitly not as a pass. Use `verifyStripeConfiguration` directly in a preflight or CI job if you want a hard gate.
866
+
867
+ `webhookUrl` is your app's public webhook URL (your base URL + `/webhooks/stripe`). It is used **only** by `verifyOnBoot`; the controller still mounts at the fixed path regardless. Without it, boot verification checks the keys and the account but not the webhook subscription — the setting most likely to be wrong.
868
+
642
869
  ### Email Options
643
870
 
644
871
  ```typescript
@@ -703,11 +930,11 @@ import {
703
930
 
704
931
  The package registers these names on module import. Consumers **must not** redefine or re-register them, or GraphQL will throw `"type already registered"`.
705
932
 
706
- **Object types:** `Invoice`, `InvoiceLineItem`, `InvoiceClient`, `Payment`, `Refund`, `PaymentPlan`, `PaymentPlanInstallment`, `PaymentConfig`, `PaymentProvider`, `PaymentMethodSetup`, `ETransferInstructions`, `OnlinePaymentSession`, `RecurringInvoice`, `RecurringInvoiceLineItem`, `PaginatedInvoices`, `PaginatedPaymentPlans`, `PaginatedRecurringInvoices`, `InlinePaymentIntent`, `StripePublicConfig`
933
+ **Object types:** `Invoice`, `InvoiceLineItem`, `InvoiceTaxComponent`, `InvoiceClient`, `Payment`, `Refund`, `PaymentPlan`, `PaymentPlanInstallment`, `PaymentConfig`, `PaymentProvider`, `PaymentMethodSetup`, `ETransferInstructions`, `OnlinePaymentSession`, `RecurringInvoice`, `RecurringInvoiceLineItem`, `PaginatedInvoices`, `PaginatedPaymentPlans`, `PaginatedRecurringInvoices`, `InlinePaymentIntent`, `StripePublicConfig`
707
934
 
708
935
  **Enums:** `InvoiceStatus`, `PaymentMethod`, `PaymentStatus`, `RefundStatus`, `PaymentPlanStatus`, `InstallmentStatus`, `RecurringIntervalUnit`, `RecurringInvoiceStatus`, `PaymentIntentStatus`
709
936
 
710
- **Input types:** `CreateInvoiceInput`, `CreateLineItemInput`, `CreateRefundInput`, `CreateInstallmentInput`, `CreatePaymentPlanInput`, `CreateRecurringInvoiceInput`, `CreateRecurringLineItemInput`, `RecordManualPaymentInput`, `UpdatePaymentConfigInput`, `UpdateInvoiceMetadataInput`, `UpdateRecurringTemplateInput`, `UpsertPaymentProviderInput`, `InvoiceFilterInput`, `PaymentPlanFilterInput`, `RecurringInvoiceFilterInput`
937
+ **Input types:** `CreateInvoiceInput`, `CreateLineItemInput`, `CreateTaxComponentInput`, `CreateRefundInput`, `CreateInstallmentInput`, `CreatePaymentPlanInput`, `CreateRecurringInvoiceInput`, `CreateRecurringLineItemInput`, `RecordManualPaymentInput`, `UpdatePaymentConfigInput`, `UpdateInvoiceMetadataInput`, `UpdateRecurringTemplateInput`, `UpsertPaymentProviderInput`, `InvoiceFilterInput`, `PaymentPlanFilterInput`, `RecurringInvoiceFilterInput`
711
938
 
712
939
  ---
713
940
 
@@ -731,6 +958,7 @@ All exceptions extend `PaymentException` (a plain `Error` subclass) with a stabl
731
958
  | `PaymentIntentNotReusableException` | `PAYMENT_INTENT_NOT_REUSABLE` |
732
959
  | `PaymentIntentNotCancellableException` | `PAYMENT_INTENT_NOT_CANCELLABLE` |
733
960
  | `PaymentIntentOperationNotSupportedException` | `PAYMENT_INTENT_OPERATION_NOT_SUPPORTED` |
961
+ | `InvalidTaxConfigurationException` | `INVALID_TAX_CONFIGURATION` |
734
962
 
735
963
  Repository adapters are responsible for translating ORM-specific unique-constraint errors (e.g. Prisma `P2002`) into `UniqueConstraintViolationException` so core services stay ORM-agnostic.
736
964
 
@@ -1,7 +1,9 @@
1
1
  import { CreateLineItemInput } from './create-line-item.input';
2
+ import { CreateTaxComponentInput } from './create-tax-component.input';
2
3
  export declare class CreateInvoiceInput {
3
4
  clientDetailsId: string;
4
5
  taxRate?: number;
6
+ taxComponents?: CreateTaxComponentInput[];
5
7
  currency?: string;
6
8
  referenceType?: string;
7
9
  referenceId?: string;
@@ -18,6 +18,7 @@ const class_transformer_1 = require("class-transformer");
18
18
  const class_validator_1 = require("class-validator");
19
19
  const graphql_type_json_1 = __importDefault(require("graphql-type-json"));
20
20
  const create_line_item_input_1 = require("./create-line-item.input");
21
+ const create_tax_component_input_1 = require("./create-tax-component.input");
21
22
  const is_metadata_1 = require("./validators/is-metadata");
22
23
  let CreateInvoiceInput = class CreateInvoiceInput {
23
24
  };
@@ -36,6 +37,13 @@ __decorate([
36
37
  (0, class_validator_1.Max)(1),
37
38
  __metadata("design:type", Number)
38
39
  ], CreateInvoiceInput.prototype, "taxRate", void 0);
40
+ __decorate([
41
+ (0, graphql_1.Field)(() => [create_tax_component_input_1.CreateTaxComponentInput], { nullable: true }),
42
+ (0, class_validator_1.IsOptional)(),
43
+ (0, class_validator_1.ValidateNested)({ each: true }),
44
+ (0, class_transformer_1.Type)(() => create_tax_component_input_1.CreateTaxComponentInput),
45
+ __metadata("design:type", Array)
46
+ ], CreateInvoiceInput.prototype, "taxComponents", void 0);
39
47
  __decorate([
40
48
  (0, graphql_1.Field)({ nullable: true, defaultValue: 'CAD' }),
41
49
  (0, class_validator_1.IsOptional)(),
@@ -1 +1 @@
1
- {"version":3,"file":"create-invoice.input.js","sourceRoot":"","sources":["../../src/dto/create-invoice.input.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,6CAA0D;AAC1D,yDAAyC;AACzC,qDAWyB;AACzB,0EAA4C;AAC5C,qEAA+D;AAC/D,0DAAsD;AAG/C,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;CAyD9B,CAAA;AAzDY,gDAAkB;AAI7B;IAHC,IAAA,eAAK,GAAE;IACP,IAAA,wBAAM,GAAE;IACR,IAAA,4BAAU,GAAE;;2DACW;AAOxB;IALC,IAAA,eAAK,EAAC,GAAG,EAAE,CAAC,eAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACtC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,CAAC,CAAC;;mDACU;AAMjB;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAC9C,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,CAAC,CAAC;;oDACK;AAMlB;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,GAAG,CAAC;;yDACQ;AAKvB;IAHC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAM,GAAE;;uDACY;AAKrB;IAHC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAM,GAAE;8BACD,IAAI;iDAAC;AAMb;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,IAAI,CAAC;;iDACD;AAMf;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,IAAI,CAAC;;uDACK;AAKrB;IAHC,IAAA,eAAK,EAAC,GAAG,EAAE,CAAC,2BAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5C,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAU,GAAE;;oDACqB;AAMlC;IAJC,IAAA,eAAK,EAAC,GAAG,EAAE,CAAC,CAAC,4CAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACtD,IAAA,4BAAU,GAAE;IACZ,IAAA,gCAAc,EAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9B,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,4CAAmB,CAAC;;qDACE;6BAxDvB,kBAAkB;IAD9B,IAAA,mBAAS,GAAE;GACC,kBAAkB,CAyD9B"}
1
+ {"version":3,"file":"create-invoice.input.js","sourceRoot":"","sources":["../../src/dto/create-invoice.input.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,6CAA0D;AAC1D,yDAAyC;AACzC,qDAWyB;AACzB,0EAA4C;AAC5C,qEAA+D;AAC/D,6EAAuE;AACvE,0DAAsD;AAG/C,IAAM,kBAAkB,GAAxB,MAAM,kBAAkB;CAsE9B,CAAA;AAtEY,gDAAkB;AAI7B;IAHC,IAAA,eAAK,GAAE;IACP,IAAA,wBAAM,GAAE;IACR,IAAA,4BAAU,GAAE;;2DACW;AAOxB;IALC,IAAA,eAAK,EAAC,GAAG,EAAE,CAAC,eAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACtC,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,qBAAG,EAAC,CAAC,CAAC;IACN,IAAA,qBAAG,EAAC,CAAC,CAAC;;mDACU;AAajB;IAJC,IAAA,eAAK,EAAC,GAAG,EAAE,CAAC,CAAC,oDAAuB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1D,IAAA,4BAAU,GAAE;IACZ,IAAA,gCAAc,EAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9B,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,oDAAuB,CAAC;;yDACM;AAM1C;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAC9C,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,CAAC,CAAC;;oDACK;AAMlB;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,GAAG,CAAC;;yDACQ;AAKvB;IAHC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAM,GAAE;;uDACY;AAKrB;IAHC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAM,GAAE;8BACD,IAAI;iDAAC;AAMb;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,IAAI,CAAC;;iDACD;AAMf;IAJC,IAAA,eAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACzB,IAAA,4BAAU,GAAE;IACZ,IAAA,0BAAQ,GAAE;IACV,IAAA,2BAAS,EAAC,IAAI,CAAC;;uDACK;AAKrB;IAHC,IAAA,eAAK,EAAC,GAAG,EAAE,CAAC,2BAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC5C,IAAA,4BAAU,GAAE;IACZ,IAAA,wBAAU,GAAE;;oDACqB;AAMlC;IAJC,IAAA,eAAK,EAAC,GAAG,EAAE,CAAC,CAAC,4CAAmB,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IACtD,IAAA,4BAAU,GAAE;IACZ,IAAA,gCAAc,EAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9B,IAAA,wBAAI,EAAC,GAAG,EAAE,CAAC,4CAAmB,CAAC;;qDACE;6BArEvB,kBAAkB;IAD9B,IAAA,mBAAS,GAAE;GACC,kBAAkB,CAsE9B"}
@@ -0,0 +1,7 @@
1
+ export declare class CreateTaxComponentInput {
2
+ name: string;
3
+ rate: number;
4
+ authority?: string;
5
+ registrationNumber?: string;
6
+ sortOrder?: number;
7
+ }
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.CreateTaxComponentInput = void 0;
13
+ const graphql_1 = require("@nestjs/graphql");
14
+ const class_validator_1 = require("class-validator");
15
+ let CreateTaxComponentInput = class CreateTaxComponentInput {
16
+ };
17
+ exports.CreateTaxComponentInput = CreateTaxComponentInput;
18
+ __decorate([
19
+ (0, graphql_1.Field)(),
20
+ (0, class_validator_1.IsString)(),
21
+ (0, class_validator_1.IsNotEmpty)(),
22
+ (0, class_validator_1.MaxLength)(60),
23
+ __metadata("design:type", String)
24
+ ], CreateTaxComponentInput.prototype, "name", void 0);
25
+ __decorate([
26
+ (0, graphql_1.Field)(() => graphql_1.Float),
27
+ (0, class_validator_1.IsNumber)(),
28
+ (0, class_validator_1.Min)(0),
29
+ (0, class_validator_1.Max)(1),
30
+ __metadata("design:type", Number)
31
+ ], CreateTaxComponentInput.prototype, "rate", void 0);
32
+ __decorate([
33
+ (0, graphql_1.Field)({ nullable: true }),
34
+ (0, class_validator_1.IsOptional)(),
35
+ (0, class_validator_1.IsString)(),
36
+ (0, class_validator_1.MaxLength)(120),
37
+ __metadata("design:type", String)
38
+ ], CreateTaxComponentInput.prototype, "authority", void 0);
39
+ __decorate([
40
+ (0, graphql_1.Field)({ nullable: true }),
41
+ (0, class_validator_1.IsOptional)(),
42
+ (0, class_validator_1.IsString)(),
43
+ (0, class_validator_1.MaxLength)(60),
44
+ __metadata("design:type", String)
45
+ ], CreateTaxComponentInput.prototype, "registrationNumber", void 0);
46
+ __decorate([
47
+ (0, graphql_1.Field)(() => graphql_1.Int, { nullable: true }),
48
+ (0, class_validator_1.IsOptional)(),
49
+ (0, class_validator_1.IsNumber)(),
50
+ (0, class_validator_1.Min)(0),
51
+ __metadata("design:type", Number)
52
+ ], CreateTaxComponentInput.prototype, "sortOrder", void 0);
53
+ exports.CreateTaxComponentInput = CreateTaxComponentInput = __decorate([
54
+ (0, graphql_1.InputType)()
55
+ ], CreateTaxComponentInput);
56
+ //# sourceMappingURL=create-tax-component.input.js.map