@skillsmith/core 0.6.3 → 0.7.2

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 (130) hide show
  1. package/CHANGELOG.md +22 -1
  2. package/README.md +18 -0
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/src/api/client.d.ts.map +1 -1
  5. package/dist/src/api/client.events.d.ts.map +1 -1
  6. package/dist/src/api/client.events.js +10 -1
  7. package/dist/src/api/client.events.js.map +1 -1
  8. package/dist/src/api/client.health.d.ts.map +1 -1
  9. package/dist/src/api/client.health.js +7 -1
  10. package/dist/src/api/client.health.js.map +1 -1
  11. package/dist/src/api/client.js +11 -1
  12. package/dist/src/api/client.js.map +1 -1
  13. package/dist/src/api/utils.d.ts +14 -3
  14. package/dist/src/api/utils.d.ts.map +1 -1
  15. package/dist/src/api/utils.js +14 -4
  16. package/dist/src/api/utils.js.map +1 -1
  17. package/dist/src/exports/services.d.ts +0 -1
  18. package/dist/src/exports/services.d.ts.map +1 -1
  19. package/dist/src/exports/services.js +14 -3
  20. package/dist/src/exports/services.js.map +1 -1
  21. package/dist/src/index.d.ts +2 -1
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +4 -1
  24. package/dist/src/index.js.map +1 -1
  25. package/dist/src/types.d.ts +12 -0
  26. package/dist/src/types.d.ts.map +1 -1
  27. package/dist/tests/api/client.auth.test.js +50 -0
  28. package/dist/tests/api/client.auth.test.js.map +1 -1
  29. package/dist/tests/api/utils.test.js +7 -3
  30. package/dist/tests/api/utils.test.js.map +1 -1
  31. package/dist/tests/{billing/stripe-validators.test.d.ts → security/sanitization-stripe-ids.test.d.ts} +1 -1
  32. package/dist/tests/security/sanitization-stripe-ids.test.d.ts.map +1 -0
  33. package/dist/tests/{billing/stripe-validators.test.js → security/sanitization-stripe-ids.test.js} +1 -1
  34. package/dist/tests/security/sanitization-stripe-ids.test.js.map +1 -0
  35. package/dist/tests/shared.test.js.map +1 -1
  36. package/package.json +2 -8
  37. package/dist/src/billing/BillingService.d.ts +0 -101
  38. package/dist/src/billing/BillingService.d.ts.map +0 -1
  39. package/dist/src/billing/BillingService.helpers.d.ts +0 -15
  40. package/dist/src/billing/BillingService.helpers.d.ts.map +0 -1
  41. package/dist/src/billing/BillingService.helpers.js +0 -45
  42. package/dist/src/billing/BillingService.helpers.js.map +0 -1
  43. package/dist/src/billing/BillingService.js +0 -263
  44. package/dist/src/billing/BillingService.js.map +0 -1
  45. package/dist/src/billing/BillingService.types.d.ts +0 -52
  46. package/dist/src/billing/BillingService.types.d.ts.map +0 -1
  47. package/dist/src/billing/BillingService.types.js +0 -6
  48. package/dist/src/billing/BillingService.types.js.map +0 -1
  49. package/dist/src/billing/GDPRComplianceService.d.ts +0 -81
  50. package/dist/src/billing/GDPRComplianceService.d.ts.map +0 -1
  51. package/dist/src/billing/GDPRComplianceService.js +0 -361
  52. package/dist/src/billing/GDPRComplianceService.js.map +0 -1
  53. package/dist/src/billing/StripeClient.d.ts +0 -119
  54. package/dist/src/billing/StripeClient.d.ts.map +0 -1
  55. package/dist/src/billing/StripeClient.js +0 -405
  56. package/dist/src/billing/StripeClient.js.map +0 -1
  57. package/dist/src/billing/StripeReconciliationJob.d.ts +0 -50
  58. package/dist/src/billing/StripeReconciliationJob.d.ts.map +0 -1
  59. package/dist/src/billing/StripeReconciliationJob.js +0 -365
  60. package/dist/src/billing/StripeReconciliationJob.js.map +0 -1
  61. package/dist/src/billing/StripeWebhookHandler.d.ts +0 -49
  62. package/dist/src/billing/StripeWebhookHandler.d.ts.map +0 -1
  63. package/dist/src/billing/StripeWebhookHandler.js +0 -162
  64. package/dist/src/billing/StripeWebhookHandler.js.map +0 -1
  65. package/dist/src/billing/gdpr-types.d.ts +0 -103
  66. package/dist/src/billing/gdpr-types.d.ts.map +0 -1
  67. package/dist/src/billing/gdpr-types.js +0 -7
  68. package/dist/src/billing/gdpr-types.js.map +0 -1
  69. package/dist/src/billing/index.d.ts +0 -18
  70. package/dist/src/billing/index.d.ts.map +0 -1
  71. package/dist/src/billing/index.js +0 -19
  72. package/dist/src/billing/index.js.map +0 -1
  73. package/dist/src/billing/reconciliation-helpers.d.ts +0 -16
  74. package/dist/src/billing/reconciliation-helpers.d.ts.map +0 -1
  75. package/dist/src/billing/reconciliation-helpers.js +0 -53
  76. package/dist/src/billing/reconciliation-helpers.js.map +0 -1
  77. package/dist/src/billing/reconciliation-types.d.ts +0 -71
  78. package/dist/src/billing/reconciliation-types.d.ts.map +0 -1
  79. package/dist/src/billing/reconciliation-types.js +0 -7
  80. package/dist/src/billing/reconciliation-types.js.map +0 -1
  81. package/dist/src/billing/stripe-client-types.d.ts +0 -45
  82. package/dist/src/billing/stripe-client-types.d.ts.map +0 -1
  83. package/dist/src/billing/stripe-client-types.js +0 -7
  84. package/dist/src/billing/stripe-client-types.js.map +0 -1
  85. package/dist/src/billing/stripe-helpers.d.ts +0 -17
  86. package/dist/src/billing/stripe-helpers.d.ts.map +0 -1
  87. package/dist/src/billing/stripe-helpers.js +0 -50
  88. package/dist/src/billing/stripe-helpers.js.map +0 -1
  89. package/dist/src/billing/types.d.ts +0 -266
  90. package/dist/src/billing/types.d.ts.map +0 -1
  91. package/dist/src/billing/types.js +0 -23
  92. package/dist/src/billing/types.js.map +0 -1
  93. package/dist/src/billing/webhook-handlers.d.ts +0 -56
  94. package/dist/src/billing/webhook-handlers.d.ts.map +0 -1
  95. package/dist/src/billing/webhook-handlers.js +0 -303
  96. package/dist/src/billing/webhook-handlers.js.map +0 -1
  97. package/dist/src/billing/webhook-types.d.ts +0 -42
  98. package/dist/src/billing/webhook-types.d.ts.map +0 -1
  99. package/dist/src/billing/webhook-types.js +0 -7
  100. package/dist/src/billing/webhook-types.js.map +0 -1
  101. package/dist/tests/billing/BillingService.test.d.ts +0 -7
  102. package/dist/tests/billing/BillingService.test.d.ts.map +0 -1
  103. package/dist/tests/billing/BillingService.test.js +0 -168
  104. package/dist/tests/billing/BillingService.test.js.map +0 -1
  105. package/dist/tests/billing/GDPRCompliance.test.d.ts +0 -7
  106. package/dist/tests/billing/GDPRCompliance.test.d.ts.map +0 -1
  107. package/dist/tests/billing/GDPRCompliance.test.js +0 -380
  108. package/dist/tests/billing/GDPRCompliance.test.js.map +0 -1
  109. package/dist/tests/billing/StripeClient.test.d.ts +0 -18
  110. package/dist/tests/billing/StripeClient.test.d.ts.map +0 -1
  111. package/dist/tests/billing/StripeClient.test.js +0 -566
  112. package/dist/tests/billing/StripeClient.test.js.map +0 -1
  113. package/dist/tests/billing/StripeReconciliation.test.d.ts +0 -7
  114. package/dist/tests/billing/StripeReconciliation.test.d.ts.map +0 -1
  115. package/dist/tests/billing/StripeReconciliation.test.js +0 -266
  116. package/dist/tests/billing/StripeReconciliation.test.js.map +0 -1
  117. package/dist/tests/billing/StripeWebhookHandler.test.d.ts +0 -16
  118. package/dist/tests/billing/StripeWebhookHandler.test.d.ts.map +0 -1
  119. package/dist/tests/billing/StripeWebhookHandler.test.js +0 -240
  120. package/dist/tests/billing/StripeWebhookHandler.test.js.map +0 -1
  121. package/dist/tests/billing/stripe-helpers.test.d.ts +0 -7
  122. package/dist/tests/billing/stripe-helpers.test.d.ts.map +0 -1
  123. package/dist/tests/billing/stripe-helpers.test.js +0 -91
  124. package/dist/tests/billing/stripe-helpers.test.js.map +0 -1
  125. package/dist/tests/billing/stripe-validators.test.d.ts.map +0 -1
  126. package/dist/tests/billing/stripe-validators.test.js.map +0 -1
  127. package/dist/tests/billing/webhook-handlers.test.d.ts +0 -16
  128. package/dist/tests/billing/webhook-handlers.test.d.ts.map +0 -1
  129. package/dist/tests/billing/webhook-handlers.test.js +0 -519
  130. package/dist/tests/billing/webhook-handlers.test.js.map +0 -1
