@fragno-dev/stripe 0.0.2 → 0.1.0

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.
@@ -1,8 +1,8 @@
1
- import { createFragment, defineRoute, defineRoutes } from "@fragno-dev/core";
2
1
  import { createClientBuilder } from "@fragno-dev/core/client";
3
- import { defineFragmentWithDatabase } from "@fragno-dev/db/fragment";
4
2
  import Stripe from "stripe";
5
3
  import { column, idColumn, schema } from "@fragno-dev/db/schema";
4
+ import { defineFragment, defineRoutes, instantiate } from "@fragno-dev/core";
5
+ import { withDatabase } from "@fragno-dev/db";
6
6
  import { z } from "zod";
7
7
  import { FragnoApiError } from "@fragno-dev/core/api";
8
8
 
@@ -43,10 +43,108 @@ function stripeSubscriptionToInternalSubscription(stripeSubscription) {
43
43
  trialEnd: toDate(stripeSubscription.trial_end),
44
44
  cancelAtPeriodEnd: stripeSubscription.cancel_at_period_end ?? false,
45
45
  cancelAt: toDate(stripeSubscription.cancel_at),
46
+ createdAt: toDate(stripeSubscription.created),
46
47
  seats: firstItem.quantity ?? null
47
48
  };
48
49
  }
49
50
 
51
+ //#endregion
52
+ //#region src/definition.ts
53
+ const LOG_PREFIX = "[Stripe Fragment]";
54
+ const defaultLogger = {
55
+ info: (...data) => console.info(LOG_PREFIX, ...data),
56
+ error: (...data) => console.error(LOG_PREFIX, ...data),
57
+ warn: (...data) => console.warn(LOG_PREFIX, ...data),
58
+ debug: (...data) => console.debug(LOG_PREFIX, ...data),
59
+ log: (...data) => console.log(LOG_PREFIX, ...data)
60
+ };
61
+ const asExternalSubscription = (subscription) => ({
62
+ ...subscription,
63
+ id: subscription.id.externalId,
64
+ status: subscription.status
65
+ });
66
+ function createStripeServices(deps, db) {
67
+ const services = {
68
+ getStripeClient() {
69
+ return deps.stripe;
70
+ },
71
+ createSubscription: async (data) => {
72
+ return (await db.create("subscription", data)).externalId;
73
+ },
74
+ updateSubscription: async (id, data) => {
75
+ await db.update("subscription", id, (b) => b.set({
76
+ ...data,
77
+ updatedAt: new Date()
78
+ }));
79
+ },
80
+ getSubscriptionByStripeId: async (stripeSubscriptionId) => {
81
+ const result = await db.findFirst("subscription", (b) => b.whereIndex("idx_stripe_subscription_id", (eb) => eb("stripeSubscriptionId", "=", stripeSubscriptionId)));
82
+ if (!result) return null;
83
+ return asExternalSubscription(result);
84
+ },
85
+ getSubscriptionsByStripeCustomerId: async (stripeCustomerId) => {
86
+ return (await db.find("subscription", (b) => b.whereIndex("idx_stripe_customer_id", (eb) => eb("stripeCustomerId", "=", stripeCustomerId)))).map(asExternalSubscription);
87
+ },
88
+ getSubscriptionById: async (id) => {
89
+ const result = await db.findFirst("subscription", (b) => b.whereIndex("primary", (eb) => eb("id", "=", id)));
90
+ if (!result) return null;
91
+ return asExternalSubscription(result);
92
+ },
93
+ getSubscriptionsByReferenceId: async (referenceId) => {
94
+ const result = await db.find("subscription", (b) => b.whereIndex("idx_reference_id", (eb) => eb("referenceId", "=", referenceId)));
95
+ if (result.length == 0) return [];
96
+ return result.map(asExternalSubscription);
97
+ },
98
+ deleteSubscription: async (id) => {
99
+ await db.delete("subscription", id);
100
+ },
101
+ deleteSubscriptionsByReferenceId: async (referenceId) => {
102
+ const uow = db.createUnitOfWork().find("subscription", (b) => b.whereIndex("idx_reference_id", (eb) => eb("referenceId", "=", referenceId)));
103
+ const [subscriptions] = await uow.executeRetrieve();
104
+ subscriptions.forEach((sub) => sub && uow.delete("subscription", sub.id));
105
+ return await uow.executeMutations();
106
+ },
107
+ getAllSubscriptions: async () => {
108
+ return (await db.find("subscription", (b) => b.whereIndex("primary"))).map(asExternalSubscription);
109
+ },
110
+ syncStripeSubscriptions: async (referenceId, stripeCustomerId) => {
111
+ const stripeSubscriptions = await deps.stripe.subscriptions.list({
112
+ customer: stripeCustomerId,
113
+ status: "all"
114
+ });
115
+ if (stripeSubscriptions.data.length === 0) {
116
+ await services.deleteSubscriptionsByReferenceId(referenceId);
117
+ return { success: true };
118
+ }
119
+ const uow = db.createUnitOfWork().find("subscription", (b) => b.whereIndex("idx_reference_id", (eb) => eb("referenceId", "=", referenceId)));
120
+ const [existingSubscriptions] = await uow.executeRetrieve();
121
+ for (const stripeSubscription of stripeSubscriptions.data) {
122
+ const existingSubscription = existingSubscriptions.find((sub) => sub.stripeSubscriptionId === stripeSubscription.id);
123
+ if (existingSubscription) uow.update("subscription", existingSubscription.id, (b) => b.set({
124
+ ...stripeSubscriptionToInternalSubscription(stripeSubscription),
125
+ updatedAt: new Date()
126
+ }).check());
127
+ else uow.create("subscription", {
128
+ ...stripeSubscriptionToInternalSubscription(stripeSubscription),
129
+ referenceId: referenceId ?? null,
130
+ updatedAt: new Date()
131
+ });
132
+ }
133
+ return uow.executeMutations();
134
+ }
135
+ };
136
+ return services;
137
+ }
138
+ const stripeFragmentDefinition = defineFragment("stripe").extend(withDatabase(stripeSchema)).withDependencies(({ config }) => {
139
+ const stripeClient = new Stripe(config.stripeSecretKey, config.stripeClientOptions ?? {});
140
+ return {
141
+ stripe: stripeClient,
142
+ log: config.logger ? config.logger : defaultLogger
143
+ };
144
+ }).providesBaseService(({ deps }) => {
145
+ return { ...createStripeServices(deps, deps.db) };
146
+ }).build();
147
+
50
148
  //#endregion
