@owlmeans/server-payment 0.1.18-rc.20 → 0.1.18-rc.21

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 (144) hide show
  1. package/README.md +1 -1
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-payment/SKILL.md +402 -260
  4. package/build/config.d.ts +15 -1
  5. package/build/config.d.ts.map +1 -1
  6. package/build/config.js +99 -3
  7. package/build/config.js.map +1 -1
  8. package/build/consts.d.ts +27 -0
  9. package/build/consts.d.ts.map +1 -1
  10. package/build/consts.js +27 -0
  11. package/build/consts.js.map +1 -1
  12. package/build/consumer/capture.d.ts +74 -0
  13. package/build/consumer/capture.d.ts.map +1 -0
  14. package/build/consumer/capture.js +291 -0
  15. package/build/consumer/capture.js.map +1 -0
  16. package/build/consumer/format.d.ts +27 -0
  17. package/build/consumer/format.d.ts.map +1 -0
  18. package/build/consumer/format.js +81 -0
  19. package/build/consumer/format.js.map +1 -0
  20. package/build/consumer/handlers.d.ts +28 -0
  21. package/build/consumer/handlers.d.ts.map +1 -0
  22. package/build/consumer/handlers.js +173 -0
  23. package/build/consumer/handlers.js.map +1 -0
  24. package/build/consumer/index.d.ts +7 -0
  25. package/build/consumer/index.d.ts.map +1 -0
  26. package/build/consumer/index.js +6 -0
  27. package/build/consumer/index.js.map +1 -0
  28. package/build/consumer/mail.d.ts +27 -0
  29. package/build/consumer/mail.d.ts.map +1 -0
  30. package/build/consumer/mail.js +314 -0
  31. package/build/consumer/mail.js.map +1 -0
  32. package/build/consumer/origin.d.ts +14 -0
  33. package/build/consumer/origin.d.ts.map +1 -0
  34. package/build/consumer/origin.js +47 -0
  35. package/build/consumer/origin.js.map +1 -0
  36. package/build/consumer/reconcile.d.ts +12 -0
  37. package/build/consumer/reconcile.d.ts.map +1 -0
  38. package/build/consumer/reconcile.js +317 -0
  39. package/build/consumer/reconcile.js.map +1 -0
  40. package/build/consumer/records.d.ts +78 -0
  41. package/build/consumer/records.d.ts.map +1 -0
  42. package/build/consumer/records.js +296 -0
  43. package/build/consumer/records.js.map +1 -0
  44. package/build/consumer/service.d.ts +51 -0
  45. package/build/consumer/service.d.ts.map +1 -0
  46. package/build/consumer/service.js +760 -0
  47. package/build/consumer/service.js.map +1 -0
  48. package/build/consumer/withdrawal.d.ts +57 -0
  49. package/build/consumer/withdrawal.d.ts.map +1 -0
  50. package/build/consumer/withdrawal.js +247 -0
  51. package/build/consumer/withdrawal.js.map +1 -0
  52. package/build/index.d.ts +4 -2
  53. package/build/index.d.ts.map +1 -1
  54. package/build/index.js +4 -2
  55. package/build/index.js.map +1 -1
  56. package/build/model.d.ts +6 -1
  57. package/build/model.d.ts.map +1 -1
  58. package/build/model.js +134 -4
  59. package/build/model.js.map +1 -1
  60. package/build/observer.d.ts +4 -0
  61. package/build/observer.d.ts.map +1 -1
  62. package/build/observer.js +13 -0
  63. package/build/observer.js.map +1 -1
  64. package/build/plugins/checkout-plugins.d.ts +49 -0
  65. package/build/plugins/checkout-plugins.d.ts.map +1 -0
  66. package/build/plugins/checkout-plugins.js +124 -0
  67. package/build/plugins/checkout-plugins.js.map +1 -0
  68. package/build/plugins/estimate.d.ts.map +1 -1
  69. package/build/plugins/estimate.js +41 -8
  70. package/build/plugins/estimate.js.map +1 -1
  71. package/build/plugins/events.d.ts +2 -0
  72. package/build/plugins/events.d.ts.map +1 -1
  73. package/build/plugins/events.js +126 -9
  74. package/build/plugins/events.js.map +1 -1
  75. package/build/plugins/fx.d.ts +5 -0
  76. package/build/plugins/fx.d.ts.map +1 -1
  77. package/build/plugins/fx.js +25 -0
  78. package/build/plugins/fx.js.map +1 -1
  79. package/build/plugins/portal.d.ts.map +1 -1
  80. package/build/plugins/portal.js +11 -1
  81. package/build/plugins/portal.js.map +1 -1
  82. package/build/plugins/stripe.d.ts +21 -2
  83. package/build/plugins/stripe.d.ts.map +1 -1
  84. package/build/plugins/stripe.js +397 -61
  85. package/build/plugins/stripe.js.map +1 -1
  86. package/build/resource.d.ts +8 -1
  87. package/build/resource.d.ts.map +1 -1
  88. package/build/resource.js +59 -2
  89. package/build/resource.js.map +1 -1
  90. package/build/service.d.ts +2 -1
  91. package/build/service.d.ts.map +1 -1
  92. package/build/service.js +28 -5
  93. package/build/service.js.map +1 -1
  94. package/build/subscription.d.ts +6 -0
  95. package/build/subscription.d.ts.map +1 -1
  96. package/build/subscription.js +1 -0
  97. package/build/subscription.js.map +1 -1
  98. package/build/sync.d.ts +7 -0
  99. package/build/sync.d.ts.map +1 -1
  100. package/build/sync.js +108 -15
  101. package/build/sync.js.map +1 -1
  102. package/build/types.d.ts +707 -4
  103. package/build/types.d.ts.map +1 -1
  104. package/build/utils.d.ts +22 -1
  105. package/build/utils.d.ts.map +1 -1
  106. package/build/utils.js +27 -1
  107. package/build/utils.js.map +1 -1
  108. package/package.json +14 -13
  109. package/src/config.ts +111 -7
  110. package/src/consts.ts +34 -0
  111. package/src/consumer/capture.ts +362 -0
  112. package/src/consumer/format.ts +90 -0
  113. package/src/consumer/handlers.ts +211 -0
  114. package/src/consumer/index.ts +6 -0
  115. package/src/consumer/mail.ts +368 -0
  116. package/src/consumer/origin.ts +63 -0
  117. package/src/consumer/reconcile.ts +329 -0
  118. package/src/consumer/records.ts +374 -0
  119. package/src/consumer/service.ts +868 -0
  120. package/src/consumer/withdrawal.ts +302 -0
  121. package/src/index.ts +6 -4
  122. package/src/model.ts +148 -6
  123. package/src/observer.ts +15 -2
  124. package/src/plugins/checkout-plugins.ts +155 -0
  125. package/src/plugins/estimate.ts +49 -9
  126. package/src/plugins/events.ts +135 -11
  127. package/src/plugins/fx.ts +29 -0
  128. package/src/plugins/portal.ts +11 -1
  129. package/src/plugins/stripe.ts +476 -60
  130. package/src/resource.ts +87 -4
  131. package/src/service.ts +28 -6
  132. package/src/subscription.ts +7 -0
  133. package/src/sync.ts +124 -17
  134. package/src/types.ts +756 -6
  135. package/src/utils.ts +56 -7
  136. package/tests/checkout-consumer.spec.ts +348 -0
  137. package/tests/checkout-plugins.spec.ts +164 -0
  138. package/tests/consumer-events.spec.ts +218 -0
  139. package/tests/consumer-fixtures.ts +132 -0
  140. package/tests/consumer-ops.spec.ts +351 -0
  141. package/tests/consumer-rights.integration.spec.ts +150 -0
  142. package/tests/consumer-rights.spec.ts +501 -0
  143. package/tests/context.ts +20 -2
  144. package/tests/fake-stripe.ts +188 -18
