@nexo-labs/payload-stripe-inventory 1.6.11 → 1.6.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.
@@ -1,9 +1,6 @@
1
- import { S as generateUserInventory, _ as PricingType, a as getPermissionsSlugs, b as permissionSlugs, d as COLLECTION_SLUG_PRICES, f as COLLECTION_SLUG_PRODUCTS, g as PricingPlanInterval, m as MAX_UNLOCKS_PER_WEEK, n as countWeeklyUnlocksQuery, p as COLLECTION_SLUG_USER, r as checkIfUserCanUnlockQuery, u as COLLECTION_SLUG_CUSTOMERS, x as generateCustomerInventory, y as formatOptions } from "../src-BmlQoR4x.mjs";
2
- import Stripe from "stripe";
3
- import { headers } from "next/headers.js";
4
- import { NextResponse } from "next/server.js";
5
- import { COLLECTION_SLUG_TAXONOMY, buildTaxonomyRelationship } from "@nexo-labs/payload-taxonomies";
1
+ import { S as generateUserInventory, _ as PricingType, a as checkIfUserCanUnlockQuery, b as permissionSlugs, d as COLLECTION_SLUG_PRICES, f as COLLECTION_SLUG_PRODUCTS, g as PricingPlanInterval, i as countWeeklyUnlocksQuery, m as MAX_UNLOCKS_PER_WEEK, p as COLLECTION_SLUG_USER, u as COLLECTION_SLUG_CUSTOMERS, x as generateCustomerInventory, y as formatOptions } from "../src-I_DPhIL5.mjs";
6
2
  import { stripePlugin } from "@payloadcms/plugin-stripe";
3
+ import Stripe from "stripe";
7
4
 
8
5
  //#region src/server/utils/payload/upsert.ts
9
6
  const payloadUpsert = async ({ payload, collection, data, where }) => {
@@ -31,8 +28,13 @@ const payloadUpsert = async ({ payload, collection, data, where }) => {
31
28
 
32
29
  //#endregion
33
30
  //#region src/server/utils/stripe/stripe-builder.ts
31
+ let stripeInstance = null;
34
32
  const stripeBuilder = () => {
35
- return new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2024-09-30.acacia" });
33
+ if (stripeInstance) return stripeInstance;
34
+ const secretKey = process.env.STRIPE_SECRET_KEY;
35
+ if (!secretKey) throw new Error("STRIPE_SECRET_KEY environment variable is not set");
36
+ stripeInstance = new Stripe(secretKey, { apiVersion: "2024-09-30.acacia" });
37
+ return stripeInstance;
36
38
  };
37
39
 
38
40
  //#endregion
@@ -42,23 +44,23 @@ const updatePrices = async (payload) => {
42
44
  limit: 100,
43
45
  active: true
44
46
  })).data.map((price) => priceUpsert(price, payload));