@@ -1,91 +0,0 @@
1
- /**
2
- * SMI-3415: Stripe Helper Functions Tests
3
- *
4
- * Tests for mapSubscriptionStatus and getPriceIdForTier utilities.
5
- */
6
- import { describe, it, expect } from 'vitest';
7
- import { mapSubscriptionStatus, getPriceIdForTier } from '../../src/billing/stripe-helpers.js';
8
- // ============================================================================
9
- // Test Fixtures
10
- // ============================================================================
11
- function createTestPrices() {
12
- return {
13
- individual: {
14
- monthly: 'price_ind_monthly',
15
- annual: 'price_ind_annual',
16
- },
17
- team: {
18
- monthly: 'price_team_monthly',
19
- annual: 'price_team_annual',
20
- },
21
- enterprise: {
22
- monthly: 'price_ent_monthly',
23
- annual: 'price_ent_annual',
24
- },
25
- };
26
- }
27
- // ============================================================================
28
- // mapSubscriptionStatus Tests
29
- // ============================================================================
30
- describe('mapSubscriptionStatus', () => {
31
- it('should map active status', () => {
32
- expect(mapSubscriptionStatus('active')).toBe('active');
33
- });
34
- it('should map past_due status', () => {
35
- expect(mapSubscriptionStatus('past_due')).toBe('past_due');
36
- });
37
- it('should map canceled status', () => {
38
- expect(mapSubscriptionStatus('canceled')).toBe('canceled');
39
- });
40
- it('should map trialing status', () => {
41
- expect(mapSubscriptionStatus('trialing')).toBe('trialing');
42
- });
43
- it('should map paused status', () => {
44
- expect(mapSubscriptionStatus('paused')).toBe('paused');
45
- });
46
- it('should map incomplete status', () => {
47
- expect(mapSubscriptionStatus('incomplete')).toBe('incomplete');
48
- });
49
- it('should map incomplete_expired status', () => {
50
- expect(mapSubscriptionStatus('incomplete_expired')).toBe('incomplete_expired');
51
- });
52
- it('should map unpaid status', () => {
53
- expect(mapSubscriptionStatus('unpaid')).toBe('unpaid');
54
- });
55
- it('should default to active for unknown status', () => {
56
- // Cast to bypass type safety for edge case testing
57
- expect(mapSubscriptionStatus('unknown_status')).toBe('active');
58
- });
59
- });
60
- // ============================================================================
61
- // getPriceIdForTier Tests
62
- // ============================================================================
63
- describe('getPriceIdForTier', () => {
64
- const prices = createTestPrices();
65
- it('should return null for community tier', () => {
66
- expect(getPriceIdForTier(prices, 'community', 'monthly')).toBeNull();
67
- expect(getPriceIdForTier(prices, 'community', 'annual')).toBeNull();
68
- });
69
- it('should return monthly price for individual tier', () => {
70
- expect(getPriceIdForTier(prices, 'individual', 'monthly')).toBe('price_ind_monthly');
71
- });
72
- it('should return annual price for individual tier', () => {
73
- expect(getPriceIdForTier(prices, 'individual', 'annual')).toBe('price_ind_annual');
74
- });
75
- it('should return monthly price for team tier', () => {
76
- expect(getPriceIdForTier(prices, 'team', 'monthly')).toBe('price_team_monthly');
77
- });
78
- it('should return annual price for team tier', () => {
79
- expect(getPriceIdForTier(prices, 'team', 'annual')).toBe('price_team_annual');
80
- });
81
- it('should return monthly price for enterprise tier', () => {
82
- expect(getPriceIdForTier(prices, 'enterprise', 'monthly')).toBe('price_ent_monthly');
83
- });
84
- it('should return annual price for enterprise tier', () => {
85
- expect(getPriceIdForTier(prices, 'enterprise', 'annual')).toBe('price_ent_annual');
86
- });
87
- it('should return null for unknown tier', () => {
88
- expect(getPriceIdForTier(prices, 'unknown', 'monthly')).toBeNull();
89
- });
90
- });
91
- //# sourceMappingURL=stripe-helpers.test.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"stripe-helpers.test.js","sourceRoot":"","sources":["../../../tests/billing/stripe-helpers.test.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"stripe-validators.test.d.ts","sourceRoot":"","sources":["../../../tests/billing/stripe-validators.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"stripe-validators.test.js","sourceRoot":"","sources":["../../../tests/billing/stripe-validators.test.ts"],"names":[],"mappings":"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"}
@@ -1,16 +0,0 @@
1
- /**
2
- * SMI-3415: Webhook Handler Functions Tests
3
- *
4
- * Tests for individual webhook event handler functions:
5
- * - handleSubscriptionCreated (with/without license key, email)
6
- * - handleSubscriptionUpdated (existing sub, tier change, new sub fallback)
7
- * - handleSubscriptionDeleted (with email, without existing sub)
8
- * - handleInvoicePaymentSucceeded (store invoice, missing customer)
9
- * - handleInvoicePaymentFailed (store + email, missing customer)
10
- * - handleCheckoutSessionCompleted (logging-only)
11
- * - storeLicenseKey / revokeLicenseKey (DB operations)
12
- * - extractTier / extractSeatCount / getCurrentPeriodEnd / getCurrentPeriodStart
13
- * - extractSubscriptionIdFromInvoice
14
- */
15
- export {};
16
- //# sourceMappingURL=webhook-handlers.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"webhook-handlers.test.d.ts","sourceRoot":"","sources":["../../../tests/billing/webhook-handlers.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
@@ -1,519 +0,0 @@
1
- /**
2
- * SMI-3415: Webhook Handler Functions Tests
3
- *
4
- * Tests for individual webhook event handler functions:
5
- * - handleSubscriptionCreated (with/without license key, email)
6
- * - handleSubscriptionUpdated (existing sub, tier change, new sub fallback)
7
- * - handleSubscriptionDeleted (with email, without existing sub)
8
- * - handleInvoicePaymentSucceeded (store invoice, missing customer)
9
- * - handleInvoicePaymentFailed (store + email, missing customer)
10
- * - handleCheckoutSessionCompleted (logging-only)
11
- * - storeLicenseKey / revokeLicenseKey (DB operations)
12
- * - extractTier / extractSeatCount / getCurrentPeriodEnd / getCurrentPeriodStart
13
- * - extractSubscriptionIdFromInvoice
14
- */
15
- import { describe, it, expect, vi } from 'vitest';
16
- import { handleSubscriptionCreated, handleSubscriptionUpdated, handleSubscriptionDeleted, handleInvoicePaymentSucceeded, handleInvoicePaymentFailed, handleCheckoutSessionCompleted, storeLicenseKey, revokeLicenseKey, extractTier, extractSeatCount, getCurrentPeriodEnd, getCurrentPeriodStart, extractSubscriptionIdFromInvoice, } from '../../src/billing/webhook-handlers.js';
17
- // ============================================================================
18
- // Test Fixtures
19
- // ============================================================================
20
- function createMockDb() {
21
- const runFn = vi.fn();
22
- return {
23
- prepare: vi.fn().mockReturnValue({ run: runFn, get: vi.fn(), all: vi.fn() }),
24
- exec: vi.fn(),
25
- close: vi.fn(),
26
- };
27
- }
28
- function createMockStripeClient() {
29
- return {
30
- getCustomer: vi.fn(),
31
- verifyWebhookSignature: vi.fn(),
32
- mapSubscriptionStatus: vi.fn(),
33
- };
34
- }
35
- function createMockBillingService() {
36
- return {
37
- upsertSubscription: vi.fn().mockReturnValue({ id: 'local_sub_1', tier: 'individual' }),
38
- getSubscriptionByStripeId: vi.fn().mockReturnValue(null),
39
- updateSubscriptionStatus: vi.fn(),
40
- storeInvoice: vi.fn(),
41
- isEventProcessed: vi.fn().mockReturnValue(false),
42
- recordWebhookEvent: vi.fn(),
43
- };
44
- }
45
- function createContext(overrides = {}) {
46
- return {
47
- stripe: createMockStripeClient(),
48
- billing: createMockBillingService(),
49
- db: createMockDb(),
50
- ...overrides,
51
- };
52
- }
53
- function makeSubscription(overrides = {}) {
54
- return {
55
- id: 'sub_test_1',
56
- customer: 'cus_test_1',
57
- status: 'active',
58
- metadata: { tier: 'team', seatCount: '5' },
59
- items: {
60
- data: [
61
- {
62
- id: 'si_1',
63
- price: { id: 'price_team_monthly' },
64
- quantity: 5,
65
- current_period_start: 1700000000,
66
- current_period_end: 1702592000,
67
- },
68
- ],
69
- },
70
- canceled_at: null,
71
- ...overrides,
72
- };
73
- }
74
- function makeInvoice(overrides = {}) {
75
- return {
76
- id: 'in_test_1',
77
- customer: 'cus_test_1',
78
- amount_paid: 2500,
79
- amount_due: 2500,
80
- currency: 'usd',
81
- number: 'INV-001',
82
- invoice_pdf: 'https://pdf.url',
83
- hosted_invoice_url: 'https://hosted.url',
84
- status_transitions: { paid_at: 1700000000 },
85
- period_start: 1700000000,
86
- period_end: 1702592000,
87
- parent: {
88
- subscription_details: {
89
- subscription: 'sub_test_1',
90
- },
91
- },
92
- ...overrides,
93
- };
94
- }
95
- // ============================================================================
96
- // Pure Helper Functions
97
- // ============================================================================
98
- describe('extractTier', () => {
99
- it('should extract tier from metadata', () => {
100
- const sub = makeSubscription({ metadata: { tier: 'enterprise' } });
101
- expect(extractTier(sub)).toBe('enterprise');
102
- });
103
- it('should accept all valid tiers from metadata', () => {
104
- for (const tier of ['community', 'individual', 'team', 'enterprise']) {
105
- const sub = makeSubscription({ metadata: { tier } });
106
- expect(extractTier(sub)).toBe(tier);
107
- }
108
- });
109
- it('should default to individual for missing metadata', () => {
110
- const sub = makeSubscription({ metadata: {} });
111
- expect(extractTier(sub)).toBe('individual');
112
- });
113
- it('should default to individual for invalid tier in metadata', () => {
114
- const sub = makeSubscription({ metadata: { tier: 'platinum' } });
115
- expect(extractTier(sub)).toBe('individual');
116
- });
117
- });
118
- describe('extractSeatCount', () => {
119
- it('should extract seat count from metadata', () => {
120
- const sub = makeSubscription({ metadata: { seatCount: '10' } });
121
- expect(extractSeatCount(sub)).toBe(10);
122
- });
123
- it('should fall back to item quantity when metadata missing', () => {
124
- const sub = makeSubscription({ metadata: {} });
125
- expect(extractSeatCount(sub)).toBe(5); // from fixture item quantity
126
- });
127
- it('should fall back to item quantity for non-numeric metadata', () => {
128
- const sub = makeSubscription({ metadata: { seatCount: 'abc' } });
129
- expect(extractSeatCount(sub)).toBe(5);
130
- });
131
- it('should fall back to item quantity for zero seat count', () => {
132
- const sub = makeSubscription({ metadata: { seatCount: '0' } });
133
- expect(extractSeatCount(sub)).toBe(5);
134
- });
135
- it('should fall back to item quantity for negative seat count', () => {
136
- const sub = makeSubscription({ metadata: { seatCount: '-1' } });
137
- expect(extractSeatCount(sub)).toBe(5);
138
- });
139
- it('should return 1 when no items and no metadata', () => {
140
- const sub = makeSubscription({
141
- metadata: {},
142
- items: { data: [{ quantity: undefined, current_period_end: 1702592000 }] },
143
- });
144
- expect(extractSeatCount(sub)).toBe(1);
145
- });
146
- });
147
- describe('getCurrentPeriodEnd', () => {
148
- it('should return period end from first item', () => {
149
- const sub = makeSubscription();
150
- expect(getCurrentPeriodEnd(sub)).toBe(1702592000);
151
- });
152
- it('should return current timestamp when no items', () => {
153
- const sub = makeSubscription({ items: { data: [] } });
154
- const now = Math.floor(Date.now() / 1000);
155
- const result = getCurrentPeriodEnd(sub);
156
- // Should be within 2 seconds of now
157
- expect(Math.abs(result - now)).toBeLessThan(2);
158
- });
159
- });
160
- describe('getCurrentPeriodStart', () => {
161
- it('should return period start from first item', () => {
162
- const sub = makeSubscription();
163
- expect(getCurrentPeriodStart(sub)).toBe(1700000000);
164
- });
165
- it('should return current timestamp when no items', () => {
166
- const sub = makeSubscription({ items: { data: [] } });
167
- const now = Math.floor(Date.now() / 1000);
168
- const result = getCurrentPeriodStart(sub);
169
- expect(Math.abs(result - now)).toBeLessThan(2);
170
- });
171
- });
172
- describe('extractSubscriptionIdFromInvoice', () => {
173
- it('should extract string subscription ID', () => {
174
- const invoice = makeInvoice();
175
- expect(extractSubscriptionIdFromInvoice(invoice)).toBe('sub_test_1');
176
- });
177
- it('should extract ID from subscription object', () => {
178
- const invoice = makeInvoice({
179
- parent: {
180
- subscription_details: {
181
- subscription: { id: 'sub_obj_1' },
182
- },
183
- },
184
- });
185
- expect(extractSubscriptionIdFromInvoice(invoice)).toBe('sub_obj_1');
186
- });
187
- it('should return undefined when no parent', () => {
188
- const invoice = makeInvoice({ parent: undefined });
189
- expect(extractSubscriptionIdFromInvoice(invoice)).toBeUndefined();
190
- });
191
- it('should return undefined when no subscription_details', () => {
192
- const invoice = makeInvoice({ parent: {} });
193
- expect(extractSubscriptionIdFromInvoice(invoice)).toBeUndefined();
194
- });
195
- it('should return undefined when subscription is null', () => {
196
- const invoice = makeInvoice({
197
- parent: { subscription_details: { subscription: null } },
198
- });
199
- expect(extractSubscriptionIdFromInvoice(invoice)).toBeUndefined();
200
- });
201
- });
202
- // ============================================================================
203
- // License Key DB Operations
204
- // ============================================================================
205
- describe('storeLicenseKey', () => {
206
- it('should insert a license key record', () => {
207
- const db = createMockDb();
208
- const runFn = vi.fn();
209
- vi.mocked(db.prepare).mockReturnValue({ run: runFn });
210
- storeLicenseKey(db, {
211
- subscriptionId: 'sub_1',
212
- organizationId: 'org_1',
213
- keyJwt: 'jwt_token_value',
214
- keyExpiry: new Date('2026-12-31'),
215
- });
216
- expect(db.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO license_keys'));
217
- expect(runFn).toHaveBeenCalledWith(expect.any(String), // UUID
218
- 'sub_1', 'org_1', 'jwt_token_value', expect.any(String), // SHA-256 hash
219
- '2026-12-31T00:00:00.000Z', expect.any(String) // generated_at ISO
220
- );
221
- });
222
- });
223
- describe('revokeLicenseKey', () => {
224
- it('should update license key to inactive', () => {
225
- const db = createMockDb();
226
- const runFn = vi.fn();
227
- vi.mocked(db.prepare).mockReturnValue({ run: runFn });
228
- revokeLicenseKey(db, 'sub_1', 'tier_change');
229
- expect(db.prepare).toHaveBeenCalledWith(expect.stringContaining('UPDATE license_keys'));
230
- expect(runFn).toHaveBeenCalledWith(expect.any(String), 'tier_change', 'sub_1');
231
- });
232
- });
233
- // ============================================================================
234
- // handleSubscriptionCreated
235
- // ============================================================================
236
- describe('handleSubscriptionCreated', () => {
237
- it('should create subscription record when customer found', async () => {
238
- const ctx = createContext();
239
- const sub = makeSubscription();
240
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
241
- id: 'cus_test_1',
242
- email: 'user@example.com',
243
- deleted: false,
244
- });
245
- await handleSubscriptionCreated(ctx, sub);
246
- expect(ctx.billing.upsertSubscription).toHaveBeenCalledWith(expect.objectContaining({
247
- customerId: 'cus_test_1',
248
- email: 'user@example.com',
249
- stripeSubscriptionId: 'sub_test_1',
250
- tier: 'team',
251
- seatCount: 5,
252
- }));
253
- });
254
- it('should throw when customer not found', async () => {
255
- const ctx = createContext();
256
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue(null);
257
- await expect(handleSubscriptionCreated(ctx, makeSubscription())).rejects.toThrow('Customer not found');
258
- });
259
- it('should generate and store license key for active subscription', async () => {
260
- const onLicenseKeyNeeded = vi.fn().mockResolvedValue('jwt_license_123');
261
- const db = createMockDb();
262
- const runFn = vi.fn();
263
- vi.mocked(db.prepare).mockReturnValue({ run: runFn });
264
- const ctx = createContext({ onLicenseKeyNeeded, db });
265
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
266
- id: 'cus_test_1',
267
- email: 'user@example.com',
268
- deleted: false,
269
- });
270
- await handleSubscriptionCreated(ctx, makeSubscription({ status: 'active' }));
271
- expect(onLicenseKeyNeeded).toHaveBeenCalledWith(expect.objectContaining({
272
- customerId: 'cus_test_1',
273
- tier: 'team',
274
- }));
275
- expect(db.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO license_keys'));
276
- });
277
- it('should send license key email when onEmailNeeded provided', async () => {
278
- const onLicenseKeyNeeded = vi.fn().mockResolvedValue('jwt_license_456');
279
- const onEmailNeeded = vi.fn().mockResolvedValue(undefined);
280
- const db = createMockDb();
281
- vi.mocked(db.prepare).mockReturnValue({ run: vi.fn() });
282
- const ctx = createContext({ onLicenseKeyNeeded, onEmailNeeded, db });
283
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
284
- id: 'cus_test_1',
285
- email: 'user@example.com',
286
- deleted: false,
287
- });
288
- await handleSubscriptionCreated(ctx, makeSubscription({ status: 'active' }));
289
- expect(onEmailNeeded).toHaveBeenCalledWith(expect.objectContaining({
290
- type: 'license_key',
291
- email: 'user@example.com',
292
- data: expect.objectContaining({ licenseKey: 'jwt_license_456', tier: 'team' }),
293
- }));
294
- });
295
- it('should skip license key for non-active subscription', async () => {
296
- const onLicenseKeyNeeded = vi.fn();
297
- const ctx = createContext({ onLicenseKeyNeeded });
298
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
299
- id: 'cus_test_1',
300
- email: 'user@example.com',
301
- deleted: false,
302
- });
303
- await handleSubscriptionCreated(ctx, makeSubscription({ status: 'trialing' }));
304
- expect(onLicenseKeyNeeded).not.toHaveBeenCalled();
305
- });
306
- });
307
- // ============================================================================
308
- // handleSubscriptionUpdated
309
- // ============================================================================
310
- describe('handleSubscriptionUpdated', () => {
311
- it('should create subscription if not found locally', async () => {
312
- const ctx = createContext();
313
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue(null);
314
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
315
- id: 'cus_test_1',
316
- email: 'user@example.com',
317
- deleted: false,
318
- });
319
- await handleSubscriptionUpdated(ctx, makeSubscription());
320
- expect(ctx.billing.upsertSubscription).toHaveBeenCalled();
321
- });
322
- it('should update status for existing subscription', async () => {
323
- const ctx = createContext();
324
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue({
325
- id: 'local_sub_1',
326
- tier: 'team',
327
- });
328
- const sub = makeSubscription({ status: 'past_due', canceled_at: null });
329
- await handleSubscriptionUpdated(ctx, sub);
330
- expect(ctx.billing.updateSubscriptionStatus).toHaveBeenCalledWith('sub_test_1', 'past_due', null);
331
- });
332
- it('should pass canceled_at date when present', async () => {
333
- const ctx = createContext();
334
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue({
335
- id: 'local_sub_1',
336
- tier: 'team',
337
- });
338
- const sub = makeSubscription({ status: 'canceled', canceled_at: 1700000000 });
339
- await handleSubscriptionUpdated(ctx, sub);
340
- expect(ctx.billing.updateSubscriptionStatus).toHaveBeenCalledWith('sub_test_1', 'canceled', new Date(1700000000 * 1000));
341
- });
342
- it('should regenerate license key on tier change', async () => {
343
- const onLicenseKeyNeeded = vi.fn().mockResolvedValue('new_jwt');
344
- const db = createMockDb();
345
- const runFn = vi.fn();
346
- vi.mocked(db.prepare).mockReturnValue({ run: runFn });
347
- const ctx = createContext({ onLicenseKeyNeeded, db });
348
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue({
349
- id: 'local_sub_1',
350
- tier: 'individual', // old tier
351
- });
352
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
353
- id: 'cus_test_1',
354
- email: 'user@example.com',
355
- });
356
- // New tier is 'team' from metadata
357
- await handleSubscriptionUpdated(ctx, makeSubscription({ metadata: { tier: 'team' } }));
358
- // Should revoke old key
359
- expect(db.prepare).toHaveBeenCalledWith(expect.stringContaining('UPDATE license_keys'));
360
- // Should generate new key
361
- expect(onLicenseKeyNeeded).toHaveBeenCalledWith(expect.objectContaining({ tier: 'team' }));
362
- // Should store new key
363
- expect(db.prepare).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO license_keys'));
364
- });
365
- it('should not regenerate license key when tier unchanged', async () => {
366
- const onLicenseKeyNeeded = vi.fn();
367
- const ctx = createContext({ onLicenseKeyNeeded });
368
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue({
369
- id: 'local_sub_1',
370
- tier: 'team', // same as metadata
371
- });
372
- await handleSubscriptionUpdated(ctx, makeSubscription({ metadata: { tier: 'team' } }));
373
- expect(onLicenseKeyNeeded).not.toHaveBeenCalled();
374
- });
375
- });
376
- // ============================================================================
377
- // handleSubscriptionDeleted
378
- // ============================================================================
379
- describe('handleSubscriptionDeleted', () => {
380
- it('should cancel subscription and revoke license key', async () => {
381
- const db = createMockDb();
382
- const runFn = vi.fn();
383
- vi.mocked(db.prepare).mockReturnValue({ run: runFn });
384
- const ctx = createContext({ db });
385
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue({
386
- id: 'local_sub_1',
387
- tier: 'team',
388
- });
389
- await handleSubscriptionDeleted(ctx, makeSubscription());
390
- expect(ctx.billing.updateSubscriptionStatus).toHaveBeenCalledWith('sub_test_1', 'canceled', expect.any(Date));
391
- expect(db.prepare).toHaveBeenCalledWith(expect.stringContaining('UPDATE license_keys'));
392
- });
393
- it('should send cancellation email', async () => {
394
- const onEmailNeeded = vi.fn().mockResolvedValue(undefined);
395
- const db = createMockDb();
396
- vi.mocked(db.prepare).mockReturnValue({ run: vi.fn() });
397
- const ctx = createContext({ onEmailNeeded, db });
398
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue({
399
- id: 'local_sub_1',
400
- tier: 'team',
401
- });
402
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
403
- id: 'cus_test_1',
404
- email: 'user@example.com',
405
- });
406
- await handleSubscriptionDeleted(ctx, makeSubscription());
407
- expect(onEmailNeeded).toHaveBeenCalledWith(expect.objectContaining({
408
- type: 'subscription_canceled',
409
- email: 'user@example.com',
410
- }));
411
- });
412
- it('should do nothing when subscription not found locally', async () => {
413
- const ctx = createContext();
414
- vi.mocked(ctx.billing.getSubscriptionByStripeId).mockReturnValue(null);
415
- await handleSubscriptionDeleted(ctx, makeSubscription());
416
- expect(ctx.billing.updateSubscriptionStatus).not.toHaveBeenCalled();
417
- });
418
- });
419
- // ============================================================================
420
- // handleInvoicePaymentSucceeded
421
- // ============================================================================
422
- describe('handleInvoicePaymentSucceeded', () => {
423
- it('should store paid invoice', async () => {
424
- const ctx = createContext();
425
- const invoice = makeInvoice();
426
- await handleInvoicePaymentSucceeded(ctx, invoice);
427
- expect(ctx.billing.storeInvoice).toHaveBeenCalledWith(expect.objectContaining({
428
- customerId: 'cus_test_1',
429
- stripeInvoiceId: 'in_test_1',
430
- status: 'paid',
431
- amountCents: 2500,
432
- currency: 'usd',
433
- subscriptionId: 'sub_test_1',
434
- }));
435
- });
436
- it('should handle customer as object', async () => {
437
- const ctx = createContext();
438
- const invoice = makeInvoice({ customer: { id: 'cus_obj_1' } });
439
- await handleInvoicePaymentSucceeded(ctx, invoice);
440
- expect(ctx.billing.storeInvoice).toHaveBeenCalledWith(expect.objectContaining({ customerId: 'cus_obj_1' }));
441
- });
442
- it('should skip when no customer ID', async () => {
443
- const ctx = createContext();
444
- const invoice = makeInvoice({ customer: null });
445
- await handleInvoicePaymentSucceeded(ctx, invoice);
446
- expect(ctx.billing.storeInvoice).not.toHaveBeenCalled();
447
- });
448
- it('should handle missing status_transitions.paid_at', async () => {
449
- const ctx = createContext();
450
- const invoice = makeInvoice({ status_transitions: {} });
451
- await handleInvoicePaymentSucceeded(ctx, invoice);
452
- expect(ctx.billing.storeInvoice).toHaveBeenCalledWith(expect.objectContaining({
453
- paidAt: expect.any(Date),
454
- }));
455
- });
456
- });
457
- // ============================================================================
458
- // handleInvoicePaymentFailed
459
- // ============================================================================
460
- describe('handleInvoicePaymentFailed', () => {
461
- it('should store open invoice', async () => {
462
- const ctx = createContext();
463
- const invoice = makeInvoice();
464
- await handleInvoicePaymentFailed(ctx, invoice);
465
- expect(ctx.billing.storeInvoice).toHaveBeenCalledWith(expect.objectContaining({
466
- customerId: 'cus_test_1',
467
- stripeInvoiceId: 'in_test_1',
468
- status: 'open',
469
- amountCents: 2500,
470
- }));
471
- });
472
- it('should send payment failed email', async () => {
473
- const onEmailNeeded = vi.fn().mockResolvedValue(undefined);
474
- const ctx = createContext({ onEmailNeeded });
475
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
476
- id: 'cus_test_1',
477
- email: 'user@example.com',
478
- });
479
- await handleInvoicePaymentFailed(ctx, makeInvoice());
480
- expect(onEmailNeeded).toHaveBeenCalledWith(expect.objectContaining({
481
- type: 'payment_failed',
482
- email: 'user@example.com',
483
- data: expect.objectContaining({
484
- invoiceId: 'in_test_1',
485
- amount: 2500,
486
- }),
487
- }));
488
- });
489
- it('should skip when no customer ID', async () => {
490
- const ctx = createContext();
491
- const invoice = makeInvoice({ customer: null });
492
- await handleInvoicePaymentFailed(ctx, invoice);
493
- expect(ctx.billing.storeInvoice).not.toHaveBeenCalled();
494
- });
495
- it('should skip email when customer has no email', async () => {
496
- const onEmailNeeded = vi.fn();
497
- const ctx = createContext({ onEmailNeeded });
498
- vi.mocked(ctx.stripe.getCustomer).mockResolvedValue({
499
- id: 'cus_test_1',
500
- email: null,
501
- });
502
- await handleInvoicePaymentFailed(ctx, makeInvoice());
503
- expect(onEmailNeeded).not.toHaveBeenCalled();
504
- });
505
- });
506
- // ============================================================================
507
- // handleCheckoutSessionCompleted
508
- // ============================================================================
509
- describe('handleCheckoutSessionCompleted', () => {
510
- it('should not throw (logging-only handler)', () => {
511
- const session = {
512
- id: 'cs_test_1',
513
- customer: 'cus_test_1',
514
- subscription: 'sub_test_1',
515
- };
516
- expect(() => handleCheckoutSessionCompleted(session)).not.toThrow();
517
- });
518
- });
519
- //# sourceMappingURL=webhook-handlers.test.js.map