@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.
- package/README.md +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-payment/SKILL.md +402 -260
- package/build/config.d.ts +15 -1
- package/build/config.d.ts.map +1 -1
- package/build/config.js +99 -3
- package/build/config.js.map +1 -1
- package/build/consts.d.ts +27 -0
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +27 -0
- package/build/consts.js.map +1 -1
- package/build/consumer/capture.d.ts +74 -0
- package/build/consumer/capture.d.ts.map +1 -0
- package/build/consumer/capture.js +291 -0
- package/build/consumer/capture.js.map +1 -0
- package/build/consumer/format.d.ts +27 -0
- package/build/consumer/format.d.ts.map +1 -0
- package/build/consumer/format.js +81 -0
- package/build/consumer/format.js.map +1 -0
- package/build/consumer/handlers.d.ts +28 -0
- package/build/consumer/handlers.d.ts.map +1 -0
- package/build/consumer/handlers.js +173 -0
- package/build/consumer/handlers.js.map +1 -0
- package/build/consumer/index.d.ts +7 -0
- package/build/consumer/index.d.ts.map +1 -0
- package/build/consumer/index.js +6 -0
- package/build/consumer/index.js.map +1 -0
- package/build/consumer/mail.d.ts +27 -0
- package/build/consumer/mail.d.ts.map +1 -0
- package/build/consumer/mail.js +314 -0
- package/build/consumer/mail.js.map +1 -0
- package/build/consumer/origin.d.ts +14 -0
- package/build/consumer/origin.d.ts.map +1 -0
- package/build/consumer/origin.js +47 -0
- package/build/consumer/origin.js.map +1 -0
- package/build/consumer/reconcile.d.ts +12 -0
- package/build/consumer/reconcile.d.ts.map +1 -0
- package/build/consumer/reconcile.js +317 -0
- package/build/consumer/reconcile.js.map +1 -0
- package/build/consumer/records.d.ts +78 -0
- package/build/consumer/records.d.ts.map +1 -0
- package/build/consumer/records.js +296 -0
- package/build/consumer/records.js.map +1 -0
- package/build/consumer/service.d.ts +51 -0
- package/build/consumer/service.d.ts.map +1 -0
- package/build/consumer/service.js +760 -0
- package/build/consumer/service.js.map +1 -0
- package/build/consumer/withdrawal.d.ts +57 -0
- package/build/consumer/withdrawal.d.ts.map +1 -0
- package/build/consumer/withdrawal.js +247 -0
- package/build/consumer/withdrawal.js.map +1 -0
- package/build/index.d.ts +4 -2
- package/build/index.d.ts.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/model.d.ts +6 -1
- package/build/model.d.ts.map +1 -1
- package/build/model.js +134 -4
- package/build/model.js.map +1 -1
- package/build/observer.d.ts +4 -0
- package/build/observer.d.ts.map +1 -1
- package/build/observer.js +13 -0
- package/build/observer.js.map +1 -1
- package/build/plugins/checkout-plugins.d.ts +49 -0
- package/build/plugins/checkout-plugins.d.ts.map +1 -0
- package/build/plugins/checkout-plugins.js +124 -0
- package/build/plugins/checkout-plugins.js.map +1 -0
- package/build/plugins/estimate.d.ts.map +1 -1
- package/build/plugins/estimate.js +41 -8
- package/build/plugins/estimate.js.map +1 -1
- package/build/plugins/events.d.ts +2 -0
- package/build/plugins/events.d.ts.map +1 -1
- package/build/plugins/events.js +126 -9
- package/build/plugins/events.js.map +1 -1
- package/build/plugins/fx.d.ts +5 -0
- package/build/plugins/fx.d.ts.map +1 -1
- package/build/plugins/fx.js +25 -0
- package/build/plugins/fx.js.map +1 -1
- package/build/plugins/portal.d.ts.map +1 -1
- package/build/plugins/portal.js +11 -1
- package/build/plugins/portal.js.map +1 -1
- package/build/plugins/stripe.d.ts +21 -2
- package/build/plugins/stripe.d.ts.map +1 -1
- package/build/plugins/stripe.js +397 -61
- package/build/plugins/stripe.js.map +1 -1
- package/build/resource.d.ts +8 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +59 -2
- package/build/resource.js.map +1 -1
- package/build/service.d.ts +2 -1
- package/build/service.d.ts.map +1 -1
- package/build/service.js +28 -5
- package/build/service.js.map +1 -1
- package/build/subscription.d.ts +6 -0
- package/build/subscription.d.ts.map +1 -1
- package/build/subscription.js +1 -0
- package/build/subscription.js.map +1 -1
- package/build/sync.d.ts +7 -0
- package/build/sync.d.ts.map +1 -1
- package/build/sync.js +108 -15
- package/build/sync.js.map +1 -1
- package/build/types.d.ts +707 -4
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts +22 -1
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +27 -1
- package/build/utils.js.map +1 -1
- package/package.json +14 -13
- package/src/config.ts +111 -7
- package/src/consts.ts +34 -0
- package/src/consumer/capture.ts +362 -0
- package/src/consumer/format.ts +90 -0
- package/src/consumer/handlers.ts +211 -0
- package/src/consumer/index.ts +6 -0
- package/src/consumer/mail.ts +368 -0
- package/src/consumer/origin.ts +63 -0
- package/src/consumer/reconcile.ts +329 -0
- package/src/consumer/records.ts +374 -0
- package/src/consumer/service.ts +868 -0
- package/src/consumer/withdrawal.ts +302 -0
- package/src/index.ts +6 -4
- package/src/model.ts +148 -6
- package/src/observer.ts +15 -2
- package/src/plugins/checkout-plugins.ts +155 -0
- package/src/plugins/estimate.ts +49 -9
- package/src/plugins/events.ts +135 -11
- package/src/plugins/fx.ts +29 -0
- package/src/plugins/portal.ts +11 -1
- package/src/plugins/stripe.ts +476 -60
- package/src/resource.ts +87 -4
- package/src/service.ts +28 -6
- package/src/subscription.ts +7 -0
- package/src/sync.ts +124 -17
- package/src/types.ts +756 -6
- package/src/utils.ts +56 -7
- package/tests/checkout-consumer.spec.ts +348 -0
- package/tests/checkout-plugins.spec.ts +164 -0
- package/tests/consumer-events.spec.ts +218 -0
- package/tests/consumer-fixtures.ts +132 -0
- package/tests/consumer-ops.spec.ts +351 -0
- package/tests/consumer-rights.integration.spec.ts +150 -0
- package/tests/consumer-rights.spec.ts +501 -0
- package/tests/context.ts +20 -2
- package/tests/fake-stripe.ts +188 -18
package/tests/fake-stripe.ts
CHANGED
|
@@ -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,
|
|
16
|
+
declareConsumerRights, declarePaymentPlan, declarePaymentPricing, declarePaymentProduct, portalBranding,
|
|
17
|
+
stripeSecrets,
|
|
15
18
|
} from '../src/config.js'
|
|
16
19
|
import {
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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 => {
|
|
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)
|
|
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 =
|
|
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
|
-
|
|
573
|
-
|
|
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
|
-
|
|
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: [],
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
return { ctx, stripe, state, stores, observed }
|
|
971
|
+
return { ctx, stripe, state, stores, observed, mails: mailer.captured, mailer }
|
|
802
972
|
}
|