@endora-commerce/mod-payments 0.100.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.
Files changed (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/index.d.ts +32 -0
  4. package/dist/admin/index.d.ts.map +1 -0
  5. package/dist/admin/index.js +61 -0
  6. package/dist/admin/index.js.map +1 -0
  7. package/dist/admin/zones/OrderPaymentsTab.d.ts +42 -0
  8. package/dist/admin/zones/OrderPaymentsTab.d.ts.map +1 -0
  9. package/dist/admin/zones/OrderPaymentsTab.js +104 -0
  10. package/dist/admin/zones/OrderPaymentsTab.js.map +1 -0
  11. package/dist/backend/adapters/built-in-adapters.d.ts +58 -0
  12. package/dist/backend/adapters/built-in-adapters.d.ts.map +1 -0
  13. package/dist/backend/adapters/built-in-adapters.js +83 -0
  14. package/dist/backend/adapters/built-in-adapters.js.map +1 -0
  15. package/dist/backend/drivers/bank-transfer-driver.d.ts +28 -0
  16. package/dist/backend/drivers/bank-transfer-driver.d.ts.map +1 -0
  17. package/dist/backend/drivers/bank-transfer-driver.js +26 -0
  18. package/dist/backend/drivers/bank-transfer-driver.js.map +1 -0
  19. package/dist/backend/drivers/gateway-adapter-port.d.ts +71 -0
  20. package/dist/backend/drivers/gateway-adapter-port.d.ts.map +1 -0
  21. package/dist/backend/drivers/gateway-adapter-port.js +18 -0
  22. package/dist/backend/drivers/gateway-adapter-port.js.map +1 -0
  23. package/dist/backend/drivers/pickup-driver.d.ts +23 -0
  24. package/dist/backend/drivers/pickup-driver.d.ts.map +1 -0
  25. package/dist/backend/drivers/pickup-driver.js +19 -0
  26. package/dist/backend/drivers/pickup-driver.js.map +1 -0
  27. package/dist/backend/email-templates/transactional-defaults.d.ts +6 -0
  28. package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
  29. package/dist/backend/email-templates/transactional-defaults.js +27 -0
  30. package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
  31. package/dist/backend/entities/payment.entity.d.ts +26 -0
  32. package/dist/backend/entities/payment.entity.d.ts.map +1 -0
  33. package/dist/backend/entities/payment.entity.js +100 -0
  34. package/dist/backend/entities/payment.entity.js.map +1 -0
  35. package/dist/backend/index.d.ts +88 -0
  36. package/dist/backend/index.d.ts.map +1 -0
  37. package/dist/backend/index.js +318 -0
  38. package/dist/backend/index.js.map +1 -0
  39. package/dist/backend/routes.customer.d.ts +20 -0
  40. package/dist/backend/routes.customer.d.ts.map +1 -0
  41. package/dist/backend/routes.customer.js +24 -0
  42. package/dist/backend/routes.customer.js.map +1 -0
  43. package/dist/backend/routes.d.ts +36 -0
  44. package/dist/backend/routes.d.ts.map +1 -0
  45. package/dist/backend/routes.js +44 -0
  46. package/dist/backend/routes.js.map +1 -0
  47. package/dist/backend/services/gateway-refund-registry.d.ts +122 -0
  48. package/dist/backend/services/gateway-refund-registry.d.ts.map +1 -0
  49. package/dist/backend/services/gateway-refund-registry.js +106 -0
  50. package/dist/backend/services/gateway-refund-registry.js.map +1 -0
  51. package/dist/backend/services/payment-email-notifier.d.ts +71 -0
  52. package/dist/backend/services/payment-email-notifier.d.ts.map +1 -0
  53. package/dist/backend/services/payment-email-notifier.js +73 -0
  54. package/dist/backend/services/payment-email-notifier.js.map +1 -0
  55. package/dist/backend/services/payment-email-renderer.d.ts +22 -0
  56. package/dist/backend/services/payment-email-renderer.d.ts.map +1 -0
  57. package/dist/backend/services/payment-email-renderer.js +17 -0
  58. package/dist/backend/services/payment-email-renderer.js.map +1 -0
  59. package/dist/backend/services/payment-placement-apply-port.d.ts +33 -0
  60. package/dist/backend/services/payment-placement-apply-port.d.ts.map +1 -0
  61. package/dist/backend/services/payment-placement-apply-port.js +68 -0
  62. package/dist/backend/services/payment-placement-apply-port.js.map +1 -0
  63. package/dist/backend/services/payment-read-port.d.ts +33 -0
  64. package/dist/backend/services/payment-read-port.d.ts.map +1 -0
  65. package/dist/backend/services/payment-read-port.js +64 -0
  66. package/dist/backend/services/payment-read-port.js.map +1 -0
  67. package/dist/backend/services/payment-reference-port.d.ts +29 -0
  68. package/dist/backend/services/payment-reference-port.d.ts.map +1 -0
  69. package/dist/backend/services/payment-reference-port.js +70 -0
  70. package/dist/backend/services/payment-reference-port.js.map +1 -0
  71. package/dist/backend/services/payment-refund.d.ts +55 -0
  72. package/dist/backend/services/payment-refund.d.ts.map +1 -0
  73. package/dist/backend/services/payment-refund.js +101 -0
  74. package/dist/backend/services/payment-refund.js.map +1 -0
  75. package/dist/backend/services/payment-retry-service.d.ts +45 -0
  76. package/dist/backend/services/payment-retry-service.d.ts.map +1 -0
  77. package/dist/backend/services/payment-retry-service.js +187 -0
  78. package/dist/backend/services/payment-retry-service.js.map +1 -0
  79. package/dist/backend/services/payment-service.d.ts +62 -0
  80. package/dist/backend/services/payment-service.d.ts.map +1 -0
  81. package/dist/backend/services/payment-service.js +134 -0
  82. package/dist/backend/services/payment-service.js.map +1 -0
  83. package/dist/backend/services/receive-payment-handler.d.ts +260 -0
  84. package/dist/backend/services/receive-payment-handler.d.ts.map +1 -0
  85. package/dist/backend/services/receive-payment-handler.js +356 -0
  86. package/dist/backend/services/receive-payment-handler.js.map +1 -0
  87. package/dist/backend/services/registry-singleton.d.ts +16 -0
  88. package/dist/backend/services/registry-singleton.d.ts.map +1 -0
  89. package/dist/backend/services/registry-singleton.js +17 -0
  90. package/dist/backend/services/registry-singleton.js.map +1 -0
  91. package/dist/manifest.d.ts +210 -0
  92. package/dist/manifest.d.ts.map +1 -0
  93. package/dist/manifest.js +182 -0
  94. package/dist/manifest.js.map +1 -0
  95. package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts +28 -0
  96. package/dist/migrations/20260925T115728_payments_refunded_amount.d.ts.map +1 -0
  97. package/dist/migrations/20260925T115728_payments_refunded_amount.js +32 -0
  98. package/dist/migrations/20260925T115728_payments_refunded_amount.js.map +1 -0
  99. package/dist/migrations/index.d.ts +27 -0
  100. package/dist/migrations/index.d.ts.map +1 -0
  101. package/dist/migrations/index.js +29 -0
  102. package/dist/migrations/index.js.map +1 -0
  103. package/docs/payments.md +69 -0
  104. package/i18n/en.json +5 -0
  105. package/i18n/pl.json +5 -0
  106. package/package.json +96 -0
  107. package/tailwind.css +14 -0
@@ -0,0 +1,318 @@
1
+ import { lazyPort } from '@endora-commerce/platform/kernel';
2
+ import { builtInPaymentAdapters } from './adapters/built-in-adapters.js';
3
+ import { Payment } from './entities/payment.entity.js';
4
+ import { ReceivePaymentHandler, } from './services/receive-payment-handler.js';
5
+ import { PaymentService } from './services/payment-service.js';
6
+ import { PaymentRetryService } from './services/payment-retry-service.js';
7
+ import { PaymentReadService } from './services/payment-read-port.js';
8
+ import { PaymentReferenceService } from './services/payment-reference-port.js';
9
+ import { PaymentPlacementApplyService } from './services/payment-placement-apply-port.js';
10
+ import { PaymentRefundProvider } from './services/payment-refund.js';
11
+ import { gatewayRefundRegistry } from './services/registry-singleton.js';
12
+ import { PaymentEmailNotifier } from './services/payment-email-notifier.js';
13
+ import { resolvePaymentEmailRenderer } from './services/payment-email-renderer.js';
14
+ import { registerPaymentsRoutes } from './routes.js';
15
+ import { registerPaymentsCustomerRoutes } from './routes.customer.js';
16
+ import { PAYMENT_STATUS_CHANGED_DEFAULT } from './email-templates/transactional-defaults.js';
17
+ export function registerModule(ctx) {
18
+ ctx.di.register({
19
+ /**
20
+ * The refund-handler table, as the container's value — the delivery-side
21
+ * twin of `payment_methods`' `paymentAdapterRegistry`, and registered the
22
+ * same way and for the same reason (feature 075, Phase P).
23
+ *
24
+ * **Ungated, and the only name over this registry** (D-99.7). It used to
25
+ * have a gated twin, `gatewayRefundRegistryPort`, for the *pull* direction —
26
+ * "which handler settles this refund" — but that question is asked inside
27
+ * this module, by `PaymentRefundProvider`, and no module ever resolved the
28
+ * port. A *push* must not be gated: the four gateways contribute their
29
+ * handler from a boot hook, and a gate there would refuse the contribution
30
+ * rather than defer it, so a gateway would silently stay unregistered until
31
+ * the next restart after an operator switched `payments` back on. That
32
+ * asymmetry is now stated in the contract rather than only here, because a
33
+ * rule in a comment ships in no package. The absent-owner policy sits inside
34
+ * the registry, where the whole design puts it: a handler whose module is
35
+ * off is skipped at enumeration and the obligation lands on
36
+ * `pending_manual` (D-71), which a gate over the push would drop instead of
37
+ * record.
38
+ */
39
+ gatewayRefundRegistry: ctx.asFunction(() => gatewayRefundRegistry).singleton(),
40
+ // Contribution point, defaulted to no sender: a deployment without a
41
+ // transactional-email surface sends nothing rather than failing to settle.
42
+ paymentEmailSender: ctx
43
+ .asFunction(() => () => undefined)
44
+ .singleton(),
45
+ paymentEmailNotifier: ctx
46
+ .asFunction(({ emFactory }) => new PaymentEmailNotifier({
47
+ emFactory,
48
+ orderRead: lazyPort(ctx, 'orderReadPort'),
49
+ customerAccountRead: lazyPort(ctx, 'customerAccountReadPort'),
50
+ // Read per call: a root contributes the sender after
51
+ // `transactional_emails` announces it, which is later than this.
52
+ getTransactionalEmailSender: () => ctx.cradle().paymentEmailSender(),
53
+ }))
54
+ .singleton(),
55
+ });
56
+ ctx.di.providePort('paymentService', ctx
57
+ .asFunction(({ emFactory }) => new PaymentService(emFactory, lazyPort(ctx, 'orderReadPort')))
58
+ .singleton());
59
+ /**
60
+ * The buyer's own retry (issue #264), as this module's own service rather
61
+ * than a route body.
62
+ *
63
+ * It is not a published port: nothing outside this module calls it, and the
64
+ * one surface it has is the HTTP route below. The adapter registry is read
65
+ * per call — an operator can switch a gateway off between two requests, and
66
+ * the registry filters its enumeration on the contributor's effective state.
67
+ */
68
+ ctx.di.register({
69
+ paymentRetryService: ctx
70
+ .asFunction(() => new PaymentRetryService({
71
+ orderRead: lazyPort(ctx, 'orderReadPort'),
72
+ customerAccountRead: lazyPort(ctx, 'customerAccountReadPort'),
73
+ // Lazily, even though this module owns it: capturing a gate into a
74
+ // singleton is a gate that keeps answering after an operator
75
+ // switches the owner off.
76
+ paymentService: lazyPort(ctx, 'paymentService'),
77
+ // The terminality read (feature 085 Phase D): the same port the
78
+ // settlement handler writes through, resolved for its read half.
79
+ orderTransition: lazyPort(ctx, 'orderTransitionPort'),
80
+ paymentAdapterRegistry: () => ctx.cradle().paymentAdapterRegistry,
81
+ // No `catch` around the port call, for the reason `orders` and
82
+ // `carts` give at the same seam: a disabled-module throw read as
83
+ // "no restriction" would be fail-open on a path whose job is to
84
+ // restrict.
85
+ assertOrganizationCanTransact: async (organizationId) => {
86
+ await ctx.cradle().organizationReadPort.assertCanTransact(organizationId);
87
+ },
88
+ }))
89
+ .singleton(),
90
+ });
91
+ /**
92
+ * How a return settlement refunds a payment (feature 046 R5), as this
93
+ * module's port instead of a class both roots constructed (T143c).
94
+ *
95
+ * `PaymentRefundProvider` resolves the order's PSP adapter out of
96
+ * `gatewayRefundRegistry` — this module's registry — so a root's instance
97
+ * refunded through a switched-off `payments`. The bridge `returns` receives
98
+ * forwards to this name per settlement, which is where the gate belongs.
99
+ */
100
+ ctx.di.providePort('paymentRefundPort', ctx
101
+ .asFunction(() => new PaymentRefundProvider(lazyPort(ctx, 'orderReadPort'), lazyPort(ctx, 'paymentMethodReadPort')))
102
+ .singleton());
103
+ // ---------------------------------------------------------------------------
104
+ // Feature 075, Phase P — the published surface.
105
+ //
106
+ // `paymentReadPort` replaces sixteen hand-written `em.findOne(Payment, …)`
107
+ // calls in the four gateways, and `receivePaymentPort` is the ingress they
108
+ // already share, now expressed without the `Payment` entity in its result
109
+ // type.
110
+ //
111
+ // **The refund registry is published under its push name only** (D-99.7). It
112
+ // once had a second, gated registration here — `gatewayRefundRegistryPort`,
113
+ // over the same instance — which nothing ever resolved, while the doc block in
114
+ // `@endora-commerce/contracts` described the *push* seam above it. An out-of-tree gateway
115
+ // reading only the published contracts would have resolved the gate from its
116
+ // boot hook and exited 1. The contract now names `gatewayRefundRegistry`, the
117
+ // ungated registration above, and says why it is ungated; the pull is
118
+ // intra-module and unpublished.
119
+ // ---------------------------------------------------------------------------
120
+ ctx.di.providePort('paymentReadPort', ctx.asFunction(({ emFactory }) => new PaymentReadService(emFactory)).singleton());
121
+ /**
122
+ * The payment row order placement opens, on **placement's** `EntityManager`
123
+ * (feature 080, T048; D-169, D-179).
124
+ *
125
+ * `orders` wrote it with this module's `Payment` class until T048 — the last
126
+ * cross-module entity-class reach in the tree. D-168 leaves a packaged
127
+ * `payments` no entity class for a stranger to name, so what crosses is now
128
+ * this module's own interface and the statement belongs to the module that
129
+ * owns the table. `PaymentPlacementApplyPort` stays out of
130
+ * `@endora-commerce/contracts` because it takes a MikroORM `EntityManager` and
131
+ * FR-034 keeps that package free of them — and it is declared on
132
+ * **`@endora-commerce/mod-orders/ports`**, not this module's own, which is
133
+ * D-171.1 and not an accident. The two modules reach each other, so only one
134
+ * npm edge can exist (a mutual devDependency between two module packages is a
135
+ * build deadlock, `noEmitOnError` making the loser emit nothing), and it runs
136
+ * in the direction of the binding manifest edge: this module declares `orders`
137
+ * in its `dependencies`, so `mod-payments` build-depending on `mod-orders`
138
+ * adds no claim its manifest does not already make.
139
+ *
140
+ * The licence is conditional and the condition is **this** module's:
141
+ * `PaymentPlacementApplyService` names the interface at its `implements`
142
+ * clause, and the registration below carries the explicit type argument. Those
143
+ * are the two places `tsc` checks conformance — TS2420 and TS2345 — and both
144
+ * resolve the interface wherever it was declared. Dropping either turns the
145
+ * seam into D-77's rejected alternative, which is what
146
+ * `check:port-shape`'s `declared-elsewhere-without-implements` refuses.
147
+ *
148
+ * It is a **gated** registration like every other name here, and that gate is
149
+ * the half `orders` writing the row itself could never have. The reachable
150
+ * consequence is narrow, because `assertPaymentMethodUsable` already refuses a
151
+ * method whose adapter this module contributes while this module is absent —
152
+ * D-179 measured that and found checkout already fail-closed, where no money
153
+ * is at stake. What that guard deliberately tolerates is a method whose
154
+ * adapter **no** module ever registered, and such a placement went on writing
155
+ * a row into this module's own table with an operator having switched it off:
156
+ * issue #188's shape, which the gate closes. `orders` declares the edge
157
+ * `refuses-without` and wraps the call in no `catch`.
158
+ */
159
+ ctx.di.providePort('paymentPlacementApplyPort', ctx.asFunction(() => new PaymentPlacementApplyService()).singleton());
160
+ /**
161
+ * The write side of `findByExternalReference`, published for the four
162
+ * gateways that were doing it with this module's entity and their own
163
+ * `EntityManager`.
164
+ *
165
+ * A port, not a registration: unlike the registry above, this is a pull with
166
+ * a failure mode. A gateway that has just opened a PaymentIntent and cannot
167
+ * record its identifier has to hear so — the provider object is live, and an
168
+ * event it sends would arrive with nothing on this side able to resolve it.
169
+ */
170
+ ctx.di.providePort('paymentReferencePort', ctx
171
+ .asFunction(({ emFactory }) => new PaymentReferenceService(emFactory))
172
+ .singleton());
173
+ ctx.di.providePort('paymentEmailRendererPort', ctx
174
+ .asFunction(() => ({
175
+ render: (rendererKey, emailContext) => resolvePaymentEmailRenderer(rendererKey)(emailContext),
176
+ }))
177
+ .singleton());
178
+ ctx.di.providePort('receivePaymentPort', ctx
179
+ .asFunction(() => {
180
+ const handler = () => ctx.cradle().receivePaymentHandler;
181
+ return {
182
+ receive: (input) => handler().receive(input),
183
+ reflectRefund: (input) => handler().reflectRefund(input),
184
+ };
185
+ })
186
+ .singleton());
187
+ /**
188
+ * The settlement handler, with the two cross-module reads it used to make
189
+ * with somebody else's entity now made through their ports (feature 075,
190
+ * C-W3).
191
+ *
192
+ * Both edges were ledgered as blocked on this constructor: the four gateways
193
+ * built their own handler, so it could take no port they could not build.
194
+ * They resolve `receivePaymentPort` since the C-W3 gateway cuts, so this is
195
+ * the only construction left and `ctx` is in hand.
196
+ *
197
+ * **The `Order` entity import inside the handler is gone since feature 080's
198
+ * T048**, and the seam it stood for is not: `payments_order_fk` still holds
199
+ * the payment row and the order's payment status co-transactional (D-78 point
200
+ * 2), so the write still runs on the settlement's own `EntityManager` — it is
201
+ * `orderPaymentStatusApplyPort` that performs it now, an interface `orders`
202
+ * declares in its `ports/` directory precisely because an `EntityManager`
203
+ * parameter bars it from `@endora-commerce/contracts` (FR-034). D-168 leaves a
204
+ * packaged `orders` no entity class for this module to name, which is what
205
+ * made the conversion due before that module moves.
206
+ *
207
+ * The three `orders`/`payment_methods` ports this constructor takes are not
208
+ * three of a kind, and the difference is which transaction each runs in:
209
+ * `paymentMethodReadPort` is a read of a row the settlement never writes,
210
+ * `orderPaymentStatusApplyPort` is the co-transactional write above, and
211
+ * `orderTransitionPort` is called after the commit.
212
+ *
213
+ * **`orderTransitionPort` replaces two of the three names this used to take**
214
+ * (feature 085 Phase D). `orderStatusAnnouncePort` goes because the transition
215
+ * seam emits the templated `.after` events itself, so announcing beside it
216
+ * would double every subscriber's reaction; `paymentOrderStatusRegistry` goes
217
+ * because "is this a status code" was never the question the ingress needed
218
+ * answered, and the port answers the real one — may this order go there —
219
+ * against the configured graph.
220
+ */
221
+ ctx.di.providePort('receivePaymentHandler', ctx
222
+ .asFunction(({ emFactory, eventBus }) => new ReceivePaymentHandler(emFactory, lazyPort(ctx, 'paymentMethodReadPort'), lazyPort(ctx, 'orderTransitionPort'), lazyPort(ctx, 'orderPaymentStatusApplyPort'), ctx.log, eventBus))
223
+ .singleton());
224
+ ctx.subscribe('payment.received.v1', async (payload) => {
225
+ const { orderId } = payload;
226
+ await ctx.cradle().paymentEmailNotifier.notify(orderId, 'paid', null);
227
+ });
228
+ ctx.subscribe('payment.failed.v1', async (payload) => {
229
+ const { orderId, failureReason } = payload;
230
+ await ctx
231
+ .cradle()
232
+ .paymentEmailNotifier.notify(orderId, 'failed', failureReason ?? null);
233
+ });
234
+ ctx.routes(async (app) => {
235
+ const { requireAdmin } = ctx.cradle();
236
+ await registerPaymentsRoutes(app, {
237
+ requireAdmin,
238
+ // Lazily, even though the module owns both ports: route *registration*
239
+ // runs inside `buildServer` whatever the module's effective state is, so
240
+ // destructuring the gates here would stop the next start instead of
241
+ // stopping the routes (D-40).
242
+ receiveHandler: lazyPort(ctx, 'receivePaymentHandler'),
243
+ paymentService: lazyPort(ctx, 'paymentService'),
244
+ });
245
+ await registerPaymentsCustomerRoutes(app, {
246
+ requireCustomer: (req, reply) => ctx.cradle().requireCustomer(req, reply),
247
+ resolveCustomerAccountId: (req) => ctx.cradle().customerAccountIdResolver(req),
248
+ // Resolvable here, unlike the two above: this is a plain registration
249
+ // rather than a gate, and every port it holds is lazy.
250
+ retryService: ctx.cradle().paymentRetryService,
251
+ });
252
+ });
253
+ /**
254
+ * The default subject and content for the 1 transactional email this
255
+ * module declares in its manifest (T143a).
256
+ *
257
+ * These were fourteen `emailDefaultsRegistry.register(...)` calls in
258
+ * `composition.ts`, each importing a template constant out of the module that
259
+ * owns it — a root reaching into seven modules to hand their own content to
260
+ * an eighth. Each module registers its own now.
261
+ *
262
+ * `ctx.onBoot` rather than a registration: the registry is *read* once, by
263
+ * `transactional_emails`' boot reconciler inside its plugin body. Boot hooks
264
+ * run during composition and plugin bodies only when the Fastify app is
265
+ * built, so this always lands first — by construction, not by ordering luck.
266
+ */
267
+ ctx.onBoot(async () => {
268
+ const defaults = lazyPort(ctx, 'emailDefaultsPort');
269
+ defaults.register('payment_status_changed', PAYMENT_STATUS_CHANGED_DEFAULT, 'payments');
270
+ });
271
+ /**
272
+ * The four payment kinds this module implements, pushed into the registry
273
+ * `payment_methods` holds (T143a).
274
+ *
275
+ * `ctx.onBoot` rather than a registration, for the reason the other
276
+ * push-at-boot contributions give: the registry is *read* — by the
277
+ * payment-method admin surface, by storefront eligibility and by
278
+ * order placement — and a registration declares without resolving.
279
+ *
280
+ * It belongs in this module rather than in a root even though the registry is
281
+ * a deliberate **process** singleton (`payment_methods/services/registry-singleton.ts`,
282
+ * which the four gateway modules import directly from their install hooks).
283
+ * The singleton decides *where* a descriptor lands, not *who* may declare
284
+ * one, and this module owns the adapter classes.
285
+ *
286
+ * Each one names this module as it lands (issue #96): the registry filters
287
+ * its enumeration on the contributing module's effective state, so an entry
288
+ * that does not know who contributed it is an entry that keeps being offered
289
+ * to a buyer after an operator switches its owner off. Re-registration by the
290
+ * same owner is silent, which is what the old `isRegistered` guard was
291
+ * really for — the test suite performs several hundred compositions against
292
+ * this one process singleton.
293
+ */
294
+ ctx.onBoot(() => {
295
+ const registry = ctx.cradle().paymentAdapterRegistry;
296
+ for (const adapter of builtInPaymentAdapters()) {
297
+ registry.register(adapter, 'payments');
298
+ }
299
+ });
300
+ }
301
+ /**
302
+ * The module's persisted entity classes, on the `./backend` subpath, as one
303
+ * array and **no named class export** (D-168).
304
+ *
305
+ * This is the shape the platform reads when the package is *installed*: the
306
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
307
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
308
+ * which is the third source of `check:module-boundary`'s `table->owner` map and
309
+ * the package pass of `check-entity-tenant-classification`. A missing array is
310
+ * answered with `[]` — zero entities registered, no error anywhere.
311
+ *
312
+ * One class, and the one that used to cross a boundary: `orders` constructed
313
+ * `Payment` directly until T048, which was the last cross-module entity-class
314
+ * reach in the tree. What crosses now is `PaymentPlacementApplyPort`, and D-168
315
+ * leaves a stranger no supported spelling for the class itself.
316
+ */
317
+ export const entities = [Payment];
318
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
2
+ import type { PaymentRetryService } from './services/payment-retry-service.js';
3
+ /**
4
+ * The buyer's own payment surface (issue #264).
5
+ *
6
+ * POST /api/v1/orders/:orderId/payments/retry — pay an unpaid order again
7
+ *
8
+ * It sits in this module rather than in `orders` because the attempt row and
9
+ * the adapter dispatch are this module's, and because `payments` already
10
+ * declares `orders` — the reverse edge would close a cycle. It is the customer
11
+ * twin of `POST /api/v1/admin/orders/:id/payments/retry`, and both run through
12
+ * the same `PaymentService.openRetry`.
13
+ */
14
+ export interface PaymentsCustomerRoutesDeps {
15
+ requireCustomer: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
16
+ resolveCustomerAccountId: (req: FastifyRequest) => string;
17
+ retryService: PaymentRetryService;
18
+ }
19
+ export declare function registerPaymentsCustomerRoutes(app: FastifyInstance, deps: PaymentsCustomerRoutesDeps): Promise<void>;
20
+ //# sourceMappingURL=routes.customer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.customer.d.ts","sourceRoot":"","sources":["../../src/backend/routes.customer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAE/E;;;;;;;;;;GAUG;AACH,MAAM,WAAW,0BAA0B;IACzC,eAAe,EAAE,CAAC,GAAG,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,wBAAwB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK,MAAM,CAAC;IAC1D,YAAY,EAAE,mBAAmB,CAAC;CACnC;AAED,wBAAsB,8BAA8B,CAClD,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,0BAA0B,GAC/B,OAAO,CAAC,IAAI,CAAC,CA4Bf"}
@@ -0,0 +1,24 @@
1
+ import { ERROR_CODES, OrganizationCannotTransactError } from '@endora-commerce/contracts';
2
+ import { HttpError } from '@endora-commerce/platform/http';
3
+ export async function registerPaymentsCustomerRoutes(app, deps) {
4
+ app.post('/api/v1/orders/:orderId/payments/retry', { preHandler: deps.requireCustomer }, async (request, reply) => {
5
+ const callerId = deps.resolveCustomerAccountId(request);
6
+ try {
7
+ const result = await deps.retryService.retryForCustomer({
8
+ orderId: request.params.orderId,
9
+ customerAccountId: callerId,
10
+ });
11
+ return reply.send({ data: result });
12
+ }
13
+ catch (err) {
14
+ // The same mapping placement uses for the same refusal, so a buyer of a
15
+ // suspended Organization is told the same thing whether they are
16
+ // placing an order or paying one.
17
+ if (err instanceof OrganizationCannotTransactError) {
18
+ throw new HttpError(423, ERROR_CODES.FORBIDDEN, 'Your Organization cannot transact in its current status.', { code: 'organization_cannot_transact', status: err.status });
19
+ }
20
+ throw err;
21
+ }
22
+ });
23
+ }
24
+ //# sourceMappingURL=routes.customer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.customer.js","sourceRoot":"","sources":["../../src/backend/routes.customer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,+BAA+B,EAAE,MAAM,4BAA4B,CAAC;AAC1F,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAoB3D,MAAM,CAAC,KAAK,UAAU,8BAA8B,CAClD,GAAoB,EACpB,IAAgC;IAEhC,GAAG,CAAC,IAAI,CACN,wCAAwC,EACxC,EAAE,UAAU,EAAE,IAAI,CAAC,eAAe,EAAE,EACpC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC;QACxD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC;gBACtD,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,OAAO;gBAC/B,iBAAiB,EAAE,QAAQ;aAC5B,CAAC,CAAC;YACH,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;QACtC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,wEAAwE;YACxE,iEAAiE;YACjE,kCAAkC;YAClC,IAAI,GAAG,YAAY,+BAA+B,EAAE,CAAC;gBACnD,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,SAAS,EACrB,0DAA0D,EAC1D,EAAE,IAAI,EAAE,8BAA8B,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAC7D,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import type { ReceivePaymentHandler } from './services/receive-payment-handler.js';
3
+ import type { PaymentService } from './services/payment-service.js';
4
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
5
+ /**
6
+ * Payments routes (feature 034).
7
+ *
8
+ * POST /api/v1/payments/receive — receive_payment ingress (FR-022)
9
+ * POST /api/v1/admin/orders/:id/payments/retry — open a retry Payment (FR-024)
10
+ * GET /api/v1/admin/orders/:id/payments — full payment history (FR-026)
11
+ *
12
+ * The buyer's own retry is a separate file (`routes.customer.ts`, issue #264):
13
+ * it authorises differently, guards the Organization's ability to transact, and
14
+ * starts the gateway session this one deliberately does not.
15
+ *
16
+ * The ingress is admin-guarded for the MVP; a signed PSP-webhook auth path is a
17
+ * follow-up (the offline reference adapters are settled by an admin anyway).
18
+ *
19
+ * All three gate on this module's own codes. They used to gate on `catalog:read`
20
+ * and `catalog:write`, which meant an operator who could edit a product could
21
+ * read every payment's provider payload and declare an arbitrary payment
22
+ * settled. The reasoning for the pair, and for there being no third code for the
23
+ * ingress, is in `manifest.ts` beside the declaration.
24
+ *
25
+ * The ingress body is `receivePaymentRequestSchema` rather than
26
+ * `receivePaymentSchema`: the two differ only in `providerDetails`, which is
27
+ * bounded for the operator-supplied path and left open for the gateway modules
28
+ * that build the payload in code.
29
+ */
30
+ export interface PaymentsRoutesDeps {
31
+ requireAdmin: RequireAdminFactory;
32
+ receiveHandler: ReceivePaymentHandler;
33
+ paymentService: PaymentService;
34
+ }
35
+ export declare function registerPaymentsRoutes(app: FastifyInstance, deps: PaymentsRoutesDeps): Promise<void>;
36
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uCAAuC,CAAC;AACnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AACpE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,mBAAmB,CAAC;IAClC,cAAc,EAAE,qBAAqB,CAAC;IACtC,cAAc,EAAE,cAAc,CAAC;CAChC;AAED,wBAAsB,sBAAsB,CAC1C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,IAAI,CAAC,CAoCf"}
@@ -0,0 +1,44 @@
1
+ import { receivePaymentRequestSchema } from '@endora-commerce/contracts';
2
+ export async function registerPaymentsRoutes(app, deps) {
3
+ app.post('/api/v1/payments/receive', {
4
+ preHandler: deps.requireAdmin('payments:write'),
5
+ schema: { body: receivePaymentRequestSchema },
6
+ }, async (request) => {
7
+ const body = receivePaymentRequestSchema.parse(request.body);
8
+ const result = await deps.receiveHandler.receive(body);
9
+ return { data: result };
10
+ });
11
+ app.post('/api/v1/admin/orders/:id/payments/retry', { preHandler: deps.requireAdmin('payments:write') }, async (request, reply) => {
12
+ // `opened` is discarded here on purpose: the operator asked for the next
13
+ // attempt and gets it, whether it had to be created or was already open.
14
+ // The buyer-facing twin routes on that difference — see
15
+ // `routes.customer.ts` — because only it starts a provider session.
16
+ const { payment } = await deps.paymentService.openRetry(request.params.id);
17
+ reply.status(201);
18
+ return { data: serializePayment(payment) };
19
+ });
20
+ app.get('/api/v1/admin/orders/:id/payments', { preHandler: deps.requireAdmin('payments:read') }, async (request) => {
21
+ const payments = await deps.paymentService.listForOrder(request.params.id);
22
+ return { data: payments.map(serializePayment) };
23
+ });
24
+ }
25
+ function serializePayment(p) {
26
+ return {
27
+ id: p.id,
28
+ orderId: p.orderId,
29
+ paymentMethodId: p.paymentMethodId,
30
+ status: p.status,
31
+ amount: Number(p.amount),
32
+ refundedAmount: Number(p.refundedAmount ?? '0'),
33
+ refundedAt: p.providerDetails?.['refundedAt'] ?? null,
34
+ refundReference: p.providerDetails?.['refundReference'] ?? null,
35
+ updatedAt: p.updatedAt ? p.updatedAt.toISOString() : null,
36
+ currency: p.currency,
37
+ paidAt: p.paidAt ? p.paidAt.toISOString() : null,
38
+ externalReference: p.externalReference ?? null,
39
+ providerDetails: p.providerDetails ?? null,
40
+ failureReason: p.failureReason ?? null,
41
+ attemptNo: p.attemptNo,
42
+ };
43
+ }
44
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAoCzE,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,GAAoB,EACpB,IAAwB;IAExB,GAAG,CAAC,IAAI,CACN,0BAA0B,EAC1B;QACE,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC;QAC/C,MAAM,EAAE,EAAE,IAAI,EAAE,2BAA2B,EAAE;KAC9C,EACD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAC1B,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,yCAAyC,EACzC,EAAE,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,gBAAgB,CAAC,EAAE,EACnD,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,yEAAyE;QACzE,yEAAyE;QACzE,wDAAwD;QACxD,oEAAoE;QACpE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3E,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAClB,OAAO,EAAE,IAAI,EAAE,gBAAgB,CAAC,OAAO,CAAC,EAAE,CAAC;IAC7C,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,mCAAmC,EACnC,EAAE,UAAU,EAAE,IAAI,CAAC,YAAY,CAAC,eAAe,CAAC,EAAE,EAClD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3E,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;IAClD,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,gBAAgB,CAAC,CAczB;IACC,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,OAAO,EAAE,CAAC,CAAC,OAAO;QAClB,eAAe,EAAE,CAAC,CAAC,eAAe;QAClC,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;QACxB,cAAc,EAAE,MAAM,CAAC,CAAC,CAAC,cAAc,IAAI,GAAG,CAAC;QAC/C,UAAU,EAAG,CAAC,CAAC,eAAe,EAAE,CAAC,YAAY,CAAwB,IAAI,IAAI;QAC7E,eAAe,EAAG,CAAC,CAAC,eAAe,EAAE,CAAC,iBAAiB,CAAwB,IAAI,IAAI;QACvF,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QACzD,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QAChD,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,IAAI,IAAI;QAC9C,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,IAAI;QAC1C,aAAa,EAAE,CAAC,CAAC,aAAa,IAAI,IAAI;QACtC,SAAS,EAAE,CAAC,CAAC,SAAS;KACvB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,122 @@
1
+ import type { PaymentRefundInput, PaymentRefundResult } from '@endora-commerce/contracts';
2
+ /**
3
+ * GatewayRefundRegistry (feature 049) — the generic seam through which a PSP
4
+ * vendor module (e.g. Stripe) fulfils refunds for `kind === 'gateway'` payments.
5
+ *
6
+ * Mirrors the PaymentAdapterRegistry pattern: a vendor module imports this
7
+ * process-wide singleton and registers its refund handler from its boot hook,
8
+ * so the platform has exactly one table of handlers however many times it is
9
+ * composed. `PaymentRefundProvider` consults it for gateway payments and falls
10
+ * back to `pending_manual` when no handler answers — keeping the `payments` and
11
+ * `returns` modules provider-agnostic.
12
+ *
13
+ * **Every entry names the module that contributed it (feature 074, FR-024).**
14
+ * This is a contribution registry in D-39's sense: the push is ungated — a boot
15
+ * hook runs whatever the contributing module's effective state is, and gating
16
+ * the push would make a deactivation survive as a permanently missing entry —
17
+ * so the presence question is answered *here*, at enumeration, keyed on the
18
+ * owner recorded with the entry. Until this shipped the registry recorded no
19
+ * owner and stated no policy, and D-44 §7 named it the one live instance of
20
+ * that gap: a switched-off gateway went on refunding through its own PSP API.
21
+ *
22
+ * **The policy this registry states: skip an absent owner's handler, and say
23
+ * so (D-71).** `get`, `resolve` and `list` answer as if it were not registered;
24
+ * `entry`, `ownerOf`, {@link GatewayRefundRegistry.absentOwnerFor} and `listAll`
25
+ * deliberately do not filter, because a settlement screen has to keep showing
26
+ * the handler *and* the reason it is unavailable — and because the caller has to
27
+ * be able to tell "switched off" from "never installed", which one empty
28
+ * `resolve` cannot. Three things decide the skip, and the money is on the other
29
+ * side of each:
30
+ *
31
+ * 1. A module that is off must not act. Refunding through a PSP the operator
32
+ * switched off charges that PSP's API with that operator's credentials —
33
+ * the opposite of "behaves as if never installed" (Principle XVII).
34
+ * 2. **The obligation is not dropped with the handler.** The caller has to say
35
+ * what happens next, and the two situations it can be in are not the same
36
+ * one: a deployment that never installed a PSP integration records
37
+ * `pending_manual` and settles by hand, while a gateway an operator switched
38
+ * off is a capability that is *supposed* to be there and can be back in one
39
+ * click. D-71 rules the second a **refusal**, not an outcome — see
40
+ * `PaymentRefundProvider`.
41
+ * 3. {@link resolve} answers with the sole registered handler when the order
42
+ * names no adapter. Honouring an absent owner would route exactly those
43
+ * refunds — the ones with the least information behind them — into a
44
+ * switched-off gateway.
45
+ *
46
+ * Feature 075's Phase C took the cut Phase P set up: the two shapes `returns`
47
+ * declares are read from `@endora-commerce/contracts` now, so this file names no other
48
+ * module. The declaration itself stays here rather than being replaced by the
49
+ * package's structurally identical `GatewayRefundRegistryPort`, and the reason
50
+ * is `absentOwnerFor`: the published port is the **contribution** surface the
51
+ * gateways push into, and it deliberately does not carry the question D-71
52
+ * added — "which module *would* have handled this, but is switched off". That
53
+ * question is what keeps a refusal apart from `pending_manual`, and it is asked
54
+ * by this module's own `PaymentRefundProvider` over this module's own registry,
55
+ * which is not a cross-module call at all.
56
+ */
57
+ export interface GatewayRefundHandler {
58
+ /** The payment adapter key this handler serves (e.g. `stripe`). */
59
+ readonly adapterKey: string;
60
+ refund(input: PaymentRefundInput): Promise<PaymentRefundResult>;
61
+ }
62
+ export interface RegistryLogger {
63
+ warn(message: string): void;
64
+ }
65
+ /** One contributed handler, with the module that contributed it. */
66
+ export interface GatewayRefundEntry {
67
+ readonly handler: GatewayRefundHandler;
68
+ readonly module: string;
69
+ }
70
+ export declare class GatewayRefundRegistry {
71
+ private readonly log;
72
+ private readonly isModulePresent;
73
+ private readonly handlers;
74
+ /**
75
+ * @param log collision warnings.
76
+ * @param isModulePresent the effective-state probe. Defaults to
77
+ * always-present, so a registry a unit test builds for itself keeps
78
+ * answering about the handlers that test registered; the process singleton
79
+ * wires it to the kernel's effective state.
80
+ */
81
+ constructor(log?: RegistryLogger, isModulePresent?: (moduleId: string) => boolean);
82
+ register(handler: GatewayRefundHandler, moduleId: string): void;
83
+ unregister(adapterKey: string): void;
84
+ /** The contributed entry, presence-blind. Diagnostics read this. */
85
+ entry(adapterKey: string): GatewayRefundEntry | undefined;
86
+ /** The module that contributed `adapterKey`, or `null` when nobody did. */
87
+ ownerOf(adapterKey: string): string | null;
88
+ /**
89
+ * The module that would have handled this refund but is not present (D-71).
90
+ *
91
+ * The question a caller asks *after* {@link resolve} declined, because one
92
+ * empty `resolve` covers two situations that are not interchangeable: a
93
+ * gateway an operator switched off is a capability restorable in one click
94
+ * and must be **refused**, while an adapter nobody ever registered is a
95
+ * deployment with no PSP integration, which records the obligation and
96
+ * settles by hand.
97
+ *
98
+ * It mirrors `resolve`'s two arms exactly, which is why the unkeyed one is
99
+ * here at all: `resolve` answers with the sole registered handler when the
100
+ * order names no adapter, and point 3 of the docblock above says that is the
101
+ * worst arm to get wrong — the refunds with the least information behind
102
+ * them. Presence-blind in the same sense `ownerOf` is.
103
+ */
104
+ absentOwnerFor(adapterKey?: string | null): string | null;
105
+ /** The handler, or `undefined` when unregistered or its owner is absent. */
106
+ get(adapterKey: string): GatewayRefundHandler | undefined;
107
+ /**
108
+ * Resolve the handler for a gateway payment.
109
+ * - Known `adapterKey` → exact handler only (never guess another PSP).
110
+ * - Missing key → sole registered handler when exactly one exists.
111
+ *
112
+ * Both arms drop a handler whose owner is absent, and the second arm counts
113
+ * only the handlers that are available: with one gateway installed and
114
+ * switched off, "the sole registered handler" must not be it.
115
+ */
116
+ resolve(adapterKey?: string | null): GatewayRefundHandler | undefined;
117
+ /** Adapter keys whose owner is present (stable insertion order). */
118
+ list(): string[];
119
+ /** Every registered adapter key, presence-blind. */
120
+ listAll(): string[];
121
+ }
122
+ //# sourceMappingURL=gateway-refund-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gateway-refund-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/gateway-refund-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAE1F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsDG;AACH,MAAM,WAAW,oBAAoB;IACnC,mEAAmE;IACnE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;CACjE;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAMD,oEAAoE;AACpE,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,qBAAqB;IAW9B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAXlC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyC;IAElE;;;;;;OAMG;gBAEgB,GAAG,GAAE,cAA8B,EACnC,eAAe,GAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAoB;IAG9E,QAAQ,CAAC,OAAO,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAW/D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIpC,oEAAoE;IACpE,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,kBAAkB,GAAG,SAAS;IAIzD,2EAA2E;IAC3E,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI1C;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI;IAczD,4EAA4E;IAC5E,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS;IAMzD;;;;;;;;OAQG;IACH,OAAO,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,GAAG,SAAS;IAUrE,oEAAoE;IACpE,IAAI,IAAI,MAAM,EAAE;IAMhB,oDAAoD;IACpD,OAAO,IAAI,MAAM,EAAE;CAGpB"}