@owlmeans/server-payment 0.1.18-rc.11 → 0.1.18-rc.13

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 (69) hide show
  1. package/README.md +1 -1
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/server-payment/SKILL.md +67 -7
  4. package/build/config.d.ts +15 -1
  5. package/build/config.d.ts.map +1 -1
  6. package/build/config.js +40 -2
  7. package/build/config.js.map +1 -1
  8. package/build/consts.d.ts +10 -0
  9. package/build/consts.d.ts.map +1 -1
  10. package/build/consts.js +10 -0
  11. package/build/consts.js.map +1 -1
  12. package/build/index.d.ts +2 -0
  13. package/build/index.d.ts.map +1 -1
  14. package/build/index.js +1 -0
  15. package/build/index.js.map +1 -1
  16. package/build/model.d.ts.map +1 -1
  17. package/build/model.js +2 -1
  18. package/build/model.js.map +1 -1
  19. package/build/plugins/estimate.d.ts +43 -0
  20. package/build/plugins/estimate.d.ts.map +1 -0
  21. package/build/plugins/estimate.js +235 -0
  22. package/build/plugins/estimate.js.map +1 -0
  23. package/build/plugins/events.d.ts.map +1 -1
  24. package/build/plugins/events.js +17 -4
  25. package/build/plugins/events.js.map +1 -1
  26. package/build/plugins/fx.d.ts +26 -0
  27. package/build/plugins/fx.d.ts.map +1 -0
  28. package/build/plugins/fx.js +56 -0
  29. package/build/plugins/fx.js.map +1 -0
  30. package/build/plugins/portal.d.ts.map +1 -1
  31. package/build/plugins/portal.js +6 -2
  32. package/build/plugins/portal.js.map +1 -1
  33. package/build/plugins/stripe.d.ts +2 -1
  34. package/build/plugins/stripe.d.ts.map +1 -1
  35. package/build/plugins/stripe.js +51 -19
  36. package/build/plugins/stripe.js.map +1 -1
  37. package/build/service.d.ts.map +1 -1
  38. package/build/service.js +6 -0
  39. package/build/service.js.map +1 -1
  40. package/build/sync.d.ts +5 -0
  41. package/build/sync.d.ts.map +1 -1
  42. package/build/sync.js +80 -14
  43. package/build/sync.js.map +1 -1
  44. package/build/types.d.ts +46 -1
  45. package/build/types.d.ts.map +1 -1
  46. package/build/utils.d.ts +3 -1
  47. package/build/utils.d.ts.map +1 -1
  48. package/build/utils.js +3 -1
  49. package/build/utils.js.map +1 -1
  50. package/package.json +16 -16
  51. package/src/config.ts +47 -5
  52. package/src/consts.ts +11 -0
  53. package/src/index.ts +2 -0
  54. package/src/model.ts +2 -1
  55. package/src/plugins/estimate.ts +299 -0
  56. package/src/plugins/events.ts +17 -4
  57. package/src/plugins/fx.ts +93 -0
  58. package/src/plugins/portal.ts +6 -2
  59. package/src/plugins/stripe.ts +55 -19
  60. package/src/service.ts +6 -0
  61. package/src/sync.ts +101 -13
  62. package/src/types.ts +49 -2
  63. package/src/utils.ts +6 -2
  64. package/tests/checkout.spec.ts +118 -2
  65. package/tests/estimate.spec.ts +240 -0
  66. package/tests/events.spec.ts +13 -0
  67. package/tests/fake-stripe.ts +130 -6
  68. package/tests/portal.spec.ts +19 -2
  69. package/tests/sync.spec.ts +114 -0