@@ -9,23 +9,30 @@ import type { PlanCapability } from '@owlmeans/payment'
9
9
  import { applyQuery, firstMatch, matchCriteria, RecordExists, UnknownRecordError } from '@owlmeans/resource'
10
10
  import type { Criteria, ListOptions, ResourceRecord } from '@owlmeans/resource'
11
11
  import type { MongoResource } from '@owlmeans/mongo-resource'
12
+ import { makeConsoleMailerService, MAILER_SERVICE } from '@owlmeans/mailer'
13
+ import type { MailMessage } from '@owlmeans/mailer'
12
14
  import type { Context as ApiContext } from '@owlmeans/server-api'
13
15
  import {
14
- declarePaymentPlan, declarePaymentPricing, declarePaymentProduct, portalBranding, stripeSecrets,
16
+ declareConsumerRights, declarePaymentPlan, declarePaymentPricing, declarePaymentProduct, portalBranding,
17
+ stripeSecrets,
15
18
  } from '../src/config.js'
16
19
  import {
17
- RES_PAYGATE_CUSTOMER, RES_PAYMENT_FINGERPRINT, RES_PAYMENT_FULFILLMENT, RES_PAYMENT_SUBSCRIPTION,
20
+ RES_BILLING_PROFILE, RES_CONSUMER_CONSENT, RES_CONSUMER_DECLARATION, RES_CONSUMER_EVENT, RES_PAYGATE_CUSTOMER,
21
+ RES_PAYMENT_FINGERPRINT, RES_PAYMENT_FULFILLMENT, RES_PAYMENT_PURCHASE, RES_PAYMENT_SUBSCRIPTION,
18
22
  RES_PAYMENT_USAGE, RES_PAYMENT_USAGE_COUNTER, RES_PAYMENT_WEBHOOK,
19
23
  } from '../src/consts.js'
