@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,121 @@
|
|
|
1
|
+
import { LIB_NAMESPACE, resolveI18nResource } from '@owlmeans/i18n'
|
|
2
|
+
import '../i18n.js'
|
|
3
|
+
import { ConsentKind, CONSUMER_RIGHTS_RESOURCE } from '../consts.js'
|
|
4
|
+
import { ConsumerRightsError } from '../errors.js'
|
|
5
|
+
import type { ProductPlan } from '../types.js'
|
|
6
|
+
import { consumerRightsPolicyHelper } from './policy.js'
|
|
7
|
+
import type { ConsentStatement, CopyTree, CopyValues, LegalLabels } from './types.js'
|
|
8
|
+
import type { ConsumerCopyHelper } from './copy/types.js'
|
|
9
|
+
|
|
10
|
+
const isTree = (value: unknown): value is CopyTree =>
|
|
11
|
+
value != null && typeof value === 'object' && !Array.isArray(value)
|
|
12
|
+
|
|
13
|
+
export const createConsumerCopyHelper = (): ConsumerCopyHelper => {
|
|
14
|
+
const merge = (base: CopyTree, over: Record<string, unknown>): CopyTree => {
|
|
15
|
+
const result: CopyTree = { ...base }
|
|
16
|
+
for (const [key, value] of Object.entries(over)) {
|
|
17
|
+
const current = result[key]
|
|
18
|
+
if (isTree(value)) {
|
|
19
|
+
result[key] = merge(isTree(current) ? current : {}, value)
|
|
20
|
+
} else if (typeof value === 'string') {
|
|
21
|
+
result[key] = value
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const resolve = (lng: string): Record<string, unknown> | null =>
|
|
29
|
+
resolveI18nResource(lng, CONSUMER_RIGHTS_RESOURCE, LIB_NAMESPACE)
|
|
30
|
+
|
|
31
|
+
const consumerRightsCopy = (lng: string): CopyTree => {
|
|
32
|
+
const base = consumerRightsPolicyHelper.baseLanguageOf(lng)
|
|
33
|
+
let copy = merge({}, resolve('en') ?? {})
|
|
34
|
+
if (base !== '' && base !== 'en') {
|
|
35
|
+
copy = merge(copy, resolve(base) ?? {})
|
|
36
|
+
}
|
|
37
|
+
if (lng !== base && lng.trim() !== '') {
|
|
38
|
+
copy = merge(copy, resolve(lng) ?? {})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return copy
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const textAt = (copy: CopyTree, path: string): string | undefined => {
|
|
45
|
+
const value = path.split('.').reduce<unknown>((node, key) => isTree(node) ? node[key] : undefined, copy)
|
|
46
|
+
|
|
47
|
+
return typeof value === 'string' ? value : undefined
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const consumerText = (lng: string, path: string, vars: CopyValues = {}, context?: string): string => {
|
|
51
|
+
const copy = consumerRightsCopy(lng)
|
|
52
|
+
const variant = context != null && context !== '' ? `${path}_${context}` : null
|
|
53
|
+
const key = variant != null && textAt(copy, variant) != null ? variant : path
|
|
54
|
+
const value = textAt(copy, key)
|
|
55
|
+
if (value == null) {
|
|
56
|
+
throw new ConsumerRightsError(`copy:${path}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return value.replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, name: string) => {
|
|
60
|
+
const filled = vars[name]
|
|
61
|
+
if (filled == null) {
|
|
62
|
+
throw new ConsumerRightsError(`copy:${key}:${name}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return String(filled)
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const placeholdersOf = (text: string): string[] =>
|
|
70
|
+
[...new Set([...text.matchAll(/\{\{\s*([\w-]+)\s*\}\}/g)].map(match => match[1]))]
|
|
71
|
+
|
|
72
|
+
const legalLabelsOf = (lng: string): LegalLabels => ({
|
|
73
|
+
withdrawal: {
|
|
74
|
+
function: consumerText(lng, 'withdrawal.function'),
|
|
75
|
+
confirm: consumerText(lng, 'withdrawal.confirm'),
|
|
76
|
+
},
|
|
77
|
+
cancellation: {
|
|
78
|
+
function: consumerText(lng, 'cancellation.function'),
|
|
79
|
+
confirm: consumerText(lng, 'cancellation.confirm'),
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const startContextOf = (plan: Pick<ProductPlan, 'withdrawal'> | null | undefined): 'units' | undefined =>
|
|
84
|
+
plan?.withdrawal?.components?.some(component => component.basis === 'units') === true ? 'units' : undefined
|
|
85
|
+
|
|
86
|
+
const consentStatementOf = (
|
|
87
|
+
lng: string, kind: ConsentKind, vars: { trader: string, plan?: string, context?: string },
|
|
88
|
+
): ConsentStatement => {
|
|
89
|
+
const branch = kind === ConsentKind.SubscriptionStart ? 'subscription-start' : 'performance-consent'
|
|
90
|
+
const values: CopyValues = { trader: vars.trader, ...(vars.plan != null ? { plan: vars.plan } : {}) }
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
request: consumerText(lng, `${branch}.request`, values, vars.context),
|
|
94
|
+
acknowledgement: consumerText(lng, `${branch}.acknowledgement`, values, vars.context),
|
|
95
|
+
checkbox: consumerText(lng, `${branch}.checkbox`, values, vars.context),
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { consumerRightsCopy, consumerText, placeholdersOf, legalLabelsOf, startContextOf, consentStatementOf }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const consumerCopyHelper = createConsumerCopyHelper()
|
|
103
|
+
|
|
104
|
+
/** @deprecated compat:factory-refactor — use `consumerCopyHelper.consumerRightsCopy(…)` */
|
|
105
|
+
export const consumerRightsCopy = (lng: string): CopyTree => consumerCopyHelper.consumerRightsCopy(lng)
|
|
106
|
+
|
|
107
|
+
/** @deprecated compat:factory-refactor — use `consumerCopyHelper.consumerText(…)` */
|
|
108
|
+
export const consumerText = (lng: string, path: string, vars?: CopyValues, context?: string): string =>
|
|
109
|
+
consumerCopyHelper.consumerText(lng, path, vars, context)
|
|
110
|
+
|
|
111
|
+
/** @deprecated compat:factory-refactor — use `consumerCopyHelper.legalLabelsOf(…)` */
|
|
112
|
+
export const legalLabelsOf = (lng: string): LegalLabels => consumerCopyHelper.legalLabelsOf(lng)
|
|
113
|
+
|
|
114
|
+
/** @deprecated compat:factory-refactor — use `consumerCopyHelper.startContextOf(…)` */
|
|
115
|
+
export const startContextOf = (plan: Pick<ProductPlan, 'withdrawal'> | null | undefined): 'units' | undefined =>
|
|
116
|
+
consumerCopyHelper.startContextOf(plan)
|
|
117
|
+
|
|
118
|
+
/** @deprecated compat:factory-refactor — use `consumerCopyHelper.consentStatementOf(…)` */
|
|
119
|
+
export const consentStatementOf = (
|
|
120
|
+
lng: string, kind: ConsentKind, vars: { trader: string, plan?: string, context?: string },
|
|
121
|
+
): ConsentStatement => consumerCopyHelper.consentStatementOf(lng, kind, vars)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DeadlinePolicy } from '../types.local.js'
|
|
2
|
+
import type { WithdrawalDeadlineRule, WithdrawalWindow } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/** The withdrawal period: its exclusive end, its last day for display, and whether it is open. */
|
|
5
|
+
export interface WithdrawalDeadlineHelper {
|
|
6
|
+
/**
|
|
7
|
+
* The EXCLUSIVE end of a withdrawal period — the first instant at which it is over.
|
|
8
|
+
*
|
|
9
|
+
* The period starts the day after the purchase (the purchase day is not counted) and lasts `days`
|
|
10
|
+
* UTC calendar days; a last day on a Saturday or Sunday moves to the Monday after; `marginDays`
|
|
11
|
+
* are then added for public holidays and time zones; the result is the start of the next UTC day.
|
|
12
|
+
* Wed 2026-09-23 → 2026-10-13T00:00Z, Sat 2026-09-26 → 2026-10-18T00:00Z, Sun 2026-12-20 →
|
|
13
|
+
* 2027-01-10T00:00Z (14 days, margin 5). Shown to a person as its last included day
|
|
14
|
+
* (`lastWithdrawalDayOf`), never as the exclusive instant.
|
|
15
|
+
*
|
|
16
|
+
* @throws ConsumerRightsError (`deadline`) for an invalid date or rule.
|
|
17
|
+
*/
|
|
18
|
+
withdrawalDeadlineOf: (purchasedAt: Date, rule?: WithdrawalDeadlineRule | DeadlinePolicy) => Date
|
|
19
|
+
/**
|
|
20
|
+
* The last day a withdrawal is still in time, for display: the UTC calendar day of `deadline − 1 ms`
|
|
21
|
+
* (a deadline is the EXCLUSIVE first instant after the period). 2026-10-13T00:00Z → 2026-10-12;
|
|
22
|
+
* show it as a UTC date, "until the end of 12 October 2026".
|
|
23
|
+
*/
|
|
24
|
+
lastWithdrawalDayOf: (deadline: Date) => Date
|
|
25
|
+
/**
|
|
26
|
+
* Whether a withdrawal window is open at `at`: a deadline exists, `at` is before it, and the
|
|
27
|
+
* purchase was neither withdrawn from nor refunded. A bare deadline is accepted as well.
|
|
28
|
+
*/
|
|
29
|
+
withdrawalOpen: (window: WithdrawalWindow | Date | null | undefined, at?: Date) => boolean
|
|
30
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ConsumerRightsError } from '../errors.js'
|
|
2
|
+
import { DAY_MS } from './consts.js'
|
|
3
|
+
import type { DeadlinePolicy } from './types.local.js'
|
|
4
|
+
import type { WithdrawalDeadlineRule, WithdrawalWindow } from './types.js'
|
|
5
|
+
import type { WithdrawalDeadlineHelper } from './deadline/types.js'
|
|
6
|
+
|
|
7
|
+
export const createWithdrawalDeadlineHelper = (): WithdrawalDeadlineHelper => {
|
|
8
|
+
const ruleOf = (rule?: WithdrawalDeadlineRule | DeadlinePolicy): Required<WithdrawalDeadlineRule> => {
|
|
9
|
+
if (rule != null && 'withdrawalDays' in rule) {
|
|
10
|
+
return { days: rule.withdrawalDays, weekendRollover: rule.deadline.weekendRollover, marginDays: rule.deadline.marginDays }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return { days: rule?.days ?? 14, weekendRollover: rule?.weekendRollover ?? true, marginDays: rule?.marginDays ?? 5 }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const withdrawalDeadlineOf = (purchasedAt: Date, rule?: WithdrawalDeadlineRule | DeadlinePolicy): Date => {
|
|
17
|
+
const { days, weekendRollover, marginDays } = ruleOf(rule)
|
|
18
|
+
if (Number.isNaN(purchasedAt.getTime()) || !Number.isSafeInteger(days) || days < 0
|
|
19
|
+
|| !Number.isSafeInteger(marginDays) || marginDays < 0) {
|
|
20
|
+
throw new ConsumerRightsError('deadline')
|
|
21
|
+
}
|
|
22
|
+
const purchaseDay = Date.UTC(purchasedAt.getUTCFullYear(), purchasedAt.getUTCMonth(), purchasedAt.getUTCDate())
|
|
23
|
+
let lastDay = purchaseDay + days * DAY_MS
|
|
24
|
+
if (weekendRollover) {
|
|
25
|
+
const weekday = new Date(lastDay).getUTCDay()
|
|
26
|
+
lastDay += weekday === 6 ? 2 * DAY_MS : weekday === 0 ? DAY_MS : 0
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return new Date(lastDay + (marginDays + 1) * DAY_MS)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const lastWithdrawalDayOf = (deadline: Date): Date => {
|
|
33
|
+
const last = new Date(deadline.getTime() - 1)
|
|
34
|
+
|
|
35
|
+
return new Date(Date.UTC(last.getUTCFullYear(), last.getUTCMonth(), last.getUTCDate()))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const withdrawalOpen = (window: WithdrawalWindow | Date | null | undefined, at: Date = new Date()): boolean => {
|
|
39
|
+
if (window == null) {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
const { deadline, withdrawnAt, refundedAt } = window instanceof Date ? { deadline: window } as WithdrawalWindow : window
|
|
43
|
+
|
|
44
|
+
return deadline != null && withdrawnAt == null && refundedAt == null && at.getTime() < deadline.getTime()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { withdrawalDeadlineOf, lastWithdrawalDayOf, withdrawalOpen }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const withdrawalDeadlineHelper = createWithdrawalDeadlineHelper()
|
|
51
|
+
|
|
52
|
+
/** @deprecated compat:factory-refactor — use `withdrawalDeadlineHelper.withdrawalDeadlineOf(…)` */
|
|
53
|
+
export const withdrawalDeadlineOf = (purchasedAt: Date, rule?: WithdrawalDeadlineRule | DeadlinePolicy): Date =>
|
|
54
|
+
withdrawalDeadlineHelper.withdrawalDeadlineOf(purchasedAt, rule)
|
|
55
|
+
|
|
56
|
+
/** @deprecated compat:factory-refactor — use `withdrawalDeadlineHelper.lastWithdrawalDayOf(…)` */
|
|
57
|
+
export const lastWithdrawalDayOf = (deadline: Date): Date => withdrawalDeadlineHelper.lastWithdrawalDayOf(deadline)
|
|
58
|
+
|
|
59
|
+
/** @deprecated compat:factory-refactor — use `withdrawalDeadlineHelper.withdrawalOpen(…)` */
|
|
60
|
+
export const withdrawalOpen = (window: WithdrawalWindow | Date | null | undefined, at?: Date): boolean =>
|
|
61
|
+
withdrawalDeadlineHelper.withdrawalOpen(window, at)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FifoAllocation, FifoLot, FifoLotUsage, FifoSpend } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/** First-in-first-out allocation of spends over granted lots. */
|
|
4
|
+
export interface FifoHelper {
|
|
5
|
+
/**
|
|
6
|
+
* First in, first out: every spend, in time order, takes from the oldest lot granted AT OR BEFORE
|
|
7
|
+
* it that still has units, then the next one; what no such lot covers is `unallocated` (a lot
|
|
8
|
+
* granted later never pays for an earlier spend). Lots come back in grant order. Pure — the input
|
|
9
|
+
* arrays are not reordered.
|
|
10
|
+
*/
|
|
11
|
+
allocateFifo: (lots: FifoLot[], spends: FifoSpend[]) => FifoAllocation
|
|
12
|
+
/**
|
|
13
|
+
* The units of a lot used strictly AFTER an instant — its consent. Without one (`null`), nothing
|
|
14
|
+
* is deductible: before consent the consumer bears no cost.
|
|
15
|
+
*/
|
|
16
|
+
unitsUsedAfter: (lot: Pick<FifoLotUsage, 'slices'>, after: Date | null | undefined) => number
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { FifoAllocation, FifoLot, FifoLotUsage, FifoSpend } from './types.js'
|
|
2
|
+
import type { FifoHelper } from './fifo/types.js'
|
|
3
|
+
|
|
4
|
+
export const createFifoHelper = (): FifoHelper => {
|
|
5
|
+
const allocateFifo = (lots: FifoLot[], spends: FifoSpend[]): FifoAllocation => {
|
|
6
|
+
const usage: FifoLotUsage[] = [...lots]
|
|
7
|
+
.map((lot, index) => ({ lot, index }))
|
|
8
|
+
.sort((a, b) => a.lot.at.getTime() - b.lot.at.getTime() || a.index - b.index)
|
|
9
|
+
.map(({ lot }) => ({ id: lot.id, at: lot.at, granted: Math.max(0, lot.units), used: 0, slices: [] }))
|
|
10
|
+
const ordered = [...spends]
|
|
11
|
+
.map((spend, index) => ({ spend, index }))
|
|
12
|
+
.sort((a, b) => a.spend.at.getTime() - b.spend.at.getTime() || a.index - b.index)
|
|
13
|
+
|
|
14
|
+
let unallocated = 0
|
|
15
|
+
for (const { spend } of ordered) {
|
|
16
|
+
let left = Math.max(0, spend.units)
|
|
17
|
+
for (const lot of usage) {
|
|
18
|
+
if (left <= 0 || lot.at.getTime() > spend.at.getTime()) {
|
|
19
|
+
break
|
|
20
|
+
}
|
|
21
|
+
const take = Math.min(left, lot.granted - lot.used)
|
|
22
|
+
if (take > 0) {
|
|
23
|
+
lot.used += take
|
|
24
|
+
lot.slices.push({ at: spend.at, units: take })
|
|
25
|
+
left -= take
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
unallocated += left
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { lots: usage, unallocated }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const unitsUsedAfter = (lot: Pick<FifoLotUsage, 'slices'>, after: Date | null | undefined): number =>
|
|
35
|
+
after == null ? 0 : lot.slices
|
|
36
|
+
.filter(slice => slice.at.getTime() > after.getTime())
|
|
37
|
+
.reduce((sum, slice) => sum + slice.units, 0)
|
|
38
|
+
|
|
39
|
+
return { allocateFifo, unitsUsedAfter }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const fifoHelper = createFifoHelper()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export * from './policy.js'
|
|
2
|
+
export * from './consts.js'
|
|
3
|
+
export type * from './types.js'
|
|
4
|
+
export type * from './copy/types.js'
|
|
5
|
+
export type * from './deadline/types.js'
|
|
6
|
+
export type * from './fifo/types.js'
|
|
7
|
+
export type * from './policy/types.js'
|
|
8
|
+
export type * from './refund/types.js'
|
|
9
|
+
export type * from './revive/types.js'
|
|
10
|
+
export * from './deadline.js'
|
|
11
|
+
export * from './refund.js'
|
|
12
|
+
export * from './fifo.js'
|
|
13
|
+
export * from './cancel.js'
|
|
14
|
+
export * from './copy.js'
|
|
15
|
+
export * from './refusal.js'
|
|
16
|
+
export * from './revive.js'
|
|
17
|
+
export * from './protocols.js'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ConsumerRightsLinks, ConsumerRightsPolicy } from '../../types.js'
|
|
2
|
+
|
|
3
|
+
/** The consumer-rights policy's invariants, its languages and its links. */
|
|
4
|
+
export interface ConsumerRightsPolicyHelper {
|
|
5
|
+
/**
|
|
6
|
+
* @throws ConsumerRightsError (`policy:<field>`) when the text version is empty, a country or a
|
|
7
|
+
* currency is malformed, the period is shorter than 14 days, the margin is outside 0..7 days, the
|
|
8
|
+
* consent context is not a lowercase key, the default language has no links, a link is not an
|
|
9
|
+
* absolute https URL, or the withdrawal information is missing while the withdrawal function or the
|
|
10
|
+
* performance consent is on.
|
|
11
|
+
*/
|
|
12
|
+
assertConsumerRightsPolicy: (policy: ConsumerRightsPolicy) => ConsumerRightsPolicy
|
|
13
|
+
/** `de-DE` → `de`. */
|
|
14
|
+
baseLanguageOf: (lng: string | null | undefined) => string
|
|
15
|
+
/**
|
|
16
|
+
* The links of one language, field by field over the default language's — a language that only
|
|
17
|
+
* translates the Billing Terms still points at the default withdrawal information.
|
|
18
|
+
*/
|
|
19
|
+
linksOf: (policy: Pick<ConsumerRightsPolicy, 'links' | 'defaultLanguage'>, lng?: string | null) => ConsumerRightsLinks
|
|
20
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ConsumerRightsError } from '../errors.js'
|
|
2
|
+
import type { ConsumerRightsLinks, ConsumerRightsPolicy } from '../types.js'
|
|
3
|
+
import { CONSENT_CONTEXT, COUNTRY, CURRENCY } from './consts.local.js'
|
|
4
|
+
import { DEFAULT_CONSUMER_RIGHTS } from './consts.js'
|
|
5
|
+
import type { ConsumerRightsDeclaration } from './types.js'
|
|
6
|
+
import type { ConsumerRightsPolicyHelper } from './policy/types.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A declaration filled with `DEFAULT_CONSUMER_RIGHTS` and asserted. Only the policy's own fields are
|
|
10
|
+
* copied — anything else on `def` (a server's mail options) never reaches the advertised record.
|
|
11
|
+
*/
|
|
12
|
+
export const makeConsumerRightsPolicy = (def: ConsumerRightsDeclaration): ConsumerRightsPolicy => {
|
|
13
|
+
const defaults = DEFAULT_CONSUMER_RIGHTS
|
|
14
|
+
const present = <K extends string, V>(key: K, value: V | undefined): { [P in K]?: V } =>
|
|
15
|
+
(value == null ? {} : { [key]: value }) as { [P in K]?: V }
|
|
16
|
+
|
|
17
|
+
return consumerRightsPolicyHelper.assertConsumerRightsPolicy({
|
|
18
|
+
textVersion: def.textVersion,
|
|
19
|
+
countries: [...(def.countries ?? defaults.countries)].map(country => country.toUpperCase()),
|
|
20
|
+
unknownCountry: def.unknownCountry ?? defaults.unknownCountry,
|
|
21
|
+
withdrawalDays: def.withdrawalDays ?? defaults.withdrawalDays,
|
|
22
|
+
deadline: { ...defaults.deadline, ...def.deadline },
|
|
23
|
+
mechanisms: { ...defaults.mechanisms, ...def.mechanisms },
|
|
24
|
+
...present('currencies', def.currencies),
|
|
25
|
+
...present('languages', def.languages),
|
|
26
|
+
defaultLanguage: def.defaultLanguage ?? defaults.defaultLanguage,
|
|
27
|
+
links: def.links,
|
|
28
|
+
...present('renewalOpensWindow', def.renewalOpensWindow ?? defaults.renewalOpensWindow),
|
|
29
|
+
...present('startRequestTtlSeconds', def.startRequestTtlSeconds ?? defaults.startRequestTtlSeconds),
|
|
30
|
+
...present('exemptBusinesses', def.exemptBusinesses ?? defaults.exemptBusinesses),
|
|
31
|
+
...present('consentContext', def.consentContext),
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const createConsumerRightsPolicyHelper = (): ConsumerRightsPolicyHelper => {
|
|
36
|
+
const isHttpsUrl = (value: unknown): boolean => {
|
|
37
|
+
if (typeof value !== 'string') {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
return new URL(value).protocol === 'https:'
|
|
42
|
+
} catch {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const fail = (field: string): never => {
|
|
48
|
+
throw new ConsumerRightsError(`policy:${field}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const assertConsumerRightsPolicy = (policy: ConsumerRightsPolicy): ConsumerRightsPolicy => {
|
|
52
|
+
if (typeof policy.textVersion !== 'string' || policy.textVersion.trim() === '') fail('text-version')
|
|
53
|
+
if (policy.mechanisms == null || typeof policy.mechanisms !== 'object') fail('mechanisms')
|
|
54
|
+
if (!Array.isArray(policy.countries) || policy.countries.some(country => !COUNTRY.test(country))) fail('countries')
|
|
55
|
+
if (policy.unknownCountry !== 'protect' && policy.unknownCountry !== 'ignore') fail('unknown-country')
|
|
56
|
+
if (!Number.isSafeInteger(policy.withdrawalDays) || policy.withdrawalDays < 14) fail('withdrawal-days')
|
|
57
|
+
const margin = policy.deadline?.marginDays ?? -1
|
|
58
|
+
if (!Number.isSafeInteger(margin) || margin < 0 || margin > 7) fail('margin-days')
|
|
59
|
+
if (Object.values(policy.currencies ?? {}).some(currency => !CURRENCY.test(currency ?? ''))) fail('currencies')
|
|
60
|
+
if (policy.startRequestTtlSeconds != null
|
|
61
|
+
&& (!Number.isSafeInteger(policy.startRequestTtlSeconds) || policy.startRequestTtlSeconds < 1)) fail('start-request-ttl')
|
|
62
|
+
if (policy.consentContext != null && !CONSENT_CONTEXT.test(policy.consentContext)) fail('consent-context')
|
|
63
|
+
|
|
64
|
+
const defaults = policy.links?.[policy.defaultLanguage]
|
|
65
|
+
if (defaults == null || !isHttpsUrl(defaults.billingTerms)) fail('links')
|
|
66
|
+
for (const [lng, links] of Object.entries(policy.links)) {
|
|
67
|
+
if (Object.values(links).some(url => url != null && !isHttpsUrl(url))) fail(`links:${lng}`)
|
|
68
|
+
}
|
|
69
|
+
if ((policy.mechanisms.withdrawal || policy.mechanisms.performanceConsent) && defaults!.withdrawalInformation == null) {
|
|
70
|
+
fail('links:withdrawal-information')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return policy
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const baseLanguageOf = (lng: string | null | undefined): string =>
|
|
77
|
+
(lng ?? '').trim().split(/[-_]/)[0].toLowerCase()
|
|
78
|
+
|
|
79
|
+
const linksOf = (policy: Pick<ConsumerRightsPolicy, 'links' | 'defaultLanguage'>, lng?: string | null): ConsumerRightsLinks => {
|
|
80
|
+
const fallback = policy.links[policy.defaultLanguage]
|
|
81
|
+
const own = lng != null ? policy.links[lng] ?? policy.links[baseLanguageOf(lng)] : undefined
|
|
82
|
+
|
|
83
|
+
return { ...fallback, ...Object.fromEntries(Object.entries(own ?? {}).filter(([, url]) => url != null)) } as ConsumerRightsLinks
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { assertConsumerRightsPolicy, baseLanguageOf, linksOf }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const consumerRightsPolicyHelper = createConsumerRightsPolicyHelper()
|
|
90
|
+
|
|
91
|
+
/** @deprecated compat:factory-refactor — use `consumerRightsPolicyHelper.assertConsumerRightsPolicy(…)` */
|
|
92
|
+
export const assertConsumerRightsPolicy = (policy: ConsumerRightsPolicy): ConsumerRightsPolicy =>
|
|
93
|
+
consumerRightsPolicyHelper.assertConsumerRightsPolicy(policy)
|
|
94
|
+
|
|
95
|
+
/** @deprecated compat:factory-refactor — use `consumerRightsPolicyHelper.baseLanguageOf(…)` */
|
|
96
|
+
export const baseLanguageOf = (lng: string | null | undefined): string => consumerRightsPolicyHelper.baseLanguageOf(lng)
|
|
97
|
+
|
|
98
|
+
/** @deprecated compat:factory-refactor — use `consumerRightsPolicyHelper.linksOf(…)` */
|
|
99
|
+
export const linksOf = (
|
|
100
|
+
policy: Pick<ConsumerRightsPolicy, 'links' | 'defaultLanguage'>, lng?: string | null,
|
|
101
|
+
): ConsumerRightsLinks => consumerRightsPolicyHelper.linksOf(policy, lng)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { contract, openProtocol, protocol, typed, type EntrypointProtocol, type OpenRequest, type OpenValue } from '@owlmeans/entrypoint'
|
|
2
|
+
import { backend, frontend, route, RouteMethod } from '@owlmeans/route'
|
|
3
|
+
import {
|
|
4
|
+
BillingProfileViewSchema, CancellationBodySchema, CancellationReceiptSchema, ConsumerRightsPublicViewSchema,
|
|
5
|
+
DeclarationReceiptSchema, PerformanceConsentBodySchema, PerformanceConsentResponseSchema,
|
|
6
|
+
PerformanceConsentViewSchema, PurchaseListSchema, SubscriptionStartBodySchema, SubscriptionStartQuerySchema,
|
|
7
|
+
SubscriptionStartResponseSchema, SubscriptionStartViewSchema, WithdrawalBodySchema,
|
|
8
|
+
WithdrawalCandidateListSchema, WithdrawalReceiptSchema,
|
|
9
|
+
} from '../model/consumer.js'
|
|
10
|
+
import type { SubscriptionStartQuery } from '../types.js'
|
|
11
|
+
import { CONSUMER_RIGHTS_API_PATH, CONSUMER_RIGHTS_CANCELLATION_SCREEN_PATH, CONSUMER_RIGHTS_PUBLIC_PATH, CONSUMER_RIGHTS_WITHDRAWAL_SCREEN_PATH } from './consts.js'
|
|
12
|
+
import type { ConsumerRightsAccountProtocols, ConsumerRightsProtocolOptions, ConsumerRightsProtocols, ConsumerRightsPublicProtocols, ConsumerRightsScreens } from './types.js'
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
const get = (alias: string, path: string, parent: EntrypointProtocol<OpenRequest, OpenValue>) =>
|
|
16
|
+
route(alias, path, backend({ parent, method: RouteMethod.GET }))
|
|
17
|
+
|
|
18
|
+
const post = (alias: string, path: string, parent: EntrypointProtocol<OpenRequest, OpenValue>) =>
|
|
19
|
+
route(alias, path, backend({ parent, method: RouteMethod.POST }))
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The standard consumer-rights protocol tree, the way `makeMarketingConsentProtocols` builds
|
|
23
|
+
* its own:
|
|
24
|
+
*
|
|
25
|
+
* - `base` under the application's guarded `parent` (its account base, whose guards and gate
|
|
26
|
+
* every route inherits) with `profile` GET `/profile`, `purchases` GET `/purchases`, `consent`
|
|
27
|
+
* GET / `giveConsent` POST `/consent`, `start` GET `/start?planSku` / `requestStart` POST
|
|
28
|
+
* `/start`, `withdrawals` GET / `withdraw` POST `/withdrawal`, `cancel` POST `/cancellation`;
|
|
29
|
+
* - with `public`, an UNGUARDED `public` subtree — `policy` GET `/policy`, `withdraw` POST
|
|
30
|
+
* `/withdrawal`, `cancel` POST `/cancellation` — like `paymentGate.webhook`, plus optional
|
|
31
|
+
* sticky frontend `screens`.
|
|
32
|
+
*
|
|
33
|
+
* Aliases are `<prefix>:<name>` (`<prefix>:public:<name>` below `public`). A tree with neither a
|
|
34
|
+
* parent nor guards is a declaration error: the account routes act on the caller's own records.
|
|
35
|
+
* Absent parts are left out of the tree rather than set to `undefined`, so `protocols(tree)` and
|
|
36
|
+
* `mapProtocols(tree, …)` walk it as is.
|
|
37
|
+
*/
|
|
38
|
+
export const makeConsumerRightsProtocols = <O extends ConsumerRightsProtocolOptions>(opts: O): ConsumerRightsProtocols<O> => {
|
|
39
|
+
if (opts.parent == null && opts.guards == null) {
|
|
40
|
+
throw new SyntaxError('consumer-rights: the account routes need either a parent or explicit guards')
|
|
41
|
+
}
|
|
42
|
+
const alias = (name: string): string => `${opts.prefix}:${name}`
|
|
43
|
+
const path = opts.path ?? CONSUMER_RIGHTS_API_PATH
|
|
44
|
+
|
|
45
|
+
const base = opts.parent != null
|
|
46
|
+
? openProtocol(route(alias('base'), path, backend({ parent: opts.parent })))
|
|
47
|
+
: openProtocol(route(alias('base'), path, backend()), {
|
|
48
|
+
guards: opts.guards,
|
|
49
|
+
...(opts.gate != null ? { gate: opts.gate } : {}),
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const account: ConsumerRightsAccountProtocols = {
|
|
53
|
+
base,
|
|
54
|
+
profile: protocol(get(alias('profile'), '/profile', base), contract(BillingProfileViewSchema)),
|
|
55
|
+
purchases: protocol(get(alias('purchases'), '/purchases', base), contract(PurchaseListSchema)),
|
|
56
|
+
consent: protocol(get(alias('consent'), '/consent', base), contract(PerformanceConsentViewSchema)),
|
|
57
|
+
giveConsent: protocol(
|
|
58
|
+
post(alias('consent:give'), '/consent', base),
|
|
59
|
+
contract(PerformanceConsentBodySchema, PerformanceConsentResponseSchema),
|
|
60
|
+
),
|
|
61
|
+
start: protocol(
|
|
62
|
+
get(alias('start'), '/start', base),
|
|
63
|
+
contract.request({ query: typed<SubscriptionStartQuery>(SubscriptionStartQuerySchema) }, SubscriptionStartViewSchema),
|
|
64
|
+
),
|
|
65
|
+
requestStart: protocol(
|
|
66
|
+
post(alias('start:request'), '/start', base),
|
|
67
|
+
contract(SubscriptionStartBodySchema, SubscriptionStartResponseSchema),
|
|
68
|
+
),
|
|
69
|
+
withdrawals: protocol(get(alias('withdrawals'), '/withdrawal', base), contract(WithdrawalCandidateListSchema)),
|
|
70
|
+
withdraw: protocol(
|
|
71
|
+
post(alias('withdraw'), '/withdrawal', base),
|
|
72
|
+
contract(WithdrawalBodySchema, WithdrawalReceiptSchema),
|
|
73
|
+
),
|
|
74
|
+
cancel: protocol(
|
|
75
|
+
post(alias('cancel'), '/cancellation', base),
|
|
76
|
+
contract(CancellationBodySchema, CancellationReceiptSchema),
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
if (opts.public == null || opts.public === false) {
|
|
80
|
+
return account as ConsumerRightsProtocols<O>
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pub = opts.public
|
|
84
|
+
const publicAlias = (name: string): string => alias(`public:${name}`)
|
|
85
|
+
const publicBase = openProtocol(route(
|
|
86
|
+
publicAlias('base'), pub.path ?? CONSUMER_RIGHTS_PUBLIC_PATH, backend(pub.parent != null ? { parent: pub.parent } : null),
|
|
87
|
+
))
|
|
88
|
+
const publicTree: ConsumerRightsPublicProtocols = {
|
|
89
|
+
base: publicBase,
|
|
90
|
+
policy: protocol(get(publicAlias('policy'), '/policy', publicBase), contract(ConsumerRightsPublicViewSchema)),
|
|
91
|
+
withdraw: protocol(
|
|
92
|
+
post(publicAlias('withdraw'), '/withdrawal', publicBase),
|
|
93
|
+
contract(WithdrawalBodySchema, DeclarationReceiptSchema),
|
|
94
|
+
),
|
|
95
|
+
cancel: protocol(
|
|
96
|
+
post(publicAlias('cancel'), '/cancellation', publicBase),
|
|
97
|
+
contract(CancellationBodySchema, DeclarationReceiptSchema),
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
const screens = pub.screens == null ? {} : {
|
|
101
|
+
screens: {
|
|
102
|
+
// Top-level frontend screens, `sticky` so the router keeps them regardless of `cfg.service`
|
|
103
|
+
// filtering — the same shape as every other legal page.
|
|
104
|
+
withdrawal: openProtocol(route(
|
|
105
|
+
publicAlias('screen:withdrawal'), pub.screens.withdrawal ?? CONSUMER_RIGHTS_WITHDRAWAL_SCREEN_PATH,
|
|
106
|
+
frontend({ parent: pub.screens.parent }),
|
|
107
|
+
), { sticky: true }),
|
|
108
|
+
cancellation: openProtocol(route(
|
|
109
|
+
publicAlias('screen:cancellation'), pub.screens.cancellation ?? CONSUMER_RIGHTS_CANCELLATION_SCREEN_PATH,
|
|
110
|
+
frontend({ parent: pub.screens.parent }),
|
|
111
|
+
), { sticky: true }),
|
|
112
|
+
} satisfies ConsumerRightsScreens,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return { ...account, public: { ...publicTree, ...screens } } as unknown as ConsumerRightsProtocols<O>
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OneTimeRefund, OneTimeRefundInput, SubscriptionRefund, SubscriptionRefundInput } from '../types.js'
|
|
2
|
+
|
|
3
|
+
/** The refund a withdrawal owes, computed in BigInt and rounded in the consumer's favour. */
|
|
4
|
+
export interface WithdrawalRefundHelper {
|
|
5
|
+
/**
|
|
6
|
+
* Split `totalMinor` in proportion to `shares` by the largest remainder: every part is the floor
|
|
7
|
+
* of its exact share, and the units left over go one each to the largest fractional parts (the
|
|
8
|
+
* earlier part on a tie). The parts always sum to the total. All-zero shares split evenly.
|
|
9
|
+
*/
|
|
10
|
+
splitByShares: (totalMinor: number, shares: number[]) => number[]
|
|
11
|
+
/**
|
|
12
|
+
* The refund of a one-time purchase (prepaid credits):
|
|
13
|
+
* `min(paid − refunded, ceil(paid × (granted − used) / granted))`. Nothing granted means nothing
|
|
14
|
+
* performed — the whole unrefunded amount. 1256 paid, 125 000 of 500 000 used → 942.
|
|
15
|
+
*/
|
|
16
|
+
oneTimeWithdrawalRefund: (input: OneTimeRefundInput) => OneTimeRefund
|
|
17
|
+
/**
|
|
18
|
+
* The refund of a subscription's first invoice (CJEU C-641/19 PE Digital). The net price splits
|
|
19
|
+
* into its components by `shareMinor` (largest remainder). A `time` component loses
|
|
20
|
+
* `floor(c × elapsedDays / periodDays)`, the days counted from `max(periodStart,
|
|
21
|
+
* servicesRequestedAt)` and floored — and nothing without a start request. A `units` component
|
|
22
|
+
* loses `floor(c × used / granted)`. The gross refund is `ceil(paid × refundNet / net)`, capped at
|
|
23
|
+
* what is still unrefunded. Net 2000 / paid 2460, services 1000 at 3 of 30 days (−100), credits
|
|
24
|
+
* 1000 with 100 000 of 500 000 used (−200) → net 1700, gross 2091.
|
|
25
|
+
*/
|
|
26
|
+
subscriptionWithdrawalRefund: (input: SubscriptionRefundInput) => SubscriptionRefund
|
|
27
|
+
}
|