@@ -0,0 +1,114 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { TaxBehavior } from '@owlmeans/payment'
3
+ import type { PricingDef } from '../src/types.js'
4
+ import { syncStripeProducts } from '../src/sync.js'
5
+ import { makeFakeContext, PLANS_PRODUCT, PRO } from './fake-stripe.js'
6
+ import type { FakeContextOptions } from './fake-stripe.js'
7
+
8
+ const exclusive: PricingDef = {
9
+ tax: { automatic: true, collectTaxId: true, estimate: false, behavior: TaxBehavior.Exclusive },
10
+ currency: { estimate: false },
11
+ }
12
+
13
+ const inclusiveAccountDefault = {
14
+ defaults: { tax_behavior: 'inclusive' as const, tax_code: null }, head_office: null,
15
+ status: 'active' as const, status_details: {},
16
+ }
17
+
18
+ /** A price already in place, matching PRO's lookup key, amount, currency and interval. */
19
+ const existingProPrice = (taxBehavior: string, id = 'price_pro_existing') => ({
20
+ id, product: PLANS_PRODUCT, lookup_key: PRO, active: true, unit_amount: 2_000, currency: 'usd',
21
+ recurring: { interval: 'month' }, tax_behavior: taxBehavior,
22
+ })
23
+
24
+ const withProProduct = async (opts: FakeContextOptions = {}) => await makeFakeContext(opts)
25
+ const proPrice = (fake: Awaited<ReturnType<typeof withProProduct>>) =>
26
+ fake.state.prices.find(item => item.lookup_key === PRO && item.active)
27
+
28
+ describe('@owlmeans/server-payment — price tax_behavior sync', () => {
29
+ test('an undeclared policy creates a price with no forced tax_behavior (Stripe defaults it to unspecified)', async () => {
30
+ const fake = await withProProduct()
31
+ await syncStripeProducts(fake.ctx, fake.stripe)
32
+ expect(proPrice(fake)?.tax_behavior).toBe('unspecified')
33
+ })
34
+
35
+ test('re-syncing an unchanged, undeclared catalogue makes no paygate call', async () => {
36
+ const fake = await withProProduct()
37
+ await syncStripeProducts(fake.ctx, fake.stripe)
38
+ fake.state.calls.length = 0
39
+ await syncStripeProducts(fake.ctx, fake.stripe)
40
+ expect(fake.state.calls).toEqual([])
41
+ })
42
+
43
+ test('a declared behavior sets tax_behavior on a freshly created price', async () => {
44
+ const fake = await withProProduct({ pricing: exclusive })
45
+ await syncStripeProducts(fake.ctx, fake.stripe)
46
+ expect(proPrice(fake)?.tax_behavior).toBe('exclusive')
47
+ })
48
+
49
+ test("a declared behavior updates a pre-existing 'unspecified' price IN PLACE — the id is kept", async () => {
50
+ const fake = await withProProduct({ pricing: exclusive, stripe: { prices: [existingProPrice('unspecified')] } })
51
+ await syncStripeProducts(fake.ctx, fake.stripe)
52
+ expect(fake.state.calls).toContain('prices.update')
53
+ // PRO's own price keeps its id (an update, not a replacement) — TEAM's plan has no price
54
+ // fixture, so it legitimately goes through `prices.create` alongside it.
55
+ expect(proPrice(fake)).toEqual(expect.objectContaining({ id: 'price_pro_existing', tax_behavior: 'exclusive' }))
56
+ })
57
+
58
+ test("an inclusive account default skips the in-place update and leaves the price 'unspecified'", async () => {
59
+ const fake = await withProProduct({
60
+ pricing: exclusive, stripe: { prices: [existingProPrice('unspecified')], taxSettings: inclusiveAccountDefault },
61
+ })
62
+ await syncStripeProducts(fake.ctx, fake.stripe)
63
+ expect(fake.state.calls).toContain('tax.settings.retrieve')
64
+ expect(fake.state.calls).not.toContain('prices.update')
65
+ expect(proPrice(fake)?.tax_behavior).toBe('unspecified')
66
+ })
67
+
68
+ test("'stripe.migrateUnspecifiedPrices' overrides the account-default guard, without even checking it", async () => {
69
+ const fake = await withProProduct({
70
+ pricing: { ...exclusive, stripe: { migrateUnspecifiedPrices: true } },
71
+ stripe: { prices: [existingProPrice('unspecified')], taxSettings: inclusiveAccountDefault },
72
+ })
73
+ await syncStripeProducts(fake.ctx, fake.stripe)
74
+ expect(fake.state.calls).not.toContain('tax.settings.retrieve')
75
+ expect(proPrice(fake)?.tax_behavior).toBe('exclusive')
76
+ })
77
+
78
+ test('a price already carrying the OPPOSITE behavior is deactivated and replaced, never mutated', async () => {
79
+ const fake = await withProProduct({ pricing: exclusive, stripe: { prices: [existingProPrice('inclusive')] } })
80
+ await syncStripeProducts(fake.ctx, fake.stripe)
81
+ const old = fake.state.prices.find(item => item.id === 'price_pro_existing')
82
+ const fresh = proPrice(fake)
83
+ expect(old?.active).toBe(false)
84
+ expect(old?.tax_behavior).toBe('inclusive')
85
+ expect(fresh?.id).not.toBe('price_pro_existing')
86
+ expect(fresh).toEqual(expect.objectContaining({ tax_behavior: 'exclusive', lookup_key: PRO }))
87
+ })
88
+
89
+ test('recurring USD catalogue prices are synchronized as EUR settlement prices', async () => {
90
+ const fake = await withProProduct({
91
+ pricing: { ...exclusive, stripe: { settlementCurrency: 'eur' } },
92
+ stripe: { fxRates: { usd: { exchangeRate: 0.853568, referenceRate: 0.8726 } } },
93
+ })
94
+ await syncStripeProducts(fake.ctx, fake.stripe)
95
+ expect(proPrice(fake)).toEqual(expect.objectContaining({ currency: 'eur', unit_amount: 1_746 }))
96
+ expect(fake.state.rawRequests).toContainEqual(expect.objectContaining({
97
+ params: { to_currency: 'eur', 'from_currencies[]': 'usd', lock_duration: 'none' },
98
+ }))
99
+ })
100
+
101
+ test('a changed reference rate replaces a recurring settlement price on the next sync', async () => {
102
+ const fake = await withProProduct({
103
+ pricing: { ...exclusive, stripe: { settlementCurrency: 'eur' } },
104
+ stripe: { fxRates: { usd: { exchangeRate: 0.853568, referenceRate: 0.8726 } } },
105
+ })
106
+ await syncStripeProducts(fake.ctx, fake.stripe)
107
+ const first = proPrice(fake)
108
+ fake.state.fxRates.usd = { exchangeRate: 0.88, referenceRate: 0.9 }
109
+ await syncStripeProducts(fake.ctx, fake.stripe)
110
+ expect(first?.active).toBe(false)
111
+ expect(proPrice(fake)).toEqual(expect.objectContaining({ currency: 'eur', unit_amount: 1_800 }))
112
+ expect(proPrice(fake)?.id).not.toBe(first?.id)
113
+ })
114
+ })