24
+ import { appendConsumerRights } from '../src/consumer/service.js'
20
25
  import {
21
- makeFingerprintResource, makeFulfillmentResource, makePaygateCustomerResource, makeSubscriptionResource,
22
- makeUsageCounterResource, makeUsageResource, makeWebhookResource,
26
+ makeBillingProfileResource, makeConsumerConsentResource, makeConsumerDeclarationResource,
27
+ makeConsumerEventResource, makeFingerprintResource, makeFulfillmentResource, makePaygateCustomerResource,
28
+ makePurchaseResource, makeSubscriptionResource, makeUsageCounterResource, makeUsageResource, makeWebhookResource,
23
29
  } from '../src/resource.js'
24
30
  import { appendPaymentGatewayService } from '../src/service.js'
25
31
  import { observer } from '../src/utils.js'
26
32
  import type {
27
- Config, DisputeEvent, PaymentFailedEvent, PaymentPlanDef, PortalBrandingDef, PricingDef, RefundEvent,
28
- SubscriptionEvent, TopUpCompletion,
33
+ CancellationEvent, Config, ConsentEvent, ConsumerRightsDef, ConsumerRightsOptions, DisputeEvent, PaymentFailedEvent,
34
+ PaymentPlanDef, PortalBrandingDef, PricingDef, RefundEvent, SubscriptionEvent, TopUpCompletion, UsageMeter,
35
+ WithdrawalEvent,
29
36
  } from '../src/types.js'
30
37
 
31
38
  // -----------------------------------------------------------------------------------------------
@@ -121,6 +128,8 @@ export const declareTestCatalogue = (cfg: Config, opts: CatalogueOptions = {}):
121
128
 
122
129
  type Rec = Record<string, any>
123
130
 
