@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.
- package/README.md +95 -57
- package/agent-meta/manifest.json +10 -3
- package/agent-meta/skills/entitlements/SKILL.md +159 -0
- package/agent-meta/skills/payment/SKILL.md +491 -28
- package/build/advertise.d.ts +2 -0
- package/build/advertise.d.ts.map +1 -0
- package/build/advertise.js +10 -0
- package/build/advertise.js.map +1 -0
- package/build/checkout/consts.d.ts +2 -0
- package/build/checkout/consts.d.ts.map +1 -0
- package/build/checkout/consts.js +2 -0
- package/build/checkout/consts.js.map +1 -0
- package/build/checkout/index.d.ts +6 -0
- package/build/checkout/index.d.ts.map +1 -0
- package/build/checkout/index.js +4 -0
- package/build/checkout/index.js.map +1 -0
- package/build/checkout/narrow/types.d.ts +25 -0
- package/build/checkout/narrow/types.d.ts.map +1 -0
- package/build/checkout/narrow/types.js +2 -0
- package/build/checkout/narrow/types.js.map +1 -0
- package/build/checkout/narrow.d.ts +8 -0
- package/build/checkout/narrow.d.ts.map +1 -0
- package/build/checkout/narrow.js +47 -0
- package/build/checkout/narrow.js.map +1 -0
- package/build/checkout/protocols.d.ts +8 -0
- package/build/checkout/protocols.d.ts.map +1 -0
- package/build/checkout/protocols.js +28 -0
- package/build/checkout/protocols.js.map +1 -0
- package/build/checkout/types.d.ts +29 -0
- package/build/checkout/types.d.ts.map +1 -0
- package/build/checkout/types.js +2 -0
- package/build/checkout/types.js.map +1 -0
- package/build/consts.d.ts +242 -25
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +377 -24
- package/build/consts.js.map +1 -1
- package/build/consts.local.d.ts +20 -0
- package/build/consts.local.d.ts.map +1 -0
- package/build/consts.local.js +23 -0
- package/build/consts.local.js.map +1 -0
- package/build/consumer/cancel.d.ts +11 -0
- package/build/consumer/cancel.d.ts.map +1 -0
- package/build/consumer/cancel.js +33 -0
- package/build/consumer/cancel.js.map +1 -0
- package/build/consumer/consts.d.ts +20 -0
- package/build/consumer/consts.d.ts.map +1 -0
- package/build/consumer/consts.js +39 -0
- package/build/consumer/consts.js.map +1 -0
- package/build/consumer/consts.local.d.ts +7 -0
- package/build/consumer/consts.local.d.ts.map +1 -0
- package/build/consumer/consts.local.js +13 -0
- package/build/consumer/consts.local.js.map +1 -0
- package/build/consumer/copy/types.d.ts +56 -0
- package/build/consumer/copy/types.d.ts.map +1 -0
- package/build/consumer/copy/types.js +2 -0
- package/build/consumer/copy/types.js.map +1 -0
- package/build/consumer/copy.d.ts +22 -0
- package/build/consumer/copy.d.ts.map +1 -0
- package/build/consumer/copy.js +87 -0
- package/build/consumer/copy.js.map +1 -0
- package/build/consumer/deadline/types.d.ts +30 -0
- package/build/consumer/deadline/types.d.ts.map +1 -0
- package/build/consumer/deadline/types.js +2 -0
- package/build/consumer/deadline/types.js.map +1 -0
- package/build/consumer/deadline.d.ts +12 -0
- package/build/consumer/deadline.d.ts.map +1 -0
- package/build/consumer/deadline.js +44 -0
- package/build/consumer/deadline.js.map +1 -0
- package/build/consumer/fifo/types.d.ts +17 -0
- package/build/consumer/fifo/types.d.ts.map +1 -0
- package/build/consumer/fifo/types.js +2 -0
- package/build/consumer/fifo/types.js.map +1 -0
- package/build/consumer/fifo.d.ts +4 -0
- package/build/consumer/fifo.d.ts.map +1 -0
- package/build/consumer/fifo.js +34 -0
- package/build/consumer/fifo.js.map +1 -0
- package/build/consumer/index.d.ts +18 -0
- package/build/consumer/index.d.ts.map +1 -0
- package/build/consumer/index.js +11 -0
- package/build/consumer/index.js.map +1 -0
- package/build/consumer/policy/types.d.ts +20 -0
- package/build/consumer/policy/types.d.ts.map +1 -0
- package/build/consumer/policy/types.js +2 -0
- package/build/consumer/policy/types.js.map +1 -0
- package/build/consumer/policy.d.ts +17 -0
- package/build/consumer/policy.d.ts.map +1 -0
- package/build/consumer/policy.js +91 -0
- package/build/consumer/policy.js.map +1 -0
- package/build/consumer/protocols.d.ts +20 -0
- package/build/consumer/protocols.d.ts.map +1 -0
- package/build/consumer/protocols.js +70 -0
- package/build/consumer/protocols.js.map +1 -0
- package/build/consumer/refund/types.d.ts +27 -0
- package/build/consumer/refund/types.d.ts.map +1 -0
- package/build/consumer/refund/types.js +2 -0
- package/build/consumer/refund/types.js.map +1 -0
- package/build/consumer/refund.d.ts +9 -0
- package/build/consumer/refund.d.ts.map +1 -0
- package/build/consumer/refund.js +112 -0
- package/build/consumer/refund.js.map +1 -0
- package/build/consumer/refusal.d.ts +10 -0
- package/build/consumer/refusal.d.ts.map +1 -0
- package/build/consumer/refusal.js +35 -0
- package/build/consumer/refusal.js.map +1 -0
- package/build/consumer/revive/types.d.ts +19 -0
- package/build/consumer/revive/types.d.ts.map +1 -0
- package/build/consumer/revive/types.js +2 -0
- package/build/consumer/revive/types.js.map +1 -0
- package/build/consumer/revive.d.ts +13 -0
- package/build/consumer/revive.d.ts.map +1 -0
- package/build/consumer/revive.js +43 -0
- package/build/consumer/revive.js.map +1 -0
- package/build/consumer/types.d.ts +196 -0
- package/build/consumer/types.d.ts.map +1 -0
- package/build/consumer/types.js +2 -0
- package/build/consumer/types.js.map +1 -0
- package/build/consumer/types.local.d.ts +11 -0
- package/build/consumer/types.local.d.ts.map +1 -0
- package/build/consumer/types.local.js +2 -0
- package/build/consumer/types.local.js.map +1 -0
- package/build/countries.d.ts +3 -0
- package/build/countries.d.ts.map +1 -0
- package/build/countries.js +4 -0
- package/build/countries.js.map +1 -0
- package/build/entitled.d.ts +14 -0
- package/build/entitled.d.ts.map +1 -0
- package/build/entitled.js +17 -0
- package/build/entitled.js.map +1 -0
- package/build/entitlement/types.d.ts +29 -0
- package/build/entitlement/types.d.ts.map +1 -0
- package/build/entitlement/types.js +2 -0
- package/build/entitlement/types.js.map +1 -0
- package/build/entitlement.d.ts +7 -55
- package/build/entitlement.d.ts.map +1 -1
- package/build/entitlement.js +55 -86
- package/build/entitlement.js.map +1 -1
- package/build/errors.d.ts +189 -0
- package/build/errors.d.ts.map +1 -1
- package/build/errors.js +361 -0
- package/build/errors.js.map +1 -1
- package/build/estimate/types.d.ts +34 -0
- package/build/estimate/types.d.ts.map +1 -0
- package/build/estimate/types.js +2 -0
- package/build/estimate/types.js.map +1 -0
- package/build/estimate.d.ts +11 -0
- package/build/estimate.d.ts.map +1 -0
- package/build/estimate.js +76 -0
- package/build/estimate.js.map +1 -0
- package/build/i18n/consumer-rights/be.json +131 -0
- package/build/i18n/consumer-rights/de.json +131 -0
- package/build/i18n/consumer-rights/en.json +131 -0
- package/build/i18n/consumer-rights/es.json +131 -0
- package/build/i18n/consumer-rights/fr.json +131 -0
- package/build/i18n/consumer-rights/pl.json +131 -0
- package/build/i18n/consumer-rights/ru.json +131 -0
- package/build/i18n/consumer-rights/uk.json +131 -0
- package/build/i18n/errors/be.json +30 -0
- package/build/i18n/errors/de.json +30 -0
- package/build/i18n/errors/en.json +30 -0
- package/build/i18n/errors/es.json +30 -0
- package/build/i18n/errors/fr.json +30 -0
- package/build/i18n/errors/pl.json +30 -0
- package/build/i18n/errors/ru.json +30 -0
- package/build/i18n/errors/uk.json +30 -0
- package/build/i18n/fr.json +39 -0
- package/build/i18n.js +53 -8
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +18 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +11 -1
- package/build/index.js.map +1 -1
- package/build/limit/types.d.ts +44 -0
- package/build/limit/types.d.ts.map +1 -0
- package/build/limit/types.js +2 -0
- package/build/limit/types.js.map +1 -0
- package/build/limit.d.ts +23 -0
- package/build/limit.d.ts.map +1 -0
- package/build/limit.js +97 -0
- package/build/limit.js.map +1 -0
- package/build/model/checkout.d.ts +2 -3
- package/build/model/checkout.d.ts.map +1 -1
- package/build/model/checkout.js +12 -7
- package/build/model/checkout.js.map +1 -1
- package/build/model/consts.d.ts +15 -0
- package/build/model/consts.d.ts.map +1 -0
- package/build/model/consts.js +193 -0
- package/build/model/consts.js.map +1 -0
- package/build/model/consts.local.d.ts +35 -0
- package/build/model/consts.local.d.ts.map +1 -0
- package/build/model/consts.local.js +65 -0
- package/build/model/consts.local.js.map +1 -0
- package/build/model/consumer.d.ts +23 -0
- package/build/model/consumer.d.ts.map +1 -0
- package/build/model/consumer.js +272 -0
- package/build/model/consumer.js.map +1 -0
- package/build/model/estimate.d.ts +5 -0
- package/build/model/estimate.d.ts.map +1 -0
- package/build/model/estimate.js +69 -0
- package/build/model/estimate.js.map +1 -0
- package/build/model/index.d.ts +7 -2
- package/build/model/index.d.ts.map +1 -1
- package/build/model/index.js +7 -2
- package/build/model/index.js.map +1 -1
- package/build/model/limit.d.ts +7 -0
- package/build/model/limit.d.ts.map +1 -0
- package/build/model/limit.js +32 -0
- package/build/model/limit.js.map +1 -0
- package/build/model/plan.d.ts.map +1 -1
- package/build/model/plan.js +24 -11
- package/build/model/plan.js.map +1 -1
- package/build/model/portal.d.ts +4 -0
- package/build/model/portal.d.ts.map +1 -0
- package/build/model/portal.js +22 -0
- package/build/model/portal.js.map +1 -0
- package/build/model/pricing.d.ts +4 -0
- package/build/model/pricing.d.ts.map +1 -0
- package/build/model/pricing.js +31 -0
- package/build/model/pricing.js.map +1 -0
- package/build/model/view.d.ts +3 -0
- package/build/model/view.d.ts.map +1 -0
- package/build/model/view.js +15 -0
- package/build/model/view.js.map +1 -0
- package/build/pricing/types.d.ts +13 -0
- package/build/pricing/types.d.ts.map +1 -0
- package/build/pricing/types.js +2 -0
- package/build/pricing/types.js.map +1 -0
- package/build/pricing.d.ts +13 -0
- package/build/pricing.d.ts.map +1 -0
- package/build/pricing.js +74 -0
- package/build/pricing.js.map +1 -0
- package/build/promo/types.d.ts +14 -0
- package/build/promo/types.d.ts.map +1 -0
- package/build/promo/types.js +2 -0
- package/build/promo/types.js.map +1 -0
- package/build/promo.d.ts +7 -0
- package/build/promo.d.ts.map +1 -0
- package/build/promo.js +25 -0
- package/build/promo.js.map +1 -0
- package/build/regions/types.d.ts +32 -0
- package/build/regions/types.d.ts.map +1 -0
- package/build/regions/types.js +2 -0
- package/build/regions/types.js.map +1 -0
- package/build/regions.d.ts +15 -0
- package/build/regions.d.ts.map +1 -0
- package/build/regions.js +43 -0
- package/build/regions.js.map +1 -0
- package/build/service.d.ts.map +1 -1
- package/build/service.js +24 -9
- package/build/service.js.map +1 -1
- package/build/types.d.ts +619 -52
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +4 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/view/types.d.ts +33 -0
- package/build/view/types.d.ts.map +1 -0
- package/build/view/types.js +2 -0
- package/build/view/types.js.map +1 -0
- package/build/view.d.ts +13 -0
- package/build/view.d.ts.map +1 -0
- package/build/view.js +78 -0
- package/build/view.js.map +1 -0
- package/package.json +14 -12
- package/src/advertise.ts +13 -0
- package/src/checkout/consts.ts +1 -0
- package/src/checkout/index.ts +5 -0
- package/src/checkout/narrow/types.ts +27 -0
- package/src/checkout/narrow.ts +63 -0
- package/src/checkout/protocols.ts +39 -0
- package/src/checkout/types.ts +27 -0
- package/src/consts.local.ts +34 -0
- package/src/consts.ts +417 -24
- package/src/consumer/cancel.ts +40 -0
- package/src/consumer/consts.local.ts +16 -0
- package/src/consumer/consts.ts +46 -0
- package/src/consumer/copy/types.ts +54 -0
- package/src/consumer/copy.ts +121 -0
- package/src/consumer/deadline/types.ts +30 -0
- package/src/consumer/deadline.ts +61 -0
- package/src/consumer/fifo/types.ts +17 -0
- package/src/consumer/fifo.ts +42 -0
- package/src/consumer/index.ts +17 -0
- package/src/consumer/policy/types.ts +20 -0
- package/src/consumer/policy.ts +101 -0
- package/src/consumer/protocols.ts +116 -0
- package/src/consumer/refund/types.ts +27 -0
- package/src/consumer/refund.ts +136 -0
- package/src/consumer/refusal.ts +35 -0
- package/src/consumer/revive/types.ts +23 -0
- package/src/consumer/revive.ts +82 -0
- package/src/consumer/types.local.ts +10 -0
- package/src/consumer/types.ts +189 -0
- package/src/countries.ts +5 -0
- package/src/entitled.ts +20 -0
- package/src/entitlement/types.ts +29 -0
- package/src/entitlement.ts +67 -103
- package/src/errors.ts +445 -0
- package/src/estimate/types.ts +34 -0
- package/src/estimate.ts +90 -0
- package/src/i18n/consumer-rights/be.json +131 -0
- package/src/i18n/consumer-rights/de.json +131 -0
- package/src/i18n/consumer-rights/en.json +131 -0
- package/src/i18n/consumer-rights/es.json +131 -0
- package/src/i18n/consumer-rights/fr.json +131 -0
- package/src/i18n/consumer-rights/pl.json +131 -0
- package/src/i18n/consumer-rights/ru.json +131 -0
- package/src/i18n/consumer-rights/uk.json +131 -0
- package/src/i18n/errors/be.json +30 -0
- package/src/i18n/errors/de.json +30 -0
- package/src/i18n/errors/en.json +30 -0
- package/src/i18n/errors/es.json +30 -0
- package/src/i18n/errors/fr.json +30 -0
- package/src/i18n/errors/pl.json +30 -0
- package/src/i18n/errors/ru.json +30 -0
- package/src/i18n/errors/uk.json +30 -0
- package/src/i18n/fr.json +39 -0
- package/src/i18n.ts +58 -8
- package/src/index.ts +18 -2
- package/src/limit/types.ts +41 -0
- package/src/limit.ts +132 -0
- package/src/model/checkout.ts +12 -7
- package/src/model/consts.local.ts +83 -0
- package/src/model/consts.ts +206 -0
- package/src/model/consumer.ts +294 -0
- package/src/model/estimate.ts +73 -0
- package/src/model/index.ts +7 -2
- package/src/model/limit.ts +36 -0
- package/src/model/plan.ts +25 -12
- package/src/model/portal.ts +25 -0
- package/src/model/pricing.ts +34 -0
- package/src/model/view.ts +17 -0
- package/src/pricing/types.ts +13 -0
- package/src/pricing.ts +110 -0
- package/src/promo/types.ts +18 -0
- package/src/promo.ts +46 -0
- package/src/regions/types.ts +42 -0
- package/src/regions.ts +85 -0
- package/src/service.ts +32 -15
- package/src/types.local.ts +5 -0
- package/src/types.ts +669 -49
- package/src/view/types.ts +39 -0
- package/src/view.ts +119 -0
- package/tests/checkout-narrow.spec.ts +75 -0
- package/tests/consumer-copy.spec.ts +378 -0
- package/tests/consumer-deadline.spec.ts +85 -0
- package/tests/consumer-fifo.spec.ts +61 -0
- package/tests/consumer-protocols.spec.ts +103 -0
- package/tests/consumer-refund.spec.ts +134 -0
- package/tests/consumer-regions.spec.ts +140 -0
- package/tests/consumer-wire.spec.ts +170 -0
- package/tests/contract.spec.ts +183 -0
- package/tests/entitlement.spec.ts +38 -25
- package/tests/errors.spec.ts +168 -0
- package/tests/estimate.spec.ts +139 -0
- package/tests/limit.spec.ts +117 -0
- package/tests/pricing.spec.ts +30 -0
- package/tests/view.spec.ts +130 -0
- package/build/.gitkeep +0 -0
- package/src/model/subscription.ts +0 -63
- package/src/model/utils.ts +0 -32
- package/src/modules.ts +0 -43
- package/tests/entitlement.spec.d.ts +0 -2
- package/tests/entitlement.spec.d.ts.map +0 -1
- package/tests/entitlement.spec.js +0 -72
- 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
|
-
}
|
package/src/model/utils.ts
DELETED
|
@@ -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 +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":"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"}
|