51
149
  //#region src/webhook/handlers.ts
52
150
  /**
@@ -161,7 +259,7 @@ async function customerSubscriptionUpdatedHandler({ event, services, deps }) {
161
259
  const customerId = getId(stripeSubscription.customer);
162
260
  let subscription = await services.getSubscriptionByStripeId(stripeSubscription.id);
163
261
  if (!subscription) {
164
- const customerSubs = await services.getSubscriptionByStripeCustomerId(customerId);
262
+ const customerSubs = await services.getSubscriptionsByStripeCustomerId(customerId);
165
263
  if (customerSubs.length > 1) {
166
264
  subscription = customerSubs.find((sub) => sub.status === "active" || sub.status === "trialing") ?? null;
167
265
  if (!subscription) {
@@ -212,7 +310,7 @@ const eventToHandler = {
212
310
 
213
311
  //#endregion
214
312
  //#region src/routes/webhooks.ts
215
- const webhookRoutesFactory = defineRoutes().create(({ config, deps, services }) => {
313
+ const webhookRoutesFactory = defineRoutes(stripeFragmentDefinition).create(({ config, deps, services, defineRoute }) => {
216
314
  return [defineRoute({
217
315
  method: "POST",
218
316
  path: "/webhook",
@@ -295,9 +393,38 @@ const CustomerResponseSchema = z.object({
295
393
  preferred_locales: z.array(z.string()).nullable().optional()
296
394
  });
297
395
 
396
+ //#endregion
397
+ //#region src/routes/errors.ts
398
+ function isStripeError(error) {
399
+ return typeof error === "object" && error !== null && "type" in error && typeof error.type === "string" && "message" in error && typeof error.message === "string";
400
+ }
401
+ function stripeToApiError(error) {
402
+ if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.message.includes("there are no changes to confirm")) return new FragnoApiError({
403
+ message: "Trying to upgrade to same subscription plan",
404
+ code: "UPGRADE_HAS_NO_EFFECT"
405
+ }, 500);
406
+ if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.message.includes("is already set to be canceled at period end")) return new FragnoApiError({
407
+ message: "Subscription is already set to be canceled at period end",
408
+ code: "SUBSCRIPTION_ALREADY_CANCELED"
409
+ }, 500);
410
+ if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.message.includes("the subscription update feature in the portal configuration is disabled")) return new FragnoApiError({
411
+ message: "Subscription cannot be updated to this plan",
412
+ code: "SUBSCRIPTION_UPDATE_NOT_ALLOWED"
413
+ }, 500);
414
+ if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.message.includes("does not include `promotion_code`")) return new FragnoApiError({
415
+ message: "Cannot apply promotion code when updating subscription",
416
+ code: "SUBSCRIPTION_UPDATE_PROMO_CODE_NOT_ALLOWED"
417
+ }, 500);
418
+ if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.code == "promotion_code_customer_not_first_time") return new FragnoApiError({
419
+ message: "This promotion code cannot be redeemed because you have already used it before.",
420
+ code: "PROMOTION_CODE_CUSTOMER_NOT_FIRST_TIME"
421
+ }, 500);
422
+ return error;
423
+ }
424
+
298
425
  //#endregion
299
426
  //#region src/routes/customers.ts
300
- const customersRoutesFactory = defineRoutes().create(({ deps, config }) => {
427
+ const customersRoutesFactory = defineRoutes(stripeFragmentDefinition).create(({ deps, config, defineRoute }) => {
301
428
  return [defineRoute({
302
429
  method: "GET",
303
430
  path: "/admin/customers",
@@ -325,6 +452,35 @@ const customersRoutesFactory = defineRoutes().create(({ deps, config }) => {
325
452
  hasMore: customers.has_more
326
453
  });
327
454
  }
455
+ }), defineRoute({
456
+ method: "POST",
457
+ path: "/portal",
458
+ inputSchema: z.object({ returnUrl: z.url().describe("URL to redirect to after completing billing portal") }),
459
+ outputSchema: z.object({
460
+ url: z.url().describe("URL to redirect to after cancellation"),
461
+ redirect: z.boolean().describe("Whether to redirect to the URL")
462
+ }),
463
+ errorCodes: ["NO_STRIPE_CUSTOMER_FOR_ENTITY"],
464
+ handler: async (context, { json, error }) => {
465
+ const body = await context.input.valid();
466
+ const { stripeCustomerId } = await config.resolveEntityFromRequest(context);
467
+ if (!stripeCustomerId) return error({
468
+ message: "No stripe customer to create billing portal for",
469
+ code: "NO_STRIPE_CUSTOMER_FOR_ENTITY"
470
+ }, 400);
471
+ try {
472
+ const portalSession = await deps.stripe.billingPortal.sessions.create({
473
+ customer: stripeCustomerId,
474
+ return_url: body.returnUrl
475
+ });
476
+ return json({
477
+ url: portalSession.url,
478
+ redirect: true
479
+ });
480
+ } catch (err) {
481
+ throw stripeToApiError(err);
482
+ }
483
+ }
328
484
  })];
329
485
  });
330
486
 
@@ -361,33 +517,14 @@ const SubscriptionUpgradeRequestSchema = z.object({
361
517
  quantity: z.number().positive().describe("Number of seats"),
362
518
  successUrl: z.url().describe("Redirect URL after successful checkout"),
363
519
  cancelUrl: z.url().describe("Redirect URL if checkout is cancelled"),
364
- returnUrl: z.string().optional().describe("Return URL for billing portal")
520
+ returnUrl: z.string().optional().describe("Return URL for billing portal"),
521
+ promotionCode: z.string().optional().describe("Promotion code to apply"),
522
+ subscriptionId: z.string().optional().describe("Subscription ID to upgrade, if none provided assume the active subscription of the user.")
365
523
  });
366
524
 
367
- //#endregion
368
- //#region src/routes/errors.ts
369
- function isStripeError(error) {
370
- return typeof error === "object" && error !== null && "type" in error && typeof error.type === "string" && "message" in error && typeof error.message === "string";
371
- }
372
- function stripeToApiError(error) {
373
- if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.message.includes("there are no changes to confirm")) return new FragnoApiError({
374
- message: "Trying to upgrade to same subscription plan",
375
- code: "UPGRADE_HAS_NO_EFFECT"
376
- }, 500);
377
- if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.message.includes("is already set to be canceled at period end")) return new FragnoApiError({
378
- message: "Subscription is already set to be canceled at period end",
379
- code: "SUBSCRIPTION_ALREADY_CANCELED"
380
- }, 500);
381
- if (isStripeError(error) && error.type === "StripeInvalidRequestError" && error.message.includes("the subscription update feature in the portal configuration is disabled")) return new FragnoApiError({
382
- message: "Subscription cannot be updated to this plan",
383
- code: "SUBSCRIPTION_UPDATE_NOT_ALLOWED"
384
- }, 500);
385
- return error;
386
- }
387
-
388
525
  //#endregion
389
526
  //#region src/routes/subscriptions.ts
390
- const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, config }) => {
527
+ const subscriptionsRoutesFactory = defineRoutes(stripeFragmentDefinition).create(({ deps, services, config, defineRoute }) => {
391
528
  return [
392
529
  defineRoute({
393
530
  method: "GET",
@@ -415,19 +552,30 @@ const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, conf
415
552
  "SUBSCRIPTION_NOT_FOUND",
416
553
  "CUSTOMER_SUBSCRIPTION_MISMATCH",
417
554
  "UPGRADE_HAS_NO_EFFECT",
418
- "SUBSCRIPTION_UPDATE_NOT_ALLOWED"
555
+ "SUBSCRIPTION_UPDATE_NOT_ALLOWED",
556
+ "SUBSCRIPTION_UPDATE_PROMO_CODE_NOT_ALLOWED",
557
+ "PROMOTION_CODE_CUSTOMER_NOT_FIRST_TIME",
558
+ "MULTIPLE_ACTIVE_SUBSCRIPTIONS",
559
+ "NO_ACTIVE_SUBSCRIPTIONS"
419
560
  ],
420
561
  handler: async (context, { json, error }) => {
421
562
  const body = await context.input.valid();
422
- const user = await config.resolveEntityFromRequest(context);
423
- let customerId = user.stripeCustomerId;
563
+ const entity = await config.resolveEntityFromRequest(context);
564
+ let customerId = entity.stripeCustomerId;
424
565
  let existingSubscription = null;
425
- if (user.subscriptionId) {
426
- existingSubscription = await services.getSubscriptionById(user.subscriptionId);
427
- if (!existingSubscription) return error({
428
- message: "Could not retrieve existing subscription",
429
- code: "SUBSCRIPTION_NOT_FOUND"
430
- }, 404);
566
+ if (entity.stripeCustomerId) {
567
+ const existingSubscriptions = await services.getSubscriptionsByStripeCustomerId(entity.stripeCustomerId);
568
+ let activeSubscriptions = existingSubscriptions.filter((s) => s.status !== "canceled");
569
+ if (body.subscriptionId) activeSubscriptions = activeSubscriptions.filter((s) => s.id === body.subscriptionId);
570
+ if (activeSubscriptions.length > 1) return error({
571
+ message: "Multiple active subscriptions found for customer, please specify which subscription to upgrade",
572
+ code: "MULTIPLE_ACTIVE_SUBSCRIPTIONS"
573
+ }, 400);
574
+ if (activeSubscriptions.length === 0) return error({
575
+ message: "No active subscriptions found for customer",
576
+ code: "NO_ACTIVE_SUBSCRIPTIONS"
577
+ }, 400);
578
+ existingSubscription = activeSubscriptions[0];
431
579
  if (customerId && existingSubscription.stripeCustomerId && existingSubscription.stripeCustomerId !== customerId) return error({
432
580
  message: "Subsciption being updated does not belong to Stripe Customer that was provided",
433
581
  code: "CUSTOMER_SUBSCRIPTION_MISMATCH"
@@ -436,33 +584,50 @@ const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, conf
436
584
  }
437
585
  if (!customerId) {
438
586
  const existingLinkedCustomer = await deps.stripe.customers.search({
439
- query: `metadata['referenceId']:'${user.referenceId}'`,
587
+ query: `metadata['referenceId']:'${entity.referenceId}'`,
440
588
  limit: 1
441
589
  });
442
590
  if (existingLinkedCustomer.data.length === 1) customerId = existingLinkedCustomer.data[0].id;
443
591
  else {
444
- if (!user.customerEmail || !user.referenceId) return error({
445
- message: "New Stripe Customer must be created, but customerEmail or referenceID has not been provide",
592
+ if (!entity.customerEmail || !entity.referenceId) return error({
593
+ message: "New Stripe Customer must be created, but customerEmail or referenceID has not been provided",
446
594
  code: "MISSING_CUSTOMER_INFO"
447
595
  });
448
596
  const newCustomer = await deps.stripe.customers.create({
449
- email: user.customerEmail,
597
+ email: entity.customerEmail,
450
598
  metadata: {
451
- referenceId: user.referenceId,
452
- ...user.stripeMetadata
599
+ referenceId: entity.referenceId,
600
+ ...entity.stripeMetadata
453
601
  }
454
602
  });
455
603
  customerId = newCustomer.id;
456
604
  }
457
605
  try {
458
- await config.onStripeCustomerCreated(customerId, user.referenceId);
606
+ await config.onStripeCustomerCreated(customerId, entity.referenceId);
459
607
  } catch (error$1) {
460
608
  deps.log.error("onStripeCustomerCreated failed!", error$1);
461
609
  }
462
610
  }
611
+ let promotionCodeId = void 0;
612
+ if (body.promotionCode) {
613
+ const promotionCodes = await deps.stripe.promotionCodes.list({
614
+ code: body.promotionCode,
615
+ active: true,
616
+ limit: 1
617
+ });
618
+ if (promotionCodes.data.length > 0) promotionCodeId = promotionCodes.data[0].id;
619
+ }
463
620
  if (existingSubscription?.status === "active" || existingSubscription?.status === "trialing") {
464
621
  const stripeSubscription = await deps.stripe.subscriptions.retrieve(existingSubscription.stripeSubscriptionId);
465
622
  try {
623
+ if (existingSubscription.cancelAt != null) {
624
+ deps.log.info("un-cancelling subscription", stripeSubscription.id);
625
+ await deps.stripe.subscriptions.update(stripeSubscription.id, { cancel_at_period_end: false });
626
+ return json({
627
+ url: body.returnUrl || body.successUrl,
628
+ redirect: true
629
+ });
630
+ }
466
631
  const portalSession = await deps.stripe.billingPortal.sessions.create({
467
632
  customer: customerId,
468
633
  return_url: body.returnUrl || body.successUrl,
@@ -478,7 +643,8 @@ const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, conf
478
643
  id: stripeSubscription.items.data[0]?.id,
479
644
  quantity: body.quantity,
480
645
  price: body.priceId
481
- }]
646
+ }],
647
+ ...promotionCodeId && { discounts: [{ promotion_code: promotionCodeId }] }
482
648
  }
483
649
  }
484
650
  });
@@ -499,7 +665,8 @@ const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, conf
499
665
  quantity: body.quantity
500
666
  }],
501
667
  mode: "subscription",
502
- metadata: { referenceId: user.referenceId }
668
+ metadata: { referenceId: entity.referenceId },
669
+ ...promotionCodeId && { discounts: [{ promotion_code: promotionCodeId }] }
503
670
  });
504
671
  return json({
505
672
  url: checkoutSession.url,
@@ -511,7 +678,10 @@ const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, conf
511
678
  defineRoute({
512
679
  method: "POST",
513
680
  path: "/subscription/cancel",
514
- inputSchema: z.object({ returnUrl: z.url().describe("URL to redirect to after cancellation is complete") }),
681
+ inputSchema: z.object({
682
+ returnUrl: z.url().describe("URL to redirect to after cancellation is complete"),
683
+ subscriptionId: z.string().optional().describe("Which subscription to cancel, if there are multiple active subscriptions")
684
+ }),
515
685
  outputSchema: z.object({
516
686
  url: z.url().describe("URL to redirect to after cancellation"),
517
687
  redirect: z.boolean().describe("Whether to redirect to the URL")
@@ -519,30 +689,30 @@ const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, conf
519
689
  errorCodes: [
520
690
  "SUBSCRIPTION_NOT_FOUND",
521
691
  "NO_SUBSCRIPTION_TO_CANCEL",
522
- "SUBSCRIPTION_ALREADY_CANCELED"
692
+ "SUBSCRIPTION_ALREADY_CANCELED",
693
+ "NO_STRIPE_CUSTOMER_LINKED",
694
+ "MULTIPLE_SUBSCRIPTIONS_FOUND"
523
695
  ],
524
696
  handler: async (context, { json, error }) => {
525
697
  const body = await context.input.valid();
526
- const { subscriptionId } = await config.resolveEntityFromRequest(context);
527
- if (!subscriptionId) return error({
528
- message: "No subscription to cancel",
698
+ const { stripeCustomerId } = await config.resolveEntityFromRequest(context);
699
+ if (!stripeCustomerId) return error({
700
+ message: "No stripe customer linked to entity",
701
+ code: "NO_STRIPE_CUSTOMER_LINKED"
702
+ }, 400);
703
+ let activeSubscriptions = (await services.getSubscriptionsByStripeCustomerId(stripeCustomerId)).filter((s) => s.status === "active");
704
+ if (body.subscriptionId) activeSubscriptions = activeSubscriptions.filter((s) => s.id === body.subscriptionId);
705
+ if (activeSubscriptions.length === 0) return error({
706
+ message: "No active subscription to cancel",
529
707
  code: "NO_SUBSCRIPTION_TO_CANCEL"
530
708
  }, 404);
531
- const subscription = await services.getSubscriptionById(subscriptionId);
532
- if (!subscription) return error({
533
- message: "Subscription not found",
534
- code: "SUBSCRIPTION_NOT_FOUND"
535
- }, 404);
536
- if (subscription.status === "canceled") return error({
537
- message: "Subscription is already canceled",
538
- code: "SUBSCRIPTION_ALREADY_CANCELED"
709
+ if (activeSubscriptions.length > 1) return error({
710
+ message: "Multiple active subscriptions found",
711
+ code: "MULTIPLE_SUBSCRIPTIONS_FOUND"
539
712
  }, 400);
713
+ const activeSubscription = activeSubscriptions[0];
540
714
  try {
541
- const activeSubscriptions = await deps.stripe.subscriptions.list({
542
- customer: subscription.stripeCustomerId,
543
- status: "active"
544
- });
545
- const stripeSubscription = activeSubscriptions.data.find((sub) => sub.id === subscription.stripeSubscriptionId);
715
+ const stripeSubscription = await deps.stripe.subscriptions.retrieve(activeSubscription.stripeSubscriptionId);
546
716
  if (!stripeSubscription) return error({
547
717
  message: "Active subscription not found in Stripe",
548
718
  code: "SUBSCRIPTION_NOT_FOUND"
@@ -552,7 +722,7 @@ const subscriptionsRoutesFactory = defineRoutes().create(({ deps, services, conf
552
722
  redirect: false
553
723
  });
554
724
  const portalSession = await deps.stripe.billingPortal.sessions.create({
555
- customer: subscription.stripeCustomerId,
725
+ customer: activeSubscription.stripeCustomerId,
556
726
  return_url: body.returnUrl,
557
727
  flow_data: {
558
728
  type: "subscription_cancel",
@@ -602,7 +772,7 @@ const ProductResponseSchema = z.object({
602
772
 
603
773
  //#endregion
604
774
  //#region src/routes/products.ts
605
- const productsRoutesFactory = defineRoutes().create(({ deps, config }) => {
775
+ const productsRoutesFactory = defineRoutes(stripeFragmentDefinition).create(({ deps, config, defineRoute }) => {
606
776
  return [defineRoute({
607
777
  method: "GET",
608
778
  path: "/admin/products",
@@ -672,7 +842,7 @@ const PriceResponseSchema = z.object({
672
842
 
673
843
  //#endregion
674
844
  //#region src/routes/prices.ts
675
- const pricesRoutesFactory = defineRoutes().create(({ deps, config }) => {
845
+ const pricesRoutesFactory = defineRoutes(stripeFragmentDefinition).create(({ deps, config, defineRoute }) => {
676
846
  return [defineRoute({
677
847
  method: "GET",
678
848
  path: "/admin/products/:productId/prices",
@@ -707,143 +877,24 @@ const pricesRoutesFactory = defineRoutes().create(({ deps, config }) => {
707
877
 
708
878
  //#endregion
709
879
  //#region src/index.ts
710
- const LOG_PREFIX = "[Stripe Fragment]";
711
- const defaultLogger = {
712
- info: (...data) => console.info(LOG_PREFIX, ...data),
713
- error: (...data) => console.error(LOG_PREFIX, ...data),
714
- warn: (...data) => console.warn(LOG_PREFIX, ...data),
715
- debug: (...data) => console.debug(LOG_PREFIX, ...data),
716
- log: (...data) => console.log(LOG_PREFIX, ...data)
717
- };
718
- const stripeFragmentDefinition = defineFragmentWithDatabase("stripe").withDatabase(stripeSchema, "stripe").withDependencies(({ config }) => {
719
- const stripeClient = new Stripe(config.stripeSecretKey, config.stripeClientOptions ?? {});
720
- return {
721
- stripe: stripeClient,
722
- log: config.logger ? config.logger : defaultLogger
723
- };
724
- }).providesService(({ deps, db, defineService }) => {
725
- return defineService({ ...createStripeServices(deps, db) });
726
- });
727
- function createStripeServices(deps, db) {
728
- const services = {
729
- getStripeClient() {
730
- return deps.stripe;
731
- },
732
- createSubscription: async (data) => {
733
- return (await db.create("subscription", data)).externalId;
734
- },
735
- updateSubscription: async (id, data) => {
736
- await db.update("subscription", id, (b) => b.set({
737
- ...data,
738
- updatedAt: new Date()
739
- }));
740
- },
741
- getSubscriptionByStripeId: async (stripeSubscriptionId) => {
742
- const result = await db.findFirst("subscription", (b) => b.whereIndex("idx_stripe_subscription_id", (eb) => eb("stripeSubscriptionId", "=", stripeSubscriptionId)));
743
- if (!result) return null;
744
- return {
745
- ...result,
746
- id: result.id.externalId,
747
- status: result.status
748
- };
749
- },
750
- getSubscriptionByStripeCustomerId: async (stripeCustomerId) => {
751
- return (await db.find("subscription", (b) => b.whereIndex("idx_stripe_customer_id", (eb) => eb("stripeCustomerId", "=", stripeCustomerId)))).map((subscription) => ({
752
- ...subscription,
753
- id: subscription.id.externalId,
754
- status: subscription.status
755
- }));
756
- },
757
- getSubscriptionById: async (id) => {
758
- const result = await db.findFirst("subscription", (b) => b.whereIndex("primary", (eb) => eb("id", "=", id)));
759
- if (!result) return null;
760
- return {
761
- ...result,
762
- id: result.id.externalId,
763
- status: result.status
764
- };
765
- },
766
- getSubscriptionByReferenceId: async (referenceId) => {
767
- const result = await db.findFirst("subscription", (b) => b.whereIndex("idx_reference_id", (eb) => eb("referenceId", "=", referenceId)));
768
- if (!result) return null;
769
- return {
770
- ...result,
771
- id: result.id.externalId,
772
- status: result.status
773
- };
774
- },
775
- deleteSubscription: async (id) => {
776
- await db.delete("subscription", id);
777
- },
778
- deleteSubscriptionByReferenceId: async (referenceId) => {
779
- const uow = db.createUnitOfWork().find("subscription", (b) => b.whereIndex("idx_reference_id", (eb) => eb("referenceId", "=", referenceId)));
780
- const [subscriptions] = await uow.executeRetrieve();
781
- const subscription = subscriptions[0];
782
- if (subscription) {
783
- uow.delete("subscription", subscription.id);
784
- const success = await uow.executeMutations();
785
- if (!success) throw new Error("Failed to deleted subscription, conflict on subscription resource");
786
- return true;
787
- }
788
- return false;
789
- },
790
- getAllSubscriptions: async () => {
791
- return (await db.find("subscription", (b) => b.whereIndex("primary"))).map((subscription) => ({
792
- ...subscription,
793
- id: subscription.id.externalId,
794
- status: subscription.status
795
- }));
796
- },
797
- syncStripeSubscription: async (referenceId, stripeCustomerId) => {
798
- const stripeSubscriptions = await deps.stripe.subscriptions.list({
799
- customer: stripeCustomerId,
800
- limit: 1,
801
- status: "all"
802
- });
803
- if (stripeSubscriptions.data.length === 0) {
804
- await services.deleteSubscriptionByReferenceId(referenceId);
805
- return;
806
- }
807
- const stripeSubscription = stripeSubscriptions.data[0];
808
- const existingSubscription = await db.findFirst("subscription", (b) => b.whereIndex("idx_stripe_subscription_id", (eb) => eb("stripeSubscriptionId", "=", stripeSubscription.id)));
809
- if (existingSubscription) await db.update("subscription", existingSubscription.id, (b) => b.set({
810
- ...stripeSubscriptionToInternalSubscription(stripeSubscription),
811
- updatedAt: new Date()
812
- }));
813
- else {
814
- const subscriptionData = {
815
- ...stripeSubscriptionToInternalSubscription(stripeSubscription),
816
- referenceId: referenceId ?? null
817
- };
818
- await services.createSubscription(subscriptionData);
819
- }
820
- return;
821
- }
822
- };
823
- return services;
824
- }
880
+ const routes = [
881
+ webhookRoutesFactory,
882
+ customersRoutesFactory,
883
+ subscriptionsRoutesFactory,
884
+ productsRoutesFactory,
885
+ pricesRoutesFactory
886
+ ];
825
887
  function createStripeFragment(config, fragnoConfig) {
826
- return createFragment(stripeFragmentDefinition, config, [
827
- webhookRoutesFactory,
828
- customersRoutesFactory,
829
- subscriptionsRoutesFactory,
830
- productsRoutesFactory,
831
- pricesRoutesFactory
832
- ], fragnoConfig);
888
+ return instantiate(stripeFragmentDefinition).withConfig(config).withRoutes(routes).withOptions(fragnoConfig).build();
833
889
  }
834
890
  function createStripeFragmentClients(fragnoConfig = {}) {
835
- const builder = createClientBuilder(stripeFragmentDefinition, fragnoConfig, [
836
- webhookRoutesFactory,
837
- customersRoutesFactory,
838
- subscriptionsRoutesFactory,
839
- productsRoutesFactory,
840
- pricesRoutesFactory
841
- ]);
891
+ const builder = createClientBuilder(stripeFragmentDefinition, fragnoConfig, routes);
842
892
  return {
843
893
  useCustomers: builder.createHook("/admin/customers"),
844
894
  useProducts: builder.createHook("/admin/products"),
845
895
  usePrices: builder.createHook("/admin/products/:productId/prices"),
846
896
  useSubscription: builder.createHook("/admin/subscriptions"),
897
+ useBillingPortal: builder.createMutator("POST", "/portal"),
847
898
  upgradeSubscription: builder.createMutator("POST", "/subscription/upgrade"),
848
899
  cancelSubscription: builder.createMutator("POST", "/subscription/cancel")
849
900
  };