@donotdev/functions 0.0.11 → 0.0.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.
- package/README.md +1 -1
- package/package.json +9 -9
- package/src/firebase/auth/setCustomClaims.ts +19 -5
- package/src/firebase/baseFunction.ts +11 -3
- package/src/firebase/billing/changePlan.ts +5 -1
- package/src/firebase/billing/createCheckoutSession.ts +3 -1
- package/src/firebase/billing/createCustomerPortal.ts +6 -2
- package/src/firebase/billing/webhookHandler.ts +4 -1
- package/src/firebase/crud/aggregate.ts +5 -1
- package/src/firebase/crud/create.ts +17 -4
- package/src/firebase/crud/list.ts +37 -5
- package/src/firebase/crud/update.ts +17 -4
- package/src/firebase/oauth/exchangeToken.ts +17 -4
- package/src/shared/__tests__/validation.test.ts +5 -3
- package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
- package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
- package/src/shared/errorHandling.ts +6 -6
- package/src/shared/firebase.ts +1 -1
- package/src/shared/index.ts +2 -1
- package/src/shared/logger.ts +9 -7
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
- package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
- package/src/shared/utils/external/subscription.ts +2 -2
- package/src/shared/utils/internal/auth.ts +10 -3
- package/src/shared/utils/internal/rateLimiter.ts +8 -2
- package/src/shared/utils.ts +23 -30
- package/src/supabase/auth/deleteAccount.ts +4 -11
- package/src/supabase/auth/getCustomClaims.ts +5 -3
- package/src/supabase/auth/getUserAuthStatus.ts +5 -3
- package/src/supabase/auth/removeCustomClaims.ts +10 -5
- package/src/supabase/auth/setCustomClaims.ts +9 -4
- package/src/supabase/baseFunction.ts +80 -21
- package/src/supabase/billing/cancelSubscription.ts +9 -3
- package/src/supabase/billing/changePlan.ts +20 -5
- package/src/supabase/billing/createCheckoutSession.ts +20 -5
- package/src/supabase/billing/createCustomerPortal.ts +14 -4
- package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
- package/src/supabase/crud/aggregate.ts +14 -4
- package/src/supabase/crud/create.ts +30 -11
- package/src/supabase/crud/delete.ts +11 -3
- package/src/supabase/crud/get.ts +25 -3
- package/src/supabase/crud/list.ts +106 -21
- package/src/supabase/crud/update.ts +32 -10
- package/src/supabase/helpers/authProvider.ts +5 -2
- package/src/supabase/index.ts +1 -4
- package/src/supabase/registerCrudFunctions.ts +11 -9
- package/src/supabase/utils/idempotency.ts +13 -15
- package/src/supabase/utils/monitoring.ts +5 -1
- package/src/supabase/utils/rateLimiter.ts +13 -3
- package/src/vercel/api/billing/webhook-handler.ts +6 -2
- package/src/vercel/api/crud/create.ts +7 -2
- package/src/vercel/api/crud/delete.ts +3 -1
- package/src/vercel/api/crud/get.ts +3 -1
- package/src/vercel/api/crud/list.ts +3 -1
- package/src/vercel/api/crud/update.ts +7 -2
|
@@ -61,7 +61,9 @@ const BASE_REQUEST: CreateCheckoutSessionRequest = {
|
|
|
61
61
|
// Mock factories
|
|
62
62
|
// ---------------------------------------------------------------------------
|
|
63
63
|
|
|
64
|
-
function makeStripeProvider(
|
|
64
|
+
function makeStripeProvider(
|
|
65
|
+
overrides?: Partial<StripeCheckoutProvider>
|
|
66
|
+
): StripeCheckoutProvider {
|
|
65
67
|
return {
|
|
66
68
|
createCheckoutSession: vi.fn().mockResolvedValue({
|
|
67
69
|
id: 'cs_test_session123',
|
|
@@ -71,7 +73,9 @@ function makeStripeProvider(overrides?: Partial<StripeCheckoutProvider>): Stripe
|
|
|
71
73
|
};
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
function makeAuthProvider(
|
|
76
|
+
function makeAuthProvider(
|
|
77
|
+
overrides?: Partial<AuthCheckoutProvider>
|
|
78
|
+
): AuthCheckoutProvider {
|
|
75
79
|
return {
|
|
76
80
|
getUser: vi.fn().mockResolvedValue({ customClaims: {} }),
|
|
77
81
|
...overrides,
|
|
@@ -112,14 +116,24 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
112
116
|
});
|
|
113
117
|
|
|
114
118
|
it('verifies the user before creating the session', async () => {
|
|
115
|
-
await createCheckoutAlgorithm(
|
|
119
|
+
await createCheckoutAlgorithm(
|
|
120
|
+
BASE_REQUEST,
|
|
121
|
+
stripeProvider,
|
|
122
|
+
authProvider,
|
|
123
|
+
BILLING_CONFIG
|
|
124
|
+
);
|
|
116
125
|
|
|
117
126
|
expect(authProvider.getUser).toHaveBeenCalledOnce();
|
|
118
127
|
expect(authProvider.getUser).toHaveBeenCalledWith('user_001');
|
|
119
128
|
});
|
|
120
129
|
|
|
121
130
|
it('forwards priceId, mode, successUrl, cancelUrl to Stripe', async () => {
|
|
122
|
-
await createCheckoutAlgorithm(
|
|
131
|
+
await createCheckoutAlgorithm(
|
|
132
|
+
BASE_REQUEST,
|
|
133
|
+
stripeProvider,
|
|
134
|
+
authProvider,
|
|
135
|
+
BILLING_CONFIG
|
|
136
|
+
);
|
|
123
137
|
|
|
124
138
|
expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
|
|
125
139
|
expect.objectContaining({
|
|
@@ -132,7 +146,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
132
146
|
});
|
|
133
147
|
|
|
134
148
|
it('ensures userId and billingConfigKey are set in session metadata', async () => {
|
|
135
|
-
await createCheckoutAlgorithm(
|
|
149
|
+
await createCheckoutAlgorithm(
|
|
150
|
+
BASE_REQUEST,
|
|
151
|
+
stripeProvider,
|
|
152
|
+
authProvider,
|
|
153
|
+
BILLING_CONFIG
|
|
154
|
+
);
|
|
136
155
|
|
|
137
156
|
expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
|
|
138
157
|
expect.objectContaining({
|
|
@@ -150,7 +169,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
150
169
|
customerEmail: 'user@example.com',
|
|
151
170
|
};
|
|
152
171
|
|
|
153
|
-
await createCheckoutAlgorithm(
|
|
172
|
+
await createCheckoutAlgorithm(
|
|
173
|
+
request,
|
|
174
|
+
stripeProvider,
|
|
175
|
+
authProvider,
|
|
176
|
+
BILLING_CONFIG
|
|
177
|
+
);
|
|
154
178
|
|
|
155
179
|
expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
|
|
156
180
|
expect.objectContaining({ customerEmail: 'user@example.com' })
|
|
@@ -163,7 +187,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
163
187
|
allowPromotionCodes: undefined,
|
|
164
188
|
};
|
|
165
189
|
|
|
166
|
-
await createCheckoutAlgorithm(
|
|
190
|
+
await createCheckoutAlgorithm(
|
|
191
|
+
request,
|
|
192
|
+
stripeProvider,
|
|
193
|
+
authProvider,
|
|
194
|
+
BILLING_CONFIG
|
|
195
|
+
);
|
|
167
196
|
|
|
168
197
|
expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
|
|
169
198
|
expect.objectContaining({ allowPromotionCodes: true })
|
|
@@ -176,7 +205,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
176
205
|
mode: undefined,
|
|
177
206
|
};
|
|
178
207
|
|
|
179
|
-
await createCheckoutAlgorithm(
|
|
208
|
+
await createCheckoutAlgorithm(
|
|
209
|
+
request,
|
|
210
|
+
stripeProvider,
|
|
211
|
+
authProvider,
|
|
212
|
+
BILLING_CONFIG
|
|
213
|
+
);
|
|
180
214
|
|
|
181
215
|
expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
|
|
182
216
|
expect.objectContaining({ mode: 'payment' })
|
|
@@ -233,7 +267,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
233
267
|
};
|
|
234
268
|
|
|
235
269
|
await expect(
|
|
236
|
-
createCheckoutAlgorithm(
|
|
270
|
+
createCheckoutAlgorithm(
|
|
271
|
+
request,
|
|
272
|
+
stripeProvider,
|
|
273
|
+
authProvider,
|
|
274
|
+
BILLING_CONFIG
|
|
275
|
+
)
|
|
237
276
|
).rejects.toThrow('Missing billingConfigKey in metadata');
|
|
238
277
|
});
|
|
239
278
|
|
|
@@ -244,7 +283,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
244
283
|
};
|
|
245
284
|
|
|
246
285
|
await expect(
|
|
247
|
-
createCheckoutAlgorithm(
|
|
286
|
+
createCheckoutAlgorithm(
|
|
287
|
+
request,
|
|
288
|
+
stripeProvider,
|
|
289
|
+
authProvider,
|
|
290
|
+
BILLING_CONFIG
|
|
291
|
+
)
|
|
248
292
|
).rejects.toThrow('Missing billingConfigKey in metadata');
|
|
249
293
|
});
|
|
250
294
|
|
|
@@ -255,7 +299,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
255
299
|
};
|
|
256
300
|
|
|
257
301
|
await expect(
|
|
258
|
-
createCheckoutAlgorithm(
|
|
302
|
+
createCheckoutAlgorithm(
|
|
303
|
+
request,
|
|
304
|
+
stripeProvider,
|
|
305
|
+
authProvider,
|
|
306
|
+
BILLING_CONFIG
|
|
307
|
+
)
|
|
259
308
|
).rejects.toThrow();
|
|
260
309
|
|
|
261
310
|
expect(stripeProvider.createCheckoutSession).not.toHaveBeenCalled();
|
|
@@ -274,7 +323,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
274
323
|
};
|
|
275
324
|
|
|
276
325
|
await expect(
|
|
277
|
-
createCheckoutAlgorithm(
|
|
326
|
+
createCheckoutAlgorithm(
|
|
327
|
+
request,
|
|
328
|
+
stripeProvider,
|
|
329
|
+
authProvider,
|
|
330
|
+
BILLING_CONFIG
|
|
331
|
+
)
|
|
278
332
|
).rejects.toThrow('Invalid billing config key: nonexistent_plan');
|
|
279
333
|
});
|
|
280
334
|
|
|
@@ -286,7 +340,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
286
340
|
};
|
|
287
341
|
|
|
288
342
|
await expect(
|
|
289
|
-
createCheckoutAlgorithm(
|
|
343
|
+
createCheckoutAlgorithm(
|
|
344
|
+
request,
|
|
345
|
+
stripeProvider,
|
|
346
|
+
authProvider,
|
|
347
|
+
BILLING_CONFIG
|
|
348
|
+
)
|
|
290
349
|
).rejects.toThrow('Price ID mismatch with configuration');
|
|
291
350
|
});
|
|
292
351
|
|
|
@@ -298,7 +357,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
298
357
|
};
|
|
299
358
|
|
|
300
359
|
await expect(
|
|
301
|
-
createCheckoutAlgorithm(
|
|
360
|
+
createCheckoutAlgorithm(
|
|
361
|
+
request,
|
|
362
|
+
stripeProvider,
|
|
363
|
+
authProvider,
|
|
364
|
+
BILLING_CONFIG
|
|
365
|
+
)
|
|
302
366
|
).rejects.toThrow();
|
|
303
367
|
|
|
304
368
|
expect(stripeProvider.createCheckoutSession).not.toHaveBeenCalled();
|
|
@@ -311,7 +375,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
311
375
|
|
|
312
376
|
describe('auth validation', () => {
|
|
313
377
|
it('calls getUser with the request userId', async () => {
|
|
314
|
-
await createCheckoutAlgorithm(
|
|
378
|
+
await createCheckoutAlgorithm(
|
|
379
|
+
BASE_REQUEST,
|
|
380
|
+
stripeProvider,
|
|
381
|
+
authProvider,
|
|
382
|
+
BILLING_CONFIG
|
|
383
|
+
);
|
|
315
384
|
|
|
316
385
|
expect(authProvider.getUser).toHaveBeenCalledWith('user_001');
|
|
317
386
|
});
|
|
@@ -322,7 +391,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
322
391
|
});
|
|
323
392
|
|
|
324
393
|
await expect(
|
|
325
|
-
createCheckoutAlgorithm(
|
|
394
|
+
createCheckoutAlgorithm(
|
|
395
|
+
BASE_REQUEST,
|
|
396
|
+
stripeProvider,
|
|
397
|
+
authProvider,
|
|
398
|
+
BILLING_CONFIG
|
|
399
|
+
)
|
|
326
400
|
).rejects.toThrow('User not found');
|
|
327
401
|
});
|
|
328
402
|
|
|
@@ -332,7 +406,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
332
406
|
});
|
|
333
407
|
|
|
334
408
|
await expect(
|
|
335
|
-
createCheckoutAlgorithm(
|
|
409
|
+
createCheckoutAlgorithm(
|
|
410
|
+
BASE_REQUEST,
|
|
411
|
+
stripeProvider,
|
|
412
|
+
authProvider,
|
|
413
|
+
BILLING_CONFIG
|
|
414
|
+
)
|
|
336
415
|
).rejects.toThrow();
|
|
337
416
|
|
|
338
417
|
expect(stripeProvider.createCheckoutSession).not.toHaveBeenCalled();
|
|
@@ -346,11 +425,18 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
346
425
|
describe('Stripe error handling', () => {
|
|
347
426
|
it('propagates Stripe API errors', async () => {
|
|
348
427
|
stripeProvider = makeStripeProvider({
|
|
349
|
-
createCheckoutSession: vi
|
|
428
|
+
createCheckoutSession: vi
|
|
429
|
+
.fn()
|
|
430
|
+
.mockRejectedValue(new Error('Stripe network error')),
|
|
350
431
|
});
|
|
351
432
|
|
|
352
433
|
await expect(
|
|
353
|
-
createCheckoutAlgorithm(
|
|
434
|
+
createCheckoutAlgorithm(
|
|
435
|
+
BASE_REQUEST,
|
|
436
|
+
stripeProvider,
|
|
437
|
+
authProvider,
|
|
438
|
+
BILLING_CONFIG
|
|
439
|
+
)
|
|
354
440
|
).rejects.toThrow('Stripe network error');
|
|
355
441
|
});
|
|
356
442
|
|
|
@@ -362,7 +448,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
362
448
|
});
|
|
363
449
|
|
|
364
450
|
await expect(
|
|
365
|
-
createCheckoutAlgorithm(
|
|
451
|
+
createCheckoutAlgorithm(
|
|
452
|
+
BASE_REQUEST,
|
|
453
|
+
stripeProvider,
|
|
454
|
+
authProvider,
|
|
455
|
+
BILLING_CONFIG
|
|
456
|
+
)
|
|
366
457
|
).rejects.toThrow('Your card was declined');
|
|
367
458
|
});
|
|
368
459
|
|
|
@@ -374,7 +465,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
374
465
|
});
|
|
375
466
|
|
|
376
467
|
await expect(
|
|
377
|
-
createCheckoutAlgorithm(
|
|
468
|
+
createCheckoutAlgorithm(
|
|
469
|
+
BASE_REQUEST,
|
|
470
|
+
stripeProvider,
|
|
471
|
+
authProvider,
|
|
472
|
+
BILLING_CONFIG
|
|
473
|
+
)
|
|
378
474
|
).rejects.toThrow('Too many requests');
|
|
379
475
|
});
|
|
380
476
|
|
|
@@ -386,7 +482,12 @@ describe('createCheckoutAlgorithm', () => {
|
|
|
386
482
|
});
|
|
387
483
|
|
|
388
484
|
await expect(
|
|
389
|
-
createCheckoutAlgorithm(
|
|
485
|
+
createCheckoutAlgorithm(
|
|
486
|
+
BASE_REQUEST,
|
|
487
|
+
stripeProvider,
|
|
488
|
+
authProvider,
|
|
489
|
+
BILLING_CONFIG
|
|
490
|
+
)
|
|
390
491
|
).rejects.toBe(unexpected);
|
|
391
492
|
});
|
|
392
493
|
});
|
|
@@ -80,7 +80,10 @@ vi.mock('../../logger.js', () => ({
|
|
|
80
80
|
// Reset idempotency store between tests so events are never pre-marked
|
|
81
81
|
import { resetIdempotencyStore } from '../idempotency.js';
|
|
82
82
|
import { processWebhook } from '../webhookHandler.js';
|
|
83
|
-
import type {
|
|
83
|
+
import type {
|
|
84
|
+
UpdateSubscriptionFn,
|
|
85
|
+
WebhookAuthProvider,
|
|
86
|
+
} from '../webhookHandler.js';
|
|
84
87
|
|
|
85
88
|
// ---------------------------------------------------------------------------
|
|
86
89
|
// Shared test fixtures
|
|
@@ -217,15 +220,15 @@ describe('processWebhook — signature verification', () => {
|
|
|
217
220
|
const stripe = {
|
|
218
221
|
webhooks: {
|
|
219
222
|
constructEvent: vi.fn(() => {
|
|
220
|
-
throw new Error(
|
|
223
|
+
throw new Error(
|
|
224
|
+
'No signatures found matching the expected signature'
|
|
225
|
+
);
|
|
221
226
|
}),
|
|
222
227
|
},
|
|
223
228
|
subscriptions: { retrieve: vi.fn() },
|
|
224
229
|
};
|
|
225
230
|
|
|
226
|
-
await expect(
|
|
227
|
-
callProcessWebhook({ stripe })
|
|
228
|
-
).rejects.toThrow();
|
|
231
|
+
await expect(callProcessWebhook({ stripe })).rejects.toThrow();
|
|
229
232
|
});
|
|
230
233
|
});
|
|
231
234
|
|
|
@@ -260,7 +263,10 @@ describe('processWebhook — idempotency', () => {
|
|
|
260
263
|
updateSubscription: updateSubscription2,
|
|
261
264
|
});
|
|
262
265
|
|
|
263
|
-
expect(result).toEqual({
|
|
266
|
+
expect(result).toEqual({
|
|
267
|
+
success: true,
|
|
268
|
+
message: 'Event already processed',
|
|
269
|
+
});
|
|
264
270
|
expect(updateSubscription2).not.toHaveBeenCalled();
|
|
265
271
|
});
|
|
266
272
|
|
|
@@ -279,8 +285,16 @@ describe('processWebhook — idempotency', () => {
|
|
|
279
285
|
const updateSubscription1 = makeUpdateSubscription();
|
|
280
286
|
const updateSubscription2 = makeUpdateSubscription();
|
|
281
287
|
|
|
282
|
-
await callProcessWebhook({
|
|
283
|
-
|
|
288
|
+
await callProcessWebhook({
|
|
289
|
+
stripe: makeStripe(event1),
|
|
290
|
+
config,
|
|
291
|
+
updateSubscription: updateSubscription1,
|
|
292
|
+
});
|
|
293
|
+
await callProcessWebhook({
|
|
294
|
+
stripe: makeStripe(event2),
|
|
295
|
+
config,
|
|
296
|
+
updateSubscription: updateSubscription2,
|
|
297
|
+
});
|
|
284
298
|
|
|
285
299
|
expect(updateSubscription1).toHaveBeenCalledTimes(1);
|
|
286
300
|
expect(updateSubscription2).toHaveBeenCalledTimes(1);
|
|
@@ -349,7 +363,11 @@ describe('event routing — checkout.session.completed (StripeSubscription)', ()
|
|
|
349
363
|
const config = makeConfig();
|
|
350
364
|
const updateSubscription = makeUpdateSubscription();
|
|
351
365
|
|
|
352
|
-
await callProcessWebhook({
|
|
366
|
+
await callProcessWebhook({
|
|
367
|
+
stripe: makeStripe(event),
|
|
368
|
+
config,
|
|
369
|
+
updateSubscription,
|
|
370
|
+
});
|
|
353
371
|
|
|
354
372
|
expect(updateSubscription).toHaveBeenCalledOnce();
|
|
355
373
|
const [userId, data] = (updateSubscription as any).mock.calls[0];
|
|
@@ -370,9 +388,16 @@ describe('event routing — checkout.session.completed (StripeSubscription)', ()
|
|
|
370
388
|
customer: 'cus_hook',
|
|
371
389
|
});
|
|
372
390
|
|
|
373
|
-
await callProcessWebhook({
|
|
391
|
+
await callProcessWebhook({
|
|
392
|
+
stripe: makeStripe(event),
|
|
393
|
+
config,
|
|
394
|
+
updateSubscription: makeUpdateSubscription(),
|
|
395
|
+
});
|
|
374
396
|
|
|
375
|
-
expect(onSubscriptionCreated).toHaveBeenCalledWith(
|
|
397
|
+
expect(onSubscriptionCreated).toHaveBeenCalledWith(
|
|
398
|
+
'u_hook',
|
|
399
|
+
expect.any(Object)
|
|
400
|
+
);
|
|
376
401
|
});
|
|
377
402
|
|
|
378
403
|
it('does NOT throw when onSubscriptionCreated hook throws (non-critical)', async () => {
|
|
@@ -387,7 +412,11 @@ describe('event routing — checkout.session.completed (StripeSubscription)', ()
|
|
|
387
412
|
});
|
|
388
413
|
|
|
389
414
|
await expect(
|
|
390
|
-
callProcessWebhook({
|
|
415
|
+
callProcessWebhook({
|
|
416
|
+
stripe: makeStripe(event),
|
|
417
|
+
config,
|
|
418
|
+
updateSubscription: makeUpdateSubscription(),
|
|
419
|
+
})
|
|
391
420
|
).resolves.toEqual({ success: true, message: 'Webhook processed' });
|
|
392
421
|
});
|
|
393
422
|
|
|
@@ -459,7 +488,11 @@ describe('event routing — checkout.session.completed (StripePayment)', () => {
|
|
|
459
488
|
const config = makeConfig();
|
|
460
489
|
const updateSubscription = makeUpdateSubscription();
|
|
461
490
|
|
|
462
|
-
await callProcessWebhook({
|
|
491
|
+
await callProcessWebhook({
|
|
492
|
+
stripe: makeStripe(event),
|
|
493
|
+
config,
|
|
494
|
+
updateSubscription,
|
|
495
|
+
});
|
|
463
496
|
|
|
464
497
|
expect(updateSubscription).toHaveBeenCalledOnce();
|
|
465
498
|
const [userId, data] = (updateSubscription as any).mock.calls[0];
|
|
@@ -486,9 +519,16 @@ describe('event routing — checkout.session.completed (StripePayment)', () => {
|
|
|
486
519
|
customer: 'cus_pay_hook',
|
|
487
520
|
});
|
|
488
521
|
|
|
489
|
-
await callProcessWebhook({
|
|
522
|
+
await callProcessWebhook({
|
|
523
|
+
stripe: makeStripe(event),
|
|
524
|
+
config,
|
|
525
|
+
updateSubscription: makeUpdateSubscription(),
|
|
526
|
+
});
|
|
490
527
|
|
|
491
|
-
expect(onPurchaseSuccess).toHaveBeenCalledWith(
|
|
528
|
+
expect(onPurchaseSuccess).toHaveBeenCalledWith(
|
|
529
|
+
'u_pay_hook',
|
|
530
|
+
expect.any(Object)
|
|
531
|
+
);
|
|
492
532
|
});
|
|
493
533
|
|
|
494
534
|
it('does NOT throw when onPurchaseSuccess hook throws (non-critical)', async () => {
|
|
@@ -509,7 +549,11 @@ describe('event routing — checkout.session.completed (StripePayment)', () => {
|
|
|
509
549
|
});
|
|
510
550
|
|
|
511
551
|
await expect(
|
|
512
|
-
callProcessWebhook({
|
|
552
|
+
callProcessWebhook({
|
|
553
|
+
stripe: makeStripe(event),
|
|
554
|
+
config,
|
|
555
|
+
updateSubscription: makeUpdateSubscription(),
|
|
556
|
+
})
|
|
513
557
|
).resolves.toEqual({ success: true, message: 'Webhook processed' });
|
|
514
558
|
});
|
|
515
559
|
});
|
|
@@ -568,9 +612,16 @@ describe('event routing — invoice.payment_succeeded', () => {
|
|
|
568
612
|
metadata: { userId: 'u_hook_renewal', billingConfigKey: 'pro_monthly' },
|
|
569
613
|
}));
|
|
570
614
|
|
|
571
|
-
await callProcessWebhook({
|
|
615
|
+
await callProcessWebhook({
|
|
616
|
+
stripe,
|
|
617
|
+
config,
|
|
618
|
+
updateSubscription: makeUpdateSubscription(),
|
|
619
|
+
});
|
|
572
620
|
|
|
573
|
-
expect(onSubscriptionRenewed).toHaveBeenCalledWith(
|
|
621
|
+
expect(onSubscriptionRenewed).toHaveBeenCalledWith(
|
|
622
|
+
'u_hook_renewal',
|
|
623
|
+
expect.any(Object)
|
|
624
|
+
);
|
|
574
625
|
});
|
|
575
626
|
|
|
576
627
|
it('does NOT throw when onSubscriptionRenewed hook throws (non-critical)', async () => {
|
|
@@ -593,7 +644,11 @@ describe('event routing — invoice.payment_succeeded', () => {
|
|
|
593
644
|
}));
|
|
594
645
|
|
|
595
646
|
await expect(
|
|
596
|
-
callProcessWebhook({
|
|
647
|
+
callProcessWebhook({
|
|
648
|
+
stripe,
|
|
649
|
+
config,
|
|
650
|
+
updateSubscription: makeUpdateSubscription(),
|
|
651
|
+
})
|
|
597
652
|
).resolves.toEqual({ success: true, message: 'Webhook processed' });
|
|
598
653
|
});
|
|
599
654
|
|
|
@@ -647,7 +702,10 @@ describe('event routing — invoice.payment_succeeded', () => {
|
|
|
647
702
|
const stripe = makeStripe(event);
|
|
648
703
|
stripe.subscriptions.retrieve = vi.fn(async () => ({
|
|
649
704
|
id: 'sub_payment_type',
|
|
650
|
-
metadata: {
|
|
705
|
+
metadata: {
|
|
706
|
+
userId: 'u_payment_type',
|
|
707
|
+
billingConfigKey: 'lifetime_purchase',
|
|
708
|
+
},
|
|
651
709
|
}));
|
|
652
710
|
const updateSubscription = makeUpdateSubscription();
|
|
653
711
|
|
|
@@ -723,9 +781,16 @@ describe('event routing — customer.subscription.deleted', () => {
|
|
|
723
781
|
};
|
|
724
782
|
const event = makeEvent('customer.subscription.deleted', subscription);
|
|
725
783
|
|
|
726
|
-
await callProcessWebhook({
|
|
784
|
+
await callProcessWebhook({
|
|
785
|
+
stripe: makeStripe(event),
|
|
786
|
+
config,
|
|
787
|
+
updateSubscription: makeUpdateSubscription(),
|
|
788
|
+
});
|
|
727
789
|
|
|
728
|
-
expect(onSubscriptionCancelled).toHaveBeenCalledWith(
|
|
790
|
+
expect(onSubscriptionCancelled).toHaveBeenCalledWith(
|
|
791
|
+
'u_cancel_hook',
|
|
792
|
+
expect.any(Object)
|
|
793
|
+
);
|
|
729
794
|
});
|
|
730
795
|
|
|
731
796
|
it('does NOT throw when onSubscriptionCancelled hook throws (non-critical)', async () => {
|
|
@@ -744,7 +809,11 @@ describe('event routing — customer.subscription.deleted', () => {
|
|
|
744
809
|
const event = makeEvent('customer.subscription.deleted', subscription);
|
|
745
810
|
|
|
746
811
|
await expect(
|
|
747
|
-
callProcessWebhook({
|
|
812
|
+
callProcessWebhook({
|
|
813
|
+
stripe: makeStripe(event),
|
|
814
|
+
config,
|
|
815
|
+
updateSubscription: makeUpdateSubscription(),
|
|
816
|
+
})
|
|
748
817
|
).resolves.toEqual({ success: true, message: 'Webhook processed' });
|
|
749
818
|
});
|
|
750
819
|
|
|
@@ -776,7 +845,11 @@ describe('event routing — customer.subscription.deleted', () => {
|
|
|
776
845
|
const event = makeEvent('customer.subscription.deleted', subscription);
|
|
777
846
|
const updateSubscription = makeUpdateSubscription();
|
|
778
847
|
|
|
779
|
-
await callProcessWebhook({
|
|
848
|
+
await callProcessWebhook({
|
|
849
|
+
stripe: makeStripe(event),
|
|
850
|
+
config,
|
|
851
|
+
updateSubscription,
|
|
852
|
+
});
|
|
780
853
|
|
|
781
854
|
expect(updateSubscription).not.toHaveBeenCalled();
|
|
782
855
|
});
|
|
@@ -824,7 +897,10 @@ describe('event routing — invoice.payment_failed', () => {
|
|
|
824
897
|
|
|
825
898
|
await callProcessWebhook({ stripe: makeStripe(event), config });
|
|
826
899
|
|
|
827
|
-
expect(onPurchaseFailure).toHaveBeenCalledWith(
|
|
900
|
+
expect(onPurchaseFailure).toHaveBeenCalledWith(
|
|
901
|
+
'u_fail_pay',
|
|
902
|
+
expect.any(Object)
|
|
903
|
+
);
|
|
828
904
|
});
|
|
829
905
|
|
|
830
906
|
it('calls onPaymentFailed for StripeSubscription billing items', async () => {
|
|
@@ -839,7 +915,10 @@ describe('event routing — invoice.payment_failed', () => {
|
|
|
839
915
|
|
|
840
916
|
await callProcessWebhook({ stripe: makeStripe(event), config });
|
|
841
917
|
|
|
842
|
-
expect(onPaymentFailed).toHaveBeenCalledWith(
|
|
918
|
+
expect(onPaymentFailed).toHaveBeenCalledWith(
|
|
919
|
+
'u_fail_sub',
|
|
920
|
+
expect.any(Object)
|
|
921
|
+
);
|
|
843
922
|
});
|
|
844
923
|
|
|
845
924
|
it('does NOT throw when onPurchaseFailure hook throws (non-critical)', async () => {
|
|
@@ -922,7 +1001,11 @@ describe('event routing — invoice.payment_failed', () => {
|
|
|
922
1001
|
const event = makeEvent('invoice.payment_failed', invoice);
|
|
923
1002
|
const updateSubscription = makeUpdateSubscription();
|
|
924
1003
|
|
|
925
|
-
await callProcessWebhook({
|
|
1004
|
+
await callProcessWebhook({
|
|
1005
|
+
stripe: makeStripe(event),
|
|
1006
|
+
config,
|
|
1007
|
+
updateSubscription,
|
|
1008
|
+
});
|
|
926
1009
|
|
|
927
1010
|
expect(updateSubscription).not.toHaveBeenCalled();
|
|
928
1011
|
});
|
|
@@ -952,7 +1035,9 @@ describe('event routing — unknown event types', () => {
|
|
|
952
1035
|
});
|
|
953
1036
|
|
|
954
1037
|
it('handles payment_intent.succeeded gracefully (not in router)', async () => {
|
|
955
|
-
const event = makeEvent('payment_intent.succeeded', {
|
|
1038
|
+
const event = makeEvent('payment_intent.succeeded', {
|
|
1039
|
+
id: 'pi_not_routed',
|
|
1040
|
+
});
|
|
956
1041
|
|
|
957
1042
|
const result = await callProcessWebhook({ stripe: makeStripe(event) });
|
|
958
1043
|
|
|
@@ -997,7 +1082,10 @@ describe('state machine transitions', () => {
|
|
|
997
1082
|
|
|
998
1083
|
it('checkout.session.completed → status=active (StripePayment)', async () => {
|
|
999
1084
|
const event = makeEvent('checkout.session.completed', {
|
|
1000
|
-
metadata: {
|
|
1085
|
+
metadata: {
|
|
1086
|
+
userId: 'u_sm_pay_active',
|
|
1087
|
+
billingConfigKey: 'lifetime_purchase',
|
|
1088
|
+
},
|
|
1001
1089
|
subscription: null,
|
|
1002
1090
|
customer: 'cus_sm_pay_active',
|
|
1003
1091
|
});
|
|
@@ -1086,6 +1174,9 @@ describe('processWebhook — return value', () => {
|
|
|
1086
1174
|
await callProcessWebhook({ stripe: makeStripe(event) });
|
|
1087
1175
|
const result = await callProcessWebhook({ stripe: makeStripe(event) });
|
|
1088
1176
|
|
|
1089
|
-
expect(result).toEqual({
|
|
1177
|
+
expect(result).toEqual({
|
|
1178
|
+
success: true,
|
|
1179
|
+
message: 'Event already processed',
|
|
1180
|
+
});
|
|
1090
1181
|
});
|
|
1091
1182
|
});
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* @author AMBROISE PARK Consulting
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { HttpsError } from 'firebase-functions/v2/https';
|
|
13
12
|
import * as v from 'valibot';
|
|
14
13
|
|
|
15
14
|
import { DoNotDevError, EntityHookError } from '@donotdev/core/server';
|
|
@@ -26,7 +25,7 @@ export { DoNotDevError };
|
|
|
26
25
|
* @since 0.0.1
|
|
27
26
|
* @author AMBROISE PARK Consulting
|
|
28
27
|
*/
|
|
29
|
-
export function handleError(error: unknown): never {
|
|
28
|
+
export async function handleError(error: unknown): Promise<never> {
|
|
30
29
|
// W11: Log only in development to avoid double-logging with platform loggers.
|
|
31
30
|
// Known DoNotDevError, ValiError, and EntityHookError are handled by callers.
|
|
32
31
|
if (
|
|
@@ -97,7 +96,7 @@ export function handleError(error: unknown): never {
|
|
|
97
96
|
|
|
98
97
|
// Platform-specific error throwing
|
|
99
98
|
if (isFirebaseEnvironment()) {
|
|
100
|
-
throwFirebaseError(code, message, details);
|
|
99
|
+
await throwFirebaseError(code, message, details);
|
|
101
100
|
} else if (isVercelEnvironment()) {
|
|
102
101
|
throwVercelError(code, message, details);
|
|
103
102
|
} else {
|
|
@@ -143,12 +142,13 @@ function isVercelEnvironment(): boolean {
|
|
|
143
142
|
* @since 0.0.1
|
|
144
143
|
* @author AMBROISE PARK Consulting
|
|
145
144
|
*/
|
|
146
|
-
function throwFirebaseError(
|
|
145
|
+
async function throwFirebaseError(
|
|
147
146
|
code: string,
|
|
148
147
|
message: string,
|
|
149
148
|
details?: any
|
|
150
|
-
): never {
|
|
151
|
-
//
|
|
149
|
+
): Promise<never> {
|
|
150
|
+
// Dynamic import — firebase-functions is only loaded in Firebase environments
|
|
151
|
+
const { HttpsError } = await import('firebase-functions/v2/https');
|
|
152
152
|
throw new HttpsError(code as any, message, details);
|
|
153
153
|
}
|
|
154
154
|
|
package/src/shared/firebase.ts
CHANGED
package/src/shared/index.ts
CHANGED
|
@@ -19,8 +19,9 @@ export * from './oauth/index.js';
|
|
|
19
19
|
export {
|
|
20
20
|
createTimestamp,
|
|
21
21
|
toTimestamp,
|
|
22
|
-
toISOString,
|
|
23
22
|
isTimestamp,
|
|
24
23
|
transformFirestoreData,
|
|
25
24
|
prepareForFirestore,
|
|
26
25
|
} from './firebase.js';
|
|
26
|
+
// Note: toISOString is exported from ./utils/external/date.js (handles DateValue).
|
|
27
|
+
// The Firebase-specific toISOString (FirestoreTimestamp only) is available via direct import from ./firebase.js.
|
package/src/shared/logger.ts
CHANGED
|
@@ -28,10 +28,8 @@ if (typeof globalThis !== 'undefined' && 'window' in globalThis) {
|
|
|
28
28
|
throw new Error('Server logger cannot be imported on client side');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// ServerShim:
|
|
32
|
-
|
|
33
|
-
throw new Error('Server logger requires Node.js environment');
|
|
34
|
-
}
|
|
31
|
+
// ServerShim: Warn if not in Node.js (e.g., Deno) but don't throw — allow fallback logging
|
|
32
|
+
const _isNodeEnv = typeof process !== 'undefined' && !!process.versions?.node;
|
|
35
33
|
|
|
36
34
|
let sentryEnabled = false;
|
|
37
35
|
let sentryClient: any = null;
|
|
@@ -247,9 +245,13 @@ export const logger = {
|
|
|
247
245
|
context,
|
|
248
246
|
error: context?.error,
|
|
249
247
|
metadata: {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
248
|
+
...(_isNodeEnv
|
|
249
|
+
? {
|
|
250
|
+
pid: process.pid,
|
|
251
|
+
nodeVersion: process.version,
|
|
252
|
+
platform: process.platform,
|
|
253
|
+
}
|
|
254
|
+
: {}),
|
|
253
255
|
...context?.metadata,
|
|
254
256
|
},
|
|
255
257
|
};
|