@owlmeans/server-payment 0.1.18-rc.2 → 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 +109 -25
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/server-payment/SKILL.md +475 -52
- package/build/actions/index.d.ts +1 -0
- package/build/actions/index.d.ts.map +1 -1
- package/build/actions/index.js +1 -0
- package/build/actions/index.js.map +1 -1
- package/build/actions/resync-subscriptions.d.ts +3 -0
- package/build/actions/resync-subscriptions.d.ts.map +1 -0
- package/build/actions/resync-subscriptions.js +7 -0
- package/build/actions/resync-subscriptions.js.map +1 -0
- package/build/actions/resync.d.ts +1 -0
- package/build/actions/resync.d.ts.map +1 -1
- package/build/actions/resync.js +7 -3
- package/build/actions/resync.js.map +1 -1
- package/build/actions/webhook.d.ts.map +1 -1
- package/build/actions/webhook.js +6 -3
- package/build/actions/webhook.js.map +1 -1
- package/build/config.d.ts +60 -5
- package/build/config.d.ts.map +1 -1
- package/build/config.js +237 -5
- package/build/config.js.map +1 -1
- package/build/consts.d.ts +75 -3
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +94 -5
- 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/entitlement.d.ts +10 -0
- package/build/entitlement.d.ts.map +1 -0
- package/build/entitlement.js +69 -0
- package/build/entitlement.js.map +1 -0
- package/build/entrypoints.d.ts +1 -1
- package/build/entrypoints.d.ts.map +1 -1
- package/build/entrypoints.js +2 -1
- package/build/entrypoints.js.map +1 -1
- package/build/gate.d.ts +32 -4
- package/build/gate.d.ts.map +1 -1
- package/build/gate.js +59 -19
- package/build/gate.js.map +1 -1
- package/build/index.d.ts +18 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +15 -3
- package/build/index.js.map +1 -1
- package/build/limit.d.ts +13 -0
- package/build/limit.d.ts.map +1 -0
- package/build/limit.js +47 -0
- package/build/limit.js.map +1 -0
- package/build/model.d.ts +10 -1
- package/build/model.d.ts.map +1 -1
- package/build/model.js +215 -17
- package/build/model.js.map +1 -1
- package/build/observer.d.ts +9 -0
- package/build/observer.d.ts.map +1 -1
- package/build/observer.js +38 -12
- package/build/observer.js.map +1 -1
- package/build/plan.d.ts +22 -0
- package/build/plan.d.ts.map +1 -0
- package/build/plan.js +72 -0
- package/build/plan.js.map +1 -0
- 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 +43 -0
- package/build/plugins/estimate.d.ts.map +1 -0
- package/build/plugins/estimate.js +268 -0
- package/build/plugins/estimate.js.map +1 -0
- package/build/plugins/events.d.ts +45 -10
- package/build/plugins/events.d.ts.map +1 -1
- package/build/plugins/events.js +605 -136
- package/build/plugins/events.js.map +1 -1
- package/build/plugins/fx.d.ts +31 -0
- package/build/plugins/fx.d.ts.map +1 -0
- package/build/plugins/fx.js +81 -0
- package/build/plugins/fx.js.map +1 -0
- package/build/plugins/portal.d.ts +37 -0
- package/build/plugins/portal.d.ts.map +1 -0
- package/build/plugins/portal.js +265 -0
- package/build/plugins/portal.js.map +1 -0
- package/build/plugins/refunds.d.ts +33 -0
- package/build/plugins/refunds.d.ts.map +1 -0
- package/build/plugins/refunds.js +80 -0
- package/build/plugins/refunds.js.map +1 -0
- package/build/plugins/stripe.d.ts +36 -4
- package/build/plugins/stripe.d.ts.map +1 -1
- package/build/plugins/stripe.js +492 -97
- package/build/plugins/stripe.js.map +1 -1
- package/build/plugins/webhook-manager.d.ts +46 -0
- package/build/plugins/webhook-manager.d.ts.map +1 -0
- package/build/plugins/webhook-manager.js +231 -0
- package/build/plugins/webhook-manager.js.map +1 -0
- package/build/reconcile.d.ts +15 -0
- package/build/reconcile.d.ts.map +1 -0
- package/build/reconcile.js +88 -0
- package/build/reconcile.js.map +1 -0
- package/build/resource.d.ts +12 -1
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +97 -6
- package/build/resource.js.map +1 -1
- package/build/service.d.ts +28 -3
- package/build/service.d.ts.map +1 -1
- package/build/service.js +146 -19
- package/build/service.js.map +1 -1
- package/build/subscription.d.ts +48 -0
- package/build/subscription.d.ts.map +1 -0
- package/build/subscription.js +176 -0
- package/build/subscription.js.map +1 -0
- package/build/sync.d.ts +28 -1
- package/build/sync.d.ts.map +1 -1
- package/build/sync.js +208 -31
- package/build/sync.js.map +1 -1
- package/build/types.d.ts +1115 -36
- package/build/types.d.ts.map +1 -1
- package/build/usage.d.ts +63 -0
- package/build/usage.d.ts.map +1 -0
- package/build/usage.js +363 -0
- package/build/usage.js.map +1 -0
- package/build/utils.d.ts +52 -1
- package/build/utils.d.ts.map +1 -1
- package/build/utils.js +69 -7
- package/build/utils.js.map +1 -1
- package/package.json +17 -13
- package/src/actions/index.ts +1 -0
- package/src/actions/resync-subscriptions.ts +10 -0
- package/src/actions/resync.ts +6 -3
- package/src/actions/webhook.ts +5 -3
- package/src/config.ts +264 -8
- package/src/consts.ts +114 -6
- 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/entitlement.ts +84 -0
- package/src/entrypoints.ts +2 -1
- package/src/gate.ts +88 -21
- package/src/index.ts +24 -3
- package/src/limit.ts +57 -0
- package/src/model.ts +237 -18
- package/src/observer.ts +44 -11
- package/src/plan.ts +89 -0
- package/src/plugins/checkout-plugins.ts +155 -0
- package/src/plugins/estimate.ts +339 -0
- package/src/plugins/events.ts +677 -121
- package/src/plugins/fx.ts +122 -0
- package/src/plugins/portal.ts +306 -0
- package/src/plugins/refunds.ts +108 -0
- package/src/plugins/stripe.ts +581 -96
- package/src/plugins/webhook-manager.ts +270 -0
- package/src/reconcile.ts +103 -0
- package/src/resource.ts +152 -7
- package/src/service.ts +174 -18
- package/src/subscription.ts +231 -0
- package/src/sync.ts +249 -29
- package/src/types.ts +1227 -32
- package/src/usage.ts +453 -0
- package/src/utils.ts +127 -10
- package/tests/checkout-consumer.spec.ts +348 -0
- package/tests/checkout-plugins.spec.ts +164 -0
- package/tests/checkout.spec.ts +184 -83
- 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 +109 -0
- package/tests/entitlement.spec.ts +103 -0
- package/tests/estimate.spec.ts +240 -0
- package/tests/events.spec.ts +356 -0
- package/tests/fake-stripe.ts +972 -0
- package/tests/gate.spec.ts +62 -72
- package/tests/limit-gate.spec.ts +68 -0
- package/tests/portal.spec.ts +200 -0
- package/tests/protocol.spec.ts +23 -6
- package/tests/sync.spec.ts +114 -0
- package/tests/usage.integration.spec.ts +101 -0
- package/tests/usage.spec.ts +171 -0
- package/tests/webhook-manager.spec.ts +152 -0
package/src/model.ts
CHANGED
|
@@ -1,37 +1,256 @@
|
|
|
1
1
|
import type { JSONSchemaType } from 'ajv'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { DateSchema } from '@owlmeans/auth'
|
|
3
|
+
import {
|
|
4
|
+
CancellationKindSchema, CheckoutPricingModeSchema, ConsentKindSchema, ConsumerRegionSchema,
|
|
5
|
+
DeclarationChannelSchema, DeclarationKindSchema, PurchaseKindSchema, SubscriptionStatusSchema,
|
|
6
|
+
} from '@owlmeans/payment'
|
|
7
|
+
import type {
|
|
8
|
+
BillingProfileRecord, ConsumerConsentRecord, ConsumerDeclarationRecord, ConsumerEventRecord, FingerprintRecord,
|
|
9
|
+
PaygateCustomerRecord, PaymentFulfillmentRecord, PaymentSubscriptionRecord, PaymentUsageCounterRecord,
|
|
10
|
+
PaymentUsageRecord, PaymentWebhookRecord, PurchaseRecord,
|
|
11
|
+
} from './types.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Record schemas. Every stored property is declared: the resource coerces a write through its
|
|
15
|
+
* schema, and a property the schema does not know is written as a string. Dates follow the record
|
|
16
|
+
* convention (`DateSchema`). None of these records declares an ObjectId reference — `entityId`
|
|
17
|
+
* is an organization key and every other id is the paygate's.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const str = { type: 'string' } as const
|
|
21
|
+
const optStr = { type: 'string', nullable: true } as const
|
|
22
|
+
const num = { type: 'number' } as const
|
|
23
|
+
const optNum = { type: 'number', nullable: true } as const
|
|
24
|
+
const optBool = { type: 'boolean', nullable: true } as const
|
|
25
|
+
const date = DateSchema
|
|
26
|
+
const optDate = { ...DateSchema, nullable: true }
|
|
27
|
+
const id = { type: 'string', nullable: true } as const
|
|
5
28
|
|
|
6
29
|
export const PaygateCustomerSchema = {
|
|
7
30
|
type: 'object',
|
|
8
31
|
properties: {
|
|
9
|
-
id
|
|
10
|
-
|
|
11
|
-
email: { type: 'string', nullable: true }, name: { type: 'string', nullable: true },
|
|
12
|
-
taxId: { type: 'string', nullable: true },
|
|
32
|
+
id, paygate: str, externalId: str, entityId: optStr, profileId: optStr, email: optStr, name: optStr,
|
|
33
|
+
taxId: optStr, country: optStr, currency: optStr, deletedAt: optDate,
|
|
13
34
|
},
|
|
14
|
-
required: ['paygate', 'externalId'],
|
|
35
|
+
required: ['paygate', 'externalId'],
|
|
36
|
+
additionalProperties: false,
|
|
15
37
|
} as unknown as JSONSchemaType<PaygateCustomerRecord>
|
|
16
38
|
|
|
17
39
|
export const PaymentSubscriptionSchema = {
|
|
18
40
|
type: 'object',
|
|
19
41
|
properties: {
|
|
20
|
-
id
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
id, entityId: str, planSku: str, productSku: str, service: str, paygate: str, externalId: str,
|
|
43
|
+
itemId: optStr, priceId: optStr, status: SubscriptionStatusSchema, externalStatus: optStr, rank: num,
|
|
44
|
+
periodStart: optDate, periodEnd: optDate, cancelAtPeriodEnd: optBool, canceledAt: optDate,
|
|
45
|
+
endedAt: optDate, pausedAt: optDate, trialEnd: optDate, latestInvoiceId: optStr, customerId: optStr,
|
|
46
|
+
disputedAt: optDate, disputeStatus: optStr, createdAt: date, updatedAt: optDate, syncedAt: optDate,
|
|
47
|
+
lastEventId: optStr, initialPropagatedAt: optDate, currency: optStr,
|
|
48
|
+
checkoutSessionId: optStr, purchaseId: optStr, firstInvoiceId: optStr, country: optStr, email: optStr,
|
|
49
|
+
amountTotalMinor: optNum, amountTaxMinor: optNum, termsAccepted: optBool, startRequestId: optStr,
|
|
50
|
+
withdrawnAt: optDate,
|
|
51
|
+
propagated: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
nullable: true,
|
|
54
|
+
properties: {
|
|
55
|
+
planSku: str, rank: num, status: SubscriptionStatusSchema, cancelAtPeriodEnd: optBool,
|
|
56
|
+
pausedAt: optDate, renewedInvoiceId: optStr,
|
|
57
|
+
},
|
|
58
|
+
required: ['planSku', 'rank', 'status'],
|
|
59
|
+
additionalProperties: false,
|
|
60
|
+
},
|
|
25
61
|
},
|
|
26
|
-
required: [
|
|
27
|
-
|
|
62
|
+
required: [
|
|
63
|
+
'entityId', 'planSku', 'productSku', 'service', 'paygate', 'externalId', 'status', 'rank', 'createdAt',
|
|
64
|
+
],
|
|
65
|
+
additionalProperties: false,
|
|
28
66
|
} as unknown as JSONSchemaType<PaymentSubscriptionRecord>
|
|
29
67
|
|
|
68
|
+
export const PaymentFulfillmentSchema = {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
id, entityId: str, productSku: str, planSku: optStr, service: str, paygate: str, externalId: str,
|
|
72
|
+
paymentIntentId: optStr, chargeId: optStr, invoiceId: optStr, mode: CheckoutPricingModeSchema,
|
|
73
|
+
units: optNum, amountMinor: optNum, sourceChargeAmountMinor: optNum, amountCurrency: optStr,
|
|
74
|
+
chargeAmountMinor: optNum, currency: optStr, createdAt: date,
|
|
75
|
+
fulfilledAt: optDate, failedAt: optDate, refundedMinor: optNum, refundedAt: optDate,
|
|
76
|
+
disputedAt: optDate, disputeStatus: optStr,
|
|
77
|
+
country: optStr, email: optStr, profileId: optStr, amountTotalMinor: optNum, amountTaxMinor: optNum,
|
|
78
|
+
termsAccepted: optBool, purchaseId: optStr,
|
|
79
|
+
},
|
|
80
|
+
required: ['entityId', 'productSku', 'service', 'paygate', 'externalId', 'mode', 'createdAt'],
|
|
81
|
+
additionalProperties: false,
|
|
82
|
+
} as unknown as JSONSchemaType<PaymentFulfillmentRecord>
|
|
83
|
+
|
|
84
|
+
export const PaymentWebhookSchema = {
|
|
85
|
+
type: 'object',
|
|
86
|
+
properties: {
|
|
87
|
+
id, paygate: str, service: str, url: str, externalId: str,
|
|
88
|
+
/** The signing secret: field-encrypted when the database is configured with a key. */
|
|
89
|
+
secret: { type: 'string', secure: true },
|
|
90
|
+
apiVersion: str, events: { type: 'array', items: str }, hash: str, createdAt: date, updatedAt: optDate,
|
|
91
|
+
},
|
|
92
|
+
required: ['paygate', 'service', 'url', 'externalId', 'secret', 'apiVersion', 'events', 'hash', 'createdAt'],
|
|
93
|
+
additionalProperties: false,
|
|
94
|
+
} as unknown as JSONSchemaType<PaymentWebhookRecord>
|
|
95
|
+
|
|
96
|
+
export const PaymentUsageSchema = {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
id, entityId: str, limitKey: str, window: str, delta: num, eventKey: str, ref: optStr, reason: optStr,
|
|
100
|
+
planSku: optStr, createdAt: date, releasedAt: optDate,
|
|
101
|
+
},
|
|
102
|
+
required: ['entityId', 'limitKey', 'window', 'delta', 'eventKey', 'createdAt'],
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
} as unknown as JSONSchemaType<PaymentUsageRecord>
|
|
105
|
+
|
|
106
|
+
export const PaymentUsageCounterSchema = {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
id, entityId: str, limitKey: str, window: str, used: num, limit: num, planSku: optStr,
|
|
110
|
+
overSince: optDate, updatedAt: date, reconciledAt: optDate,
|
|
111
|
+
},
|
|
112
|
+
required: ['entityId', 'limitKey', 'window', 'used', 'limit', 'updatedAt'],
|
|
113
|
+
additionalProperties: false,
|
|
114
|
+
} as unknown as JSONSchemaType<PaymentUsageCounterRecord>
|
|
115
|
+
|
|
30
116
|
export const FingerprintSchema = {
|
|
31
117
|
type: 'object',
|
|
32
118
|
properties: {
|
|
33
|
-
id
|
|
34
|
-
|
|
119
|
+
id, sku: str, hash: str, productId: optStr, externalId: optStr,
|
|
120
|
+
prices: {
|
|
121
|
+
type: 'array',
|
|
122
|
+
nullable: true,
|
|
123
|
+
items: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
planSku: str, priceId: str, lookupKey: str, currency: str, unitAmount: num,
|
|
127
|
+
// An array, never a currency-keyed map: the resource coerces a map's values to strings.
|
|
128
|
+
options: {
|
|
129
|
+
type: 'array',
|
|
130
|
+
items: {
|
|
131
|
+
type: 'object', properties: { currency: str, unitAmount: num },
|
|
132
|
+
required: ['currency', 'unitAmount'], additionalProperties: false,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
taxBehavior: optStr, interval: optStr, sourceUnitAmount: num, sourceCurrency: str, syncedAt: date,
|
|
136
|
+
},
|
|
137
|
+
required: [
|
|
138
|
+
'planSku', 'priceId', 'lookupKey', 'currency', 'unitAmount', 'options', 'sourceUnitAmount',
|
|
139
|
+
'sourceCurrency', 'syncedAt',
|
|
140
|
+
],
|
|
141
|
+
additionalProperties: false,
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
updatedAt: date,
|
|
35
145
|
},
|
|
36
|
-
required: ['sku', 'hash'
|
|
146
|
+
required: ['sku', 'hash', 'updatedAt'],
|
|
147
|
+
additionalProperties: false,
|
|
37
148
|
} as unknown as JSONSchemaType<FingerprintRecord>
|
|
149
|
+
|
|
150
|
+
/** An optional enum: `null` must be listed for a validator to accept it. */
|
|
151
|
+
const nullableEnum = (values: readonly unknown[]) => ({ type: 'string', enum: [...values, null], nullable: true })
|
|
152
|
+
|
|
153
|
+
/** The request evidence every consumer act carries (`RequestOrigin`). */
|
|
154
|
+
const origin = {
|
|
155
|
+
ip: optStr, forwardedFor: optStr, userAgent: optStr, ipCountry: optStr, acceptLanguage: optStr, via: optStr,
|
|
156
|
+
} as const
|
|
157
|
+
|
|
158
|
+
const links = {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
billingTerms: str, withdrawalInformation: optStr, withdrawalForm: optStr, withdrawalFunction: optStr,
|
|
162
|
+
cancellation: optStr,
|
|
163
|
+
},
|
|
164
|
+
required: ['billingTerms'],
|
|
165
|
+
additionalProperties: false,
|
|
166
|
+
} as const
|
|
167
|
+
|
|
168
|
+
export const BillingProfileSchema = {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
id, entityId: str, country: str, region: ConsumerRegionSchema, currency: str, language: str,
|
|
172
|
+
source: { type: 'string', enum: ['checkout', 'customer', 'manual'] }, paygate: str, customerId: optStr,
|
|
173
|
+
sessionId: optStr, ipCountry: optStr, email: optStr, name: optStr, business: optBool, lockedAt: date,
|
|
174
|
+
createdAt: date, updatedAt: optDate,
|
|
175
|
+
},
|
|
176
|
+
required: ['entityId', 'country', 'region', 'currency', 'language', 'source', 'paygate', 'lockedAt', 'createdAt'],
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
} as unknown as JSONSchemaType<BillingProfileRecord>
|
|
179
|
+
|
|
180
|
+
export const PurchaseSchema = {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
id, purchaseId: str, contractRef: str, entityId: str, kind: PurchaseKindSchema, paygate: str,
|
|
184
|
+
sessionId: optStr, subscriptionId: optStr, paymentIntentId: optStr, invoiceId: optStr, invoiceNumber: optStr,
|
|
185
|
+
invoiceLineId: optStr, productSku: str, planSku: optStr, profileId: optStr, country: optStr,
|
|
186
|
+
region: nullableEnum(ConsumerRegionSchema.enum), ipCountry: optStr, inScope: { type: 'boolean' },
|
|
187
|
+
language: str, email: optStr, name: optStr, business: optBool, currency: str, amountSubtotalMinor: num,
|
|
188
|
+
amountTaxMinor: num, amountTotalMinor: num, presentmentCurrency: optStr, presentmentAmountMinor: optNum,
|
|
189
|
+
netAmountMinor: optNum, amountCurrency: optStr, units: optNum, taxBehavior: optStr, termsAccepted: optBool,
|
|
190
|
+
textVersion: optStr, copyVersion: optStr, startRequestId: optStr, servicesStartedAt: optDate,
|
|
191
|
+
confirmationMailAt: optDate, purchasedAt: date, deadline: optDate, consentId: optStr, consentedAt: optDate,
|
|
192
|
+
withdrawalId: optStr, withdrawnAt: optDate, refundedMinor: optNum, refundedAt: optDate, cancellationId: optStr,
|
|
193
|
+
cancelEffectiveAt: optDate, createdAt: date, updatedAt: optDate,
|
|
194
|
+
},
|
|
195
|
+
required: [
|
|
196
|
+
'purchaseId', 'contractRef', 'entityId', 'kind', 'paygate', 'productSku', 'inScope', 'language', 'currency',
|
|
197
|
+
'amountSubtotalMinor', 'amountTaxMinor', 'amountTotalMinor', 'purchasedAt', 'createdAt',
|
|
198
|
+
],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
} as unknown as JSONSchemaType<PurchaseRecord>
|
|
201
|
+
|
|
202
|
+
export const ConsumerConsentSchema = {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
id, kind: ConsentKindSchema, entityId: str, profileId: optStr, name: optStr, email: optStr,
|
|
206
|
+
purchaseIds: { type: 'array', items: str },
|
|
207
|
+
planSku: optStr, planName: optStr, textVersion: str, copyVersion: str, language: str, uiLanguage: optStr, trader: str,
|
|
208
|
+
text: {
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: { request: str, acknowledgement: str, checkbox: str },
|
|
211
|
+
required: ['request', 'acknowledgement', 'checkbox'],
|
|
212
|
+
additionalProperties: false,
|
|
213
|
+
},
|
|
214
|
+
links, deadline: optDate, decidedAt: date, expiresAt: optDate, ...origin,
|
|
215
|
+
},
|
|
216
|
+
required: [
|
|
217
|
+
'kind', 'entityId', 'purchaseIds', 'textVersion', 'copyVersion', 'language', 'trader', 'text', 'links',
|
|
218
|
+
'decidedAt',
|
|
219
|
+
],
|
|
220
|
+
additionalProperties: false,
|
|
221
|
+
} as unknown as JSONSchemaType<ConsumerConsentRecord>
|
|
222
|
+
|
|
223
|
+
export const ConsumerDeclarationSchema = {
|
|
224
|
+
type: 'object',
|
|
225
|
+
properties: {
|
|
226
|
+
id, kind: DeclarationKindSchema, channel: DeclarationChannelSchema, entityId: optStr, purchaseId: optStr,
|
|
227
|
+
subscriptionId: optStr, contractRef: optStr, name: str, email: str,
|
|
228
|
+
cancellationKind: nullableEnum(CancellationKindSchema.enum), reason: optStr,
|
|
229
|
+
effective: nullableEnum(['earliest', 'date']), requestedDate: optStr,
|
|
230
|
+
language: str, textVersion: optStr, copyVersion: str, receivedAt: date, matched: { type: 'boolean' },
|
|
231
|
+
profileId: optStr, duplicateOf: optStr, status: str, refundMinor: optNum, currency: optStr,
|
|
232
|
+
effectiveAt: optDate, ...origin,
|
|
233
|
+
},
|
|
234
|
+
required: ['kind', 'channel', 'name', 'email', 'language', 'copyVersion', 'receivedAt', 'matched', 'status'],
|
|
235
|
+
additionalProperties: false,
|
|
236
|
+
} as unknown as JSONSchemaType<ConsumerDeclarationRecord>
|
|
237
|
+
|
|
238
|
+
export const ConsumerEventSchema = {
|
|
239
|
+
type: 'object',
|
|
240
|
+
properties: {
|
|
241
|
+
id, recordId: str,
|
|
242
|
+
recordKind: { type: 'string', enum: ['purchase', 'consent', 'declaration', 'profile', 'checkout'] },
|
|
243
|
+
entityId: optStr,
|
|
244
|
+
action: {
|
|
245
|
+
type: 'string',
|
|
246
|
+
enum: [
|
|
247
|
+
'mail', 'computed', 'meter', 'refund', 'credit-note', 'subscription-cancel', 'cancel-scheduled', 'observers',
|
|
248
|
+
'lock', 'lock-mismatch', 'relock', 'unlock', 'duplicate', 'checkout-terms-fallback',
|
|
249
|
+
],
|
|
250
|
+
},
|
|
251
|
+
step: optStr, ok: { type: 'boolean' }, skipped: optBool, externalId: optStr, amountMinor: optNum,
|
|
252
|
+
currency: optStr, detail: optStr, error: optStr, at: date,
|
|
253
|
+
},
|
|
254
|
+
required: ['recordId', 'recordKind', 'action', 'ok', 'at'],
|
|
255
|
+
additionalProperties: false,
|
|
256
|
+
} as unknown as JSONSchemaType<ConsumerEventRecord>
|
package/src/observer.ts
CHANGED
|
@@ -1,21 +1,54 @@
|
|
|
1
1
|
import { createLazyService } from '@owlmeans/context'
|
|
2
2
|
import { PAYMENT_OBSERVER } from './consts.js'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CancellationCallback, CompletionObserver, Config, ConsentCallback, Context, DisputeCallback,
|
|
5
|
+
PaymentFailedCallback, RefundCallback, SubscriptionCallback, TopUpCallback, WithdrawalCallback,
|
|
6
|
+
} from './types.js'
|
|
4
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The application's side of payment completion. Callbacks run sequentially and are awaited; a
|
|
10
|
+
* throw escapes to the webhook so the paygate delivers the event again — every callback must be
|
|
11
|
+
* idempotent by the `eventKey` (`externalId` for a top-up) it is handed.
|
|
12
|
+
*
|
|
13
|
+
* The consumer-rights callbacks (`onConsent`, `onWithdrawal`, `onCancellation`) run after the
|
|
14
|
+
* records and the paygate steps of their act; a throw is recorded as an `observers` event and
|
|
15
|
+
* retried by the consumer-rights `reconcile()` — they, too, are idempotent by `eventKey`.
|
|
16
|
+
*/
|
|
5
17
|
export const makeCompletionObserverService = (
|
|
6
18
|
alias: string = PAYMENT_OBSERVER,
|
|
7
19
|
): CompletionObserver => {
|
|
8
|
-
const
|
|
9
|
-
const
|
|
20
|
+
const topUp: TopUpCallback[] = []
|
|
21
|
+
const subscription: SubscriptionCallback[] = []
|
|
22
|
+
const refund: RefundCallback[] = []
|
|
23
|
+
const dispute: DisputeCallback[] = []
|
|
24
|
+
const paymentFailed: PaymentFailedCallback[] = []
|
|
25
|
+
const consent: ConsentCallback[] = []
|
|
26
|
+
const withdrawal: WithdrawalCallback[] = []
|
|
27
|
+
const cancellation: CancellationCallback[] = []
|
|
28
|
+
|
|
29
|
+
const run = async <E>(callbacks: Array<(event: E, ctx: never) => Promise<void>>, event: E, ctx: unknown) => {
|
|
30
|
+
for (const callback of callbacks) {
|
|
31
|
+
await callback(event, ctx as never)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
10
35
|
return createLazyService<CompletionObserver>(alias, {
|
|
11
|
-
onTopUp: cb => {
|
|
12
|
-
onSubscription: cb => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
},
|
|
36
|
+
onTopUp: cb => { topUp.push(cb) },
|
|
37
|
+
onSubscription: cb => { subscription.push(cb) },
|
|
38
|
+
onRefund: cb => { refund.push(cb) },
|
|
39
|
+
onDispute: cb => { dispute.push(cb) },
|
|
40
|
+
onPaymentFailed: cb => { paymentFailed.push(cb) },
|
|
41
|
+
onConsent: cb => { consent.push(cb) },
|
|
42
|
+
onWithdrawal: cb => { withdrawal.push(cb) },
|
|
43
|
+
onCancellation: cb => { cancellation.push(cb) },
|
|
44
|
+
propagateTopUp: async (completion, ctx) => { await run(topUp, completion, ctx) },
|
|
45
|
+
propagateSubscription: async (event, ctx) => { await run(subscription, event, ctx) },
|
|
46
|
+
propagateRefund: async (event, ctx) => { await run(refund, event, ctx) },
|
|
47
|
+
propagateDispute: async (event, ctx) => { await run(dispute, event, ctx) },
|
|
48
|
+
propagatePaymentFailed: async (event, ctx) => { await run(paymentFailed, event, ctx) },
|
|
49
|
+
propagateConsent: async (event, ctx) => { await run(consent, event, ctx) },
|
|
50
|
+
propagateWithdrawal: async (event, ctx) => { await run(withdrawal, event, ctx) },
|
|
51
|
+
propagateCancellation: async (event, ctx) => { await run(cancellation, event, ctx) },
|
|
19
52
|
}, service => async () => { service.initialized = true })
|
|
20
53
|
}
|
|
21
54
|
|
package/src/plan.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ENTITLING_STATUSES, INTERNAL_PAYGATE, PlanRequired } from '@owlmeans/payment'
|
|
2
|
+
import type { ProductPlan } from '@owlmeans/payment'
|
|
3
|
+
import type { Context as ApiContext } from '@owlmeans/server-api'
|
|
4
|
+
import { payment, subscriptions } from './utils.js'
|
|
5
|
+
import type { EffectivePlan, PaymentPlan, PaymentProduct, PaymentSubscriptionRecord } from './types.js'
|
|
6
|
+
|
|
7
|
+
/** A plan's rank; an absent rank reads as `0`. */
|
|
8
|
+
export const planRank = (plan: Pick<ProductPlan, 'rank'>): number => plan.rank ?? 0
|
|
9
|
+
|
|
10
|
+
/** A plan by sku, or `null` when the catalogue no longer declares it. */
|
|
11
|
+
export const findPlan = async (ctx: ApiContext, planSku: string): Promise<PaymentPlan | null> => {
|
|
12
|
+
try {
|
|
13
|
+
return await payment(ctx).plan(planSku) as PaymentPlan
|
|
14
|
+
} catch {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** A product by sku, or `null`. */
|
|
20
|
+
export const findProduct = async (ctx: ApiContext, productSku: string): Promise<PaymentProduct | null> => {
|
|
21
|
+
try {
|
|
22
|
+
return await payment(ctx).product(productSku) as PaymentProduct
|
|
23
|
+
} catch {
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Every plan of every declared product. */
|
|
29
|
+
export const catalogPlans = async (ctx: ApiContext): Promise<PaymentPlan[]> => {
|
|
30
|
+
const products = await payment(ctx).products()
|
|
31
|
+
const plans: PaymentPlan[] = []
|
|
32
|
+
for (const product of products) {
|
|
33
|
+
plans.push(...await payment(ctx).allPlans(product.sku) as PaymentPlan[])
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return plans
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The declared free plan an entity falls back to: the lowest rank, then the sku. */
|
|
40
|
+
export const freePlanOf = async (ctx: ApiContext): Promise<PaymentPlan | null> =>
|
|
41
|
+
(await catalogPlans(ctx))
|
|
42
|
+
.filter(plan => plan.free === true)
|
|
43
|
+
.sort((a, b) => planRank(a) - planRank(b) || a.sku.localeCompare(b.sku))[0] ?? null
|
|
44
|
+
|
|
45
|
+
/** An internal grant with a period end stops entitling at that instant; a paygate row never does. */
|
|
46
|
+
const stillEntitles = (row: PaymentSubscriptionRecord, at: Date): boolean =>
|
|
47
|
+
row.paygate !== INTERNAL_PAYGATE || row.periodEnd == null || new Date(row.periodEnd).getTime() > at.getTime()
|
|
48
|
+
|
|
49
|
+
const unknownPlans = new Set<string>()
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The plan an entity holds: its highest-ranked subscription in an entitling status (the catalogue
|
|
53
|
+
* rank, the stored one for a plan the catalogue no longer declares; the newest first on a tie),
|
|
54
|
+
* else the declared free plan.
|
|
55
|
+
*
|
|
56
|
+
* @throws PlanRequired when there is neither.
|
|
57
|
+
*/
|
|
58
|
+
export const resolveEffectivePlan = async (
|
|
59
|
+
ctx: ApiContext, entityId: string, at: Date = new Date(),
|
|
60
|
+
): Promise<EffectivePlan> => {
|
|
61
|
+
const fallback = await freePlanOf(ctx)
|
|
62
|
+
const { items } = await subscriptions(ctx).list(
|
|
63
|
+
{ entityId, status: [...ENTITLING_STATUSES] }, { size: 0 },
|
|
64
|
+
)
|
|
65
|
+
const candidates: Array<{ plan: PaymentPlan, row: PaymentSubscriptionRecord, rank: number }> = []
|
|
66
|
+
for (const row of items.filter(item => stillEntitles(item, at))) {
|
|
67
|
+
const plan = await findPlan(ctx, row.planSku)
|
|
68
|
+
if (plan == null) {
|
|
69
|
+
if (!unknownPlans.has(row.planSku)) {
|
|
70
|
+
unknownPlans.add(row.planSku)
|
|
71
|
+
console.warn(`[payment] subscription "${row.externalId}" names unknown plan "${row.planSku}"; ignored`)
|
|
72
|
+
}
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
candidates.push({ plan, row, rank: planRank(plan) })
|
|
76
|
+
}
|
|
77
|
+
candidates.sort((a, b) => b.rank - a.rank
|
|
78
|
+
|| new Date(b.row.createdAt).getTime() - new Date(a.row.createdAt).getTime())
|
|
79
|
+
|
|
80
|
+
const best = candidates[0]
|
|
81
|
+
if (best != null) {
|
|
82
|
+
return { plan: best.plan, subscription: best.row, fallback }
|
|
83
|
+
}
|
|
84
|
+
if (fallback != null) {
|
|
85
|
+
return { plan: fallback, subscription: null, fallback }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new PlanRequired(entityId)
|
|
89
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { CheckoutLimitExceeded, narrowAmountPolicy } from '@owlmeans/payment'
|
|
2
|
+
import type { AmountNarrowing, AmountPolicyView } from '@owlmeans/payment'
|
|
3
|
+
import type { Context as ApiContext } from '@owlmeans/server-api'
|
|
4
|
+
import { STRIPE_SESSION_TTL_MAX_SECONDS, STRIPE_SESSION_TTL_MIN_SECONDS } from '../consts.js'
|
|
5
|
+
import type {
|
|
6
|
+
CheckoutAttempt, CheckoutNarrowInput, CheckoutPlugin, CheckoutSettled,
|
|
7
|
+
} from '../types.js'
|
|
8
|
+
|
|
9
|
+
export interface CheckoutPluginRegistry {
|
|
10
|
+
use: (plugin: CheckoutPlugin) => void
|
|
11
|
+
list: () => readonly CheckoutPlugin[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The gateway's checkout plugins, the way `ExecutionService.use` seats its own: a plugin with an
|
|
16
|
+
* alias already registered replaces it in place (a layer wired twice must not answer twice), one
|
|
17
|
+
* without an alias is appended.
|
|
18
|
+
*/
|
|
19
|
+
export const makeCheckoutPluginRegistry = (): CheckoutPluginRegistry => {
|
|
20
|
+
const plugins: CheckoutPlugin[] = []
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
use: plugin => {
|
|
24
|
+
const at = plugin.alias != null ? plugins.findIndex(entry => entry.alias === plugin.alias) : -1
|
|
25
|
+
if (at < 0) {
|
|
26
|
+
plugins.push(plugin)
|
|
27
|
+
} else {
|
|
28
|
+
plugins[at] = plugin
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
list: () => [...plugins],
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An entity's amount policy as every plugin narrows it now — the ONE computation both
|
|
37
|
+
* `gateway.amountPolicy` and checkout enforcement use, so a control and a refusal cannot disagree.
|
|
38
|
+
* A plugin's error propagates: a narrowing that cannot be computed fails closed.
|
|
39
|
+
*/
|
|
40
|
+
export const narrowAmountFor = async (
|
|
41
|
+
ctx: ApiContext, plugins: readonly CheckoutPlugin[], input: CheckoutNarrowInput,
|
|
42
|
+
): Promise<AmountPolicyView> => {
|
|
43
|
+
const narrowings: AmountNarrowing[] = []
|
|
44
|
+
for (const plugin of plugins) {
|
|
45
|
+
if (plugin.narrow == null) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
const narrowing = await plugin.narrow(ctx, input)
|
|
49
|
+
if (narrowing != null) {
|
|
50
|
+
narrowings.push(narrowing)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return narrowAmountPolicy(input.base, narrowings, {
|
|
55
|
+
productSku: input.productSku, ...(input.planSku != null ? { planSku: input.planSku } : {}),
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Refuse an amount the narrowed view does not allow: any amount while `blocked`, else one above
|
|
61
|
+
* the narrowed maximum.
|
|
62
|
+
*
|
|
63
|
+
* @throws CheckoutLimitExceeded (409)
|
|
64
|
+
*/
|
|
65
|
+
export const assertAmountAllowed = (view: AmountPolicyView, amountMinor: number): void => {
|
|
66
|
+
const limit = view.limit
|
|
67
|
+
if (limit == null || (!limit.blocked && amountMinor <= limit.maximumMinor)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
throw new CheckoutLimitExceeded({
|
|
71
|
+
reason: limit.reason ?? 'limit', maximumMinor: limit.maximumMinor, currency: limit.currency,
|
|
72
|
+
...(limit.resetsAt != null ? { resetsAt: limit.resetsAt } : {}),
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The session lifetime: the smallest `sessionTtlSeconds` any plugin declares, clamped to Stripe's
|
|
78
|
+
* 30 minutes – 24 hours; `undefined` when none declares one (Stripe's own default).
|
|
79
|
+
*/
|
|
80
|
+
export const sessionTtlOf = (plugins: readonly CheckoutPlugin[]): number | undefined => {
|
|
81
|
+
const declared = plugins
|
|
82
|
+
.map(plugin => plugin.sessionTtlSeconds)
|
|
83
|
+
.filter((ttl): ttl is number => typeof ttl === 'number' && Number.isFinite(ttl))
|
|
84
|
+
if (declared.length === 0) {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Math.min(STRIPE_SESSION_TTL_MAX_SECONDS, Math.max(STRIPE_SESSION_TTL_MIN_SECONDS, Math.floor(Math.min(...declared))))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A plugin that admitted an attempt, with the reservation it holds. */
|
|
92
|
+
export interface Admitted {
|
|
93
|
+
plugin: CheckoutPlugin
|
|
94
|
+
reservationId?: string
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Tell plugins how a checkout ended. A plugin's error is logged, never raised: a hold carries its
|
|
99
|
+
* own TTL, and the webhook that reports the outcome must not be redelivered for it.
|
|
100
|
+
*/
|
|
101
|
+
export const settleCheckout = async (
|
|
102
|
+
ctx: ApiContext, plugins: readonly CheckoutPlugin[], settled: CheckoutSettled,
|
|
103
|
+
): Promise<void> => {
|
|
104
|
+
for (const plugin of plugins) {
|
|
105
|
+
if (plugin.settled == null) {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await plugin.settled(ctx, settled)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(`[payment] checkout plugin "${plugin.alias ?? 'anonymous'}" failed to settle`, error)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Release what admitted plugins hold for a checkout that never became usable. */
|
|
117
|
+
export const releaseAdmitted = async (
|
|
118
|
+
ctx: ApiContext, admitted: readonly Admitted[], attempt: CheckoutAttempt, sessionId?: string,
|
|
119
|
+
): Promise<void> => {
|
|
120
|
+
for (const { plugin, reservationId } of admitted) {
|
|
121
|
+
await settleCheckout(ctx, [plugin], {
|
|
122
|
+
entityId: attempt.entityId, productSku: attempt.productSku,
|
|
123
|
+
...(attempt.planSku != null ? { planSku: attempt.planSku } : {}),
|
|
124
|
+
...(sessionId != null ? { sessionId } : {}),
|
|
125
|
+
...(reservationId != null ? { reservationId } : {}),
|
|
126
|
+
...(attempt.amountMinor != null ? { amountMinor: attempt.amountMinor } : {}),
|
|
127
|
+
outcome: 'failed', at: new Date(),
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Ask every plugin to admit the attempt, in order. A veto releases what the earlier plugins
|
|
134
|
+
* admitted and propagates.
|
|
135
|
+
*/
|
|
136
|
+
export const admitCheckout = async (
|
|
137
|
+
ctx: ApiContext, plugins: readonly CheckoutPlugin[], attempt: CheckoutAttempt,
|
|
138
|
+
): Promise<Admitted[]> => {
|
|
139
|
+
const admitted: Admitted[] = []
|
|
140
|
+
for (const plugin of plugins) {
|
|
141
|
+
if (plugin.admit == null) {
|
|
142
|
+
admitted.push({ plugin })
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const admission = await plugin.admit(ctx, attempt)
|
|
147
|
+
admitted.push({ plugin, ...(admission?.reservationId != null ? { reservationId: admission.reservationId } : {}) })
|
|
148
|
+
} catch (error) {
|
|
149
|
+
await releaseAdmitted(ctx, admitted.filter(entry => entry.plugin.admit != null), attempt)
|
|
150
|
+
throw error
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return admitted
|
|
155
|
+
}
|