45
- const pricesByProductId = (await Promise.all(promises)).mapNotNull((t) => t).reduce((acc, { productId, priceId }) => {
47
+ const pricesByProductId = (await Promise.all(promises)).filter((p) => p !== null).reduce((acc, { productId, priceId }) => {
46
48
  if (!acc[productId]) acc[productId] = [];
47
49
  acc[productId].push(priceId);
48
50
  return acc;
49
51
  }, {});
50
- Object.entries(pricesByProductId).map(async ([productId, prices$1]) => {
52
+ await Promise.all(Object.entries(pricesByProductId).map(async ([productId, prices$1]) => {
51
53
  await payload.update({
52
54
  collection: COLLECTION_SLUG_PRODUCTS,
53
55
  data: { prices: prices$1 },
54
56
  where: { stripeID: { equals: productId } }
55
57
  });
56
- });
58
+ }));
57
59
  };
58
60
  async function priceUpsert(price, payload) {
59
61
  const stripeProductID = typeof price.product === "string" ? price.product : price.product.id;
60
62
  if (price.deleted !== void 0) {
61
- priceDeleted(price, payload);
63
+ await priceDeleted(price, payload);
62
64
  return null;
63
65
  }
64
66
  if (price.unit_amount == null) return null;
@@ -99,14 +101,15 @@ const priceDeleted = async (price, payload) => {
99
101
  //#endregion
100
102
  //#region src/server/actions/product.ts
101
103
  const updateProducts = async (payload) => {
102
- (await (await stripeBuilder()).products.list({
104
+ const products$1 = await (await stripeBuilder()).products.list({
103
105
  limit: 100,
104
106
  active: true
105
- })).data.forEach((product) => productSync(product, payload));
107
+ });
108
+ await Promise.all(products$1.data.map((product) => productSync(product, payload)));
106
109
  };
107
110
  const productSync = async (object, payload) => {
108
111
  const { id: stripeProductID, name, description, images } = object;
109
- if (object.deleted !== void 0) return productDeleted(object, payload);
112
+ if (object.deleted !== void 0) return await productDeleted(object, payload);
110
113
  try {
111
114
  await payloadUpsert({
112
115
  payload,
@@ -153,7 +156,7 @@ async function syncCustomerByEmail({ email, payload }) {
153
156
  where: { email: { equals: email } }
154
157
  })).docs?.[0]?.id;
155
158
  await payload.update({
156
- collection: "users",
159
+ collection: COLLECTION_SLUG_USER,
157
160
  data: { customer: customerId },
158
161
  where: { email: { equals: email } }
159
162
  });
@@ -196,7 +199,8 @@ async function getUserIdByEmail({ email, payload }) {
196
199
  //#region src/server/utils/stripe/get-customer.ts
197
200
  async function getCustomer({ stripe, email }) {
198
201
  stripe = stripe ?? stripeBuilder();
199
- const customers$1 = await stripe.customers.search({ query: `email:'${email}'` });
202
+ const sanitizedEmail = email.replace(/'/g, "\\'");
203
+ const customers$1 = await stripe.customers.search({ query: `email:'${sanitizedEmail}'` });
200
204
  return customers$1.data.length ? customers$1.data[0] : null;
201
205
  }
202
206
  async function resolveStripeCustomer({ customer }) {
@@ -257,7 +261,7 @@ async function findOrCreateCustomer({ email, payload, stripeId }) {
257
261
 
258
262
  //#endregion
259
263
  //#region src/server/actions/subscription.ts
260
- const subscriptionUpsert = async (subscription, payload, onSubscriptionUpdate) => {
264
+ const subscriptionUpsert = async (subscription, payload, onSubscriptionUpdate, resolveSubscriptionPermissions) => {
261
265
  const { id: stripeID, status, customer: stripeCustomer } = subscription;
262
266
  const customer = await resolveStripeCustomer({ customer: stripeCustomer });
263
267
  const error = (message) => payload.logger.error("Subscription Upsert: ", message);
@@ -299,7 +303,7 @@ const subscriptionUpsert = async (subscription, payload, onSubscriptionUpdate) =
299
303
  const inventory = customer$1.inventory;
300
304
  inventory.subscriptions[stripeID] = {
301
305
  ...subscription,
302
- permissions: getPermissionsSlugs({ permissions: product.roles })
306
+ permissions: await resolveSubscriptionPermissions(subscription, product, payload)
303
307
  };
304
308
  info(`INVENTORY OF THE SUBSCRIPTION ${inventory}`);
305
309
  await upsertCustomerInventoryAndSyncWithUser(payload, inventory, email, stripeId);
@@ -347,7 +351,7 @@ const subscriptionDeleted = async (subscription, payload, onSubscriptionUpdate)
347
351
  payload.logger.error("No customer found for subscription");
348
352
  return;
349
353
  }
350
- const inventory = customer$1.inventory;
354
+ const inventory = customer$1.inventory ?? generateCustomerInventory();
351
355
  delete inventory.subscriptions[id];
352
356
  await upsertCustomerInventoryAndSyncWithUser(payload, inventory, email, stripeId);
353
357
  const userId = await getUserIdByEmail({
@@ -392,12 +396,11 @@ const paymentSucceeded = async (paymentIntent, payload) => {
392
396
  payload,
393
397
  stripeId: stripeCustomer.id
394
398
  });
395
- if (!customer) return;
396
399
  if (!customer) {
397
- payload.logger.error(`User not found for payment: ${stripeCustomer.email}`);
400
+ payload.logger.error(`Customer not found for payment: ${stripeCustomer.email}`);
398
401
  return;
399
402
  }
400
- let inventory = customer.inventory;
403
+ const inventory = customer.inventory ?? generateCustomerInventory();
401
404
  inventory.payments[id] = paymentIntent;
402
405
  await payload.update({
403
406
  collection: COLLECTION_SLUG_CUSTOMERS,
@@ -437,8 +440,11 @@ const invoiceSucceeded = async (invoiceIntent, payload) => {
437
440
  payload,
438
441
  stripeId: stripeCustomer.id
439
442
  });
440
- if (!customer) return;
441
- let inventory = customer.inventory;
443
+ if (!customer) {
444
+ payload.logger.error(`Customer not found for invoice: ${stripeCustomer.email}`);
445
+ return;
446
+ }
447
+ const inventory = customer.inventory ?? generateCustomerInventory();
442
448
  inventory.invoices[id] = invoiceIntent;
443
449
  await payload.update({
444
450
  collection: COLLECTION_SLUG_CUSTOMERS,
@@ -468,13 +474,6 @@ const customerDeleted = async (customer, payload) => {
468
474
  }
469
475
  };
470
476
 
471
- //#endregion
472
- //#region src/server/access/get-current-user-query.ts
473
- async function getCurrentUserQuery(payload) {
474
- const headers$1 = await headers();
475
- return (await payload.auth({ headers: headers$1 })).user;
476
- }
477
-
478
477
  //#endregion
479
478
  //#region src/server/actions/unlock-item-for-user-action.ts
480
479
  const addUniqueUnlock = (unlocks, collection, contentId) => {
@@ -485,34 +484,59 @@ const addUniqueUnlock = (unlocks, collection, contentId) => {
485
484
  dateUnlocked: /* @__PURE__ */ new Date()
486
485
  }];
487
486
  };
488
- const unlockItemForUser = async (getPayload, collection, contentId) => {
489
- const payload = await getPayload();
490
- const user = await getCurrentUserQuery(payload);
491
- if (!user || !user.id) return { error: "Usuario no válido" };
492
- const item = await payload.findByID({
493
- collection,
494
- id: contentId.toString()
495
- });
496
- if (!item) return { error: "Elemento no encontrado" };
497
- if (!checkIfUserCanUnlockQuery(user, getPermissionsSlugs({ permissions: item.permissions }))) return { error: "No tienes permisos para desbloquear este elemento" };
498
- if (countWeeklyUnlocksQuery(user) >= MAX_UNLOCKS_PER_WEEK) return { error: `Has alcanzado el límite de ${MAX_UNLOCKS_PER_WEEK} desbloqueos para esta semana` };
499
- const inventory = user.inventory ?? generateUserInventory();
500
- const updatedUnlocks = addUniqueUnlock(inventory.unlocks, collection, contentId);
501
- if (updatedUnlocks.length === inventory.unlocks.length) return { data: true };
502
- try {
503
- await payload.update({
504
- collection: COLLECTION_SLUG_USER,
505
- id: user.id.toString(),
506
- data: { inventory: {
507
- ...inventory,
508
- unlocks: updatedUnlocks
509
- } }
487
+ /**
488
+ * Creates an unlock action with the specified content permissions resolver.
489
+ *
490
+ * @param resolveContentPermissions - Callback to resolve permissions required by content
491
+ * @returns A function that unlocks items for users
492
+ *
493
+ * @example
494
+ * ```typescript
495
+ * const unlockItem = createUnlockAction(async (content, payload) => {
496
+ * return content.requiredPermissions || [];
497
+ * });
498
+ *
499
+ * // Use in server actions
500
+ * await unlockItem(payload, user, 'posts', 123);
501
+ * ```
502
+ */
503
+ const createUnlockAction = (resolveContentPermissions) => {
504
+ /**
505
+ * Unlocks an item for a user, adding it to their inventory.
506
+ *
507
+ * @param payload - The Payload instance
508
+ * @param user - The authenticated user
509
+ * @param collection - The collection slug of the item to unlock
510
+ * @param contentId - The ID of the item to unlock
511
+ * @returns Result indicating success or error message
512
+ */
513
+ return async (payload, user, collection, contentId) => {
514
+ if (!user || !user.id) return { error: "Usuario no válido" };
515
+ const item = await payload.findByID({
516
+ collection,
517
+ id: contentId.toString()
510
518
  });
511
- return { data: true };
512
- } catch (error) {
513
- console.error("Error al actualizar el inventario del usuario:", error);
514
- return { error: "Error al actualizar el inventario del usuario" };
515
- }
519
+ if (!item) return { error: "Elemento no encontrado" };
520
+ if (!checkIfUserCanUnlockQuery(user, await resolveContentPermissions(item, payload))) return { error: "No tienes permisos para desbloquear este elemento" };
521
+ if (countWeeklyUnlocksQuery(user) >= MAX_UNLOCKS_PER_WEEK) return { error: `Has alcanzado el límite de ${MAX_UNLOCKS_PER_WEEK} desbloqueos para esta semana` };
522
+ const inventory = user.inventory ?? generateUserInventory();
523
+ const updatedUnlocks = addUniqueUnlock(inventory.unlocks, collection, contentId);
524
+ if (updatedUnlocks.length === inventory.unlocks.length) return { data: true };
525
+ try {
526
+ await payload.update({
527
+ collection: COLLECTION_SLUG_USER,
528
+ id: user.id.toString(),
529
+ data: { inventory: {
530
+ ...inventory,
531
+ unlocks: updatedUnlocks
532
+ } }
533
+ });
534
+ return { data: true };
535
+ } catch (error) {
536
+ console.error("Error al actualizar el inventario del usuario:", error);
537
+ return { error: "Error al actualizar el inventario del usuario" };
538
+ }
539
+ };
516
540
  };
517
541
 
518
542
  //#endregion
@@ -549,148 +573,351 @@ async function getCustomerFromStripeOrCreate(email, name) {
549
573
  }
550
574
 
551
575
  //#endregion
552
- //#region src/server/api/handle-donation.ts
553
- async function handleDonation(request, getPayload, getRoutes) {
554
- const payload = await getPayload();
555
- const payloadUser = await getCurrentUserQuery(payload);
556
- if (!payloadUser || !payloadUser.email) throw new Error("You must be logged in to make a donation");
557
- const amountParam = new URL(request.url).searchParams.get("amount");
558
- if (!amountParam) throw new Error("Amount is required");
559
- const amount = parseInt(amountParam);
560
- if (amount < 100) throw new Error("Minimum donation amount is €1");
561
- const stripe = stripeBuilder();
562
- const customerId = await getCustomerFromStripeOrCreate(payloadUser.email, payloadUser.name);
563
- await upsertCustomerInventoryAndSyncWithUser(payload, payloadUser.customer?.inventory, payloadUser.email, customerId);
564
- const metadata = { type: "donation" };
565
- const session = await stripe.checkout.sessions.create({
566
- customer: customerId,
567
- payment_method_types: ["card"],
568
- line_items: [{
569
- price_data: {
570
- currency: "eur",
571
- product_data: {
572
- name: "Donación - Portal Escohotado",
573
- description: "Apoyo al mantenimiento del legado digital de Antonio Escohotado"
576
+ //#region src/server/endpoints/validators/request-validator.ts
577
+ /**
578
+ * Creates a JSON response using Web API Response
579
+ */
580
+ function jsonResponse(data, options) {
581
+ return new Response(JSON.stringify(data), {
582
+ headers: { "Content-Type": "application/json" },
583
+ ...options
584
+ });
585
+ }
586
+ /**
587
+ * Creates a redirect response using Web API Response
588
+ * @param url - The URL to redirect to
589
+ * @param status - HTTP status code (default: 303 See Other)
590
+ */
591
+ function redirectResponse(url, status = 303) {
592
+ return new Response(null, {
593
+ status,
594
+ headers: { Location: url }
595
+ });
596
+ }
597
+ /**
598
+ * Creates an error response
599
+ */
600
+ function errorResponse(message, status = 400) {
601
+ return jsonResponse({ error: message }, { status });
602
+ }
603
+ /**
604
+ * Validates that the request has an authenticated user
605
+ * Uses the config's resolveUser if provided, otherwise uses request.user
606
+ */
607
+ async function validateAuthenticatedRequest(request, config) {
608
+ if (config.checkPermissions) {
609
+ if (!await config.checkPermissions(request)) return {
610
+ success: false,
611
+ error: errorResponse("Permission denied", 403)
612
+ };
613
+ }
614
+ let user = null;
615
+ if (config.resolveUser) user = await config.resolveUser(request);
616
+ else user = request.user;
617
+ if (!user) return {
618
+ success: false,
619
+ error: errorResponse("You must be logged in to access this endpoint", 401)
620
+ };
621
+ if (!user.email) return {
622
+ success: false,
623
+ error: errorResponse("User email is required", 400)
624
+ };
625
+ return {
626
+ success: true,
627
+ user,
628
+ payload: request.payload
629
+ };
630
+ }
631
+
632
+ //#endregion
633
+ //#region src/server/endpoints/handlers/checkout-handler.ts
634
+ /**
635
+ * Creates a handler for Stripe checkout sessions (subscriptions)
636
+ *
637
+ * @param config - Endpoint configuration
638
+ * @returns PayloadHandler for checkout endpoint
639
+ */
640
+ function createCheckoutHandler(config) {
641
+ return async (request) => {
642
+ try {
643
+ const validated = await validateAuthenticatedRequest(request, config);
644
+ if (!validated.success) return validated.error;
645
+ const { user, payload } = validated;
646
+ if (!user.email) return errorResponse("User email is required", 400);
647
+ const priceId = new URL(request.url || "").searchParams.get("priceId");
648
+ if (!priceId) return errorResponse("priceId is required", 400);
649
+ const stripe = stripeBuilder();
650
+ const customerId = await getCustomerFromStripeOrCreate(user.email, user.name);
651
+ await upsertCustomerInventoryAndSyncWithUser(payload, user.customer?.inventory, user.email, customerId);
652
+ const metadata = { type: "subscription" };
653
+ const checkoutResult = await stripe.checkout.sessions.create({
654
+ success_url: `${process.env.DOMAIN}${config.routes.subscriptionPageHref}?success=${Date.now()}`,
655
+ cancel_url: `${process.env.DOMAIN}${config.routes.subscriptionPageHref}?error=${Date.now()}`,
656
+ mode: "subscription",
657
+ customer: customerId,
658
+ client_reference_id: String(user.id),
659
+ line_items: [{
660
+ price: priceId,
661
+ quantity: 1
662
+ }],
663
+ metadata,
664
+ tax_id_collection: { enabled: true },
665
+ customer_update: {
666
+ name: "auto",
667
+ address: "auto",
668
+ shipping: "auto"
574
669
  },
575
- unit_amount: amount
576
- },
577
- quantity: 1
578
- }],
579
- mode: "payment",
580
- success_url: `${process.env.DOMAIN}${getRoutes().nextJS.subscriptionPageHref}?success=donation`,
581
- cancel_url: `${process.env.DOMAIN}${getRoutes().nextJS.subscriptionPageHref}?error=donation_cancelled`,
582
- metadata,
583
- payment_intent_data: { metadata },
584
- invoice_creation: {
585
- enabled: true,
586
- invoice_data: { metadata }
670
+ subscription_data: { metadata }
671
+ });
672
+ if (checkoutResult.url) return redirectResponse(checkoutResult.url, 303);
673
+ return errorResponse("Failed to create checkout URL", 406);
674
+ } catch (error) {
675
+ console.error("[Stripe Checkout Error]", error);
676
+ return errorResponse(error instanceof Error ? error.message : "Unknown error occurred", 500);
587
677
  }
588
- });
589
- return NextResponse.json({ url: session.url });
678
+ };
590
679
  }
591
680
 
592
681
  //#endregion
593
- //#region src/server/api/handle-update.ts
594
- async function handleUpdate(request, getPayload, getRoutes) {
595
- const payload = await getPayload();
596
- const payloadUser = await getCurrentUserQuery(payload);
597
- if (!payloadUser) throw new Error("You must be logged in to access this page");
598
- const { searchParams } = new URL(request.url);
599
- const subscriptionId = searchParams.get("subscriptionId");
600
- const cancelAtPeriodEnd = searchParams.get("cancelAtPeriodEnd") === "true";
601
- if (!subscriptionId) throw Error("SubscriptionId could not be found.");
602
- await stripeBuilder().subscriptions.update(subscriptionId, { cancel_at_period_end: cancelAtPeriodEnd });
603
- const customer = payloadUser.customer;
604
- console.error("UPDATE: customer", customer);
605
- const inventory = customer.inventory;
606
- if (inventory && inventory.subscriptions && inventory.subscriptions[subscriptionId]) inventory.subscriptions[subscriptionId].cancel_at_period_end = cancelAtPeriodEnd;
607
- await upsertCustomerInventoryAndSyncWithUser(payload, inventory, customer.email);
608
- const routes = getRoutes();
609
- return NextResponse.redirect(`${process.env.DOMAIN}${routes.nextJS.subscriptionPageHref}?refresh=${Date.now()}`, 303);
682
+ //#region src/server/endpoints/handlers/donation-handler.ts
683
+ /**
684
+ * Creates a handler for one-time donation payments
685
+ *
686
+ * @param config - Endpoint configuration
687
+ * @returns PayloadHandler for donation endpoint
688
+ */
689
+ function createDonationHandler(config) {
690
+ return async (request) => {
691
+ try {
692
+ const validated = await validateAuthenticatedRequest(request, config);
693
+ if (!validated.success) return validated.error;
694
+ const { user, payload } = validated;
695
+ if (!user.email) return errorResponse("User email is required", 400);
696
+ const amountParam = new URL(request.url || "").searchParams.get("amount");
697
+ if (!amountParam) return errorResponse("amount is required", 400);
698
+ const amount = parseInt(amountParam, 10);
699
+ if (isNaN(amount) || amount < 100) return errorResponse("Minimum donation amount is 1 EUR (100 cents)", 400);
700
+ const stripe = stripeBuilder();
701
+ const customerId = await getCustomerFromStripeOrCreate(user.email, user.name);
702
+ await upsertCustomerInventoryAndSyncWithUser(payload, user.customer?.inventory, user.email, customerId);
703
+ const donationPageHref = config.routes.donationPageHref || config.routes.subscriptionPageHref;
704
+ const metadata = { type: "donation" };
705
+ return jsonResponse({ url: (await stripe.checkout.sessions.create({
706
+ customer: customerId,
707
+ payment_method_types: ["card"],
708
+ line_items: [{
709
+ price_data: {
710
+ currency: "eur",
711
+ product_data: {
712
+ name: "Donation",
713
+ description: "One-time donation"
714
+ },
715
+ unit_amount: amount
716
+ },
717
+ quantity: 1
718
+ }],
719
+ mode: "payment",
720
+ success_url: `${process.env.DOMAIN}${donationPageHref}?success=donation`,
721
+ cancel_url: `${process.env.DOMAIN}${donationPageHref}?error=donation_cancelled`,
722
+ metadata,
723
+ payment_intent_data: { metadata },
724
+ invoice_creation: {
725
+ enabled: true,
726
+ invoice_data: { metadata }
727
+ }
728
+ })).url });
729
+ } catch (error) {
730
+ console.error("[Stripe Donation Error]", error);
731
+ return errorResponse(error instanceof Error ? error.message : "Unknown error occurred", 500);
732
+ }
733
+ };
610
734
  }
611
735
 
612
736
  //#endregion
613
- //#region src/server/api/handle-portal.ts
614
- async function handlePortal(request, getPayload, getRoutes) {
615
- const payload = await getPayload();
616
- const payloadUser = await getCurrentUserQuery(payload);
617
- if (!payloadUser || !payloadUser.email) throw new Error("You must be logged in to access this page");
618
- const url = new URL(request.url);
619
- const cancelSubscriptionId = url.searchParams.get("cancelSubscriptionId");
620
- const updateSubscriptionId = url.searchParams.get("updateSubscriptionId");
621
- let flowData;
622
- if (cancelSubscriptionId) flowData = {
623
- type: "subscription_cancel",
624
- subscription_cancel: { subscription: cancelSubscriptionId }
737
+ //#region src/server/endpoints/handlers/portal-handler.ts
738
+ /**
739
+ * Creates a handler for Stripe Billing Portal access
740
+ *
741
+ * @param config - Endpoint configuration
742
+ * @returns PayloadHandler for portal endpoint
743
+ */
744
+ function createPortalHandler(config) {
745
+ return async (request) => {
746
+ try {
747
+ const validated = await validateAuthenticatedRequest(request, config);
748
+ if (!validated.success) return validated.error;
749
+ const { user, payload } = validated;
750
+ if (!user.email) return errorResponse("User email is required", 400);
751
+ const url = new URL(request.url || "");
752
+ const cancelSubscriptionId = url.searchParams.get("cancelSubscriptionId");
753
+ const updateSubscriptionId = url.searchParams.get("updateSubscriptionId");
754
+ if (cancelSubscriptionId && !cancelSubscriptionId.startsWith("sub_")) return errorResponse("Invalid subscription ID format", 400);
755
+ if (updateSubscriptionId && !updateSubscriptionId.startsWith("sub_")) return errorResponse("Invalid subscription ID format", 400);
756
+ let flowData;
757
+ if (cancelSubscriptionId) flowData = {
758
+ type: "subscription_cancel",
759
+ subscription_cancel: { subscription: cancelSubscriptionId }
760
+ };
761
+ else if (updateSubscriptionId) flowData = {
762
+ type: "subscription_update",
763
+ subscription_update: { subscription: updateSubscriptionId }
764
+ };
765
+ const stripe = stripeBuilder();
766
+ const customerId = await getCustomerFromStripeOrCreate(user.email, user.name);
767
+ await upsertCustomerInventoryAndSyncWithUser(payload, user.customer?.inventory, user.email, customerId);
768
+ return redirectResponse((await stripe.billingPortal.sessions.create({
769
+ flow_data: flowData,
770
+ customer: customerId,
771
+ return_url: `${process.env.DOMAIN}${config.routes.subscriptionPageHref}`
772
+ })).url, 303);
773
+ } catch (error) {
774
+ console.error("[Stripe Portal Error]", error);
775
+ return errorResponse(error instanceof Error ? error.message : "Unknown error occurred", 500);
776
+ }
625
777
  };
626
- else if (updateSubscriptionId) flowData = {
627
- type: "subscription_update",
628
- subscription_update: { subscription: updateSubscriptionId }
778
+ }
779
+
780
+ //#endregion
781
+ //#region src/server/endpoints/handlers/update-handler.ts
782
+ /**
783
+ * Creates a handler for updating Stripe subscriptions (cancel at period end)
784
+ *
785
+ * @param config - Endpoint configuration
786
+ * @returns PayloadHandler for update endpoint
787
+ */
788
+ function createUpdateHandler(config) {
789
+ return async (request) => {
790
+ try {
791
+ const validated = await validateAuthenticatedRequest(request, config);
792
+ if (!validated.success) return validated.error;
793
+ const { user, payload } = validated;
794
+ const url = new URL(request.url || "");
795
+ const subscriptionId = url.searchParams.get("subscriptionId");
796
+ const cancelAtPeriodEnd = url.searchParams.get("cancelAtPeriodEnd") === "true";
797
+ if (!subscriptionId) return errorResponse("subscriptionId is required", 400);
798
+ if (!subscriptionId.startsWith("sub_")) return errorResponse("Invalid subscription ID format", 400);
799
+ const stripe = stripeBuilder();
800
+ const originalCancelAtPeriodEnd = (await stripe.subscriptions.retrieve(subscriptionId)).cancel_at_period_end;
801
+ await stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: cancelAtPeriodEnd });
802
+ const customer = user.customer;
803
+ const inventory = customer?.inventory;
804
+ if (inventory?.subscriptions?.[subscriptionId]) inventory.subscriptions[subscriptionId].cancel_at_period_end = cancelAtPeriodEnd;
805
+ if (customer?.email) try {
806
+ await upsertCustomerInventoryAndSyncWithUser(payload, inventory, customer.email);
807
+ } catch (syncError) {
808
+ console.error("[Stripe Update] Local sync failed, rolling back Stripe change", syncError);
809
+ await stripe.subscriptions.update(subscriptionId, { cancel_at_period_end: originalCancelAtPeriodEnd });
810
+ throw syncError;
811
+ }
812
+ return redirectResponse(`${process.env.DOMAIN}${config.routes.subscriptionPageHref}?refresh=${Date.now()}`, 303);
813
+ } catch (error) {
814
+ console.error("[Stripe Update Error]", error);
815
+ return errorResponse(error instanceof Error ? error.message : "Unknown error occurred", 500);
816
+ }
629
817
  };
630
- const stripe = stripeBuilder();
631
- const routes = getRoutes();
632
- const customerId = await getCustomerFromStripeOrCreate(payloadUser.email, payloadUser.name);
633
- await upsertCustomerInventoryAndSyncWithUser(payload, payloadUser.customer?.inventory, payloadUser.email, customerId);
634
- const session = await stripe.billingPortal.sessions.create({
635
- flow_data: flowData,
636
- customer: customerId,
637
- return_url: `${process.env.DOMAIN}${routes.nextJS.subscriptionPageHref}`
638
- });
639
- return NextResponse.redirect(session.url, 303);
640
818
  }
641
819
 
642
820
  //#endregion
643
- //#region src/server/api/handle-checkout.ts
644
- async function handleCheckout(request, getPayload, getRoutes) {
645
- const payload = await getPayload();
646
- const payloadUser = await getCurrentUserQuery(payload);
647
- const priceId = new URL(request.url).searchParams.get("priceId");
648
- if (!priceId || !payloadUser || !payloadUser.email) throw new Error("Invalid request");
649
- const stripe = stripeBuilder();
650
- const routes = getRoutes();
651
- const customerId = await getCustomerFromStripeOrCreate(payloadUser.email, payloadUser.name);
652
- await upsertCustomerInventoryAndSyncWithUser(payload, payloadUser.customer?.inventory, payloadUser.email, customerId);
653
- const metadata = { type: "subscription" };
654
- const checkoutResult = await stripe.checkout.sessions.create({
655
- success_url: `${process.env.DOMAIN}${routes.nextJS.subscriptionPageHref}?success=${Date.now()}`,
656
- cancel_url: `${process.env.DOMAIN}${routes.nextJS.subscriptionPageHref}?error=${Date.now()}`,
657
- mode: "subscription",
658
- customer: customerId,
659
- client_reference_id: String(payloadUser.id),
660
- line_items: [{
661
- price: priceId,
662
- quantity: 1
663
- }],
664
- metadata,
665
- tax_id_collection: { enabled: true },
666
- customer_update: {
667
- name: "auto",
668
- address: "auto",
669
- shipping: "auto"
821
+ //#region src/server/endpoints/index.ts
822
+ /**
823
+ * Creates all Stripe inventory endpoints
824
+ *
825
+ * @param config - Endpoint configuration
826
+ * @param basePath - Base path for endpoints (default: '/stripe')
827
+ * @returns Array of Payload endpoints
828
+ *
829
+ * @example
830
+ * ```typescript
831
+ * const endpoints = createStripeEndpoints({
832
+ * routes: { subscriptionPageHref: '/account/subscription' },
833
+ * });
834
+ * // Endpoints:
835
+ * // GET /api/stripe/checkout?priceId={id}
836
+ * // GET /api/stripe/portal
837
+ * // GET /api/stripe/update?subscriptionId={id}&cancelAtPeriodEnd={bool}
838
+ * // GET /api/stripe/donation?amount={cents}
839
+ * ```
840
+ */
841
+ function createStripeEndpoints(config, basePath = "/stripe") {
842
+ return [
843
+ {
844
+ path: `${basePath}/checkout`,
845
+ method: "get",
846
+ handler: createCheckoutHandler(config)
670
847
  },
671
- subscription_data: { metadata }
672
- });
673
- if (checkoutResult.url) return NextResponse.redirect(checkoutResult.url, 303);
674
- else return NextResponse.json("Create checkout url failed", { status: 406 });
848
+ {
849
+ path: `${basePath}/portal`,
850
+ method: "get",
851
+ handler: createPortalHandler(config)
852
+ },
853
+ {
854
+ path: `${basePath}/update`,
855
+ method: "get",
856
+ handler: createUpdateHandler(config)
857
+ },
858
+ {
859
+ path: `${basePath}/donation`,
860
+ method: "get",
861
+ handler: createDonationHandler(config)
862
+ }
863
+ ];
675
864
  }
676
865
 
677
866
  //#endregion
678
- //#region src/server/api/index.ts
679
- function createStripeInventoryHandlers(getPayload, getRoutes) {
680
- return {
681
- checkout: { GET: (request) => handleCheckout(request, getPayload, getRoutes) },
682
- portal: { GET: (request) => handlePortal(request, getPayload, getRoutes) },
683
- update: { GET: (request) => handleUpdate(request, getPayload, getRoutes) },
684
- donation: { GET: (request) => handleDonation(request, getPayload, getRoutes) }
867
+ //#region src/server/plugin/create-stripe-inventory-plugin.ts
868
+ /**
869
+ * Creates the Stripe Inventory plugin for Payload CMS
870
+ *
871
+ * This plugin:
872
+ * - Registers REST endpoints for checkout, portal, update, and donation
873
+ * - Sets up Stripe webhook handlers for subscription and payment events
874
+ * - Syncs customer data between Stripe and Payload
875
+ *
876
+ * @param config - Plugin configuration
877
+ * @returns A Payload plugin function
878
+ *
879
+ * Endpoints registered:
880
+ * - GET /api{basePath}/checkout?priceId={id} - Redirect to Stripe Checkout
881
+ * - GET /api{basePath}/portal - Redirect to Stripe Billing Portal
882
+ * - GET /api{basePath}/update?subscriptionId={id}&cancelAtPeriodEnd={bool} - Update subscription
883
+ * - GET /api{basePath}/donation?amount={cents} - Returns JSON with checkout URL
884
+ */
885
+ function createStripeInventoryPlugin(config) {
886
+ const basePath = config.basePath || "/stripe";
887
+ const endpointConfig = {
888
+ routes: config.routes,
889
+ checkPermissions: config.checkPermissions,
890
+ resolveUser: config.resolveUser
891
+ };
892
+ const onSubscriptionUpdate = config.onSubscriptionUpdate || (async () => {});
893
+ const { resolveSubscriptionPermissions, resolveContentPermissions } = config;
894
+ return (incomingConfig) => {
895
+ const stripeEndpoints = createStripeEndpoints(endpointConfig, basePath);
896
+ const configWithEndpoints = {
897
+ ...incomingConfig,
898
+ endpoints: [...incomingConfig.endpoints || [], ...stripeEndpoints]
899
+ };
900
+ return stripePlugin({
901
+ isTestKey: process.env.STRIPE_SECRET_KEY?.includes("sk_test"),
902
+ stripeSecretKey: process.env.STRIPE_SECRET_KEY || "",
903
+ stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOK_SECRET,
904
+ webhooks: {
905
+ "price.deleted": async ({ event, payload }) => await priceDeleted(event.data.object, payload),
906
+ "customer.subscription.created": async ({ event, payload }) => await subscriptionUpsert(event.data.object, payload, onSubscriptionUpdate, resolveSubscriptionPermissions),
907
+ "customer.subscription.paused": async ({ event, payload }) => await subscriptionUpsert(event.data.object, payload, onSubscriptionUpdate, resolveSubscriptionPermissions),
908
+ "customer.subscription.updated": async ({ event, payload }) => await subscriptionUpsert(event.data.object, payload, onSubscriptionUpdate, resolveSubscriptionPermissions),
909
+ "customer.subscription.deleted": async ({ event, payload }) => await subscriptionDeleted(event.data.object, payload, onSubscriptionUpdate),
910
+ "customer.deleted": async ({ event, payload }) => await customerDeleted(event.data.object, payload),
911
+ "product.deleted": async ({ event, payload }) => await productDeleted(event.data.object, payload),
912
+ "payment_intent.succeeded": async ({ event, payload }) => {
913
+ await paymentSucceeded(event.data.object, payload);
914
+ },
915
+ "invoice.paid": async ({ event, payload }) => {
916
+ await invoiceSucceeded(event.data.object, payload);
917
+ }
918
+ }
919
+ })(configWithEndpoints);
685
920
  };
686
- }
687
- function createRouteHandlers(getPayload, getRoutes) {
688
- return { GET: async (request, { params }) => {
689
- const path = (await params).stripe[0];
690
- const handlers = createStripeInventoryHandlers(getPayload, getRoutes);
691
- if (path === "checkout" || path === "portal" || path === "update" || path === "donation") return handlers[path].GET(request);
692
- return NextResponse.json({ error: "Route not found" }, { status: 404 });
693
- } };
694
921
  }
695
922
 
696
923
  //#endregion
@@ -723,6 +950,19 @@ const loggedInOrPublished = ({ req: { user } }) => {
723
950
  return { _status: { equals: "published" } };
724
951
  };
725
952
 
953
+ //#endregion
954
+ //#region src/server/access/get-user-from-request.ts
955
+ /**
956
+ * Gets the current user from a PayloadRequest without depending on next/headers.
957
+ * This is the recommended way to get the user in Payload endpoint handlers.
958
+ *
959
+ * @param request - The PayloadRequest object
960
+ * @returns The user object or null if not authenticated
961
+ */
962
+ function getUserFromRequest(request) {
963
+ return request.user;
964
+ }
965
+
726
966
  //#endregion
727
967
  //#region src/server/collections/customers.ts
728
968
  const customers = {
@@ -772,42 +1012,6 @@ const customers = {
772
1012
  ]
773
1013
  };
774
1014
 
775
- //#endregion
776
- //#region src/server/collections/fields/permission-evaluation-field.ts
777
- const permissionEvaluationField = {
778
- type: "row",
779
- fields: [{
780
- type: "select",
781
- name: "type_of_permissions",
782
- options: [
783
- {
784
- label: "Todos",
785
- value: "all"
786
- },
787
- {
788
- label: "Permisos por roles",
789
- value: "roles"
790
- },
791
- {
792
- label: "Solo para usuarios sin roles",
793
- value: "only_no_roles"
794
- },
795
- {
796
- label: "Solo invitados",
797
- value: "only_guess"
798
- }
799
- ],
800
- defaultValue: "all",
801
- label: "Tipo de permisos"
802
- }, {
803
- type: "relationship",
804
- name: "permissions",
805
- relationTo: [COLLECTION_SLUG_TAXONOMY],
806
- hasMany: false,
807
- admin: { condition: (_, siblingData) => siblingData.type_of_permissions === "roles" }
808
- }]
809
- };
810
-
811
1015
  //#endregion
812
1016
  //#region src/server/collections/prices.ts
813
1017
  const prices = {
@@ -988,44 +1192,10 @@ const products = {
988
1192
  type: "text",
989
1193
  name: "title"
990
1194
  }]
991
- },
992
- buildTaxonomyRelationship({
993
- name: "roles",
994
- label: "Roles",
995
- defaultValue: [],
996
- filterOptions: () => {
997
- return { "payload.types": { in: ["role"] } };
998
- },
999
- required: false
1000
- })
1001
- ]
1002
- };
1003
-
1004
- //#endregion
1005
- //#region src/server/plugin.ts
1006
- const plugin = (onSubscriptionUpdate) => {
1007
- return stripePlugin({
1008
- isTestKey: process.env.STRIPE_SECRET_KEY?.includes("sk_test"),
1009
- stripeSecretKey: process.env.STRIPE_SECRET_KEY || "",
1010
- stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOK_SECRET,
1011
- webhooks: {
1012
- "price.deleted": async ({ event, payload }) => await priceDeleted(event.data.object, payload),
1013
- "customer.subscription.created": async ({ event, payload }) => await subscriptionUpsert(event.data.object, payload, onSubscriptionUpdate),
1014
- "customer.subscription.paused": async ({ event, payload }) => await subscriptionUpsert(event.data.object, payload, onSubscriptionUpdate),
1015
- "customer.subscription.updated": async ({ event, payload }) => await subscriptionUpsert(event.data.object, payload, onSubscriptionUpdate),
1016
- "customer.subscription.deleted": async ({ event, payload }) => await subscriptionDeleted(event.data.object, payload, onSubscriptionUpdate),
1017
- "customer.deleted": async ({ event, payload }) => await customerDeleted(event.data.object, payload),
1018
- "product.deleted": async ({ event, payload }) => await productDeleted(event.data.object, payload),
1019
- "payment_intent.succeeded": async ({ event, payload }) => {
1020
- await paymentSucceeded(event.data.object, payload);
1021
- },
1022
- "invoice.paid": async ({ event, payload }) => {
1023
- await invoiceSucceeded(event.data.object, payload);
1024
- }
1025
1195
  }
1026
- });
1196
+ ]
1027
1197
  };
1028
1198
 
1029
1199
  //#endregion
1030
- export { createCustomerAtStripe, createRouteHandlers, createStripeInventoryHandlers, customerDeleted, customers, getCustomer, getCustomerFromStripeOrCreate, invoiceSucceeded, isAdmin, isAdminOrCurrentUser, isAdminOrPublished, isAdminOrStripeActive, isAdminOrUserFieldMatchingCurrentUser, isAnyone, loggedInOrPublished, payloadUpsert, paymentSucceeded, permissionEvaluationField, plugin, priceDeleted, priceUpsert, prices, productDeleted, productSync, products, resolveStripeCustomer, stripeBuilder, subscriptionDeleted, subscriptionUpsert, syncCustomerByEmail, unlockItemForUser, updatePrices, updateProducts, updateProductsAndPrices, upsertCustomerInventoryAndSyncWithUser };
1200
+ export { createCheckoutHandler, createCustomerAtStripe, createDonationHandler, createPortalHandler, createStripeEndpoints, createStripeInventoryPlugin, createUnlockAction, createUpdateHandler, customerDeleted, customers, errorResponse, getCustomer, getUserFromRequest, invoiceSucceeded, isAdmin, isAdminOrCurrentUser, isAdminOrPublished, isAdminOrStripeActive, isAdminOrUserFieldMatchingCurrentUser, isAnyone, jsonResponse, loggedInOrPublished, payloadUpsert, paymentSucceeded, priceDeleted, priceUpsert, prices, productDeleted, productSync, products, redirectResponse, resolveStripeCustomer, stripeBuilder, subscriptionDeleted, subscriptionUpsert, syncCustomerByEmail, updatePrices, updateProducts, updateProductsAndPrices, upsertCustomerInventoryAndSyncWithUser, validateAuthenticatedRequest };
1031
1201
  //# sourceMappingURL=index.mjs.map