@owlmeans/payment 0.1.18-rc.8 → 0.1.18
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 +95 -57
- package/agent-meta/manifest.json +10 -3
- package/agent-meta/skills/entitlements/SKILL.md +159 -0
- package/agent-meta/skills/payment/SKILL.md +491 -28
- package/build/advertise.d.ts +2 -0
- package/build/advertise.d.ts.map +1 -0
- package/build/advertise.js +10 -0
- package/build/advertise.js.map +1 -0
- package/build/checkout/consts.d.ts +2 -0
- package/build/checkout/consts.d.ts.map +1 -0
- package/build/checkout/consts.js +2 -0
- package/build/checkout/consts.js.map +1 -0
- package/build/checkout/index.d.ts +6 -0
- package/build/checkout/index.d.ts.map +1 -0
- package/build/checkout/index.js +4 -0
- package/build/checkout/index.js.map +1 -0
- package/build/checkout/narrow/types.d.ts +25 -0
- package/build/checkout/narrow/types.d.ts.map +1 -0
- package/build/checkout/narrow/types.js +2 -0
- package/build/checkout/narrow/types.js.map +1 -0
- package/build/checkout/narrow.d.ts +8 -0
- package/build/checkout/narrow.d.ts.map +1 -0
- package/build/checkout/narrow.js +47 -0
- package/build/checkout/narrow.js.map +1 -0
- package/build/checkout/protocols.d.ts +8 -0
- package/build/checkout/protocols.d.ts.map +1 -0
- package/build/checkout/protocols.js +28 -0
- package/build/checkout/protocols.js.map +1 -0
- package/build/checkout/types.d.ts +29 -0
- package/build/checkout/types.d.ts.map +1 -0
- package/build/checkout/types.js +2 -0
- package/build/checkout/types.js.map +1 -0
- package/build/consts.d.ts +242 -25
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +377 -24
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +20 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +23 -0
- package/build/consts.local.js.map +1 -0
- package/build/consumer/cancel.d.ts +11 -0
- package/build/consumer/cancel.d.ts.map +1 -0
- package/build/consumer/cancel.js +33 -0
- package/build/consumer/cancel.js.map +1 -0
- package/build/consumer/consts.d.ts +20 -0
- package/build/consumer/consts.d.ts.map +1 -0
- package/build/consumer/consts.js +39 -0
- package/build/consumer/consts.js.map +1 -0
- package/build/consumer/consts.local.d.ts +7 -0
- package/build/consumer/consts.local.d.ts.map +1 -0
- package/build/consumer/consts.local.js +13 -0
- package/build/consumer/consts.local.js.map +1 -0
- package/build/consumer/copy/types.d.ts +56 -0
- package/build/consumer/copy/types.d.ts.map +1 -0
- package/build/consumer/copy/types.js +2 -0
- package/build/consumer/copy/types.js.map +1 -0
- package/build/consumer/copy.d.ts +22 -0
- package/build/consumer/copy.d.ts.map +1 -0
- package/build/consumer/copy.js +87 -0
- package/build/consumer/copy.js.map +1 -0
- package/build/consumer/deadline/types.d.ts +30 -0
- package/build/consumer/deadline/types.d.ts.map +1 -0
- package/build/consumer/deadline/types.js +2 -0
- package/build/consumer/deadline/types.js.map +1 -0
- package/build/consumer/deadline.d.ts +12 -0
- package/build/consumer/deadline.d.ts.map +1 -0
- package/build/consumer/deadline.js +44 -0
- package/build/consumer/deadline.js.map +1 -0
- package/build/consumer/fifo/types.d.ts +17 -0
- package/build/consumer/fifo/types.d.ts.map +1 -0
- package/build/consumer/fifo/types.js +2 -0
- package/build/consumer/fifo/types.js.map +1 -0
- package/build/consumer/fifo.d.ts +4 -0
- package/build/consumer/fifo.d.ts.map +1 -0
- package/build/consumer/fifo.js +34 -0
- package/build/consumer/fifo.js.map +1 -0
- package/build/consumer/index.d.ts +18 -0
- package/build/consumer/index.d.ts.map +1 -0
- package/build/consumer/index.js +11 -0
- package/build/consumer/index.js.map +1 -0
- package/build/consumer/policy/types.d.ts +20 -0
- package/build/consumer/policy/types.d.ts.map +1 -0
- package/build/consumer/policy/types.js +2 -0
- package/build/consumer/policy/types.js.map +1 -0
- package/build/consumer/policy.d.ts +17 -0
- package/build/consumer/policy.d.ts.map +1 -0
- package/build/consumer/policy.js +91 -0
- package/build/consumer/policy.js.map +1 -0
- package/build/consumer/protocols.d.ts +20 -0
- package/build/consumer/protocols.d.ts.map +1 -0
- package/build/consumer/protocols.js +70 -0
- package/build/consumer/protocols.js.map +1 -0
- package/build/consumer/refund/types.d.ts +27 -0
- package/build/consumer/refund/types.d.ts.map +1 -0
- package/build/consumer/refund/types.js +2 -0
- package/build/consumer/refund/types.js.map +1 -0
- package/build/consumer/refund.d.ts +9 -0
- package/build/consumer/refund.d.ts.map +1 -0
- package/build/consumer/refund.js +112 -0
- package/build/consumer/refund.js.map +1 -0
- package/build/consumer/refusal.d.ts +10 -0
- package/build/consumer/refusal.d.ts.map +1 -0
- package/build/consumer/refusal.js +35 -0
- package/build/consumer/refusal.js.map +1 -0
- package/build/consumer/revive/types.d.ts +19 -0
- package/build/consumer/revive/types.d.ts.map +1 -0
- package/build/consumer/revive/types.js +2 -0
- package/build/consumer/revive/types.js.map +1 -0
- package/build/consumer/revive.d.ts +13 -0
- package/build/consumer/revive.d.ts.map +1 -0
- package/build/consumer/revive.js +43 -0
- package/build/consumer/revive.js.map +1 -0
- package/build/consumer/types.d.ts +196 -0
- package/build/consumer/types.d.ts.map +1 -0
- package/build/consumer/types.js +2 -0
- package/build/consumer/types.js.map +1 -0
- package/build/consumer/types.local.d.ts +11 -0
- package/build/consumer/types.local.d.ts.map +1 -0
- package/build/consumer/types.local.js +2 -0
- package/build/consumer/types.local.js.map +1 -0
- package/build/countries.d.ts +3 -0
- package/build/countries.d.ts.map +1 -0
- package/build/countries.js +4 -0
- package/build/countries.js.map +1 -0
- package/build/entitled.d.ts +14 -0
- package/build/entitled.d.ts.map +1 -0
- package/build/entitled.js +17 -0
- package/build/entitled.js.map +1 -0
- package/build/entitlement/types.d.ts +29 -0
- package/build/entitlement/types.d.ts.map +1 -0
- package/build/entitlement/types.js +2 -0
- package/build/entitlement/types.js.map +1 -0
- package/build/entitlement.d.ts +12 -0
- package/build/entitlement.d.ts.map +1 -0
- package/build/entitlement.js +64 -0
- package/build/entitlement.js.map +1 -0
- package/build/errors.d.ts +189 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +361 -0
- package/build/errors.js.map +1 -1
- package/build/estimate/types.d.ts +34 -0
- package/build/estimate/types.d.ts.map +1 -0
- package/build/estimate/types.js +2 -0
- package/build/estimate/types.js.map +1 -0
- package/build/estimate.d.ts +11 -0
- package/build/estimate.d.ts.map +1 -0
- package/build/estimate.js +76 -0
- package/build/estimate.js.map +1 -0
- package/build/i18n/consumer-rights/be.json +131 -0
- package/build/i18n/consumer-rights/de.json +131 -0
- package/build/i18n/consumer-rights/en.json +131 -0
- package/build/i18n/consumer-rights/es.json +131 -0
- package/build/i18n/consumer-rights/fr.json +131 -0
- package/build/i18n/consumer-rights/pl.json +131 -0
- package/build/i18n/consumer-rights/ru.json +131 -0
- package/build/i18n/consumer-rights/uk.json +131 -0
- package/build/i18n/errors/be.json +30 -0
- package/build/i18n/errors/de.json +30 -0
- package/build/i18n/errors/en.json +30 -0
- package/build/i18n/errors/es.json +30 -0
- package/build/i18n/errors/fr.json +30 -0
- package/build/i18n/errors/pl.json +30 -0
- package/build/i18n/errors/ru.json +30 -0
- package/build/i18n/errors/uk.json +30 -0
- package/build/i18n/fr.json +39 -0
- package/build/i18n.js +53 -8
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +19 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +12 -1
- package/build/index.js.map +1 -1
- package/build/limit/types.d.ts +44 -0
- package/build/limit/types.d.ts.map +1 -0
- package/build/limit/types.js +2 -0
- package/build/limit/types.js.map +1 -0
- package/build/limit.d.ts +23 -0
- package/build/limit.d.ts.map +1 -0
- package/build/limit.js +97 -0
- package/build/limit.js.map +1 -0
- package/build/model/checkout.d.ts +2 -3
- package/build/model/checkout.d.ts.map +1 -1
- package/build/model/checkout.js +12 -7
- package/build/model/checkout.js.map +1 -1
- package/build/model/consts.d.ts +15 -0
- package/build/model/consts.d.ts.map +1 -0
- package/build/model/consts.js +193 -0
- package/build/model/consts.js.map +1 -0
- package/build/model/consts.local.d.ts +35 -0
- package/build/model/consts.local.d.ts.map +1 -0
- package/build/model/consts.local.js +65 -0
- package/build/model/consts.local.js.map +1 -0
- package/build/model/consumer.d.ts +23 -0
- package/build/model/consumer.d.ts.map +1 -0
- package/build/model/consumer.js +272 -0
- package/build/model/consumer.js.map +1 -0
- package/build/model/estimate.d.ts +5 -0
- package/build/model/estimate.d.ts.map +1 -0
- package/build/model/estimate.js +69 -0
- package/build/model/estimate.js.map +1 -0
- package/build/model/index.d.ts +7 -2
- package/build/model/index.d.ts.map +1 -1
- package/build/model/index.js +7 -2
- package/build/model/index.js.map +1 -1
- package/build/model/limit.d.ts +7 -0
- package/build/model/limit.d.ts.map +1 -0
- package/build/model/limit.js +32 -0
- package/build/model/limit.js.map +1 -0
- package/build/model/plan.d.ts.map +1 -1
- package/build/model/plan.js +24 -11
- package/build/model/plan.js.map +1 -1
- package/build/model/portal.d.ts +4 -0
- package/build/model/portal.d.ts.map +1 -0
- package/build/model/portal.js +22 -0
- package/build/model/portal.js.map +1 -0
- package/build/model/pricing.d.ts +4 -0
- package/build/model/pricing.d.ts.map +1 -0
- package/build/model/pricing.js +31 -0
- package/build/model/pricing.js.map +1 -0
- package/build/model/view.d.ts +3 -0
- package/build/model/view.d.ts.map +1 -0
- package/build/model/view.js +15 -0
- package/build/model/view.js.map +1 -0
- package/build/pricing/types.d.ts +13 -0
- package/build/pricing/types.d.ts.map +1 -0
- package/build/pricing/types.js +2 -0
- package/build/pricing/types.js.map +1 -0
- package/build/pricing.d.ts +13 -0
- package/build/pricing.d.ts.map +1 -0
- package/build/pricing.js +74 -0
- package/build/pricing.js.map +1 -0
- package/build/promo/types.d.ts +14 -0
- package/build/promo/types.d.ts.map +1 -0
- package/build/promo/types.js +2 -0
- package/build/promo/types.js.map +1 -0
- package/build/promo.d.ts +7 -0
- package/build/promo.d.ts.map +1 -0
- package/build/promo.js +25 -0
- package/build/promo.js.map +1 -0
- package/build/regions/types.d.ts +32 -0
- package/build/regions/types.d.ts.map +1 -0
- package/build/regions/types.js +2 -0
- package/build/regions/types.js.map +1 -0
- package/build/regions.d.ts +15 -0
- package/build/regions.d.ts.map +1 -0
- package/build/regions.js +43 -0
- package/build/regions.js.map +1 -0
- package/build/service.d.ts.map +1 -1
- package/build/service.js +24 -9
- package/build/service.js.map +1 -1
- package/build/types.d.ts +619 -52
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +4 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/view/types.d.ts +33 -0
- package/build/view/types.d.ts.map +1 -0
- package/build/view/types.js +2 -0
- package/build/view/types.js.map +1 -0
- package/build/view.d.ts +13 -0
- package/build/view.d.ts.map +1 -0
- package/build/view.js +78 -0
- package/build/view.js.map +1 -0
- package/package.json +14 -12
- package/src/advertise.ts +13 -0
- package/src/checkout/consts.ts +1 -0
- package/src/checkout/index.ts +5 -0
- package/src/checkout/narrow/types.ts +27 -0
- package/src/checkout/narrow.ts +63 -0
- package/src/checkout/protocols.ts +39 -0
- package/src/checkout/types.ts +27 -0
- package/src/consts.local.ts +34 -0
- package/src/consts.ts +417 -24
- package/src/consumer/cancel.ts +40 -0
- package/src/consumer/consts.local.ts +16 -0
- package/src/consumer/consts.ts +46 -0
- package/src/consumer/copy/types.ts +54 -0
- package/src/consumer/copy.ts +121 -0
- package/src/consumer/deadline/types.ts +30 -0
- package/src/consumer/deadline.ts +61 -0
- package/src/consumer/fifo/types.ts +17 -0
- package/src/consumer/fifo.ts +42 -0
- package/src/consumer/index.ts +17 -0
- package/src/consumer/policy/types.ts +20 -0
- package/src/consumer/policy.ts +101 -0
- package/src/consumer/protocols.ts +116 -0
- package/src/consumer/refund/types.ts +27 -0
- package/src/consumer/refund.ts +136 -0
- package/src/consumer/refusal.ts +35 -0
- package/src/consumer/revive/types.ts +23 -0
- package/src/consumer/revive.ts +82 -0
- package/src/consumer/types.local.ts +10 -0
- package/src/consumer/types.ts +189 -0
- package/src/countries.ts +5 -0
- package/src/entitled.ts +20 -0
- package/src/entitlement/types.ts +29 -0
- package/src/entitlement.ts +86 -0
- package/src/errors.ts +445 -0
- package/src/estimate/types.ts +34 -0
- package/src/estimate.ts +90 -0
- package/src/i18n/consumer-rights/be.json +131 -0
- package/src/i18n/consumer-rights/de.json +131 -0
- package/src/i18n/consumer-rights/en.json +131 -0
- package/src/i18n/consumer-rights/es.json +131 -0
- package/src/i18n/consumer-rights/fr.json +131 -0
- package/src/i18n/consumer-rights/pl.json +131 -0
- package/src/i18n/consumer-rights/ru.json +131 -0
- package/src/i18n/consumer-rights/uk.json +131 -0
- package/src/i18n/errors/be.json +30 -0
- package/src/i18n/errors/de.json +30 -0
- package/src/i18n/errors/en.json +30 -0
- package/src/i18n/errors/es.json +30 -0
- package/src/i18n/errors/fr.json +30 -0
- package/src/i18n/errors/pl.json +30 -0
- package/src/i18n/errors/ru.json +30 -0
- package/src/i18n/errors/uk.json +30 -0
- package/src/i18n/fr.json +39 -0
- package/src/i18n.ts +58 -8
- package/src/index.ts +19 -2
- package/src/limit/types.ts +41 -0
- package/src/limit.ts +132 -0
- package/src/model/checkout.ts +12 -7
- package/src/model/consts.local.ts +83 -0
- package/src/model/consts.ts +206 -0
- package/src/model/consumer.ts +294 -0
- package/src/model/estimate.ts +73 -0
- package/src/model/index.ts +7 -2
- package/src/model/limit.ts +36 -0
- package/src/model/plan.ts +25 -12
- package/src/model/portal.ts +25 -0
- package/src/model/pricing.ts +34 -0
- package/src/model/view.ts +17 -0
- package/src/pricing/types.ts +13 -0
- package/src/pricing.ts +110 -0
- package/src/promo/types.ts +18 -0
- package/src/promo.ts +46 -0
- package/src/regions/types.ts +42 -0
- package/src/regions.ts +85 -0
- package/src/service.ts +32 -15
- package/src/types.local.ts +5 -0
- package/src/types.ts +669 -49
- package/src/view/types.ts +39 -0
- package/src/view.ts +119 -0
- package/tests/checkout-narrow.spec.ts +75 -0
- package/tests/consumer-copy.spec.ts +378 -0
- package/tests/consumer-deadline.spec.ts +85 -0
- package/tests/consumer-fifo.spec.ts +61 -0
- package/tests/consumer-protocols.spec.ts +103 -0
- package/tests/consumer-refund.spec.ts +134 -0
- package/tests/consumer-regions.spec.ts +140 -0
- package/tests/consumer-wire.spec.ts +170 -0
- package/tests/contract.spec.ts +183 -0
- package/tests/entitlement.spec.ts +103 -0
- package/tests/errors.spec.ts +168 -0
- package/tests/estimate.spec.ts +139 -0
- package/tests/limit.spec.ts +117 -0
- package/tests/pricing.spec.ts +30 -0
- package/tests/tsconfig.json +11 -0
- package/tests/view.spec.ts +130 -0
- package/tsconfig.json +6 -1
- package/build/.gitkeep +0 -0
- package/src/model/subscription.ts +0 -63
- package/src/model/utils.ts +0 -32
- package/src/modules.ts +0 -43
package/src/errors.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import { ResilientError } from '@owlmeans/error'
|
|
3
|
+
import { AuthForbidden } from '@owlmeans/auth'
|
|
4
|
+
import { BILLING_COUNTRY_LOCKED_MARKER, CANCELLATION_UNAVAILABLE_MARKER, CAPABILITY_REQUIRED_MARKER, CHECKOUT_LIMIT_MARKER, LIMIT_EXHAUSTED_FIELDS, LIMIT_EXHAUSTED_MARKER, PERFORMANCE_CONSENT_MARKER, SUBSCRIPTION_START_MARKER, WITHDRAWAL_UNAVAILABLE_MARKER } from './consts.local.js'
|
|
5
|
+
import type { BillingCountryLockedDetails, CheckoutLimitExceededDetails, LimitExhaustedDetails, PerformanceConsentRequiredDetails } from './types.js'
|
|
3
6
|
|
|
4
7
|
export class PaymentError extends ResilientError {
|
|
5
8
|
public static override typeName: string = 'PaymentError'
|
|
@@ -36,6 +39,22 @@ export class PaygateMappingError extends PaygateError {
|
|
|
36
39
|
}
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
/**
|
|
43
|
+
* A webhook delivery whose signature is missing or verifies against no known secret. The sender is
|
|
44
|
+
* refused as a client error (400): a forged or unsigned POST at a public hook is not a server fault,
|
|
45
|
+
* and answering 500 would page operators for it and invite the sender to retry it.
|
|
46
|
+
*/
|
|
47
|
+
export class PaygateSignatureError extends PaygateError {
|
|
48
|
+
public static override typeName: string = `${PaygateError.typeName}Signature`
|
|
49
|
+
/** Read by the HTTP boundary (`server-api` `declaredErrorStatus`). */
|
|
50
|
+
public static httpStatus: number = 400
|
|
51
|
+
|
|
52
|
+
constructor() {
|
|
53
|
+
super('signature')
|
|
54
|
+
this.type = PaygateSignatureError.typeName
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
export class ProductError extends PaymentError {
|
|
40
59
|
public static override typeName: string = `${PaymentError.typeName}Product`
|
|
41
60
|
|
|
@@ -90,13 +109,439 @@ export class UnknownSubscription extends SubscriptionError {
|
|
|
90
109
|
}
|
|
91
110
|
}
|
|
92
111
|
|
|
112
|
+
export class LimitUnknown extends ProductError {
|
|
113
|
+
public static override typeName: string = `${ProductError.typeName}LimitUnknown`
|
|
114
|
+
|
|
115
|
+
constructor(message: string = 'error') {
|
|
116
|
+
super(`limit-unknown:${message}`)
|
|
117
|
+
this.type = LimitUnknown.typeName
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export class LimitMisdeclared extends ProductError {
|
|
122
|
+
public static override typeName: string = `${ProductError.typeName}LimitMisdeclared`
|
|
123
|
+
|
|
124
|
+
constructor(message: string = 'error') {
|
|
125
|
+
super(`limit-misdeclared:${message}`)
|
|
126
|
+
this.type = LimitMisdeclared.typeName
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export class PlanRequired extends ProductError {
|
|
131
|
+
public static override typeName: string = `${ProductError.typeName}PlanRequired`
|
|
132
|
+
|
|
133
|
+
constructor(message: string = 'error') {
|
|
134
|
+
super(`plan-required:${message}`)
|
|
135
|
+
this.type = PlanRequired.typeName
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class PlanRankConflict extends ProductError {
|
|
140
|
+
public static override typeName: string = `${ProductError.typeName}PlanRankConflict`
|
|
141
|
+
|
|
142
|
+
constructor(message: string = 'error') {
|
|
143
|
+
super(`plan-rank-conflict:${message}`)
|
|
144
|
+
this.type = PlanRankConflict.typeName
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class WebhookSetupError extends PaygateError {
|
|
149
|
+
public static override typeName: string = `${PaygateError.typeName}WebhookSetup`
|
|
150
|
+
|
|
151
|
+
constructor(message: string = 'error') {
|
|
152
|
+
super(`webhook-setup:${message}`)
|
|
153
|
+
this.type = WebhookSetupError.typeName
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The entity has nothing the requested portal flow can act on — no paygate customer, no entitling
|
|
159
|
+
* subscription, no plan or item to change to. The entity's own state, not a fault: an HTTP boundary
|
|
160
|
+
* that honours `httpStatus` (`@owlmeans/server-api`) answers 409 Conflict.
|
|
161
|
+
*/
|
|
162
|
+
export class PortalUnavailable extends PaygateError {
|
|
163
|
+
public static override typeName: string = `${PaygateError.typeName}PortalUnavailable`
|
|
164
|
+
public static httpStatus = 409
|
|
165
|
+
|
|
166
|
+
constructor(message: string = 'error') {
|
|
167
|
+
super(`portal-unavailable:${message}`)
|
|
168
|
+
this.type = PortalUnavailable.typeName
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The entity's plan does not allow the request.
|
|
174
|
+
*
|
|
175
|
+
* Extends `AuthForbidden` so an HTTP boundary answers 403, never 500. Only `type` and `message`
|
|
176
|
+
* survive a marshal round trip, so each subclass packs its fields into the message and rebuilds
|
|
177
|
+
* them in `finalizeUnmarshal()`.
|
|
178
|
+
*/
|
|
179
|
+
export class EntitlementRefusal extends AuthForbidden {
|
|
180
|
+
public static override typeName: string = 'EntitlementRefusal'
|
|
181
|
+
|
|
182
|
+
constructor(message: string = 'error') {
|
|
183
|
+
super(`entitlement:${message}`)
|
|
184
|
+
this.type = EntitlementRefusal.typeName
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** None of the capability parameters (OR'd) is granted. Message marker `capability-required:<a|b>`. */
|
|
189
|
+
export class CapabilityRequired extends EntitlementRefusal {
|
|
190
|
+
public static override typeName: string = `${EntitlementRefusal.typeName}CapabilityRequired`
|
|
191
|
+
|
|
192
|
+
/** The refused parameters, as the gate took them. */
|
|
193
|
+
public params: string[] = []
|
|
194
|
+
|
|
195
|
+
constructor(params: string | string[] = []) {
|
|
196
|
+
const list = (Array.isArray(params) ? params : [params]).filter(param => param !== '')
|
|
197
|
+
super(`${CAPABILITY_REQUIRED_MARKER}${list.join('|')}`)
|
|
198
|
+
this.type = CapabilityRequired.typeName
|
|
199
|
+
this.applyFields()
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private applyFields(): void {
|
|
203
|
+
const at = this.message.indexOf(CAPABILITY_REQUIRED_MARKER)
|
|
204
|
+
const packed = at < 0 ? '' : this.message.slice(at + CAPABILITY_REQUIRED_MARKER.length)
|
|
205
|
+
this.params = packed.split('|').filter(param => param !== '')
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
override finalizeUnmarshal(): void {
|
|
209
|
+
this.applyFields()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* A limit has no room left. Message marker `limit-exhausted:<key>:<used>/<limit>[:<resetsAt ISO>]`.
|
|
216
|
+
*/
|
|
217
|
+
export class LimitExhausted extends EntitlementRefusal {
|
|
218
|
+
public static override typeName: string = `${EntitlementRefusal.typeName}LimitExhausted`
|
|
219
|
+
|
|
220
|
+
public limitKey: string = ''
|
|
221
|
+
public used: number = 0
|
|
222
|
+
public limit: number = 0
|
|
223
|
+
public resetsAt?: Date
|
|
224
|
+
|
|
225
|
+
/** The packed marker body of a refusal. */
|
|
226
|
+
static encode(details: LimitExhaustedDetails): string {
|
|
227
|
+
const resetsAt = details.resetsAt != null && !Number.isNaN(details.resetsAt.getTime())
|
|
228
|
+
? `:${details.resetsAt.toISOString()}` : ''
|
|
229
|
+
return `${details.key}:${details.used}/${details.limit}${resetsAt}`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
constructor(details: LimitExhaustedDetails)
|
|
233
|
+
/** A packed marker body (`LimitExhausted.encode`) or a marshaled message — the error registry's path. */
|
|
234
|
+
constructor(message: string)
|
|
235
|
+
constructor(details: LimitExhaustedDetails | string = 'error') {
|
|
236
|
+
super(`${LIMIT_EXHAUSTED_MARKER}${typeof details === 'string' ? details : LimitExhausted.encode(details)}`)
|
|
237
|
+
this.type = LimitExhausted.typeName
|
|
238
|
+
this.applyFields()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private applyFields(): void {
|
|
242
|
+
const at = this.message.indexOf(LIMIT_EXHAUSTED_MARKER)
|
|
243
|
+
const match = at < 0 ? null
|
|
244
|
+
: LIMIT_EXHAUSTED_FIELDS.exec(this.message.slice(at + LIMIT_EXHAUSTED_MARKER.length))
|
|
245
|
+
if (match == null) {
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
const [, key, used, limit, resetsAt] = match
|
|
249
|
+
this.limitKey = key
|
|
250
|
+
this.used = Number(used) || 0
|
|
251
|
+
this.limit = Number(limit) || 0
|
|
252
|
+
const date = resetsAt != null ? new Date(resetsAt) : null
|
|
253
|
+
this.resetsAt = date != null && !Number.isNaN(date.getTime()) ? date : undefined
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
override finalizeUnmarshal(): void {
|
|
257
|
+
this.applyFields()
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* A consumer-rights fault — a misdeclared policy (`policy:<field>`), a missing usage meter or
|
|
263
|
+
* mailer, a copy template without a value (`copy:<path>`). Declares no status: 500.
|
|
264
|
+
*/
|
|
265
|
+
export class ConsumerRightsError extends PaymentError {
|
|
266
|
+
public static override typeName: string = `${PaymentError.typeName}ConsumerRights`
|
|
267
|
+
|
|
268
|
+
constructor(message: string = 'error') {
|
|
269
|
+
super(`consumer-rights:${message}`)
|
|
270
|
+
this.type = ConsumerRightsError.typeName
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* The consumer's own position refuses the request — never a fault. Deliberately NOT an
|
|
276
|
+
* `AuthForbidden`: an HTTP boundary tests that family first, and a 403 would hide the declared
|
|
277
|
+
* 428/409 a client acts on.
|
|
278
|
+
*/
|
|
279
|
+
export class ConsumerRightsRefusal extends ConsumerRightsError {
|
|
280
|
+
public static override typeName: string = `${ConsumerRightsError.typeName}Refusal`
|
|
281
|
+
|
|
282
|
+
constructor(message: string = 'error') {
|
|
283
|
+
super(message)
|
|
284
|
+
this.type = ConsumerRightsRefusal.typeName
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The packed body after the LAST marker occurrence — a registry rebuild doubles the prefix. */
|
|
289
|
+
const packedAfter = (message: string, marker: string): string | null => {
|
|
290
|
+
const at = message.lastIndexOf(marker)
|
|
291
|
+
|
|
292
|
+
return at < 0 ? null : message.slice(at + marker.length)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const dateOf = (value: string | undefined): Date | undefined => {
|
|
296
|
+
const date = value != null && value !== '' ? new Date(value) : null
|
|
297
|
+
|
|
298
|
+
return date != null && !Number.isNaN(date.getTime()) ? date : undefined
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const isoOf = (date: Date | undefined): string =>
|
|
302
|
+
date != null && !Number.isNaN(date.getTime()) ? `:${date.toISOString()}` : ''
|
|
303
|
+
|
|
304
|
+
const decode = (value: string): string => {
|
|
305
|
+
try {
|
|
306
|
+
return decodeURIComponent(value)
|
|
307
|
+
} catch {
|
|
308
|
+
return value
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Billed work would spend credits of a purchase still inside its withdrawal window, and the
|
|
314
|
+
* consumer has not expressly requested performance. HTTP 428. Marker
|
|
315
|
+
* `performance-consent-required:<pending>[:<deadline ISO>]`.
|
|
316
|
+
*/
|
|
317
|
+
export class PerformanceConsentRequired extends ConsumerRightsRefusal {
|
|
318
|
+
public static override typeName: string = `${ConsumerRightsRefusal.typeName}PerformanceConsentRequired`
|
|
319
|
+
public static httpStatus = 428
|
|
320
|
+
|
|
321
|
+
public pending: number = 0
|
|
322
|
+
public deadline?: Date
|
|
323
|
+
|
|
324
|
+
static encode(details: PerformanceConsentRequiredDetails): string {
|
|
325
|
+
return `${Math.max(0, Math.floor(details.pending))}${isoOf(details.deadline)}`
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
constructor(details: PerformanceConsentRequiredDetails)
|
|
329
|
+
/** A packed marker body (`encode`) or a marshaled message — the error registry's path. */
|
|
330
|
+
constructor(message: string)
|
|
331
|
+
constructor(details: PerformanceConsentRequiredDetails | string = 'error') {
|
|
332
|
+
super(`${PERFORMANCE_CONSENT_MARKER}${typeof details === 'string' ? details : PerformanceConsentRequired.encode(details)}`)
|
|
333
|
+
this.type = PerformanceConsentRequired.typeName
|
|
334
|
+
this.applyFields()
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private applyFields(): void {
|
|
338
|
+
const match = /^(\d+)(?::(.+))?$/.exec(packedAfter(this.message, PERFORMANCE_CONSENT_MARKER) ?? '')
|
|
339
|
+
if (match == null) {
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
this.pending = Number(match[1])
|
|
343
|
+
this.deadline = dateOf(match[2])
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
override finalizeUnmarshal(): void {
|
|
347
|
+
this.applyFields()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* A subscription checkout needs a fresh express start request bound to this plan. HTTP 428.
|
|
353
|
+
* Marker `subscription-start-required:<encodeURIComponent(planSku)>`.
|
|
354
|
+
*/
|
|
355
|
+
export class SubscriptionStartRequired extends ConsumerRightsRefusal {
|
|
356
|
+
public static override typeName: string = `${ConsumerRightsRefusal.typeName}SubscriptionStartRequired`
|
|
357
|
+
public static httpStatus = 428
|
|
358
|
+
|
|
359
|
+
public planSku: string = ''
|
|
360
|
+
|
|
361
|
+
static encode(planSku: string): string {
|
|
362
|
+
return encodeURIComponent(planSku)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** `planSku` as is, or a packed marker body / marshaled message (the registry's path). */
|
|
366
|
+
constructor(planSku: string | { planSku: string } = 'error') {
|
|
367
|
+
super(`${SUBSCRIPTION_START_MARKER}${typeof planSku === 'string' ? SubscriptionStartRequired.encodeLoose(planSku) : SubscriptionStartRequired.encode(planSku.planSku)}`)
|
|
368
|
+
this.type = SubscriptionStartRequired.typeName
|
|
369
|
+
this.applyFields()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** A string that already is a marker (the registry path) passes through unencoded. */
|
|
373
|
+
private static encodeLoose(value: string): string {
|
|
374
|
+
return value.includes(SUBSCRIPTION_START_MARKER) ? value : SubscriptionStartRequired.encode(value)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private applyFields(): void {
|
|
378
|
+
const packed = packedAfter(this.message, SUBSCRIPTION_START_MARKER)
|
|
379
|
+
if (packed != null) {
|
|
380
|
+
this.planSku = decode(packed)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
override finalizeUnmarshal(): void {
|
|
385
|
+
this.applyFields()
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* The entity's billing country is fixed and the request declared another. HTTP 409. Marker
|
|
391
|
+
* `billing-country-locked:<country>[:<requested>]`.
|
|
392
|
+
*/
|
|
393
|
+
export class BillingCountryLocked extends ConsumerRightsRefusal {
|
|
394
|
+
public static override typeName: string = `${ConsumerRightsRefusal.typeName}BillingCountryLocked`
|
|
395
|
+
public static httpStatus = 409
|
|
396
|
+
|
|
397
|
+
public country: string = ''
|
|
398
|
+
public requested?: string
|
|
399
|
+
|
|
400
|
+
static encode(details: BillingCountryLockedDetails): string {
|
|
401
|
+
return details.requested != null && details.requested !== ''
|
|
402
|
+
? `${details.country}:${details.requested}` : details.country
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
constructor(details: BillingCountryLockedDetails)
|
|
406
|
+
constructor(message: string)
|
|
407
|
+
constructor(details: BillingCountryLockedDetails | string = 'error') {
|
|
408
|
+
super(`${BILLING_COUNTRY_LOCKED_MARKER}${typeof details === 'string' ? details : BillingCountryLocked.encode(details)}`)
|
|
409
|
+
this.type = BillingCountryLocked.typeName
|
|
410
|
+
this.applyFields()
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private applyFields(): void {
|
|
414
|
+
const match = /^([A-Za-z]{2})(?::([A-Za-z]{2}))?$/.exec(packedAfter(this.message, BILLING_COUNTRY_LOCKED_MARKER) ?? '')
|
|
415
|
+
if (match == null) {
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
this.country = match[1]
|
|
419
|
+
this.requested = match[2]
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
override finalizeUnmarshal(): void {
|
|
423
|
+
this.applyFields()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* The named purchase cannot be withdrawn from (`WithdrawalUnavailableReason`). HTTP 409. Marker
|
|
429
|
+
* `withdrawal-unavailable:<reason>`. Never raised on the public page, which discloses nothing.
|
|
430
|
+
*/
|
|
431
|
+
export class WithdrawalUnavailable extends ConsumerRightsRefusal {
|
|
432
|
+
public static override typeName: string = `${ConsumerRightsRefusal.typeName}WithdrawalUnavailable`
|
|
433
|
+
public static httpStatus = 409
|
|
434
|
+
|
|
435
|
+
public reason: string = ''
|
|
436
|
+
|
|
437
|
+
constructor(reason: string = 'error') {
|
|
438
|
+
super(`${WITHDRAWAL_UNAVAILABLE_MARKER}${reason}`)
|
|
439
|
+
this.type = WithdrawalUnavailable.typeName
|
|
440
|
+
this.applyFields()
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private applyFields(): void {
|
|
444
|
+
this.reason = /^[\w-]*/.exec(packedAfter(this.message, WITHDRAWAL_UNAVAILABLE_MARKER) ?? '')?.[0] ?? ''
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
override finalizeUnmarshal(): void {
|
|
448
|
+
this.applyFields()
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Nothing to cancel in-app (`CancellationUnavailableReason`). HTTP 409. Marker
|
|
454
|
+
* `cancellation-unavailable:<reason>`.
|
|
455
|
+
*/
|
|
456
|
+
export class CancellationUnavailable extends ConsumerRightsRefusal {
|
|
457
|
+
public static override typeName: string = `${ConsumerRightsRefusal.typeName}CancellationUnavailable`
|
|
458
|
+
public static httpStatus = 409
|
|
459
|
+
|
|
460
|
+
public reason: string = ''
|
|
461
|
+
|
|
462
|
+
constructor(reason: string = 'error') {
|
|
463
|
+
super(`${CANCELLATION_UNAVAILABLE_MARKER}${reason}`)
|
|
464
|
+
this.type = CancellationUnavailable.typeName
|
|
465
|
+
this.applyFields()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private applyFields(): void {
|
|
469
|
+
this.reason = /^[\w-]*/.exec(packedAfter(this.message, CANCELLATION_UNAVAILABLE_MARKER) ?? '')?.[0] ?? ''
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
override finalizeUnmarshal(): void {
|
|
473
|
+
this.applyFields()
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The amount is above what this entity may buy now (a narrowing of the plan's policy). HTTP 409.
|
|
479
|
+
* Marker `checkout-limit-exceeded:<encodeURIComponent(reason)>:<maximumMinor>:<currency>[:<ISO>]`.
|
|
480
|
+
* A payment refusal, not a consumer-rights one.
|
|
481
|
+
*/
|
|
482
|
+
export class CheckoutLimitExceeded extends PaymentError {
|
|
483
|
+
public static override typeName: string = `${PaymentError.typeName}CheckoutLimitExceeded`
|
|
484
|
+
public static httpStatus = 409
|
|
485
|
+
|
|
486
|
+
public reason: string = ''
|
|
487
|
+
public maximumMinor: number = 0
|
|
488
|
+
public currency: string = ''
|
|
489
|
+
public resetsAt?: Date
|
|
490
|
+
|
|
491
|
+
static encode(details: CheckoutLimitExceededDetails): string {
|
|
492
|
+
return `${encodeURIComponent(details.reason)}:${Math.max(0, Math.floor(details.maximumMinor))}`
|
|
493
|
+
+ `:${details.currency.toLowerCase()}${isoOf(details.resetsAt)}`
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
constructor(details: CheckoutLimitExceededDetails)
|
|
497
|
+
constructor(message: string)
|
|
498
|
+
constructor(details: CheckoutLimitExceededDetails | string = 'error') {
|
|
499
|
+
super(`${CHECKOUT_LIMIT_MARKER}${typeof details === 'string' ? details : CheckoutLimitExceeded.encode(details)}`)
|
|
500
|
+
this.type = CheckoutLimitExceeded.typeName
|
|
501
|
+
this.applyFields()
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private applyFields(): void {
|
|
505
|
+
const match = /^([^:]*):(\d+):([a-z]{3})(?::(.+))?$/.exec(packedAfter(this.message, CHECKOUT_LIMIT_MARKER) ?? '')
|
|
506
|
+
if (match == null) {
|
|
507
|
+
return
|
|
508
|
+
}
|
|
509
|
+
this.reason = decode(match[1])
|
|
510
|
+
this.maximumMinor = Number(match[2])
|
|
511
|
+
this.currency = match[3]
|
|
512
|
+
this.resetsAt = dateOf(match[4])
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
override finalizeUnmarshal(): void {
|
|
516
|
+
this.applyFields()
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
93
520
|
ResilientError.registerErrorClass(PaymentError)
|
|
94
521
|
ResilientError.registerErrorClass(PaygateError)
|
|
95
522
|
ResilientError.registerErrorClass(UnknownPaygate)
|
|
96
523
|
ResilientError.registerErrorClass(PaygateMappingError)
|
|
524
|
+
ResilientError.registerErrorClass(PaygateSignatureError)
|
|
97
525
|
ResilientError.registerErrorClass(ProductError)
|
|
98
526
|
ResilientError.registerErrorClass(UnknownProduct)
|
|
99
527
|
ResilientError.registerErrorClass(UnknownPlan)
|
|
100
528
|
ResilientError.registerErrorClass(PaymentIdentificationError)
|
|
101
529
|
ResilientError.registerErrorClass(SubscriptionError)
|
|
102
530
|
ResilientError.registerErrorClass(UnknownSubscription)
|
|
531
|
+
ResilientError.registerErrorClass(LimitUnknown)
|
|
532
|
+
ResilientError.registerErrorClass(LimitMisdeclared)
|
|
533
|
+
ResilientError.registerErrorClass(PlanRequired)
|
|
534
|
+
ResilientError.registerErrorClass(PlanRankConflict)
|
|
535
|
+
ResilientError.registerErrorClass(WebhookSetupError)
|
|
536
|
+
ResilientError.registerErrorClass(PortalUnavailable)
|
|
537
|
+
ResilientError.registerErrorClass(EntitlementRefusal)
|
|
538
|
+
ResilientError.registerErrorClass(CapabilityRequired)
|
|
539
|
+
ResilientError.registerErrorClass(LimitExhausted)
|
|
540
|
+
ResilientError.registerErrorClass(ConsumerRightsError)
|
|
541
|
+
ResilientError.registerErrorClass(ConsumerRightsRefusal)
|
|
542
|
+
ResilientError.registerErrorClass(PerformanceConsentRequired)
|
|
543
|
+
ResilientError.registerErrorClass(SubscriptionStartRequired)
|
|
544
|
+
ResilientError.registerErrorClass(BillingCountryLocked)
|
|
545
|
+
ResilientError.registerErrorClass(WithdrawalUnavailable)
|
|
546
|
+
ResilientError.registerErrorClass(CancellationUnavailable)
|
|
547
|
+
ResilientError.registerErrorClass(CheckoutLimitExceeded)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AmountEstimate, PriceEstimate, PricingPolicy } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/** The pricing policy's invariants and the tax / local-currency estimate of an amount. */
|
|
4
|
+
export interface PriceEstimateHelper {
|
|
5
|
+
/**
|
|
6
|
+
* @throws PaymentError when a tax estimate is served without automatic tax, a currency estimate is
|
|
7
|
+
* served without Adaptive Pricing, or a TTL is not a positive integer.
|
|
8
|
+
*/
|
|
9
|
+
assertPricingPolicy: (policy: PricingPolicy) => PricingPolicy
|
|
10
|
+
/**
|
|
11
|
+
* Stripe's `percentage_decimal` (e.g. `"23"`, `"8.875"`) as parts per million — `1_000_000` is
|
|
12
|
+
* `100%` — parsed digit by digit so it is exact where a float parse (`Number(x) * 10_000`) is not.
|
|
13
|
+
* Fractional digits past the fourth are truncated, not rounded: a quarter of a hundredth of a
|
|
14
|
+
* percent is already far finer than any real tax rate.
|
|
15
|
+
*
|
|
16
|
+
* @throws PaymentError when the string is not a plain (unsigned, non-exponential) decimal.
|
|
17
|
+
*/
|
|
18
|
+
ratePpmOf: (percentageDecimal: string) => number
|
|
19
|
+
/**
|
|
20
|
+
* Re-derive tax and the local-currency total for `amountMinor` from a `PriceEstimate` already
|
|
21
|
+
* fetched for the SAME product/plan/country, so a UI can re-price a custom amount (the credit
|
|
22
|
+
* dialog) without a second Stripe Tax call ($0.05 each).
|
|
23
|
+
*
|
|
24
|
+
* Exact for `amountMinor === estimate.tax.subtotalMinor` (Stripe's own numbers, echoed back).
|
|
25
|
+
* Otherwise exact only for `TaxBehavior.Exclusive` with `estimate.tax.scalable` — a linear
|
|
26
|
+
* percentage rate scales; a flat fee, a reduced-rate portion, or several inclusive rates do not, and
|
|
27
|
+
* neither does rescaling an inclusive amount (its tax portion is carved OUT of the total by a
|
|
28
|
+
* different formula this helper does not attempt). Both cases return `null` amounts, meaning
|
|
29
|
+
* "computed at checkout".
|
|
30
|
+
*
|
|
31
|
+
* @throws PaymentError when `amountMinor` is not a non-negative safe integer.
|
|
32
|
+
*/
|
|
33
|
+
estimateOf: (amountMinor: number, estimate: PriceEstimate) => AmountEstimate
|
|
34
|
+
}
|
package/src/estimate.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { PaymentError } from './errors.js'
|
|
2
|
+
import { TaxBehavior } from './consts.js'
|
|
3
|
+
import type { PriceEstimate, PricingPolicy, AmountEstimate } from './types.js'
|
|
4
|
+
import type { PriceEstimateHelper } from './estimate/types.js'
|
|
5
|
+
|
|
6
|
+
/** Stripe's own convention for a tax amount: half a unit rounds up. */
|
|
7
|
+
const roundHalfUp = (value: number): number => Math.floor(value + 0.5)
|
|
8
|
+
|
|
9
|
+
export const createPriceEstimateHelper = (): PriceEstimateHelper => {
|
|
10
|
+
const assertPricingPolicy = (policy: PricingPolicy): PricingPolicy => {
|
|
11
|
+
if (policy.tax.estimate && !policy.tax.automatic) {
|
|
12
|
+
throw new PaymentError('pricing-policy:tax-estimate-requires-automatic')
|
|
13
|
+
}
|
|
14
|
+
if (policy.currency.estimate && policy.currency.adaptive !== true) {
|
|
15
|
+
throw new PaymentError('pricing-policy:currency-estimate-requires-adaptive')
|
|
16
|
+
}
|
|
17
|
+
const feeRate = policy.currency.adaptiveFeeRate
|
|
18
|
+
if (feeRate != null) {
|
|
19
|
+
if (policy.currency.adaptive !== true) {
|
|
20
|
+
throw new PaymentError('pricing-policy:adaptive-fee-requires-adaptive')
|
|
21
|
+
}
|
|
22
|
+
if (!Number.isFinite(feeRate) || feeRate < 0 || feeRate >= 1) {
|
|
23
|
+
throw new PaymentError('pricing-policy:adaptive-fee-rate')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (const ttl of [policy.tax.estimateTtlSeconds, policy.currency.estimateTtlSeconds]) {
|
|
27
|
+
if (ttl != null && (!Number.isSafeInteger(ttl) || ttl < 1)) {
|
|
28
|
+
throw new PaymentError('pricing-policy:ttl')
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return policy
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ratePpmOf = (percentageDecimal: string): number => {
|
|
35
|
+
const match = /^(\d+)(?:\.(\d+))?$/.exec(percentageDecimal.trim())
|
|
36
|
+
if (match == null) {
|
|
37
|
+
throw new PaymentError('tax-rate:percentage')
|
|
38
|
+
}
|
|
39
|
+
const [, whole, fraction = ''] = match
|
|
40
|
+
const fractionPpm = Number(`${fraction}0000`.slice(0, 4))
|
|
41
|
+
const ppm = Number(whole) * 10_000 + fractionPpm
|
|
42
|
+
if (!Number.isSafeInteger(ppm)) {
|
|
43
|
+
throw new PaymentError('tax-rate:percentage')
|
|
44
|
+
}
|
|
45
|
+
return ppm
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const estimateOf = (amountMinor: number, estimate: PriceEstimate): AmountEstimate => {
|
|
49
|
+
if (!Number.isSafeInteger(amountMinor) || amountMinor < 0) {
|
|
50
|
+
throw new PaymentError('price-estimate:amount')
|
|
51
|
+
}
|
|
52
|
+
const { tax, behavior, local } = estimate
|
|
53
|
+
const sameAmount = amountMinor === tax.subtotalMinor
|
|
54
|
+
const rescalable = sameAmount || (behavior === TaxBehavior.Exclusive && tax.scalable)
|
|
55
|
+
if (!rescalable) {
|
|
56
|
+
return { subtotalMinor: amountMinor, taxMinor: null, totalMinor: null }
|
|
57
|
+
}
|
|
58
|
+
// `sameAmount` trusts Stripe's own numbers outright; otherwise only Exclusive ever reaches here
|
|
59
|
+
// (`rescalable` above excludes an Inclusive amount that differs from the reference).
|
|
60
|
+
const taxMinor = sameAmount ? tax.taxMinor : tax.rates.reduce(
|
|
61
|
+
(sum, rate) => sum + roundHalfUp(amountMinor * rate.ratePpm / 1_000_000), 0,
|
|
62
|
+
)
|
|
63
|
+
const totalMinor = sameAmount ? tax.totalMinor : amountMinor + taxMinor
|
|
64
|
+
const result: AmountEstimate = { subtotalMinor: amountMinor, taxMinor, totalMinor }
|
|
65
|
+
if (local != null) {
|
|
66
|
+
result.local = {
|
|
67
|
+
currency: local.currency,
|
|
68
|
+
subtotalAmount: amountMinor / 100 / local.exchangeRate,
|
|
69
|
+
taxAmount: taxMinor / 100 / local.exchangeRate,
|
|
70
|
+
totalAmount: totalMinor / 100 / local.exchangeRate,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { assertPricingPolicy, ratePpmOf, estimateOf }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const priceEstimateHelper = createPriceEstimateHelper()
|
|
80
|
+
|
|
81
|
+
/** @deprecated compat:factory-refactor — use `priceEstimateHelper.assertPricingPolicy(…)` */
|
|
82
|
+
export const assertPricingPolicy = (policy: PricingPolicy): PricingPolicy =>
|
|
83
|
+
priceEstimateHelper.assertPricingPolicy(policy)
|
|
84
|
+
|
|
85
|
+
/** @deprecated compat:factory-refactor — use `priceEstimateHelper.ratePpmOf(…)` */
|
|
86
|
+
export const ratePpmOf = (percentageDecimal: string): number => priceEstimateHelper.ratePpmOf(percentageDecimal)
|
|
87
|
+
|
|
88
|
+
/** @deprecated compat:factory-refactor — use `priceEstimateHelper.estimateOf(…)` */
|
|
89
|
+
export const estimateOf = (amountMinor: number, estimate: PriceEstimate): AmountEstimate =>
|
|
90
|
+
priceEstimateHelper.estimateOf(amountMinor, estimate)
|