@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.
Files changed (364) hide show
  1. package/README.md +95 -57
  2. package/agent-meta/manifest.json +10 -3
  3. package/agent-meta/skills/entitlements/SKILL.md +159 -0
  4. package/agent-meta/skills/payment/SKILL.md +491 -28
  5. package/build/advertise.d.ts +2 -0
  6. package/build/advertise.d.ts.map +1 -0
  7. package/build/advertise.js +10 -0
  8. package/build/advertise.js.map +1 -0
  9. package/build/checkout/consts.d.ts +2 -0
  10. package/build/checkout/consts.d.ts.map +1 -0
  11. package/build/checkout/consts.js +2 -0
  12. package/build/checkout/consts.js.map +1 -0
  13. package/build/checkout/index.d.ts +6 -0
  14. package/build/checkout/index.d.ts.map +1 -0
  15. package/build/checkout/index.js +4 -0
  16. package/build/checkout/index.js.map +1 -0
  17. package/build/checkout/narrow/types.d.ts +25 -0
  18. package/build/checkout/narrow/types.d.ts.map +1 -0
  19. package/build/checkout/narrow/types.js +2 -0
  20. package/build/checkout/narrow/types.js.map +1 -0
  21. package/build/checkout/narrow.d.ts +8 -0
  22. package/build/checkout/narrow.d.ts.map +1 -0
  23. package/build/checkout/narrow.js +47 -0
  24. package/build/checkout/narrow.js.map +1 -0
  25. package/build/checkout/protocols.d.ts +8 -0
  26. package/build/checkout/protocols.d.ts.map +1 -0
  27. package/build/checkout/protocols.js +28 -0
  28. package/build/checkout/protocols.js.map +1 -0
  29. package/build/checkout/types.d.ts +29 -0
  30. package/build/checkout/types.d.ts.map +1 -0
  31. package/build/checkout/types.js +2 -0
  32. package/build/checkout/types.js.map +1 -0
  33. package/build/consts.d.ts +242 -25
  34. package/build/consts.d.ts.map +1 -1
  35. package/build/consts.js +377 -24
  36. package/build/consts.js.map +1 -1
  37. package/build/consts.local.d.ts +20 -0
  38. package/build/consts.local.d.ts.map +1 -0
  39. package/build/consts.local.js +23 -0
  40. package/build/consts.local.js.map +1 -0
  41. package/build/consumer/cancel.d.ts +11 -0
  42. package/build/consumer/cancel.d.ts.map +1 -0
  43. package/build/consumer/cancel.js +33 -0
  44. package/build/consumer/cancel.js.map +1 -0
  45. package/build/consumer/consts.d.ts +20 -0
  46. package/build/consumer/consts.d.ts.map +1 -0
  47. package/build/consumer/consts.js +39 -0
  48. package/build/consumer/consts.js.map +1 -0
  49. package/build/consumer/consts.local.d.ts +7 -0
  50. package/build/consumer/consts.local.d.ts.map +1 -0
  51. package/build/consumer/consts.local.js +13 -0
  52. package/build/consumer/consts.local.js.map +1 -0
  53. package/build/consumer/copy/types.d.ts +56 -0
  54. package/build/consumer/copy/types.d.ts.map +1 -0
  55. package/build/consumer/copy/types.js +2 -0
  56. package/build/consumer/copy/types.js.map +1 -0
  57. package/build/consumer/copy.d.ts +22 -0
  58. package/build/consumer/copy.d.ts.map +1 -0
  59. package/build/consumer/copy.js +87 -0
  60. package/build/consumer/copy.js.map +1 -0
  61. package/build/consumer/deadline/types.d.ts +30 -0
  62. package/build/consumer/deadline/types.d.ts.map +1 -0
  63. package/build/consumer/deadline/types.js +2 -0
  64. package/build/consumer/deadline/types.js.map +1 -0
  65. package/build/consumer/deadline.d.ts +12 -0
  66. package/build/consumer/deadline.d.ts.map +1 -0
  67. package/build/consumer/deadline.js +44 -0
  68. package/build/consumer/deadline.js.map +1 -0
  69. package/build/consumer/fifo/types.d.ts +17 -0
  70. package/build/consumer/fifo/types.d.ts.map +1 -0
  71. package/build/consumer/fifo/types.js +2 -0
  72. package/build/consumer/fifo/types.js.map +1 -0
  73. package/build/consumer/fifo.d.ts +4 -0
  74. package/build/consumer/fifo.d.ts.map +1 -0
  75. package/build/consumer/fifo.js +34 -0
  76. package/build/consumer/fifo.js.map +1 -0
  77. package/build/consumer/index.d.ts +18 -0
  78. package/build/consumer/index.d.ts.map +1 -0
  79. package/build/consumer/index.js +11 -0
  80. package/build/consumer/index.js.map +1 -0
  81. package/build/consumer/policy/types.d.ts +20 -0
  82. package/build/consumer/policy/types.d.ts.map +1 -0
  83. package/build/consumer/policy/types.js +2 -0
  84. package/build/consumer/policy/types.js.map +1 -0
  85. package/build/consumer/policy.d.ts +17 -0
  86. package/build/consumer/policy.d.ts.map +1 -0
  87. package/build/consumer/policy.js +91 -0
  88. package/build/consumer/policy.js.map +1 -0
  89. package/build/consumer/protocols.d.ts +20 -0
  90. package/build/consumer/protocols.d.ts.map +1 -0
  91. package/build/consumer/protocols.js +70 -0
  92. package/build/consumer/protocols.js.map +1 -0
  93. package/build/consumer/refund/types.d.ts +27 -0
  94. package/build/consumer/refund/types.d.ts.map +1 -0
  95. package/build/consumer/refund/types.js +2 -0
  96. package/build/consumer/refund/types.js.map +1 -0
  97. package/build/consumer/refund.d.ts +9 -0
  98. package/build/consumer/refund.d.ts.map +1 -0
  99. package/build/consumer/refund.js +112 -0
  100. package/build/consumer/refund.js.map +1 -0
  101. package/build/consumer/refusal.d.ts +10 -0
  102. package/build/consumer/refusal.d.ts.map +1 -0
  103. package/build/consumer/refusal.js +35 -0
  104. package/build/consumer/refusal.js.map +1 -0
  105. package/build/consumer/revive/types.d.ts +19 -0
  106. package/build/consumer/revive/types.d.ts.map +1 -0
  107. package/build/consumer/revive/types.js +2 -0
  108. package/build/consumer/revive/types.js.map +1 -0
  109. package/build/consumer/revive.d.ts +13 -0
  110. package/build/consumer/revive.d.ts.map +1 -0
  111. package/build/consumer/revive.js +43 -0
  112. package/build/consumer/revive.js.map +1 -0
  113. package/build/consumer/types.d.ts +196 -0
  114. package/build/consumer/types.d.ts.map +1 -0
  115. package/build/consumer/types.js +2 -0
  116. package/build/consumer/types.js.map +1 -0
  117. package/build/consumer/types.local.d.ts +11 -0
  118. package/build/consumer/types.local.d.ts.map +1 -0
  119. package/build/consumer/types.local.js +2 -0
  120. package/build/consumer/types.local.js.map +1 -0
  121. package/build/countries.d.ts +3 -0
  122. package/build/countries.d.ts.map +1 -0
  123. package/build/countries.js +4 -0
  124. package/build/countries.js.map +1 -0
  125. package/build/entitled.d.ts +14 -0
  126. package/build/entitled.d.ts.map +1 -0
  127. package/build/entitled.js +17 -0
  128. package/build/entitled.js.map +1 -0
  129. package/build/entitlement/types.d.ts +29 -0
  130. package/build/entitlement/types.d.ts.map +1 -0
  131. package/build/entitlement/types.js +2 -0
  132. package/build/entitlement/types.js.map +1 -0
  133. package/build/entitlement.d.ts +12 -0
  134. package/build/entitlement.d.ts.map +1 -0
  135. package/build/entitlement.js +64 -0
  136. package/build/entitlement.js.map +1 -0
  137. package/build/errors.d.ts +189 -0
  138. package/build/errors.d.ts.map +1 -1
  139. package/build/errors.js +361 -0
  140. package/build/errors.js.map +1 -1
  141. package/build/estimate/types.d.ts +34 -0
  142. package/build/estimate/types.d.ts.map +1 -0
  143. package/build/estimate/types.js +2 -0
  144. package/build/estimate/types.js.map +1 -0
  145. package/build/estimate.d.ts +11 -0
  146. package/build/estimate.d.ts.map +1 -0
  147. package/build/estimate.js +76 -0
  148. package/build/estimate.js.map +1 -0
  149. package/build/i18n/consumer-rights/be.json +131 -0
  150. package/build/i18n/consumer-rights/de.json +131 -0
  151. package/build/i18n/consumer-rights/en.json +131 -0
  152. package/build/i18n/consumer-rights/es.json +131 -0
  153. package/build/i18n/consumer-rights/fr.json +131 -0
  154. package/build/i18n/consumer-rights/pl.json +131 -0
  155. package/build/i18n/consumer-rights/ru.json +131 -0
  156. package/build/i18n/consumer-rights/uk.json +131 -0
  157. package/build/i18n/errors/be.json +30 -0
  158. package/build/i18n/errors/de.json +30 -0
  159. package/build/i18n/errors/en.json +30 -0
  160. package/build/i18n/errors/es.json +30 -0
  161. package/build/i18n/errors/fr.json +30 -0
  162. package/build/i18n/errors/pl.json +30 -0
  163. package/build/i18n/errors/ru.json +30 -0
  164. package/build/i18n/errors/uk.json +30 -0
  165. package/build/i18n/fr.json +39 -0
  166. package/build/i18n.js +53 -8
  167. package/build/i18n.js.map +1 -1
  168. package/build/index.d.ts +19 -1
  169. package/build/index.d.ts.map +1 -1
  170. package/build/index.js +12 -1
  171. package/build/index.js.map +1 -1
  172. package/build/limit/types.d.ts +44 -0
  173. package/build/limit/types.d.ts.map +1 -0
  174. package/build/limit/types.js +2 -0
  175. package/build/limit/types.js.map +1 -0
  176. package/build/limit.d.ts +23 -0
  177. package/build/limit.d.ts.map +1 -0
  178. package/build/limit.js +97 -0
  179. package/build/limit.js.map +1 -0
  180. package/build/model/checkout.d.ts +2 -3
  181. package/build/model/checkout.d.ts.map +1 -1
  182. package/build/model/checkout.js +12 -7
  183. package/build/model/checkout.js.map +1 -1
  184. package/build/model/consts.d.ts +15 -0
  185. package/build/model/consts.d.ts.map +1 -0
  186. package/build/model/consts.js +193 -0
  187. package/build/model/consts.js.map +1 -0
  188. package/build/model/consts.local.d.ts +35 -0
  189. package/build/model/consts.local.d.ts.map +1 -0
  190. package/build/model/consts.local.js +65 -0
  191. package/build/model/consts.local.js.map +1 -0
  192. package/build/model/consumer.d.ts +23 -0
  193. package/build/model/consumer.d.ts.map +1 -0
  194. package/build/model/consumer.js +272 -0
  195. package/build/model/consumer.js.map +1 -0
  196. package/build/model/estimate.d.ts +5 -0
  197. package/build/model/estimate.d.ts.map +1 -0
  198. package/build/model/estimate.js +69 -0
  199. package/build/model/estimate.js.map +1 -0
  200. package/build/model/index.d.ts +7 -2
  201. package/build/model/index.d.ts.map +1 -1
  202. package/build/model/index.js +7 -2
  203. package/build/model/index.js.map +1 -1
  204. package/build/model/limit.d.ts +7 -0
  205. package/build/model/limit.d.ts.map +1 -0
  206. package/build/model/limit.js +32 -0
  207. package/build/model/limit.js.map +1 -0
  208. package/build/model/plan.d.ts.map +1 -1
  209. package/build/model/plan.js +24 -11
  210. package/build/model/plan.js.map +1 -1
  211. package/build/model/portal.d.ts +4 -0
  212. package/build/model/portal.d.ts.map +1 -0
  213. package/build/model/portal.js +22 -0
  214. package/build/model/portal.js.map +1 -0
  215. package/build/model/pricing.d.ts +4 -0
  216. package/build/model/pricing.d.ts.map +1 -0
  217. package/build/model/pricing.js +31 -0
  218. package/build/model/pricing.js.map +1 -0
  219. package/build/model/view.d.ts +3 -0
  220. package/build/model/view.d.ts.map +1 -0
  221. package/build/model/view.js +15 -0
  222. package/build/model/view.js.map +1 -0
  223. package/build/pricing/types.d.ts +13 -0
  224. package/build/pricing/types.d.ts.map +1 -0
  225. package/build/pricing/types.js +2 -0
  226. package/build/pricing/types.js.map +1 -0
  227. package/build/pricing.d.ts +13 -0
  228. package/build/pricing.d.ts.map +1 -0
  229. package/build/pricing.js +74 -0
  230. package/build/pricing.js.map +1 -0
  231. package/build/promo/types.d.ts +14 -0
  232. package/build/promo/types.d.ts.map +1 -0
  233. package/build/promo/types.js +2 -0
  234. package/build/promo/types.js.map +1 -0
  235. package/build/promo.d.ts +7 -0
  236. package/build/promo.d.ts.map +1 -0
  237. package/build/promo.js +25 -0
  238. package/build/promo.js.map +1 -0
  239. package/build/regions/types.d.ts +32 -0
  240. package/build/regions/types.d.ts.map +1 -0
  241. package/build/regions/types.js +2 -0
  242. package/build/regions/types.js.map +1 -0
  243. package/build/regions.d.ts +15 -0
  244. package/build/regions.d.ts.map +1 -0
  245. package/build/regions.js +43 -0
  246. package/build/regions.js.map +1 -0
  247. package/build/service.d.ts.map +1 -1
  248. package/build/service.js +24 -9
  249. package/build/service.js.map +1 -1
  250. package/build/types.d.ts +619 -52
  251. package/build/types.d.ts.map +1 -1
  252. package/build/types.local.d.ts +4 -0
  253. package/build/types.local.d.ts.map +1 -0
  254. package/build/types.local.js +2 -0
  255. package/build/types.local.js.map +1 -0
  256. package/build/view/types.d.ts +33 -0
  257. package/build/view/types.d.ts.map +1 -0
  258. package/build/view/types.js +2 -0
  259. package/build/view/types.js.map +1 -0
  260. package/build/view.d.ts +13 -0
  261. package/build/view.d.ts.map +1 -0
  262. package/build/view.js +78 -0
  263. package/build/view.js.map +1 -0
  264. package/package.json +14 -12
  265. package/src/advertise.ts +13 -0
  266. package/src/checkout/consts.ts +1 -0
  267. package/src/checkout/index.ts +5 -0
  268. package/src/checkout/narrow/types.ts +27 -0
  269. package/src/checkout/narrow.ts +63 -0
  270. package/src/checkout/protocols.ts +39 -0
  271. package/src/checkout/types.ts +27 -0
  272. package/src/consts.local.ts +34 -0
  273. package/src/consts.ts +417 -24
  274. package/src/consumer/cancel.ts +40 -0
  275. package/src/consumer/consts.local.ts +16 -0
  276. package/src/consumer/consts.ts +46 -0
  277. package/src/consumer/copy/types.ts +54 -0
  278. package/src/consumer/copy.ts +121 -0
  279. package/src/consumer/deadline/types.ts +30 -0
  280. package/src/consumer/deadline.ts +61 -0
  281. package/src/consumer/fifo/types.ts +17 -0
  282. package/src/consumer/fifo.ts +42 -0
  283. package/src/consumer/index.ts +17 -0
  284. package/src/consumer/policy/types.ts +20 -0
  285. package/src/consumer/policy.ts +101 -0
  286. package/src/consumer/protocols.ts +116 -0
  287. package/src/consumer/refund/types.ts +27 -0
  288. package/src/consumer/refund.ts +136 -0
  289. package/src/consumer/refusal.ts +35 -0
  290. package/src/consumer/revive/types.ts +23 -0
  291. package/src/consumer/revive.ts +82 -0
  292. package/src/consumer/types.local.ts +10 -0
  293. package/src/consumer/types.ts +189 -0
  294. package/src/countries.ts +5 -0
  295. package/src/entitled.ts +20 -0
  296. package/src/entitlement/types.ts +29 -0
  297. package/src/entitlement.ts +86 -0
  298. package/src/errors.ts +445 -0
  299. package/src/estimate/types.ts +34 -0
  300. package/src/estimate.ts +90 -0
  301. package/src/i18n/consumer-rights/be.json +131 -0
  302. package/src/i18n/consumer-rights/de.json +131 -0
  303. package/src/i18n/consumer-rights/en.json +131 -0
  304. package/src/i18n/consumer-rights/es.json +131 -0
  305. package/src/i18n/consumer-rights/fr.json +131 -0
  306. package/src/i18n/consumer-rights/pl.json +131 -0
  307. package/src/i18n/consumer-rights/ru.json +131 -0
  308. package/src/i18n/consumer-rights/uk.json +131 -0
  309. package/src/i18n/errors/be.json +30 -0
  310. package/src/i18n/errors/de.json +30 -0
  311. package/src/i18n/errors/en.json +30 -0
  312. package/src/i18n/errors/es.json +30 -0
  313. package/src/i18n/errors/fr.json +30 -0
  314. package/src/i18n/errors/pl.json +30 -0
  315. package/src/i18n/errors/ru.json +30 -0
  316. package/src/i18n/errors/uk.json +30 -0
  317. package/src/i18n/fr.json +39 -0
  318. package/src/i18n.ts +58 -8
  319. package/src/index.ts +19 -2
  320. package/src/limit/types.ts +41 -0
  321. package/src/limit.ts +132 -0
  322. package/src/model/checkout.ts +12 -7
  323. package/src/model/consts.local.ts +83 -0
  324. package/src/model/consts.ts +206 -0
  325. package/src/model/consumer.ts +294 -0
  326. package/src/model/estimate.ts +73 -0
  327. package/src/model/index.ts +7 -2
  328. package/src/model/limit.ts +36 -0
  329. package/src/model/plan.ts +25 -12
  330. package/src/model/portal.ts +25 -0
  331. package/src/model/pricing.ts +34 -0
  332. package/src/model/view.ts +17 -0
  333. package/src/pricing/types.ts +13 -0
  334. package/src/pricing.ts +110 -0
  335. package/src/promo/types.ts +18 -0
  336. package/src/promo.ts +46 -0
  337. package/src/regions/types.ts +42 -0
  338. package/src/regions.ts +85 -0
  339. package/src/service.ts +32 -15
  340. package/src/types.local.ts +5 -0
  341. package/src/types.ts +669 -49
  342. package/src/view/types.ts +39 -0
  343. package/src/view.ts +119 -0
  344. package/tests/checkout-narrow.spec.ts +75 -0
  345. package/tests/consumer-copy.spec.ts +378 -0
  346. package/tests/consumer-deadline.spec.ts +85 -0
  347. package/tests/consumer-fifo.spec.ts +61 -0
  348. package/tests/consumer-protocols.spec.ts +103 -0
  349. package/tests/consumer-refund.spec.ts +134 -0
  350. package/tests/consumer-regions.spec.ts +140 -0
  351. package/tests/consumer-wire.spec.ts +170 -0
  352. package/tests/contract.spec.ts +183 -0
  353. package/tests/entitlement.spec.ts +103 -0
  354. package/tests/errors.spec.ts +168 -0
  355. package/tests/estimate.spec.ts +139 -0
  356. package/tests/limit.spec.ts +117 -0
  357. package/tests/pricing.spec.ts +30 -0
  358. package/tests/tsconfig.json +11 -0
  359. package/tests/view.spec.ts +130 -0
  360. package/tsconfig.json +6 -1
  361. package/build/.gitkeep +0 -0
  362. package/src/model/subscription.ts +0 -63
  363. package/src/model/utils.ts +0 -32
  364. 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
+ }