@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.
- package/dist/index-DYkUVDtk.d.mts +103 -0
- package/dist/index-DYkUVDtk.d.mts.map +1 -0
- package/dist/index.d.mts +2 -15
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/server/index.d.mts +225 -41
- package/dist/server/index.d.mts.map +1 -1
- package/dist/server/index.mjs +423 -253
- package/dist/server/index.mjs.map +1 -1
- package/dist/{src-BmlQoR4x.mjs → src-I_DPhIL5.mjs} +15 -28
- package/dist/src-I_DPhIL5.mjs.map +1 -0
- package/package.json +2 -3
- package/dist/index-D-NQPUJO.d.mts +0 -892
- package/dist/index-D-NQPUJO.d.mts.map +0 -1
- package/dist/src-BmlQoR4x.mjs.map +0 -1
package/dist/server/index.mjs
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { S as generateUserInventory, _ as PricingType, a as
|
|
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
|
-
|
|
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)).
|
|
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
|
-
|
|
104
|
+
const products$1 = await (await stripeBuilder()).products.list({
|
|
103
105
|
limit: 100,
|
|
104
106
|
active: true
|
|
105
|
-
})
|
|
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:
|
|
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
|
|
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:
|
|
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(`
|
|
400
|
+
payload.logger.error(`Customer not found for payment: ${stripeCustomer.email}`);
|
|
398
401
|
return;
|
|
399
402
|
}
|
|
400
|
-
|
|
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)
|
|
441
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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 {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
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/
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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/
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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/
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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/
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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/
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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,
|
|
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
|