131
+ export type FakeFailure = string | Error
132
+
124
133
  export interface FakeStripeState {
125
134
  apiVersion: string
126
135
  /** Every SDK method called, in order: `webhookEndpoints.create`, … */
@@ -154,6 +163,21 @@ export interface FakeStripeState {
154
163
  fxRates: Record<string, { exchangeRate: number, baseRate?: number, referenceRate?: number, fxFeeRate?: number }>
155
164
  /** Every `rawRequest` to `/v1/fx_quotes` throws. */
156
165
  fxUnavailable: boolean
166
+ /** Completed or open checkout sessions `checkout.sessions.list` answers. */
167
+ listedSessions: Rec[]
168
+ creditNotes: Rec[]
169
+ /** Every `creditNotes.preview` params. */
170
+ creditNotePreviews: Rec[]
171
+ /** Every call's request options (`idempotencyKey`), by method. */
172
+ requestOptions: Record<string, Rec[]>
173
+ /**
174
+ * Methods that throw on their next call: name → an error message (a Stripe invalid-request
175
+ * error), an error to throw as it is, or a list of those — one per call, in order.
176
+ */
177
+ failures: Record<string, FakeFailure | FakeFailure[]>
178
+ /** Every `subscriptions.update` / `.cancel` params, in order. */
179
+ subscriptionChanges: Rec[]
180
+ expiredSessions: string[]
157
181
  }
158
182
 
159
183
  /** The API version the installed SDK defaults to, read from a real client. */
@@ -188,10 +212,21 @@ export const makeFakeStripe = (initial: Partial<FakeStripeState> = {}): { stripe
188
212
  taxCalculations: [], taxRates: {}, taxInvalidCountries: [], taxUnsupportedCountries: [],
189
213
  taxSettings: { defaults: { tax_behavior: null, tax_code: null }, head_office: null, status: 'active', status_details: {} },
190
214
  rawRequests: [], fxRates: {}, fxUnavailable: false,
215
+ listedSessions: [], creditNotes: [], creditNotePreviews: [], requestOptions: {}, failures: {},
216
+ subscriptionChanges: [], expiredSessions: [],
191
217
  ...initial,
192
218
  }
193
219
  const next = (prefix: string): string => `${prefix}_${++state.seq}`
194
- const call = (name: string): void => { state.calls.push(name) }
220
+ const call = (name: string, options?: Rec): void => {
221
+ state.calls.push(name)
222
+ if (options != null) (state.requestOptions[name] = state.requestOptions[name] ?? []).push(options)
223
+ const failure = state.failures[name]
224
+ if (failure != null) {
225
+ const next = Array.isArray(failure) ? failure.shift() : failure
226
+ if (!Array.isArray(failure) || failure.length === 0) delete state.failures[name]
227
+ if (next != null) throw typeof next === 'string' ? invalidRequest(next) : next
228
+ }
229
+ }
195
230
  const find = (list: Rec[], id: string, what: string): Rec => {
196
231
  const found = list.find(item => item.id === id)
197
232
  if (found == null) throw missing(what)
@@ -272,6 +307,52 @@ export const makeFakeStripe = (initial: Partial<FakeStripeState> = {}): { stripe
272
307
  if (subscription == null) throw missing('subscription')
273
308
  return structuredClone(subscription)
274
309
  },
310
+ update: async (id: string, params: Rec, options?: Rec) => {
311
+ call('subscriptions.update', options)
312
+ const subscription = state.subscriptions[id]
313
+ if (subscription == null) throw missing('subscription')
314
+ state.subscriptionChanges.push({ id, method: 'update', ...structuredClone(params) })
315
+ Object.assign(subscription, params)
316
+ return structuredClone(subscription)
317
+ },
318
+ cancel: async (id: string, params: Rec = {}, options?: Rec) => {
319
+ call('subscriptions.cancel', options)
320
+ const subscription = state.subscriptions[id]
321
+ if (subscription == null) throw missing('subscription')
322
+ state.subscriptionChanges.push({ id, method: 'cancel', ...structuredClone(params) })
323
+ subscription.status = 'canceled'
324
+ return structuredClone(subscription)
325
+ },
326
+ },
327
+ creditNotes: {
328
+ preview: async (params: Rec) => {
329
+ call('creditNotes.preview')
330
+ state.creditNotePreviews.push(structuredClone(params))
331
+ const invoice = state.invoices[params.invoice]
332
+ if (invoice == null) throw missing('invoice')
333
+ const rate = invoice.subtotal > 0 ? (invoice.tax ?? 0) / invoice.subtotal : 0
334
+ const amount = params.lines.reduce((sum: number, line: Rec) => sum + (line.amount ?? 0), 0)
335
+ const tax = Math.round(amount * rate)
336
+ return { id: 'cnpreview', object: 'credit_note', amount: amount + tax, total: amount + tax, subtotal: amount, currency: invoice.currency }
337
+ },
338
+ create: async (params: Rec, options?: Rec) => {
339
+ call('creditNotes.create', options)
340
+ const invoice = state.invoices[params.invoice]
341
+ if (invoice == null) throw missing('invoice')
342
+ const rate = invoice.subtotal > 0 ? (invoice.tax ?? 0) / invoice.subtotal : 0
343
+ const amount = params.lines.reduce((sum: number, line: Rec) => sum + (line.amount ?? 0), 0)
344
+ const note = {
345
+ id: next('cn'), object: 'credit_note', invoice: params.invoice, refund: params.refund, status: 'issued',
346
+ total: amount + Math.round(amount * rate), currency: invoice.currency, memo: params.memo,
347
+ metadata: { ...params.metadata }, lines: structuredClone(params.lines),
348
+ }
349
+ state.creditNotes.push(note)
350
+ return structuredClone(note)
351
+ },
352
+ list: async (params: Rec = {}) => {
353
+ call('creditNotes.list')
354
+ return page(state.creditNotes.filter(note => params.invoice == null || note.invoice === params.invoice), params)
355
+ },
275
356
  },
276
357
  invoices: {
277
358
  retrieve: async (id: string) => {
@@ -292,7 +373,17 @@ export const makeFakeStripe = (initial: Partial<FakeStripeState> = {}): { stripe
292
373
  refunds: {
293
374
  list: async (params: Rec = {}) => {
294
375
  call('refunds.list')
295
- return page(state.refunds.filter(refund => params.charge == null || refund.charge === params.charge), params)
376
+ return page(state.refunds.filter(refund => (params.charge == null || refund.charge === params.charge)
377
+ && (params.payment_intent == null || refund.payment_intent === params.payment_intent)), params)
378
+ },
379
+ create: async (params: Rec, options?: Rec) => {
380
+ call('refunds.create', options)
381
+ const refund = {
382
+ id: next('re'), object: 'refund', amount: params.amount, currency: params.currency ?? 'eur', status: 'succeeded',
383
+ payment_intent: params.payment_intent, charge: null, reason: params.reason, metadata: { ...params.metadata },
384
+ }
385
+ state.refunds.push(refund)
386
+ return structuredClone(refund)
296
387
  },
297
388
  },
298
389
  prices: {
@@ -348,13 +439,14 @@ export const makeFakeStripe = (initial: Partial<FakeStripeState> = {}): { stripe
348
439
  },
349
440
  create: async (params: Rec) => {
350
441
  call('customers.create')
351
- const customer = { id: next('cus'), object: 'customer', ...structuredClone(params) }
442
+ const customer = { id: next('cus'), object: 'customer', address: null, ...structuredClone(params) }
352
443
  state.customers[customer.id] = customer
353
444
  return structuredClone(customer)
354
445
  },
355
446
  update: async (id: string, params: Rec) => {
356
447
  call('customers.update')
357
- const customer = find(state.customers, id, 'customer')
448
+ const customer = state.customers[id]
449
+ if (customer == null) throw missing('customer')
358
450
  Object.assign(customer, params)
359
451
  return structuredClone(customer)
360
452
  },
@@ -366,6 +458,16 @@ export const makeFakeStripe = (initial: Partial<FakeStripeState> = {}): { stripe
366
458
  state.checkoutSessions.push(structuredClone(params))
367
459
  return { id: next('cs'), url: `https://checkout.example.test/${state.seq}` }
368
460
  },
461
+ expire: async (id: string) => {
462
+ call('checkout.sessions.expire')
463
+ state.expiredSessions.push(id)
464
+ return { id, status: 'expired' }
465
+ },
466
+ list: async (params: Rec = {}) => {
467
+ call('checkout.sessions.list')
468
+ return page(state.listedSessions.filter(session => (params.status == null || session.status === params.status)
469
+ && (params.created?.gte == null || session.created >= params.created.gte)), params)
470
+ },
369
471
  listLineItems: async () => {
370
472
  call('checkout.sessions.listLineItems')
371
473
  return { data: [{ quantity: state.lineItemQuantity }] }
@@ -564,13 +666,15 @@ export interface MemoryStore {
564
666
  export const memoryResource = <T extends ResourceRecord>(resource: MongoResource<T>): MemoryStore => {
565
667
  const store: MemoryStore = { rows: [], failing: new Set() }
566
668
  const unique = (resource.indexes ?? []).filter(index => index.options?.unique === true)
567
- .map(index => Object.keys(index.index as Rec))
669
+ .map(index => ({ keys: Object.keys(index.index as Rec), sparse: index.options?.sparse === true }))
568
670
  let seq = 0
569
671
  const guard = (method: string): void => {
570
672
  if (store.failing.has(method)) throw new Error(`${resource.alias}.${method} unavailable`)
571
673
  }
572
- const violates = (doc: Rec, self?: Rec): boolean => unique.some(keys => store.rows.some(row =>
573
- row !== self && keys.every(key => plain(row[key] ?? null) === plain(doc[key] ?? null))))
674
+ // A sparse unique index ignores a document that carries none of its keys (as Mongo does).
675
+ const violates = (doc: Rec, self?: Rec): boolean => unique.some(({ keys, sparse }) =>
676
+ !(sparse && keys.every(key => doc[key] == null)) && store.rows.some(row =>
677
+ row !== self && keys.every(key => plain(row[key] ?? null) === plain(doc[key] ?? null))))
574
678
  const out = (row: Rec): T => structuredClone(row) as T
575
679
  const byId = (id: string) => store.rows.find(row => row.id === id)
576
680
 
@@ -653,6 +757,13 @@ export const memoryResource = <T extends ResourceRecord>(resource: MongoResource
653
757
  },
654
758
  updateOne: async (filter: Rec, update: Rec, opts: Rec = {}) => {
655
759
  guard('updateOne')
760
+ // Like Mongo: an update is checked against the unique indexes before it lands.
761
+ const found = matchRaw(filter)
762
+ if (found != null) {
763
+ const candidate = structuredClone(found)
764
+ applyUpdate(candidate, update, false)
765
+ if (violates(candidate, found)) throw duplicate()
766
+ }
656
767
  const { doc, inserted } = upsert(filter, update, opts)
657
768
  return { matchedCount: doc != null && !inserted ? 1 : 0, upsertedCount: inserted ? 1 : 0 }
658
769
  },
@@ -710,6 +821,20 @@ export interface FakeContextOptions {
710
821
  portal?: PortalBrandingDef
711
822
  pricing?: PricingDef
712
823
  stripe?: Partial<FakeStripeState>
824
+ /** Declare a consumer-rights policy (with trader and mail options). */
825
+ consumerRights?: ConsumerRightsDef
826
+ /** The consumer-rights usage meter. */
827
+ meter?: UsageMeter
828
+ /** Register a console mailer under `MAILER_SERVICE` (default true). */
829
+ mailer?: boolean
830
+ /** Register the gateway before the application's consumer-rights call (default: after). */
831
+ gatewayFirst?: boolean
832
+ /** The application's consumer-rights options (default: managed through the fake, with `meter`). */
833
+ rights?: (stripe: Stripe) => ConsumerRightsOptions
834
+ /** The gateway's `manage` (default false). */
835
+ gatewayManage?: boolean
836
+ /** Run on the context after the registrations, before it is configured and initialized. */
837
+ wire?: (ctx: ApiContext) => void
713
838
  }
714
839
 
715
840
  export interface Observed {
@@ -718,9 +843,16 @@ export interface Observed {
718
843
  refund: RefundEvent[]
719
844
  dispute: DisputeEvent[]
720
845
  paymentFailed: PaymentFailedEvent[]
846
+ consent: ConsentEvent[]
847
+ withdrawal: WithdrawalEvent[]
848
+ cancellation: CancellationEvent[]
721
849
  /** Throw from the next `n` subscription callbacks. */
722
850
  failSubscription: number
723
851
  failTopUp: number
852
+ failWithdrawal: number
853
+ /** Run inside every top-up / subscription callback, before it records the event. */
854
+ onTopUp?: (event: TopUpCompletion) => Promise<void>
855
+ onSubscription?: (event: SubscriptionEvent) => Promise<void>
724
856
  }
725
857
 
726
858
  export interface FakeContext {
@@ -729,6 +861,10 @@ export interface FakeContext {
729
861
  state: FakeStripeState
730
862
  stores: Record<string, MemoryStore>
731
863
  observed: Observed
864
+ /** Every mail the console mailer took. */
865
+ mails: MailMessage[]
866
+ /** The console mailer — replace its `send` to simulate a transport failure. */
867
+ mailer: ReturnType<typeof makeConsoleMailerService>
732
868
  }
733
869
 
734
870
  /**
@@ -753,6 +889,9 @@ export const makeFakeContext = async (opts: FakeContextOptions = {}): Promise<Fa
753
889
  if (opts.pricing != null) {
754
890
  declarePaymentPricing(cfg, opts.pricing)
755
891
  }
892
+ if (opts.consumerRights != null) {
893
+ declareConsumerRights(cfg, opts.consumerRights)
894
+ }
756
895
 
757
896
  const ctx = makeServerContext(cfg as unknown as ServerConfig) as unknown as ApiContext
758
897
  const stores: Record<string, MemoryStore> = {}
@@ -764,17 +903,39 @@ export const makeFakeContext = async (opts: FakeContextOptions = {}): Promise<Fa
764
903
  [RES_PAYMENT_USAGE, makeUsageResource()],
765
904
  [RES_PAYMENT_USAGE_COUNTER, makeUsageCounterResource()],
766
905
  [RES_PAYMENT_FINGERPRINT, makeFingerprintResource()],
906
+ [RES_BILLING_PROFILE, makeBillingProfileResource()],
907
+ [RES_PAYMENT_PURCHASE, makePurchaseResource()],
908
+ [RES_CONSUMER_CONSENT, makeConsumerConsentResource()],
909
+ [RES_CONSUMER_DECLARATION, makeConsumerDeclarationResource()],
910
+ [RES_CONSUMER_EVENT, makeConsumerEventResource()],
767
911
  ] as const
768
912
  for (const [alias, resource] of resources) {
769
913
  stores[alias] = memoryResource(resource as unknown as MongoResource<ResourceRecord>)
770
914
  ctx.registerResource(resource as never)
771
915
  }
772
- appendPaymentGatewayService(ctx as never, { manage: false })
916
+ const { stripe, state } = makeFakeStripe(opts.stripe)
917
+ const mailer = makeConsoleMailerService(MAILER_SERVICE)
918
+ if (opts.mailer !== false) {
919
+ ctx.registerService(mailer)
920
+ }
921
+ // The consumer-rights service manages the paygate through the fake; the gateway stays unmanaged.
922
+ const rights = opts.rights?.(stripe)
923
+ ?? { manage: true, stripe: async () => stripe, ...(opts.meter != null ? { usage: opts.meter } : {}) }
924
+ const gatewayOpts = { manage: opts.gatewayManage ?? false }
925
+ if (opts.gatewayFirst === true) {
926
+ appendPaymentGatewayService(ctx as never, gatewayOpts)
927
+ appendConsumerRights(ctx as never, rights)
928
+ } else {
929
+ appendConsumerRights(ctx as never, rights)
930
+ appendPaymentGatewayService(ctx as never, gatewayOpts)
931
+ }
932
+ opts.wire?.(ctx)
773
933
  ctx.configure()
774
934
  await ctx.init()
775
935
 
776
936
  const observed: Observed = {
777
- topUp: [], subscription: [], refund: [], dispute: [], paymentFailed: [], failSubscription: 0, failTopUp: 0,
937
+ topUp: [], subscription: [], refund: [], dispute: [], paymentFailed: [], consent: [], withdrawal: [],
938
+ cancellation: [], failSubscription: 0, failTopUp: 0, failWithdrawal: 0,
778
939
  }
779
940
  const completions = observer(ctx)
780
941
  await completions.ready()
@@ -783,6 +944,7 @@ export const makeFakeContext = async (opts: FakeContextOptions = {}): Promise<Fa
783
944
  observed.failTopUp--
784
945
  throw new Error('ledger unavailable')
785
946
  }
947
+ await observed.onTopUp?.(event)
786
948
  observed.topUp.push(event)
787
949
  })
788
950
  completions.onSubscription(async event => {
@@ -790,13 +952,21 @@ export const makeFakeContext = async (opts: FakeContextOptions = {}): Promise<Fa
790
952
  observed.failSubscription--
791
953
  throw new Error('observer unavailable')
792
954
  }
955
+ await observed.onSubscription?.(event)
793
956
  observed.subscription.push(event)
794
957
  })
795
958
  completions.onRefund(async event => { observed.refund.push(event) })
796
959
  completions.onDispute(async event => { observed.dispute.push(event) })
797
960
  completions.onPaymentFailed(async event => { observed.paymentFailed.push(event) })
961
+ completions.onConsent(async event => { observed.consent.push(event) })
962
+ completions.onWithdrawal(async event => {
963
+ if (observed.failWithdrawal > 0) {
964
+ observed.failWithdrawal--
965
+ throw new Error('ledger unavailable')
966
+ }
967
+ observed.withdrawal.push(event)
968
+ })
969
+ completions.onCancellation(async event => { observed.cancellation.push(event) })
798
970
 
799
- const { stripe, state } = makeFakeStripe(opts.stripe)
800
-
801
- return { ctx, stripe, state, stores, observed }
971
+ return { ctx, stripe, state, stores, observed, mails: mailer.captured, mailer }
802
972
  }