@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
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ConsumerRightsError } from '../errors.js'
|
|
2
|
+
import type { PlanWithdrawalComponent } from '../types.js'
|
|
3
|
+
import { DAY_MS } from './consts.js'
|
|
4
|
+
import type { OneTimeRefund, OneTimeRefundInput, SubscriptionRefund, SubscriptionRefundComponent, SubscriptionRefundInput } from './types.js'
|
|
5
|
+
import type { WithdrawalRefundHelper } from './refund/types.js'
|
|
6
|
+
|
|
7
|
+
// Every amount is computed in BigInt and rounded in the consumer's favour: a deduction rounds
|
|
8
|
+
// down, a refund rounds up, and a refund never exceeds what is still unrefunded.
|
|
9
|
+
|
|
10
|
+
const big = (value: number, field: string): bigint => {
|
|
11
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
12
|
+
throw new ConsumerRightsError(`refund:${field}`)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return BigInt(value)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** A usage figure: fractions round DOWN (fewer units used, more reimbursed). */
|
|
19
|
+
const units = (value: number, field: string): bigint => {
|
|
20
|
+
if (!Number.isFinite(value)) {
|
|
21
|
+
throw new ConsumerRightsError(`refund:${field}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return BigInt(Math.max(0, Math.floor(value)))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ceilDiv = (a: bigint, b: bigint): bigint => (a + b - 1n) / b
|
|
28
|
+
const min = (a: bigint, b: bigint): bigint => a < b ? a : b
|
|
29
|
+
const clamp = (value: bigint, low: bigint, high: bigint): bigint => value < low ? low : value > high ? high : value
|
|
30
|
+
|
|
31
|
+
export const createWithdrawalRefundHelper = (): WithdrawalRefundHelper => {
|
|
32
|
+
const splitByShares = (totalMinor: number, shares: number[]): number[] => {
|
|
33
|
+
if (shares.length === 0) {
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
const total = big(totalMinor, 'total')
|
|
37
|
+
const weights = shares.map((share, index) => big(share, `share:${index}`))
|
|
38
|
+
const even = weights.every(weight => weight === 0n)
|
|
39
|
+
const effective = even ? weights.map(() => 1n) : weights
|
|
40
|
+
const sum = effective.reduce((acc, weight) => acc + weight, 0n)
|
|
41
|
+
const parts = effective.map(weight => (total * weight) / sum)
|
|
42
|
+
const remainders = effective.map((weight, index) => ({ index, rest: (total * weight) % sum }))
|
|
43
|
+
let left = total - parts.reduce((acc, part) => acc + part, 0n)
|
|
44
|
+
remainders.sort((a, b) => a.rest === b.rest ? a.index - b.index : a.rest > b.rest ? -1 : 1)
|
|
45
|
+
for (const { index } of remainders) {
|
|
46
|
+
if (left === 0n) {
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
parts[index] += 1n
|
|
50
|
+
left -= 1n
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return parts.map(Number)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const oneTimeWithdrawalRefund = (input: OneTimeRefundInput): OneTimeRefund => {
|
|
57
|
+
const paid = big(input.paidMinor, 'paid')
|
|
58
|
+
const open = paid - min(paid, big(input.refundedMinor ?? 0, 'refunded'))
|
|
59
|
+
const granted = units(input.unitsGranted, 'granted')
|
|
60
|
+
if (granted === 0n) {
|
|
61
|
+
return { refundMinor: Number(open), netRatio: 1, unitsReturned: 0 }
|
|
62
|
+
}
|
|
63
|
+
const unused = granted - clamp(units(input.unitsUsed, 'used'), 0n, granted)
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
refundMinor: Number(min(open, ceilDiv(paid * unused, granted))),
|
|
67
|
+
netRatio: Number(unused) / Number(granted),
|
|
68
|
+
unitsReturned: Number(unused),
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const subscriptionWithdrawalRefund = (input: SubscriptionRefundInput): SubscriptionRefund => {
|
|
73
|
+
const paid = big(input.paidMinor, 'paid')
|
|
74
|
+
const open = paid - min(paid, big(input.refundedMinor ?? 0, 'refunded'))
|
|
75
|
+
const net = big(input.netMinor, 'net')
|
|
76
|
+
const declared = input.components != null && input.components.length > 0
|
|
77
|
+
? input.components
|
|
78
|
+
: [{ key: 'price', basis: 'time', shareMinor: input.netMinor } satisfies PlanWithdrawalComponent]
|
|
79
|
+
const amounts = splitByShares(input.netMinor, declared.map(component => component.shareMinor))
|
|
80
|
+
|
|
81
|
+
const span = input.periodEnd.getTime() - input.periodStart.getTime()
|
|
82
|
+
if (Number.isNaN(span) || Number.isNaN(input.withdrawnAt.getTime())) {
|
|
83
|
+
throw new ConsumerRightsError('refund:period')
|
|
84
|
+
}
|
|
85
|
+
const periodDays = Math.max(1, Math.round(span / DAY_MS))
|
|
86
|
+
const requestedAt = input.servicesRequestedAt
|
|
87
|
+
const from = requestedAt == null ? null : Math.max(input.periodStart.getTime(), requestedAt.getTime())
|
|
88
|
+
const elapsedDays = from == null ? 0
|
|
89
|
+
: Math.min(periodDays, Math.max(0, Math.floor((input.withdrawnAt.getTime() - from) / DAY_MS)))
|
|
90
|
+
|
|
91
|
+
const granted = input.units != null ? units(input.units.granted, 'granted') : 0n
|
|
92
|
+
const used = input.units != null ? clamp(units(input.units.used, 'used'), 0n, granted) : 0n
|
|
93
|
+
|
|
94
|
+
let timeDeduction = 0n
|
|
95
|
+
let unitsDeduction = 0n
|
|
96
|
+
const components = declared.map((component, index): SubscriptionRefundComponent => {
|
|
97
|
+
const amount = BigInt(amounts[index])
|
|
98
|
+
let deduction = 0n
|
|
99
|
+
if (component.basis === 'time') {
|
|
100
|
+
deduction = (amount * BigInt(elapsedDays)) / BigInt(periodDays)
|
|
101
|
+
timeDeduction += deduction
|
|
102
|
+
} else if (granted > 0n) {
|
|
103
|
+
deduction = (amount * used) / granted
|
|
104
|
+
unitsDeduction += deduction
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { ...component, amountMinor: amounts[index], deductionMinor: Number(deduction) }
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const refundNet = net - min(net, timeDeduction + unitsDeduction)
|
|
111
|
+
const refund = net === 0n ? open : min(open, ceilDiv(paid * refundNet, net))
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
refundMinor: Number(refund),
|
|
115
|
+
refundNetMinor: Number(refundNet),
|
|
116
|
+
timeDeductionMinor: Number(timeDeduction),
|
|
117
|
+
unitsDeductionMinor: Number(unitsDeduction),
|
|
118
|
+
elapsedDays,
|
|
119
|
+
periodDays,
|
|
120
|
+
...(input.units != null ? { unitsUsed: Number(used), unitsGranted: Number(granted) } : {}),
|
|
121
|
+
components,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { splitByShares, oneTimeWithdrawalRefund, subscriptionWithdrawalRefund }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const withdrawalRefundHelper = createWithdrawalRefundHelper()
|
|
129
|
+
|
|
130
|
+
/** @deprecated compat:factory-refactor — use `withdrawalRefundHelper.oneTimeWithdrawalRefund(…)` */
|
|
131
|
+
export const oneTimeWithdrawalRefund = (input: OneTimeRefundInput): OneTimeRefund =>
|
|
132
|
+
withdrawalRefundHelper.oneTimeWithdrawalRefund(input)
|
|
133
|
+
|
|
134
|
+
/** @deprecated compat:factory-refactor — use `withdrawalRefundHelper.subscriptionWithdrawalRefund(…)` */
|
|
135
|
+
export const subscriptionWithdrawalRefund = (input: SubscriptionRefundInput): SubscriptionRefund =>
|
|
136
|
+
withdrawalRefundHelper.subscriptionWithdrawalRefund(input)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isResilientError, ResilientError } from '@owlmeans/error'
|
|
2
|
+
import { ConsentKind } from '../consts.js'
|
|
3
|
+
import { PerformanceConsentRequired, SubscriptionStartRequired } from '../errors.js'
|
|
4
|
+
import { CONSENT_REFUSAL_MARKERS } from './consts.local.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Which express request a refusal asks for: `performance` (spend consent), `subscription-start`,
|
|
8
|
+
* or `null` for anything else. Recognises the class after `ResilientError.ensure` — a marshaled
|
|
9
|
+
* error that crossed a hop is rebuilt first — and otherwise the marker or type name in the
|
|
10
|
+
* `message` or `type`, so an error wrapped by another layer still answers. A bare HTTP 428 with
|
|
11
|
+
* no marker (a production incident body) is the caller's to check (`@owlmeans/api/status`).
|
|
12
|
+
*/
|
|
13
|
+
export const consentRefusalOf = (error: unknown): ConsentKind | null => {
|
|
14
|
+
if (error == null || typeof error !== 'object') {
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
let candidate: unknown = error
|
|
18
|
+
if (!isResilientError(error) && error instanceof Error) {
|
|
19
|
+
try {
|
|
20
|
+
candidate = ResilientError.ensure(error)
|
|
21
|
+
} catch {
|
|
22
|
+
candidate = error
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (candidate instanceof PerformanceConsentRequired) {
|
|
26
|
+
return ConsentKind.Performance
|
|
27
|
+
}
|
|
28
|
+
if (candidate instanceof SubscriptionStartRequired) {
|
|
29
|
+
return ConsentKind.SubscriptionStart
|
|
30
|
+
}
|
|
31
|
+
const { message, type } = error as { message?: unknown, type?: unknown }
|
|
32
|
+
const texts = [message, type].filter((text): text is string => typeof text === 'string')
|
|
33
|
+
|
|
34
|
+
return CONSENT_REFUSAL_MARKERS.find(([marker]) => texts.some(text => text.includes(marker)))?.[1] ?? null
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AmountPolicyView, BillingProfileView, CancellationReceipt, CheckoutLimitView, DeclarationReceipt,
|
|
3
|
+
PerformanceConsentResponse, PerformanceConsentView, PurchaseList, PurchaseView, SubscriptionStartResponse,
|
|
4
|
+
WithdrawalCandidateList, WithdrawalReceipt,
|
|
5
|
+
} from '../../types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The consumer-rights wire values with their dates turned back into `Date`s. Each reviver copies
|
|
9
|
+
* the value; a value that already holds dates is copied as is, so reviving twice is harmless.
|
|
10
|
+
*/
|
|
11
|
+
export interface ConsumerReviveHelper {
|
|
12
|
+
revivePurchase: (purchase: PurchaseView) => PurchaseView
|
|
13
|
+
revivePurchaseList: (list: PurchaseList) => PurchaseList
|
|
14
|
+
reviveBillingProfile: (profile: BillingProfileView) => BillingProfileView
|
|
15
|
+
reviveConsentView: (view: PerformanceConsentView) => PerformanceConsentView
|
|
16
|
+
reviveConsentResponse: (response: PerformanceConsentResponse) => PerformanceConsentResponse
|
|
17
|
+
reviveStartResponse: (response: SubscriptionStartResponse) => SubscriptionStartResponse
|
|
18
|
+
reviveWithdrawalList: (list: WithdrawalCandidateList) => WithdrawalCandidateList
|
|
19
|
+
/** Any declaration receipt — withdrawal or cancellation. */
|
|
20
|
+
reviveReceipt: <T extends DeclarationReceipt | WithdrawalReceipt | CancellationReceipt>(receipt: T) => T
|
|
21
|
+
reviveCheckoutLimit: (limit: CheckoutLimitView) => CheckoutLimitView
|
|
22
|
+
reviveAmountPolicyView: (view: AmountPolicyView) => AmountPolicyView
|
|
23
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AmountPolicyView, BillingProfileView, CancellationReceipt, CheckoutLimitView, DeclarationReceipt,
|
|
3
|
+
PerformanceConsentResponse, PerformanceConsentView, PurchaseList, PurchaseView, SubscriptionStartResponse,
|
|
4
|
+
WithdrawalCandidate, WithdrawalCandidateList, WithdrawalReceipt,
|
|
5
|
+
} from '../types.js'
|
|
6
|
+
import type { Dated } from './types.local.js'
|
|
7
|
+
import type { ConsumerReviveHelper } from './revive/types.js'
|
|
8
|
+
|
|
9
|
+
// The wire carries every date as an ISO string (see `model/consumer.ts`). Each reviver copies the
|
|
10
|
+
// value and turns the named fields back into `Date`s; a value that already holds dates is copied
|
|
11
|
+
// as is, so reviving twice is harmless.
|
|
12
|
+
|
|
13
|
+
export const createConsumerReviveHelper = (): ConsumerReviveHelper => {
|
|
14
|
+
const revive = <T>(value: T, fields: readonly string[]): T => {
|
|
15
|
+
const copy: Dated = { ...(value as Dated) }
|
|
16
|
+
for (const field of fields) {
|
|
17
|
+
const raw = copy[field]
|
|
18
|
+
if (raw != null && !(raw instanceof Date)) {
|
|
19
|
+
copy[field] = new Date(raw as string)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return copy as T
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const revivePurchase = (purchase: PurchaseView): PurchaseView =>
|
|
27
|
+
revive(purchase, ['purchasedAt', 'deadline', 'consentedAt', 'withdrawnAt'])
|
|
28
|
+
|
|
29
|
+
const revivePurchaseList = (list: PurchaseList): PurchaseList =>
|
|
30
|
+
({ ...list, purchases: list.purchases.map(revivePurchase) })
|
|
31
|
+
|
|
32
|
+
const reviveBillingProfile = (profile: BillingProfileView): BillingProfileView =>
|
|
33
|
+
revive(profile, ['lockedAt'])
|
|
34
|
+
|
|
35
|
+
const reviveConsentView = (view: PerformanceConsentView): PerformanceConsentView => ({
|
|
36
|
+
...revive(view, ['deadline', 'at']),
|
|
37
|
+
purchases: view.purchases.map(revivePurchase),
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const reviveConsentResponse = (response: PerformanceConsentResponse): PerformanceConsentResponse =>
|
|
41
|
+
revive(response, ['consentedAt'])
|
|
42
|
+
|
|
43
|
+
const reviveStartResponse = (response: SubscriptionStartResponse): SubscriptionStartResponse =>
|
|
44
|
+
revive(response, ['requestedAt', 'expiresAt'])
|
|
45
|
+
|
|
46
|
+
const reviveCandidate = (candidate: WithdrawalCandidate): WithdrawalCandidate =>
|
|
47
|
+
revive(candidate, ['purchasedAt', 'deadline'])
|
|
48
|
+
|
|
49
|
+
const reviveWithdrawalList = (list: WithdrawalCandidateList): WithdrawalCandidateList =>
|
|
50
|
+
({ ...list, candidates: list.candidates.map(reviveCandidate) })
|
|
51
|
+
|
|
52
|
+
const reviveReceipt = <T extends DeclarationReceipt | WithdrawalReceipt | CancellationReceipt>(receipt: T): T =>
|
|
53
|
+
revive(receipt, ['receivedAt', 'effectiveAt'])
|
|
54
|
+
|
|
55
|
+
const reviveCheckoutLimit = (limit: CheckoutLimitView): CheckoutLimitView =>
|
|
56
|
+
revive(limit, ['resetsAt'])
|
|
57
|
+
|
|
58
|
+
const reviveAmountPolicyView = (view: AmountPolicyView): AmountPolicyView =>
|
|
59
|
+
({ ...view, limit: view.limit == null ? null : reviveCheckoutLimit(view.limit) })
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
revivePurchase, revivePurchaseList, reviveBillingProfile, reviveConsentView, reviveConsentResponse,
|
|
63
|
+
reviveStartResponse, reviveWithdrawalList, reviveReceipt, reviveCheckoutLimit, reviveAmountPolicyView,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const consumerReviveHelper = createConsumerReviveHelper()
|
|
68
|
+
|
|
69
|
+
/** @deprecated compat:factory-refactor — use `consumerReviveHelper.revivePurchaseList(…)` */
|
|
70
|
+
export const revivePurchaseList = (list: PurchaseList): PurchaseList => consumerReviveHelper.revivePurchaseList(list)
|
|
71
|
+
|
|
72
|
+
/** @deprecated compat:factory-refactor — use `consumerReviveHelper.reviveBillingProfile(…)` */
|
|
73
|
+
export const reviveBillingProfile = (profile: BillingProfileView): BillingProfileView =>
|
|
74
|
+
consumerReviveHelper.reviveBillingProfile(profile)
|
|
75
|
+
|
|
76
|
+
/** @deprecated compat:factory-refactor — use `consumerReviveHelper.reviveConsentView(…)` */
|
|
77
|
+
export const reviveConsentView = (view: PerformanceConsentView): PerformanceConsentView =>
|
|
78
|
+
consumerReviveHelper.reviveConsentView(view)
|
|
79
|
+
|
|
80
|
+
/** @deprecated compat:factory-refactor — use `consumerReviveHelper.reviveAmountPolicyView(…)` */
|
|
81
|
+
export const reviveAmountPolicyView = (view: AmountPolicyView): AmountPolicyView =>
|
|
82
|
+
consumerReviveHelper.reviveAmountPolicyView(view)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ConsumerRightsPolicy } from '../types.js'
|
|
2
|
+
import type { ConsumerRightsPublicProtocols, ConsumerRightsScreens } from './types.js'
|
|
3
|
+
|
|
4
|
+
export interface DeadlinePolicy extends Pick<ConsumerRightsPolicy, 'withdrawalDays' | 'deadline'> {}
|
|
5
|
+
|
|
6
|
+
export type PublicOf<P> = P extends { screens: object }
|
|
7
|
+
? ConsumerRightsPublicProtocols & { screens: ConsumerRightsScreens }
|
|
8
|
+
: ConsumerRightsPublicProtocols
|
|
9
|
+
|
|
10
|
+
export type Dated = Record<string, unknown>
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import type { ConsumerRightsMechanisms, ConsumerRightsPolicy, BillingProfileView, CancellationBody, CancellationReceipt, ConsumerRightsPublicView, DeclarationReceipt, PerformanceConsentBody, PerformanceConsentResponse, PerformanceConsentView, PurchaseList, SubscriptionStartBody, SubscriptionStartQuery, SubscriptionStartResponse, SubscriptionStartView, WithdrawalBody, WithdrawalCandidateList, WithdrawalReceipt, PlanWithdrawalComponent } from '../types.js'
|
|
2
|
+
import type { RouteParent } from '@owlmeans/route'
|
|
3
|
+
import type { EntrypointOptions, EntrypointProtocol, OpenRequest, OpenValue } from '@owlmeans/entrypoint'
|
|
4
|
+
import type { PublicOf } from './types.local.js'
|
|
5
|
+
|
|
6
|
+
export type BillingInterval = 'month' | 'year'
|
|
7
|
+
|
|
8
|
+
/** A copy bundle: nested keys, string leaves. */
|
|
9
|
+
export interface CopyTree {
|
|
10
|
+
[key: string]: string | CopyTree
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type CopyValues = Record<string, string | number>
|
|
14
|
+
|
|
15
|
+
export interface LegalLabels {
|
|
16
|
+
withdrawal: { function: string, confirm: string }
|
|
17
|
+
cancellation: { function: string, confirm: string }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ConsentStatement {
|
|
21
|
+
request: string
|
|
22
|
+
acknowledgement: string
|
|
23
|
+
/** The checkbox text: the request and the acknowledgement together. */
|
|
24
|
+
checkbox: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WithdrawalDeadlineRule {
|
|
28
|
+
/** Default 14. */
|
|
29
|
+
days?: number
|
|
30
|
+
/** A period ending on a Saturday or Sunday ends on the Monday after. Default on. */
|
|
31
|
+
weekendRollover?: boolean
|
|
32
|
+
/** Whole days added after the rollover, covering public holidays. Default 5. */
|
|
33
|
+
marginDays?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** What decides whether a purchase can still be withdrawn from. */
|
|
37
|
+
export interface WithdrawalWindow {
|
|
38
|
+
deadline?: Date | null
|
|
39
|
+
withdrawnAt?: Date | null
|
|
40
|
+
refundedAt?: Date | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Units granted at one instant — a purchase's credits. */
|
|
44
|
+
export interface FifoLot {
|
|
45
|
+
id: string
|
|
46
|
+
units: number
|
|
47
|
+
at: Date
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Units spent at one instant. */
|
|
51
|
+
export interface FifoSpend {
|
|
52
|
+
units: number
|
|
53
|
+
at: Date
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One lot after allocation, with every slice of it a spend took. */
|
|
57
|
+
export interface FifoLotUsage {
|
|
58
|
+
id: string
|
|
59
|
+
at: Date
|
|
60
|
+
granted: number
|
|
61
|
+
used: number
|
|
62
|
+
slices: Array<{ at: Date, units: number }>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface FifoAllocation {
|
|
66
|
+
lots: FifoLotUsage[]
|
|
67
|
+
/** Spent units no lot granted at or before the spend could cover. */
|
|
68
|
+
unallocated: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Kept as a type: its parts redeclare the same keys with narrower types, which interface `extends` refuses.
|
|
72
|
+
/** What an application must state itself; everything else defaults. */
|
|
73
|
+
export type ConsumerRightsDeclaration = Partial<Omit<ConsumerRightsPolicy, 'mechanisms' | 'deadline'>>
|
|
74
|
+
& Pick<ConsumerRightsPolicy, 'textVersion' | 'links'>
|
|
75
|
+
& { mechanisms?: Partial<ConsumerRightsMechanisms>, deadline?: Partial<ConsumerRightsPolicy['deadline']> }
|
|
76
|
+
|
|
77
|
+
export interface ConsumerRightsPublicOptions {
|
|
78
|
+
/** Default `/public/consumer-rights`. */
|
|
79
|
+
path?: string
|
|
80
|
+
/** An UNGUARDED parent only — whatever it guards, this subtree inherits. */
|
|
81
|
+
parent?: RouteParent
|
|
82
|
+
/**
|
|
83
|
+
* Declare the two public frontend screens (sticky, like every legal page): the withdrawal
|
|
84
|
+
* function and the cancellation page. Both or neither.
|
|
85
|
+
*/
|
|
86
|
+
screens?: { withdrawal?: string, cancellation?: string, parent?: RouteParent }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ConsumerRightsProtocolOptions {
|
|
90
|
+
/** Alias prefix of every declaration, e.g. `my-app:account:consumer`. */
|
|
91
|
+
prefix: string
|
|
92
|
+
/** The application's guarded parent (its account base) — its guards and gate are inherited. */
|
|
93
|
+
parent?: RouteParent
|
|
94
|
+
/** Guards of a tree mounted without a parent. */
|
|
95
|
+
guards?: EntrypointOptions['guards']
|
|
96
|
+
gate?: EntrypointOptions['gate']
|
|
97
|
+
/** Default `/consumer-rights`. */
|
|
98
|
+
path?: string
|
|
99
|
+
/** The unguarded public subtree; absent or `false` declares none. */
|
|
100
|
+
public?: false | ConsumerRightsPublicOptions
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The guarded account subtree: one entity's own purchases, consents and declarations. */
|
|
104
|
+
export interface ConsumerRightsAccountProtocols {
|
|
105
|
+
base: EntrypointProtocol<OpenRequest, OpenValue>
|
|
106
|
+
profile: EntrypointProtocol<{}, BillingProfileView>
|
|
107
|
+
purchases: EntrypointProtocol<{}, PurchaseList>
|
|
108
|
+
consent: EntrypointProtocol<{}, PerformanceConsentView>
|
|
109
|
+
giveConsent: EntrypointProtocol<{ body: PerformanceConsentBody }, PerformanceConsentResponse>
|
|
110
|
+
start: EntrypointProtocol<{ query: SubscriptionStartQuery }, SubscriptionStartView>
|
|
111
|
+
requestStart: EntrypointProtocol<{ body: SubscriptionStartBody }, SubscriptionStartResponse>
|
|
112
|
+
withdrawals: EntrypointProtocol<{}, WithdrawalCandidateList>
|
|
113
|
+
withdraw: EntrypointProtocol<{ body: WithdrawalBody }, WithdrawalReceipt>
|
|
114
|
+
cancel: EntrypointProtocol<{ body: CancellationBody }, CancellationReceipt>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface ConsumerRightsScreens {
|
|
118
|
+
withdrawal: EntrypointProtocol<OpenRequest, OpenValue>
|
|
119
|
+
cancellation: EntrypointProtocol<OpenRequest, OpenValue>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The unguarded public subtree: reachable WITHOUT a login (§ 312k BGB). Its receipts carry only
|
|
124
|
+
* what was declared and when — never whether a contract matched.
|
|
125
|
+
*/
|
|
126
|
+
export interface ConsumerRightsPublicProtocols {
|
|
127
|
+
base: EntrypointProtocol<OpenRequest, OpenValue>
|
|
128
|
+
policy: EntrypointProtocol<{}, ConsumerRightsPublicView>
|
|
129
|
+
withdraw: EntrypointProtocol<{ body: WithdrawalBody }, DeclarationReceipt>
|
|
130
|
+
cancel: EntrypointProtocol<{ body: CancellationBody }, DeclarationReceipt>
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The tree for given options — `public` (and its `screens`) present only when declared. */
|
|
134
|
+
export type ConsumerRightsProtocols<O extends ConsumerRightsProtocolOptions = ConsumerRightsProtocolOptions> =
|
|
135
|
+
O['public'] extends ConsumerRightsPublicOptions
|
|
136
|
+
? ConsumerRightsAccountProtocols & { public: PublicOf<O['public']> }
|
|
137
|
+
: ConsumerRightsAccountProtocols
|
|
138
|
+
|
|
139
|
+
export interface OneTimeRefundInput {
|
|
140
|
+
paidMinor: number
|
|
141
|
+
/** Already refunded (an earlier partial refund); the refund never exceeds the rest. */
|
|
142
|
+
refundedMinor?: number
|
|
143
|
+
unitsGranted: number
|
|
144
|
+
/** Units used AFTER consent — before it, the consumer bears no cost. Clamped to `0..granted`. */
|
|
145
|
+
unitsUsed: number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface OneTimeRefund {
|
|
149
|
+
refundMinor: number
|
|
150
|
+
/** The share of the purchase that is unused: `(granted − used) / granted`. */
|
|
151
|
+
netRatio: number
|
|
152
|
+
/** The unused units a withdrawal takes back. */
|
|
153
|
+
unitsReturned: number
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface SubscriptionRefundInput {
|
|
157
|
+
paidMinor: number
|
|
158
|
+
refundedMinor?: number
|
|
159
|
+
/** The price net of tax — the base the components split and the deductions come from. */
|
|
160
|
+
netMinor: number
|
|
161
|
+
/** Absent or empty: the whole price is one `time` component. */
|
|
162
|
+
components?: PlanWithdrawalComponent[]
|
|
163
|
+
periodStart: Date
|
|
164
|
+
periodEnd: Date
|
|
165
|
+
/** When the consumer expressly requested the services. None: no time deduction at all. */
|
|
166
|
+
servicesRequestedAt?: Date | null
|
|
167
|
+
withdrawnAt: Date
|
|
168
|
+
/** Units granted with the purchase and used after consent — applied to every `units` component. */
|
|
169
|
+
units?: { granted: number, used: number } | null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface SubscriptionRefundComponent extends PlanWithdrawalComponent {
|
|
173
|
+
/** The component's part of `netMinor`. */
|
|
174
|
+
amountMinor: number
|
|
175
|
+
deductionMinor: number
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface SubscriptionRefund {
|
|
179
|
+
/** Gross (tax-inclusive) refund, capped at what is still unrefunded. */
|
|
180
|
+
refundMinor: number
|
|
181
|
+
refundNetMinor: number
|
|
182
|
+
timeDeductionMinor: number
|
|
183
|
+
unitsDeductionMinor: number
|
|
184
|
+
elapsedDays: number
|
|
185
|
+
periodDays: number
|
|
186
|
+
unitsUsed?: number
|
|
187
|
+
unitsGranted?: number
|
|
188
|
+
components: SubscriptionRefundComponent[]
|
|
189
|
+
}
|
package/src/countries.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { COUNTRY_CURRENCIES } from './consts.js'
|
|
2
|
+
|
|
3
|
+
/** The lowercase ISO 4217 currency of a country, or `null` when this map has no entry for it. */
|
|
4
|
+
export const currencyOfCountry = (country: string): string | null =>
|
|
5
|
+
COUNTRY_CURRENCIES[country.toUpperCase()] ?? null
|
package/src/entitled.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EntrypointOptions } from '@owlmeans/entrypoint'
|
|
2
|
+
import { ENTITLEMENT_GATE } from './consts.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Declare that an entrypoint needs a paid capability.
|
|
6
|
+
*
|
|
7
|
+
* Compatibility sugar over the protocol option
|
|
8
|
+
* `{ gate: { alias: ENTITLEMENT_GATE, params } }` so a route reads as what it means. Several
|
|
9
|
+
* parameters are OR'd, matching every other gate in the framework.
|
|
10
|
+
*
|
|
11
|
+
* Putting the requirement HERE rather than in a handler is the point: the framework asserts a gate
|
|
12
|
+
* before the handler is entered, so the route table states what a feature costs and no new
|
|
13
|
+
* endpoint can forget to check.
|
|
14
|
+
*/
|
|
15
|
+
export const entitled = (
|
|
16
|
+
params: string | string[], opts?: EntrypointOptions,
|
|
17
|
+
): EntrypointOptions => ({
|
|
18
|
+
...opts,
|
|
19
|
+
gate: { alias: ENTITLEMENT_GATE, params },
|
|
20
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { PermissionSet } from '@owlmeans/auth'
|
|
2
|
+
import type { EntitlementParam } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/** The entitlement parameter grammar and the capability predicate the server gate and the browser share. */
|
|
5
|
+
export interface EntitlementParamHelper {
|
|
6
|
+
/**
|
|
7
|
+
* Grammar: `[<scope>:]<permission>[>=<n>]`.
|
|
8
|
+
*
|
|
9
|
+
* `feature:branding--whitelabel` · `renewable:credits>=100` · `production--standalone`
|
|
10
|
+
*
|
|
11
|
+
* `@` is deliberately not part of it: that is `@owlmeans/iam`'s resource-selector syntax, and a
|
|
12
|
+
* grammar that reused it would make two different things look the same in a route declaration.
|
|
13
|
+
*/
|
|
14
|
+
parseEntitlementParam: (param: string) => EntitlementParam
|
|
15
|
+
formatEntitlementParam: (parsed: EntitlementParam) => string
|
|
16
|
+
/**
|
|
17
|
+
* Whether a union of capability sets satisfies one requirement.
|
|
18
|
+
*
|
|
19
|
+
* Pure, and shared by the server gate and the browser: the UI has to know whether to render a
|
|
20
|
+
* control disabled, and a second implementation of the rule for the client would eventually
|
|
21
|
+
* disagree with the one that actually refuses.
|
|
22
|
+
*
|
|
23
|
+
* A malformed parameter answers `false` rather than throwing. A gate that crashed on a typo would
|
|
24
|
+
* take down the endpoint it guards, which is a strictly worse failure than refusing it.
|
|
25
|
+
*/
|
|
26
|
+
hasEntitlement: (capabilities: PermissionSet[] | undefined, param: string) => boolean
|
|
27
|
+
/** Everything a union grants, formatted back into parameters. The wire shape of a client read. */
|
|
28
|
+
entitlementList: (capabilities: PermissionSet[] | undefined) => string[]
|
|
29
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { PermissionSet } from '@owlmeans/auth'
|
|
2
|
+
import { CAPABILITY_LIMIT_SCOPE } from './consts.js'
|
|
3
|
+
import type { EntitlementParam } from './types.js'
|
|
4
|
+
import type { EntitlementParamHelper } from './entitlement/types.js'
|
|
5
|
+
|
|
6
|
+
export const createEntitlementParamHelper = (): EntitlementParamHelper => {
|
|
7
|
+
const parseEntitlementParam = (param: string): EntitlementParam => {
|
|
8
|
+
const [scoped, floor] = param.split('>=')
|
|
9
|
+
const colon = scoped.indexOf(':')
|
|
10
|
+
const parsed: EntitlementParam = colon >= 0
|
|
11
|
+
? { scope: scoped.slice(0, colon), permission: scoped.slice(colon + 1) }
|
|
12
|
+
: { permission: scoped }
|
|
13
|
+
|
|
14
|
+
if (floor != null) {
|
|
15
|
+
const value = Number.parseFloat(floor)
|
|
16
|
+
if (!Number.isNaN(value)) {
|
|
17
|
+
parsed.atLeast = value
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return parsed
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const formatEntitlementParam = (parsed: EntitlementParam): string =>
|
|
25
|
+
`${parsed.scope != null ? `${parsed.scope}:` : ''}${parsed.permission}` +
|
|
26
|
+
`${parsed.atLeast != null ? `>=${parsed.atLeast}` : ''}`
|
|
27
|
+
|
|
28
|
+
const hasEntitlement = (
|
|
29
|
+
capabilities: PermissionSet[] | undefined, param: string
|
|
30
|
+
): boolean => {
|
|
31
|
+
if (capabilities == null || capabilities.length < 1) {
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
if (typeof param !== 'string') {
|
|
35
|
+
return false
|
|
36
|
+
}
|
|
37
|
+
const { scope, permission, atLeast } = parseEntitlementParam(param)
|
|
38
|
+
// A limit is never a capability: its room is counted, not granted.
|
|
39
|
+
if (permission === '' || scope === CAPABILITY_LIMIT_SCOPE) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return capabilities.some(set => {
|
|
44
|
+
if (scope != null && set.scope !== scope) {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
const held = set.permissions?.[permission]
|
|
48
|
+
if (held == null || held === false) {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A floor is a numeric question: a boolean flag, however true, does not answer it.
|
|
53
|
+
return atLeast != null ? typeof held === 'number' && held >= atLeast : true
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const entitlementList = (capabilities: PermissionSet[] | undefined): string[] => {
|
|
58
|
+
const list: string[] = []
|
|
59
|
+
for (const set of capabilities ?? []) {
|
|
60
|
+
for (const [permission, value] of Object.entries(set.permissions ?? {})) {
|
|
61
|
+
if (value == null || value === false) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
list.push(formatEntitlementParam({ scope: set.scope, permission }))
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return [...new Set(list)]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { parseEntitlementParam, formatEntitlementParam, hasEntitlement, entitlementList }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export const entitlementParamHelper = createEntitlementParamHelper()
|
|
75
|
+
|
|
76
|
+
/** @deprecated compat:factory-refactor — use `entitlementParamHelper.parseEntitlementParam(…)` */
|
|
77
|
+
export const parseEntitlementParam = (param: string): EntitlementParam =>
|
|
78
|
+
entitlementParamHelper.parseEntitlementParam(param)
|
|
79
|
+
|
|
80
|
+
/** @deprecated compat:factory-refactor — use `entitlementParamHelper.formatEntitlementParam(…)` */
|
|
81
|
+
export const formatEntitlementParam = (parsed: EntitlementParam): string =>
|
|
82
|
+
entitlementParamHelper.formatEntitlementParam(parsed)
|
|
83
|
+
|
|
84
|
+
/** @deprecated compat:factory-refactor — use `entitlementParamHelper.hasEntitlement(…)` */
|
|
85
|
+
export const hasEntitlement = (capabilities: PermissionSet[] | undefined, param: string): boolean =>
|
|
86
|
+
entitlementParamHelper.hasEntitlement(capabilities, param)
|