@owlmeans/payment 0.1.18-rc.9 → 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 (366) 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 +7 -55
  134. package/build/entitlement.d.ts.map +1 -1
  135. package/build/entitlement.js +55 -86
  136. package/build/entitlement.js.map +1 -1
  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 +18 -1
  169. package/build/index.d.ts.map +1 -1
  170. package/build/index.js +11 -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 +67 -103
  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 +18 -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 +38 -25
  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/view.spec.ts +130 -0
  359. package/build/.gitkeep +0 -0
  360. package/src/model/subscription.ts +0 -63
  361. package/src/model/utils.ts +0 -32
  362. package/src/modules.ts +0 -43
  363. package/tests/entitlement.spec.d.ts +0 -2
  364. package/tests/entitlement.spec.d.ts.map +0 -1
  365. package/tests/entitlement.spec.js +0 -72
  366. package/tests/entitlement.spec.js.map +0 -1
@@ -0,0 +1,139 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ DEFAULT_PRICING_POLICY, TaxBehavior, TaxEstimateStatus, TaxType, type PriceEstimate,
4
+ type PricingPolicy, priceEstimateHelper,
5
+ } from '../src/index.js'
6
+
7
+ describe('ratePpmOf', () => {
8
+ test('parses a whole percentage', () => {
9
+ expect(priceEstimateHelper.ratePpmOf('23')).toBe(230_000)
10
+ })
11
+
12
+ test('parses a trailing zero the same as none', () => {
13
+ expect(priceEstimateHelper.ratePpmOf('23.0')).toBe(230_000)
14
+ })
15
+
16
+ test('parses fractional percentages exactly, without float drift', () => {
17
+ expect(priceEstimateHelper.ratePpmOf('8.875')).toBe(88_750)
18
+ expect(priceEstimateHelper.ratePpmOf('25.5')).toBe(255_000)
19
+ })
20
+
21
+ test('parses a zero rate', () => {
22
+ expect(priceEstimateHelper.ratePpmOf('0.0')).toBe(0)
23
+ })
24
+
25
+ test('rejects a non-decimal string', () => {
26
+ expect(() => priceEstimateHelper.ratePpmOf('abc')).toThrow()
27
+ expect(() => priceEstimateHelper.ratePpmOf('1e3')).toThrow()
28
+ expect(() => priceEstimateHelper.ratePpmOf('-1')).toThrow()
29
+ })
30
+ })
31
+
32
+ const plVat: PriceEstimate = {
33
+ country: 'PL', source: 'request', currency: 'usd', behavior: TaxBehavior.Exclusive,
34
+ tax: {
35
+ status: TaxEstimateStatus.Taxed, subtotalMinor: 1_021, taxMinor: 235, totalMinor: 1_256,
36
+ scalable: true,
37
+ rates: [{ type: TaxType.Vat, percentage: '23', ratePpm: 230_000, country: 'PL' }],
38
+ },
39
+ }
40
+
41
+ describe('estimateOf', () => {
42
+ test('echoes the reference amount exactly, as Stripe returned it (1021 -> 235 -> 1256)', () => {
43
+ const result = priceEstimateHelper.estimateOf(1_021, plVat)
44
+ expect(result).toEqual({ subtotalMinor: 1_021, taxMinor: 235, totalMinor: 1_256 })
45
+ })
46
+
47
+ test('rescales an exclusive, scalable rate to a different amount', () => {
48
+ // 3_750 * 23% = 862.5 -> rounds up to 863
49
+ const result = priceEstimateHelper.estimateOf(3_750, plVat)
50
+ expect(result.taxMinor).toBe(863)
51
+ expect(result.totalMinor).toBe(4_613)
52
+ })
53
+
54
+ test('rounds a half-cent up', () => {
55
+ // 100 * 23% = 23.0 exactly, no rounding ambiguity; use a value that lands on .5
56
+ // 50 * 23% = 11.5 -> rounds to 12
57
+ const result = priceEstimateHelper.estimateOf(50, plVat)
58
+ expect(result.taxMinor).toBe(12)
59
+ })
60
+
61
+ test('returns null amounts for a different amount when the estimate is not scalable', () => {
62
+ const flat: PriceEstimate = {
63
+ ...plVat,
64
+ tax: { ...plVat.tax, scalable: false },
65
+ }
66
+ expect(priceEstimateHelper.estimateOf(3_750, flat)).toEqual({ subtotalMinor: 3_750, taxMinor: null, totalMinor: null })
67
+ // the reference amount is still exact even when not scalable
68
+ expect(priceEstimateHelper.estimateOf(1_021, flat)).toEqual({ subtotalMinor: 1_021, taxMinor: 235, totalMinor: 1_256 })
69
+ })
70
+
71
+ test('never rescales an inclusive amount away from the reference, even when scalable', () => {
72
+ const inclusive: PriceEstimate = { ...plVat, behavior: TaxBehavior.Inclusive }
73
+ expect(priceEstimateHelper.estimateOf(1_021, inclusive)).toEqual({ subtotalMinor: 1_021, taxMinor: 235, totalMinor: 1_256 })
74
+ expect(priceEstimateHelper.estimateOf(3_750, inclusive)).toEqual({ subtotalMinor: 3_750, taxMinor: null, totalMinor: null })
75
+ })
76
+
77
+ test('carries the subtotal, tax and total in the local currency alongside a rescaled amount', () => {
78
+ const withLocal: PriceEstimate = { ...plVat, local: { currency: 'pln', exchangeRate: 0.25 } }
79
+ // $10.21 / $2.35 / $12.56, each divided by 0.25 usd-per-pln -> 40.84 / 9.40 / 50.24
80
+ const result = priceEstimateHelper.estimateOf(1_021, withLocal)
81
+ expect(result.local).toEqual({ currency: 'pln', subtotalAmount: 40.84, taxAmount: 9.4, totalAmount: 50.24 })
82
+ })
83
+
84
+ test('rejects a negative or non-integer amount', () => {
85
+ expect(() => priceEstimateHelper.estimateOf(-1, plVat)).toThrow()
86
+ expect(() => priceEstimateHelper.estimateOf(1.5, plVat)).toThrow()
87
+ })
88
+ })
89
+
90
+ describe('assertPricingPolicy', () => {
91
+ test('accepts the default policy', () => {
92
+ expect(priceEstimateHelper.assertPricingPolicy(DEFAULT_PRICING_POLICY)).toBe(DEFAULT_PRICING_POLICY)
93
+ })
94
+
95
+ test('rejects a tax estimate without automatic tax', () => {
96
+ const policy: PricingPolicy = { tax: { automatic: false, collectTaxId: false, estimate: true }, currency: { estimate: false } }
97
+ expect(() => priceEstimateHelper.assertPricingPolicy(policy)).toThrow()
98
+ })
99
+
100
+ test('rejects a currency estimate without adaptive pricing', () => {
101
+ const policy: PricingPolicy = {
102
+ tax: { automatic: true, collectTaxId: true, estimate: false },
103
+ currency: { estimate: true },
104
+ }
105
+ expect(() => priceEstimateHelper.assertPricingPolicy(policy)).toThrow()
106
+ })
107
+
108
+ test('accepts a currency estimate declared alongside adaptive pricing', () => {
109
+ const policy: PricingPolicy = {
110
+ tax: { automatic: true, collectTaxId: true, estimate: true },
111
+ currency: { adaptive: true, estimate: true },
112
+ }
113
+ expect(priceEstimateHelper.assertPricingPolicy(policy)).toBe(policy)
114
+ })
115
+
116
+ test('accepts the Adaptive Pricing fee alongside adaptive pricing', () => {
117
+ const policy: PricingPolicy = {
118
+ tax: { automatic: true, collectTaxId: true, estimate: true },
119
+ currency: { adaptive: true, adaptiveFeeRate: 0.04, estimate: true },
120
+ }
121
+ expect(priceEstimateHelper.assertPricingPolicy(policy)).toBe(policy)
122
+ })
123
+
124
+ test('rejects an Adaptive Pricing fee without adaptive pricing, or outside [0, 1)', () => {
125
+ const tax = { automatic: true, collectTaxId: true, estimate: false }
126
+ expect(() => priceEstimateHelper.assertPricingPolicy({ tax, currency: { adaptiveFeeRate: 0.04, estimate: false } })).toThrow()
127
+ for (const adaptiveFeeRate of [-0.01, 1, Number.NaN, Number.POSITIVE_INFINITY]) {
128
+ expect(() => priceEstimateHelper.assertPricingPolicy({
129
+ tax, currency: { adaptive: true, adaptiveFeeRate, estimate: false },
130
+ })).toThrow()
131
+ }
132
+ })
133
+
134
+ test('rejects a non-positive or fractional TTL', () => {
135
+ const base: PricingPolicy = { tax: { automatic: true, collectTaxId: true, estimate: false }, currency: { estimate: false } }
136
+ expect(() => priceEstimateHelper.assertPricingPolicy({ ...base, tax: { ...base.tax, estimateTtlSeconds: 0 } })).toThrow()
137
+ expect(() => priceEstimateHelper.assertPricingPolicy({ ...base, tax: { ...base.tax, estimateTtlSeconds: 1.5 } })).toThrow()
138
+ })
139
+ })
@@ -0,0 +1,117 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { LimitKind, LimitMisdeclared, LimitWindow, planLimitHelper } from '../src/index.js'
3
+ import type { EntitlementView, LimitView } from '../src/index.js'
4
+
5
+ const utc = (iso: string): Date => new Date(iso)
6
+
7
+ describe('window keys', () => {
8
+ test('subscription weeks roll at the exact anniversary across DST and month boundaries', () => {
9
+ const anchor = utc('2026-10-23T15:03:00.000Z')
10
+ const before = utc('2026-10-30T15:02:59.999Z')
11
+ const reset = utc('2026-10-30T15:03:00.000Z')
12
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.SubscriptionWeek, before, anchor))
13
+ .toBe('subscription-week:2026-10-23T15:03:00.000Z')
14
+ expect(planLimitHelper.windowBoundsOf(LimitWindow.SubscriptionWeek, before, anchor)).toEqual({ start: anchor, resetsAt: reset })
15
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.SubscriptionWeek, reset, anchor))
16
+ .toBe('subscription-week:2026-10-30T15:03:00.000Z')
17
+ expect(planLimitHelper.windowBoundsOf(LimitWindow.SubscriptionWeek, utc('2026-11-06T15:03:00.000Z'), anchor).resetsAt)
18
+ .toEqual(utc('2026-11-13T15:03:00.000Z'))
19
+ expect(() => planLimitHelper.windowBoundsOf(LimitWindow.SubscriptionWeek, before)).toThrow(LimitMisdeclared)
20
+ })
21
+
22
+ test('a day window rolls at UTC midnight', () => {
23
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.Day, utc('2026-01-31T23:59:59.999Z'))).toBe('2026-01-31')
24
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.Day, utc('2026-02-01T00:00:00.000Z'))).toBe('2026-02-01')
25
+ })
26
+
27
+ test('a month window rolls on the 1st, UTC, across a year', () => {
28
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.Month, utc('2026-12-31T23:59:59.999Z'))).toBe('2026-12')
29
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.Month, utc('2027-01-01T00:00:00.000Z'))).toBe('2027-01')
30
+ // A local time zone ahead of UTC does not move the key.
31
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.Month, utc('2026-03-31T23:30:00-02:00'))).toBe('2026-04')
32
+ })
33
+
34
+ test('lifetime and occupancy never renew', () => {
35
+ expect(planLimitHelper.windowKeyOf(LimitKind.Lifetime, undefined, utc('2026-01-01T00:00:00Z'))).toBe('lifetime')
36
+ expect(planLimitHelper.windowKeyOf(LimitKind.Occupancy, LimitWindow.Day, utc('2030-06-15T12:00:00Z'))).toBe('occupancy')
37
+ })
38
+
39
+ test('a window limit without a window is a misdeclaration', () => {
40
+ expect(() => planLimitHelper.windowKeyOf(LimitKind.Window, undefined)).toThrow(LimitMisdeclared)
41
+ })
42
+ })
43
+
44
+ describe('window bounds', () => {
45
+ test('a day spans [midnight, next midnight)', () => {
46
+ const { start, resetsAt } = planLimitHelper.windowBoundsOf(LimitWindow.Day, utc('2026-01-31T13:00:00Z'))
47
+ expect(start.toISOString()).toBe('2026-01-31T00:00:00.000Z')
48
+ expect(resetsAt.toISOString()).toBe('2026-02-01T00:00:00.000Z')
49
+ })
50
+
51
+ test('a month spans [1st, next 1st) and resetsAt belongs to the next window', () => {
52
+ const { start, resetsAt } = planLimitHelper.windowBoundsOf(LimitWindow.Month, utc('2026-12-31T23:59:59.999Z'))
53
+ expect(start.toISOString()).toBe('2026-12-01T00:00:00.000Z')
54
+ expect(resetsAt.toISOString()).toBe('2027-01-01T00:00:00.000Z')
55
+ expect(planLimitHelper.windowKeyOf(LimitKind.Window, LimitWindow.Month, resetsAt)).toBe('2027-01')
56
+ })
57
+ })
58
+
59
+ describe('limit parameters', () => {
60
+ test('format and parse round-trip', () => {
61
+ expect(planLimitHelper.formatLimitParam('seats')).toBe('limit:seats')
62
+ expect(planLimitHelper.formatLimitParam('seats', 3)).toBe('limit:seats>=3')
63
+ expect(planLimitHelper.parseLimitParam(planLimitHelper.formatLimitParam('seats'))).toEqual({ key: 'seats', atLeast: 1 })
64
+ expect(planLimitHelper.parseLimitParam(planLimitHelper.formatLimitParam('seats', 3))).toEqual({ key: 'seats', atLeast: 3 })
65
+ })
66
+
67
+ test('a capability parameter, a bare key or an empty key is not a limit', () => {
68
+ expect(planLimitHelper.parseLimitParam('feature:whitelabel')).toBeNull()
69
+ expect(planLimitHelper.parseLimitParam('seats')).toBeNull()
70
+ expect(planLimitHelper.parseLimitParam('limit:')).toBeNull()
71
+ expect(planLimitHelper.parseLimitParam('')).toBeNull()
72
+ })
73
+
74
+ test('a floor that is not positive refuses rather than throwing', () => {
75
+ expect(planLimitHelper.parseLimitParam('limit:seats>=0')).toBeNull()
76
+ expect(() => planLimitHelper.parseLimitParam(undefined as unknown as string)).not.toThrow()
77
+ expect(planLimitHelper.parseLimitParam(undefined as unknown as string)).toBeNull()
78
+ })
79
+ })
80
+
81
+ const seats: LimitView = {
82
+ key: 'seats', param: 'limit:seats', kind: LimitKind.Occupancy, limit: 3, used: 2, remaining: 1,
83
+ }
84
+
85
+ const view: EntitlementView = {
86
+ plan: {
87
+ sku: 'pro-monthly', productSku: 'pro', title: 'Pro', rank: 10, free: false,
88
+ status: 'active' as EntitlementView['plan']['status'], paygate: 'stripe',
89
+ },
90
+ capabilities: [
91
+ { param: 'feature:whitelabel', scope: 'feature', permission: 'whitelabel', value: true, granted: true },
92
+ { param: 'feature:custom-domain', scope: 'feature', permission: 'custom-domain', value: true, granted: false },
93
+ ],
94
+ limits: [seats],
95
+ at: new Date(),
96
+ }
97
+
98
+ describe('reading a view', () => {
99
+ test('room is remaining against the floor', () => {
100
+ expect(planLimitHelper.hasLimitRoom(seats)).toBe(true)
101
+ expect(planLimitHelper.hasLimitRoom(seats, 2)).toBe(false)
102
+ expect(planLimitHelper.hasLimitRoom(null)).toBe(false)
103
+ })
104
+
105
+ test('a limit is found by key', () => {
106
+ expect(planLimitHelper.limitOf(view, 'seats')).toBe(seats)
107
+ expect(planLimitHelper.limitOf(view, 'projects')).toBeNull()
108
+ expect(planLimitHelper.limitOf(null, 'seats')).toBeNull()
109
+ })
110
+
111
+ test('a capability passes only while granted, and a limit parameter never does', () => {
112
+ expect(planLimitHelper.capabilityOf(view, 'feature:whitelabel')).toBe(true)
113
+ expect(planLimitHelper.capabilityOf(view, 'feature:custom-domain')).toBe(false)
114
+ expect(planLimitHelper.capabilityOf(view, 'limit:seats')).toBe(false)
115
+ expect(planLimitHelper.capabilityOf(null, 'feature:whitelabel')).toBe(false)
116
+ })
117
+ })
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { type AmountCheckoutPolicy, checkoutPricingHelper } from '../src/index.js'
3
+
4
+ const policy: AmountCheckoutPolicy = {
5
+ currency: 'usd', minimumMinor: 500, maximumMinor: 50_000, defaultMinor: 1_000,
6
+ presetsMinor: [1_000, 2_000, 5_000, 10_000], fixedMinor: 0, rateBps: 200,
7
+ }
8
+
9
+ describe('amount checkout pricing', () => {
10
+ test('accepts exact boundaries and custom cents', () => {
11
+ expect(checkoutPricingHelper.assertCheckoutAmount(policy, 500)).toBe(500)
12
+ expect(checkoutPricingHelper.assertCheckoutAmount(policy, 501)).toBe(501)
13
+ expect(checkoutPricingHelper.assertCheckoutAmount(policy, 50_000)).toBe(50_000)
14
+ })
15
+
16
+ test('rejects values outside the net-credit bounds', () => {
17
+ expect(() => checkoutPricingHelper.assertCheckoutAmount(policy, 499)).toThrow()
18
+ expect(() => checkoutPricingHelper.assertCheckoutAmount(policy, 50_001)).toThrow()
19
+ })
20
+
21
+ test('grosses ten dollars up to ten dollars and twenty-one cents', () => {
22
+ expect(checkoutPricingHelper.chargeAmountMinor(1_000, policy)).toBe(1_021)
23
+ })
24
+
25
+ test('rejects invalid policy ordering, duplicate presets, and a 100% rate', () => {
26
+ expect(() => checkoutPricingHelper.assertAmountCheckoutPolicy({ ...policy, defaultMinor: 100 })).toThrow()
27
+ expect(() => checkoutPricingHelper.assertAmountCheckoutPolicy({ ...policy, presetsMinor: [1_000, 1_000] })).toThrow()
28
+ expect(() => checkoutPricingHelper.assertAmountCheckoutPolicy({ ...policy, rateBps: 10_000 })).toThrow()
29
+ })
30
+ })
@@ -0,0 +1,130 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import {
3
+ LimitKind, LimitWindow, PlanDuration, PlanStatus, SubscriptionStatus, entitlementViewHelper,
4
+ promoHelper,
5
+ } from '../src/index.js'
6
+ import type { EntitlementPlanView, ProductPlan } from '../src/index.js'
7
+
8
+ const until = new Date('2026-10-01T00:00:00.000Z')
9
+ const before = new Date('2026-09-15T00:00:00.000Z')
10
+ const after = new Date('2026-10-15T00:00:00.000Z')
11
+
12
+ describe('promos', () => {
13
+ test('no promo is always in force', () => {
14
+ expect(promoHelper.promoActive(undefined, undefined, after)).toBe(true)
15
+ expect(promoHelper.promoViewOf(undefined, undefined, after)).toBeUndefined()
16
+ })
17
+
18
+ test('a promo ends AT until, not after it', () => {
19
+ expect(promoHelper.promoActive({ until }, null, new Date(until.getTime() - 1))).toBe(true)
20
+ expect(promoHelper.promoActive({ until }, null, until)).toBe(false)
21
+ })
22
+
23
+ test('grandfathering keeps it for a subscription created before until — and only then', () => {
24
+ const promo = { until, grandfather: true }
25
+ expect(promoHelper.promoActive(promo, before, after)).toBe(true)
26
+ expect(promoHelper.promoActive(promo, until, after)).toBe(false)
27
+ expect(promoHelper.promoActive(promo, null, after)).toBe(false)
28
+ expect(promoHelper.promoActive({ until }, before, after)).toBe(false)
29
+ })
30
+
31
+ test('the view says whether it is kept for the plan', () => {
32
+ expect(promoHelper.promoViewOf({ until, grandfather: true }, before, after))
33
+ .toEqual({ until, grandfathered: true, active: true })
34
+ expect(promoHelper.promoViewOf({ until }, before, after)).toEqual({ until, grandfathered: false, active: false })
35
+ })
36
+ })
37
+
38
+ const plan: ProductPlan = {
39
+ productSku: 'pro', sku: 'pro-monthly', status: PlanStatus.Active, duration: PlanDuration.Monthly,
40
+ price: 20, title: 'Pro', rank: 10,
41
+ capabilities: [
42
+ { scope: 'feature', permissions: { whitelabel: true, legacy: false, unset: null } },
43
+ { scope: 'feature', permissions: { 'custom-domain': true }, promo: { until } },
44
+ ],
45
+ limits: {
46
+ seats: { kind: LimitKind.Occupancy, limit: 3, unit: 'seat' },
47
+ exports: { kind: LimitKind.Window, window: LimitWindow.Day, limit: 5 },
48
+ imports: { kind: LimitKind.Lifetime, limit: 1, promo: { until } },
49
+ },
50
+ }
51
+
52
+ describe('capability views', () => {
53
+ test('one row per granted permission; false and null are not listed', () => {
54
+ const rows = entitlementViewHelper.capabilityViewsOf(plan, null, before)
55
+ expect(rows.map(row => row.param)).toEqual(['feature:whitelabel', 'feature:custom-domain'])
56
+ expect(rows.every(row => row.granted)).toBe(true)
57
+ })
58
+
59
+ test('a lapsed promo ungrants but still lists the capability', () => {
60
+ const domain = entitlementViewHelper.capabilityViewsOf(plan, null, after).find(row => row.permission === 'custom-domain')
61
+ expect(domain).toMatchObject({ granted: false, promo: { active: false, grandfathered: false } })
62
+ })
63
+
64
+ test('a set under the reserved limit scope is not a capability', () => {
65
+ expect(entitlementViewHelper.capabilityViewsOf({ capabilities: [{ scope: 'limit', permissions: { seats: 3 } }] })).toEqual([])
66
+ })
67
+ })
68
+
69
+ describe('limit views', () => {
70
+ const at = new Date('2026-09-15T10:00:00.000Z')
71
+
72
+ test('used comes from the current window; a missing row is zero', () => {
73
+ const rows = entitlementViewHelper.limitViewsOf(plan, [
74
+ { key: 'exports', window: '2026-09-14', used: 5 },
75
+ { key: 'exports', window: '2026-09-15', used: 2 },
76
+ { key: 'seats', window: 'occupancy', used: 1 },
77
+ ], null, at)
78
+ expect(rows.find(row => row.key === 'exports')).toMatchObject({ used: 2, remaining: 3, param: 'limit:exports' })
79
+ expect(rows.find(row => row.key === 'seats')).toMatchObject({ used: 1, remaining: 2, unit: 'seat' })
80
+ expect(rows.find(row => row.key === 'imports')).toMatchObject({ used: 0, remaining: 1 })
81
+ })
82
+
83
+ test('only a window limit carries its bounds', () => {
84
+ const rows = entitlementViewHelper.limitViewsOf(plan, [], null, at)
85
+ expect(rows.find(row => row.key === 'exports')).toMatchObject({
86
+ window: LimitWindow.Day,
87
+ windowStart: new Date('2026-09-15T00:00:00.000Z'),
88
+ resetsAt: new Date('2026-09-16T00:00:00.000Z'),
89
+ })
90
+ expect(rows.find(row => row.key === 'seats')).not.toHaveProperty('resetsAt')
91
+ })
92
+
93
+ test('remaining floors at zero when the counter over-counts', () => {
94
+ const seats = entitlementViewHelper.limitViewsOf(plan, [{ key: 'seats', window: 'occupancy', used: 7 }], null, at)
95
+ .find(row => row.key === 'seats')
96
+ expect(seats).toMatchObject({ limit: 3, used: 7, remaining: 0 })
97
+ })
98
+
99
+ test('a lapsed promo makes the limit zero', () => {
100
+ expect(entitlementViewHelper.limitViewsOf(plan, [], null, after).find(row => row.key === 'imports'))
101
+ .toMatchObject({ limit: 0, remaining: 0, promo: { active: false } })
102
+ })
103
+ })
104
+
105
+ const planView: EntitlementPlanView = {
106
+ sku: 'pro-monthly', productSku: 'pro', title: 'Pro', rank: 10, free: false,
107
+ status: SubscriptionStatus.Active, paygate: 'stripe', subscriptionId: 'sub_1',
108
+ subscribedAt: before, periodEnd: after,
109
+ }
110
+
111
+ describe('the entitlement view', () => {
112
+ test('promos are measured against the subscription date on the plan view', () => {
113
+ const grandfathered: ProductPlan = {
114
+ ...plan, capabilities: [{ scope: 'feature', permissions: { beta: true }, promo: { until, grandfather: true } }],
115
+ }
116
+ const view = entitlementViewHelper.entitlementViewOf(grandfathered, planView, [], after)
117
+ expect(view.capabilities[0]).toMatchObject({ granted: true, promo: { grandfathered: true } })
118
+ expect(entitlementViewHelper.entitlementViewOf(grandfathered, { ...planView, subscribedAt: undefined }, [], after)
119
+ .capabilities[0].granted).toBe(false)
120
+ expect(view.at).toBe(after)
121
+ })
122
+
123
+ test('the wire form revives into dates', () => {
124
+ const view = entitlementViewHelper.entitlementViewOf(plan, planView, [], before)
125
+ const revived = entitlementViewHelper.reviveEntitlementView(JSON.parse(JSON.stringify(view)))
126
+ expect(revived).toEqual(view)
127
+ expect(revived.limits.find(row => row.key === 'exports')?.resetsAt).toBeInstanceOf(Date)
128
+ expect(entitlementViewHelper.reviveEntitlementView(view)).toEqual(view)
129
+ })
130
+ })
package/build/.gitkeep DELETED
File without changes
@@ -1,63 +0,0 @@
1
- import type { JSONSchemaType } from 'ajv'
2
- import type { PlanSubscription, SubscriptionPropagateBody } from '../types.js'
3
- import { ResourceValueSchema, PermissionSetSchema, DateSchema, EntityValueSchema, IdValueSchema } from '@owlmeans/auth'
4
- import { SubscriptionStatusSchema } from '../consts.js'
5
- import { CapabilityUsageSchema, LimitConfigSchema } from './utils.js'
6
-
7
- export const PlanSubscriptionSchema: JSONSchemaType<PlanSubscription> = {
8
- type: 'object',
9
- properties: {
10
- sku: ResourceValueSchema,
11
- entityId: EntityValueSchema,
12
- paymentMethod: { ...IdValueSchema, nullable: true },
13
- externalId: { ...IdValueSchema, nullable: true },
14
- createdAt: DateSchema,
15
- lastPaymentAt: { ...DateSchema, nullable: true },
16
- trialUntil: { ...DateSchema, nullable: true },
17
- startsdAt: DateSchema,
18
- expirationAt: { ...DateSchema, nullable: true },
19
- endsAt: { ...DateSchema, nullable: true },
20
- archiveAt: { ...DateSchema, nullable: true },
21
- suspendedAt: { ...DateSchema, nullable: true },
22
- canceledAt: { ...DateSchema, nullable: true },
23
- blockedAt: { ...DateSchema, nullable: true },
24
- suspendedUntil: { ...DateSchema, nullable: true },
25
- status: SubscriptionStatusSchema,
26
- capabilities: { type: 'array', items: PermissionSetSchema, nullable: true },
27
- limits: {
28
- type: 'object', required: [], nullable: true,
29
- additionalProperties: LimitConfigSchema
30
- },
31
- consumptions: {
32
- type: 'object', required: [], nullable: true,
33
- additionalProperties: CapabilityUsageSchema
34
- }
35
- },
36
- required: ['sku', 'entityId', 'createdAt', 'startsdAt', 'status'],
37
- additionalProperties: false,
38
- }
39
-
40
- export const SubscriptionPropagateBodySchema: JSONSchemaType<SubscriptionPropagateBody> = {
41
- type: 'object',
42
- properties: {
43
- ...PlanSubscriptionSchema.properties,
44
- service: { ...ResourceValueSchema, minLength: 2 },
45
- externalId: IdValueSchema,
46
- } as any, // @TODO Figure out why it doesn't work (probably different version of ajv)
47
- required: ['externalId', 'service', ...PlanSubscriptionSchema.required],
48
- additionalProperties: false,
49
- }
50
-
51
- /**
52
- * @deprecated Use SubscriptionPropogateBodySchema instead
53
- */
54
- export const SubscriptionPropogateBodySchema: JSONSchemaType<SubscriptionPropagateBody> = {
55
- type: 'object',
56
- properties: {
57
- ...PlanSubscriptionSchema.properties,
58
- service: { ...ResourceValueSchema, minLength: 2 },
59
- externalId: IdValueSchema,
60
- } as any, // @TODO Figure out why it doesn't work (probably different version of ajv)
61
- required: ['externalId', 'service', ...PlanSubscriptionSchema.required],
62
- additionalProperties: false,
63
- }
@@ -1,32 +0,0 @@
1
-
2
- import type { JSONSchemaType } from 'ajv'
3
- import type { LimitConfig, CapabilityUsage } from '../types.js'
4
- import { PlanDurationSchema } from '../consts.js'
5
- import { DateSchema } from '@owlmeans/auth'
6
-
7
- export const LimitConfigSchema: JSONSchemaType<LimitConfig> = {
8
- type: 'object',
9
- properties: {
10
- interval: PlanDurationSchema,
11
- measurment: { type: 'string', nullable: true, maxLength: 16 },
12
- limit: { type: 'number' }
13
- },
14
- required: ['interval', 'limit'],
15
- additionalProperties: false,
16
- }
17
-
18
- export const CapabilityUsageSchema: JSONSchemaType<CapabilityUsage> = {
19
- type: 'object',
20
- properties: {
21
- interval: PlanDurationSchema,
22
- measurment: { type: 'string', nullable: true, maxLength: 16 },
23
- startedAt: DateSchema,
24
- limit: { type: 'number' },
25
- refreshedAt: { ...DateSchema, nullable: true },
26
- refreshAt: { ...DateSchema, nullable: true },
27
- lastConsumedAt: { ...DateSchema, nullable: true },
28
- consumption: { type: 'number' }
29
- },
30
- required: ['interval', 'limit', 'startedAt', 'consumption'],
31
- additionalProperties: false,
32
- }
package/src/modules.ts DELETED
@@ -1,43 +0,0 @@
1
- import { body, filter, entrypoint } from '@owlmeans/entrypoint'
2
- import { route, backend, RouteMethod } from '@owlmeans/route'
3
- import { paymentApi } from './consts.js'
4
- import { SubscriptionPropagateBodySchema } from './model/subscription.js'
5
- import { CreateCheckoutBodySchema } from './model/checkout.js'
6
-
7
- export const modules = [
8
- entrypoint(route(paymentApi.subscription.base, '/subscription', backend())),
9
- entrypoint(
10
- route(paymentApi.subscription.propogate, '/propogate', backend(
11
- paymentApi.subscription.base,
12
- RouteMethod.POST
13
- )),
14
- filter(body(SubscriptionPropagateBodySchema))
15
- ),
16
- entrypoint(
17
- route(paymentApi.subscription.propagate, '/propagate', backend(
18
- paymentApi.subscription.base,
19
- RouteMethod.POST
20
- )),
21
- filter(body(SubscriptionPropagateBodySchema))
22
- )
23
- ]
24
-
25
- export const serviceModules = [
26
- entrypoint(route(paymentApi.service.base, '/payment-service', backend())),
27
- entrypoint(
28
- route(paymentApi.service.checkout.base, '/checkout', backend(paymentApi.service.base)),
29
- ),
30
- entrypoint(
31
- route(paymentApi.service.checkout.session.base, '/session', backend(paymentApi.service.checkout.base)),
32
- ),
33
- entrypoint(
34
- route(paymentApi.service.checkout.session.external.base, '/external', backend(paymentApi.service.checkout.session.base)),
35
- ),
36
- entrypoint(
37
- route(paymentApi.service.checkout.session.external.create, '/create', backend(
38
- paymentApi.service.checkout.session.external.base,
39
- RouteMethod.POST
40
- )),
41
- filter(body(CreateCheckoutBodySchema))
42
- )
43
- ]
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=entitlement.spec.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entitlement.spec.d.ts","sourceRoot":"","sources":["entitlement.spec.ts"],"names":[],"mappings":""}
@@ -1,72 +0,0 @@
1
- import { describe, test, expect } from 'bun:test';
2
- import { CAPABILITY_FEATURE_SCOPE, entitlementList, formatEntitlementParam, hasEntitlement, parseEntitlementParam, } from '../src/entitlement.js';
3
- const pro = [
4
- { scope: 'renewable', permissions: { production: 1, credits: 5000 } },
5
- { scope: CAPABILITY_FEATURE_SCOPE, permissions: { 'branding--whitelabel': true, 'domain--custom': true } },
6
- ];
7
- describe('the parameter grammar', () => {
8
- test('a bare permission has no scope and no floor', () => {
9
- expect(parseEntitlementParam('domain--custom')).toEqual({ permission: 'domain--custom' });
10
- });
11
- test('a scope is everything before the first colon', () => {
12
- expect(parseEntitlementParam('feature:branding--whitelabel'))
13
- .toEqual({ scope: 'feature', permission: 'branding--whitelabel' });
14
- });
15
- test('a floor is parsed as a number', () => {
16
- expect(parseEntitlementParam('renewable:credits>=100'))
17
- .toEqual({ scope: 'renewable', permission: 'credits', atLeast: 100 });
18
- });
19
- test('formatting round-trips', () => {
20
- for (const param of ['domain--custom', 'feature:branding--whitelabel', 'renewable:credits>=100']) {
21
- expect(formatEntitlementParam(parseEntitlementParam(param))).toBe(param);
22
- }
23
- });
24
- });
25
- describe('the predicate', () => {
26
- test('a held feature passes', () => {
27
- expect(hasEntitlement(pro, 'feature:branding--whitelabel')).toBe(true);
28
- });
29
- test('a feature nobody granted does not', () => {
30
- expect(hasEntitlement(pro, 'feature:branding--nonexistent')).toBe(false);
31
- });
32
- test('a scope that does not match is not consulted', () => {
33
- expect(hasEntitlement(pro, 'renewable:branding--whitelabel')).toBe(false);
34
- // …and without a scope, any set may answer.
35
- expect(hasEntitlement(pro, 'branding--whitelabel')).toBe(true);
36
- });
37
- test('a numeric floor compares', () => {
38
- expect(hasEntitlement(pro, 'renewable:credits>=5000')).toBe(true);
39
- expect(hasEntitlement(pro, 'renewable:credits>=5001')).toBe(false);
40
- });
41
- test('a boolean cannot satisfy a numeric floor', () => {
42
- expect(hasEntitlement(pro, 'feature:branding--whitelabel>=1')).toBe(false);
43
- });
44
- test('an explicit false is not a grant', () => {
45
- expect(hasEntitlement([{ scope: 'feature', permissions: { x: false } }], 'feature:x')).toBe(false);
46
- expect(hasEntitlement([{ scope: 'feature', permissions: { x: null } }], 'feature:x')).toBe(false);
47
- });
48
- test('no capabilities at all is a refusal, never a pass', () => {
49
- expect(hasEntitlement(undefined, 'feature:branding--whitelabel')).toBe(false);
50
- expect(hasEntitlement([], 'feature:branding--whitelabel')).toBe(false);
51
- });
52
- test('a malformed parameter refuses rather than throwing', () => {
53
- // A gate that crashed on a typo would take down the endpoint it guards.
54
- expect(() => hasEntitlement(pro, '')).not.toThrow();
55
- expect(hasEntitlement(pro, '')).toBe(false);
56
- expect(hasEntitlement(pro, 'feature:')).toBe(false);
57
- expect(hasEntitlement(pro, 'renewable:credits>=abc')).toBe(true);
58
- });
59
- });
60
- describe('listing', () => {
61
- test('unions every granted capability, scoped', () => {
62
- expect(entitlementList(pro).sort()).toEqual([
63
- 'feature:branding--whitelabel', 'feature:domain--custom',
64
- 'renewable:credits', 'renewable:production',
65
- ]);
66
- });
67
- test('skips what is not granted', () => {
68
- expect(entitlementList([{ scope: 'feature', permissions: { a: true, b: false, c: null } }]))
69
- .toEqual(['feature:a']);
70
- });
71
- });
72
- //# sourceMappingURL=entitlement.spec.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"entitlement.spec.js","sourceRoot":"","sources":["entitlement.spec.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,UAAU,CAAA;AACjD,OAAO,EACL,wBAAwB,EAAE,eAAe,EAAE,sBAAsB,EAAE,cAAc,EACjF,qBAAqB,GACtB,MAAM,uBAAuB,CAAA;AAG9B,MAAM,GAAG,GAAoB;IAC3B,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;IACrE,EAAE,KAAK,EAAE,wBAAwB,EAAE,WAAW,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,EAAE;CAC3G,CAAA;AAED,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,IAAI,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,qBAAqB,CAAC,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC,CAAA;IAC3F,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,qBAAqB,CAAC,8BAA8B,CAAC,CAAC;aAC1D,OAAO,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,sBAAsB,EAAE,CAAC,CAAA;IACtE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,qBAAqB,CAAC,wBAAwB,CAAC,CAAC;aACpD,OAAO,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAA;IACzE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAClC,KAAK,MAAM,KAAK,IAAI,CAAC,gBAAgB,EAAE,8BAA8B,EAAE,wBAAwB,CAAC,EAAE,CAAC;YACjG,MAAM,CAAC,sBAAsB,CAAC,qBAAqB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC1E,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,IAAI,CAAC,uBAAuB,EAAE,GAAG,EAAE;QACjC,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,+BAA+B,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC1E,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,gCAAgC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACzE,4CAA4C;QAC5C,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,sBAAsB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,0BAA0B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACjE,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,yBAAyB,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,0CAA0C,EAAE,GAAG,EAAE;QACpD,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,iCAAiC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC5E,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClG,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACnG,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC7E,MAAM,CAAC,cAAc,CAAC,EAAE,EAAE,8BAA8B,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC9D,wEAAwE;QACxE,MAAM,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;QACnD,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAC3C,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACnD,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,wBAAwB,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IAClE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,IAAI,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC;YAC1C,8BAA8B,EAAE,wBAAwB;YACxD,mBAAmB,EAAE,sBAAsB;SAC5C,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,IAAI,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,eAAe,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC;aACzF,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,CAAA;IAC3B,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}