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