@payloadcms/storage-r2 0.0.1-beta.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.
Files changed (70) hide show
  1. package/.prettierignore +12 -0
  2. package/.swcrc +24 -0
  3. package/LICENSE.md +22 -0
  4. package/README.md +3 -0
  5. package/eslint.config.js +18 -0
  6. package/package.json +4 -0
  7. package/src/addresses/addressesCollection.ts +76 -0
  8. package/src/addresses/defaultAddressFields.ts +83 -0
  9. package/src/addresses/defaultCountries.ts +50 -0
  10. package/src/carts/beforeChange.ts +51 -0
  11. package/src/carts/cartsCollection.ts +146 -0
  12. package/src/currencies/index.ts +29 -0
  13. package/src/endpoints/confirmOrder.ts +312 -0
  14. package/src/endpoints/initiatePayment.ts +322 -0
  15. package/src/exports/addresses.ts +2 -0
  16. package/src/exports/currencies.ts +1 -0
  17. package/src/exports/fields.ts +5 -0
  18. package/src/exports/orders.ts +1 -0
  19. package/src/exports/payments/stripe.ts +1 -0
  20. package/src/exports/plugin.ts +1 -0
  21. package/src/exports/products.ts +1 -0
  22. package/src/exports/react.ts +8 -0
  23. package/src/exports/transactions.ts +1 -0
  24. package/src/exports/translations.ts +1 -0
  25. package/src/exports/types.ts +7 -0
  26. package/src/exports/ui.ts +3 -0
  27. package/src/exports/variants.ts +5 -0
  28. package/src/fields/amountField.ts +43 -0
  29. package/src/fields/cartItemsField.ts +84 -0
  30. package/src/fields/currencyField.ts +39 -0
  31. package/src/fields/inventoryField.ts +22 -0
  32. package/src/fields/pricesField.ts +65 -0
  33. package/src/fields/statusField.ts +57 -0
  34. package/src/fields/variantsFields.ts +56 -0
  35. package/src/index.ts +275 -0
  36. package/src/orders/ordersCollection.ts +157 -0
  37. package/src/payments/adapters/stripe/confirmOrder.ts +123 -0
  38. package/src/payments/adapters/stripe/endpoints/webhooks.ts +69 -0
  39. package/src/payments/adapters/stripe/index.ts +135 -0
  40. package/src/payments/adapters/stripe/initiatePayment.ts +131 -0
  41. package/src/products/productsCollection.ts +78 -0
  42. package/src/react/provider/index.tsx +893 -0
  43. package/src/react/provider/types.ts +184 -0
  44. package/src/react/provider/utilities.ts +22 -0
  45. package/src/transactions/transactionsCollection.ts +166 -0
  46. package/src/translations/en.ts +64 -0
  47. package/src/translations/index.ts +11 -0
  48. package/src/translations/translation-schema.json +35 -0
  49. package/src/types.ts +403 -0
  50. package/src/ui/PriceInput/FormattedInput.tsx +134 -0
  51. package/src/ui/PriceInput/index.scss +28 -0
  52. package/src/ui/PriceInput/index.tsx +43 -0
  53. package/src/ui/PriceInput/utilities.ts +46 -0
  54. package/src/ui/PriceRowLabel/index.css +13 -0
  55. package/src/ui/PriceRowLabel/index.tsx +56 -0
  56. package/src/ui/VariantOptionsSelector/ErrorBox.tsx +27 -0
  57. package/src/ui/VariantOptionsSelector/OptionsSelect.tsx +78 -0
  58. package/src/ui/VariantOptionsSelector/index.css +37 -0
  59. package/src/ui/VariantOptionsSelector/index.tsx +83 -0
  60. package/src/utilities/defaultProductsValidation.ts +42 -0
  61. package/src/utilities/errorCodes.ts +14 -0
  62. package/src/utilities/getCollectionSlugMap.ts +84 -0
  63. package/src/utilities/sanitizePluginConfig.ts +80 -0
  64. package/src/variants/variantOptionsCollection.ts +59 -0
  65. package/src/variants/variantTypesCollection.ts +55 -0
  66. package/src/variants/variantsCollection/hooks/beforeChange.ts +47 -0
  67. package/src/variants/variantsCollection/hooks/validateOptions.ts +72 -0
  68. package/src/variants/variantsCollection/index.ts +119 -0
  69. package/tsconfig.json +7 -0
  70. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,157 @@
1
+ import type { CollectionConfig, Field } from 'payload'
2
+
3
+ import type { CurrenciesConfig, FieldsOverride } from '../types.js'
4
+
5
+ import { amountField } from '../fields/amountField.js'
6
+ import { cartItemsField } from '../fields/cartItemsField.js'
7
+ import { currencyField } from '../fields/currencyField.js'
8
+
9
+ type Props = {
10
+ /**
11
+ * Array of fields used for capturing the shipping address data.
12
+ */
13
+ addressFields?: Field[]
14
+ currenciesConfig?: CurrenciesConfig
15
+ /**
16
+ * Slug of the customers collection, defaults to 'users'.
17
+ */
18
+ customersSlug?: string
19
+ enableVariants?: boolean
20
+ overrides?: { fields?: FieldsOverride } & Partial<Omit<CollectionConfig, 'fields'>>
21
+ /**
22
+ * Slug of the products collection, defaults to 'products'.
23
+ */
24
+ productsSlug?: string
25
+ /**
26
+ * Slug of the transactions collection, defaults to 'transactions'.
27
+ */
28
+ transactionsSlug?: string
29
+ /**
30
+ * Slug of the variants collection, defaults to 'variants'.
31
+ */
32
+ variantsSlug?: string
33
+ }
34
+
35
+ export const ordersCollection: (props?: Props) => CollectionConfig = (props) => {
36
+ const {
37
+ addressFields,
38
+ currenciesConfig,
39
+ customersSlug = 'users',
40
+ enableVariants = false,
41
+ overrides,
42
+ productsSlug = 'products',
43
+ transactionsSlug = 'transactions',
44
+ variantsSlug = 'variants',
45
+ } = props || {}
46
+ const fieldsOverride = overrides?.fields
47
+
48
+ const defaultFields: Field[] = [
49
+ {
50
+ name: 'customer',
51
+ type: 'relationship',
52
+ label: ({ t }) =>
53
+ // @ts-expect-error - translations are not typed in plugins yet
54
+ t('plugin-ecommerce:customer'),
55
+ relationTo: customersSlug,
56
+ },
57
+ {
58
+ name: 'customerEmail',
59
+ type: 'email',
60
+ label: ({ t }) =>
61
+ // @ts-expect-error - translations are not typed in plugins yet
62
+ t('plugin-ecommerce:customerEmail'),
63
+ },
64
+ {
65
+ name: 'transactions',
66
+ type: 'relationship',
67
+ hasMany: true,
68
+ label: ({ t }) =>
69
+ // @ts-expect-error - translations are not typed in plugins yet
70
+ t('plugin-ecommerce:transactions'),
71
+ relationTo: transactionsSlug,
72
+ },
73
+ {
74
+ name: 'status',
75
+ type: 'select',
76
+ defaultValue: 'processing',
77
+ interfaceName: 'OrderStatus',
78
+ label: ({ t }) =>
79
+ // @ts-expect-error - translations are not typed in plugins yet
80
+ t('plugin-ecommerce:status'),
81
+ options: [
82
+ {
83
+ // @ts-expect-error - translations are not typed in plugins yet
84
+ label: ({ t }) => t('plugin-ecommerce:processing'),
85
+ value: 'processing',
86
+ },
87
+ {
88
+ // @ts-expect-error - translations are not typed in plugins yet
89
+ label: ({ t }) => t('plugin-ecommerce:completed'),
90
+ value: 'completed',
91
+ },
92
+ {
93
+ // @ts-expect-error - translations are not typed in plugins yet
94
+ label: ({ t }) => t('plugin-ecommerce:cancelled'),
95
+ value: 'cancelled',
96
+ },
97
+ {
98
+ // @ts-expect-error - translations are not typed in plugins yet
99
+ label: ({ t }) => t('plugin-ecommerce:refunded'),
100
+ value: 'refunded',
101
+ },
102
+ ],
103
+ },
104
+ ...(addressFields
105
+ ? [
106
+ {
107
+ name: 'shippingAddress',
108
+ type: 'group',
109
+ fields: addressFields,
110
+ label: ({ t }) =>
111
+ // @ts-expect-error - translations are not typed in plugins yet
112
+ t('plugin-ecommerce:shippingAddress'),
113
+ } as Field,
114
+ ]
115
+ : []),
116
+ ...(currenciesConfig
117
+ ? [amountField({ currenciesConfig }), currencyField({ currenciesConfig })]
118
+ : []),
119
+ cartItemsField({
120
+ enableVariants,
121
+ overrides: {
122
+ name: 'items',
123
+ label: ({ t }) =>
124
+ // @ts-expect-error - translations are not typed in plugins yet
125
+ t('plugin-ecommerce:items'),
126
+ labels: {
127
+ plural: ({ t }) =>
128
+ // @ts-expect-error - translations are not typed in plugins yet
129
+ t('plugin-ecommerce:items'),
130
+ singular: ({ t }) =>
131
+ // @ts-expect-error - translations are not typed in plugins yet
132
+ t('plugin-ecommerce:item'),
133
+ },
134
+ },
135
+ productsSlug,
136
+ variantsSlug,
137
+ }),
138
+ ]
139
+
140
+ const fields =
141
+ fieldsOverride && typeof fieldsOverride === 'function'
142
+ ? fieldsOverride({ defaultFields })
143
+ : defaultFields
144
+
145
+ const baseConfig: CollectionConfig = {
146
+ slug: 'orders',
147
+ timestamps: true,
148
+ ...overrides,
149
+ admin: {
150
+ useAsTitle: 'createdAt',
151
+ ...overrides?.admin,
152
+ },
153
+ fields,
154
+ }
155
+
156
+ return { ...baseConfig }
157
+ }
@@ -0,0 +1,123 @@
1
+ import Stripe from 'stripe'
2
+
3
+ import type { PaymentAdapter } from '../../../types.js'
4
+ import type { StripeAdapterArgs } from './index.js'
5
+
6
+ type Props = {
7
+ apiVersion?: Stripe.StripeConfig['apiVersion']
8
+ appInfo?: Stripe.StripeConfig['appInfo']
9
+ secretKey: StripeAdapterArgs['secretKey']
10
+ }
11
+
12
+ export const confirmOrder: (props: Props) => NonNullable<PaymentAdapter>['confirmOrder'] =
13
+ (props) =>
14
+ async ({ data, ordersSlug = 'orders', req, transactionsSlug = 'transactions' }) => {
15
+ const payload = req.payload
16
+ const { apiVersion, appInfo, secretKey } = props || {}
17
+
18
+ const customerEmail = data.customerEmail
19
+ const user = req.user
20
+
21
+ const paymentIntentID = data.paymentIntentID as string
22
+
23
+ if (!secretKey) {
24
+ throw new Error('Stripe secret key is required')
25
+ }
26
+
27
+ if (!paymentIntentID) {
28
+ throw new Error('PaymentIntent ID is required')
29
+ }
30
+
31
+ const stripe = new Stripe(secretKey, {
32
+ // API version can only be the latest, stripe recommends ts ignoring it
33
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
34
+ // @ts-ignore - ignoring since possible versions are not type safe, only the latest version is recognised
35
+ apiVersion: apiVersion || '2025-03-31.basil',
36
+ appInfo: appInfo || {
37
+ name: 'Stripe Payload Plugin',
38
+ url: 'https://payloadcms.com',
39
+ },
40
+ })
41
+
42
+ try {
43
+ let customer = (
44
+ await stripe.customers.list({
45
+ email: customerEmail,
46
+ })
47
+ ).data[0]
48
+
49
+ if (!customer?.id) {
50
+ customer = await stripe.customers.create({
51
+ email: customerEmail,
52
+ })
53
+ }
54
+
55
+ // Find our existing transaction by the payment intent ID
56
+ const transactionsResults = await payload.find({
57
+ collection: transactionsSlug,
58
+ where: {
59
+ 'stripe.paymentIntentID': {
60
+ equals: paymentIntentID,
61
+ },
62
+ },
63
+ })
64
+
65
+ const transaction = transactionsResults.docs[0]
66
+
67
+ if (!transactionsResults.totalDocs || !transaction) {
68
+ throw new Error('No transaction found for the provided PaymentIntent ID')
69
+ }
70
+
71
+ // Verify the payment intent exists and retrieve it
72
+ const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentID)
73
+
74
+ const cartID = paymentIntent.metadata.cartID
75
+ const cartItemsSnapshot = paymentIntent.metadata.cartItemsSnapshot
76
+ ? JSON.parse(paymentIntent.metadata.cartItemsSnapshot)
77
+ : undefined
78
+
79
+ const shippingAddress = paymentIntent.metadata.shippingAddress
80
+ ? JSON.parse(paymentIntent.metadata.shippingAddress)
81
+ : undefined
82
+
83
+ if (!cartID) {
84
+ throw new Error('Cart ID not found in the PaymentIntent metadata')
85
+ }
86
+
87
+ if (!cartItemsSnapshot || !Array.isArray(cartItemsSnapshot)) {
88
+ throw new Error('Cart items snapshot not found or invalid in the PaymentIntent metadata')
89
+ }
90
+
91
+ const order = await payload.create({
92
+ collection: ordersSlug,
93
+ data: {
94
+ amount: paymentIntent.amount,
95
+ currency: paymentIntent.currency.toUpperCase(),
96
+ ...(req.user ? { customer: req.user.id } : { customerEmail }),
97
+ items: cartItemsSnapshot,
98
+ shippingAddress,
99
+ status: 'processing',
100
+ transactions: [transaction.id],
101
+ },
102
+ })
103
+
104
+ await payload.update({
105
+ id: transaction.id,
106
+ collection: transactionsSlug,
107
+ data: {
108
+ order: order.id,
109
+ status: 'succeeded',
110
+ },
111
+ })
112
+
113
+ return {
114
+ message: 'Payment initiated successfully',
115
+ order,
116
+ orderID: order.id,
117
+ }
118
+ } catch (error) {
119
+ payload.logger.error(error, 'Error initiating payment with Stripe')
120
+
121
+ throw new Error(error instanceof Error ? error.message : 'Unknown error initiating payment')
122
+ }
123
+ }
@@ -0,0 +1,69 @@
1
+ import type { Endpoint } from 'payload'
2
+
3
+ import Stripe from 'stripe'
4
+
5
+ import type { StripeAdapterArgs } from '../index.js'
6
+
7
+ type Props = {
8
+ apiVersion?: Stripe.StripeConfig['apiVersion']
9
+ appInfo?: Stripe.StripeConfig['appInfo']
10
+ secretKey: StripeAdapterArgs['secretKey']
11
+ webhooks?: StripeAdapterArgs['webhooks']
12
+ webhookSecret: StripeAdapterArgs['webhookSecret']
13
+ }
14
+
15
+ export const webhooksEndpoint: (props: Props) => Endpoint = (props) => {
16
+ const { apiVersion, appInfo, secretKey, webhooks, webhookSecret } = props || {}
17
+
18
+ const handler: Endpoint['handler'] = async (req) => {
19
+ let returnStatus = 200
20
+
21
+ if (webhookSecret && secretKey && req.text) {
22
+ const stripe = new Stripe(secretKey, {
23
+ // API version can only be the latest, stripe recommends ts ignoring it
24
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
25
+ // @ts-ignore - ignoring since possible versions are not type safe, only the latest version is recognised
26
+ apiVersion: apiVersion || '2025-03-31.basil',
27
+ appInfo: appInfo || {
28
+ name: 'Stripe Payload Plugin',
29
+ url: 'https://payloadcms.com',
30
+ },
31
+ })
32
+
33
+ const body = await req.text()
34
+ const stripeSignature = req.headers.get('stripe-signature')
35
+
36
+ if (stripeSignature) {
37
+ let event: Stripe.Event | undefined
38
+
39
+ try {
40
+ event = stripe.webhooks.constructEvent(body, stripeSignature, webhookSecret)
41
+ } catch (err: unknown) {
42
+ const msg: string = err instanceof Error ? err.message : JSON.stringify(err)
43
+ req.payload.logger.error(`Error constructing Stripe event: ${msg}`)
44
+ returnStatus = 400
45
+ }
46
+
47
+ if (typeof webhooks === 'object' && event) {
48
+ const webhookEventHandler = webhooks[event.type]
49
+
50
+ if (typeof webhookEventHandler === 'function') {
51
+ await webhookEventHandler({
52
+ event,
53
+ req,
54
+ stripe,
55
+ })
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ return Response.json({ received: true }, { status: returnStatus })
62
+ }
63
+
64
+ return {
65
+ handler,
66
+ method: 'post',
67
+ path: '/webhooks',
68
+ }
69
+ }
@@ -0,0 +1,135 @@
1
+ import type { Field, GroupField, PayloadRequest } from 'payload'
2
+ import type { Stripe } from 'stripe'
3
+
4
+ import type {
5
+ BasePaymentAdapterArgs,
6
+ BasePaymentAdapterClientArgs,
7
+ PaymentAdapter,
8
+ PaymentAdapterClient,
9
+ } from '../../../types.js'
10
+
11
+ import { confirmOrder } from './confirmOrder.js'
12
+ import { webhooksEndpoint } from './endpoints/webhooks.js'
13
+ import { initiatePayment } from './initiatePayment.js'
14
+
15
+ type StripeWebhookHandler = (args: {
16
+ event: Stripe.Event
17
+ req: PayloadRequest
18
+ stripe: Stripe
19
+ }) => Promise<void> | void
20
+
21
+ type StripeWebhookHandlers = {
22
+ /**
23
+ * Description of the event (e.g., invoice.created or charge.refunded).
24
+ */
25
+ [webhookName: string]: StripeWebhookHandler
26
+ }
27
+
28
+ export type StripeAdapterArgs = {
29
+ /**
30
+ * This library's types only reflect the latest API version.
31
+ *
32
+ * We recommend upgrading your account's API Version to the latest version
33
+ * if you wish to use TypeScript with this library.
34
+ *
35
+ * If you wish to remain on your account's default API version,
36
+ * you may pass `null` or another version instead of the latest version,
37
+ * and add a `@ts-ignore` comment here and anywhere the types differ between API versions.
38
+ *
39
+ * @docs https://stripe.com/docs/api/versioning
40
+ */
41
+ apiVersion?: Stripe.StripeConfig['apiVersion']
42
+ appInfo?: Stripe.StripeConfig['appInfo']
43
+ publishableKey: string
44
+ secretKey: string
45
+ webhooks?: StripeWebhookHandlers
46
+ webhookSecret: string
47
+ } & BasePaymentAdapterArgs
48
+
49
+ export const stripeAdapter: (props: StripeAdapterArgs) => PaymentAdapter = (props) => {
50
+ const { apiVersion, appInfo, groupOverrides, secretKey, webhooks, webhookSecret } = props
51
+ const label = props?.label || 'Stripe'
52
+
53
+ const baseFields: Field[] = [
54
+ {
55
+ name: 'customerID',
56
+ type: 'text',
57
+ label: 'Stripe Customer ID',
58
+ },
59
+ {
60
+ name: 'paymentIntentID',
61
+ type: 'text',
62
+ label: 'Stripe PaymentIntent ID',
63
+ },
64
+ ]
65
+
66
+ const groupField: GroupField = {
67
+ name: 'stripe',
68
+ type: 'group',
69
+ ...groupOverrides,
70
+ admin: {
71
+ condition: (data) => {
72
+ const path = 'paymentMethod'
73
+
74
+ return data?.[path] === 'stripe'
75
+ },
76
+ ...groupOverrides?.admin,
77
+ },
78
+ fields:
79
+ groupOverrides?.fields && typeof groupOverrides?.fields === 'function'
80
+ ? groupOverrides.fields({ defaultFields: baseFields })
81
+ : baseFields,
82
+ }
83
+
84
+ return {
85
+ name: 'stripe',
86
+ confirmOrder: confirmOrder({
87
+ apiVersion,
88
+ appInfo,
89
+ secretKey,
90
+ }),
91
+ endpoints: [webhooksEndpoint({ apiVersion, appInfo, secretKey, webhooks, webhookSecret })],
92
+ group: groupField,
93
+ initiatePayment: initiatePayment({
94
+ apiVersion,
95
+ appInfo,
96
+ secretKey,
97
+ }),
98
+ label,
99
+ }
100
+ }
101
+
102
+ export type StripeAdapterClientArgs = {
103
+ /**
104
+ * This library's types only reflect the latest API version.
105
+ *
106
+ * We recommend upgrading your account's API Version to the latest version
107
+ * if you wish to use TypeScript with this library.
108
+ *
109
+ * If you wish to remain on your account's default API version,
110
+ * you may pass `null` or another version instead of the latest version,
111
+ * and add a `@ts-ignore` comment here and anywhere the types differ between API versions.
112
+ *
113
+ * @docs https://stripe.com/docs/api/versioning
114
+ */
115
+ apiVersion?: Stripe.StripeConfig['apiVersion']
116
+ appInfo?: Stripe.StripeConfig['appInfo']
117
+ publishableKey: string
118
+ } & BasePaymentAdapterClientArgs
119
+
120
+ export const stripeAdapterClient: (props: StripeAdapterClientArgs) => PaymentAdapterClient = (
121
+ props,
122
+ ) => {
123
+ return {
124
+ name: 'stripe',
125
+ confirmOrder: true,
126
+ initiatePayment: true,
127
+ label: 'Card',
128
+ }
129
+ }
130
+
131
+ export type InitiatePaymentReturnType = {
132
+ clientSecret: string
133
+ message: string
134
+ paymentIntentID: string
135
+ }
@@ -0,0 +1,131 @@
1
+ import Stripe from 'stripe'
2
+
3
+ import type { PaymentAdapter } from '../../../types.js'
4
+ import type { InitiatePaymentReturnType, StripeAdapterArgs } from './index.js'
5
+
6
+ type Props = {
7
+ apiVersion?: Stripe.StripeConfig['apiVersion']
8
+ appInfo?: Stripe.StripeConfig['appInfo']
9
+ secretKey: StripeAdapterArgs['secretKey']
10
+ }
11
+
12
+ export const initiatePayment: (props: Props) => NonNullable<PaymentAdapter>['initiatePayment'] =
13
+ (props) =>
14
+ async ({ data, req, transactionsSlug }) => {
15
+ const payload = req.payload
16
+ const { apiVersion, appInfo, secretKey } = props || {}
17
+
18
+ const customerEmail = data.customerEmail
19
+ const currency = data.currency
20
+ const cart = data.cart
21
+ const amount = cart.subtotal
22
+ const billingAddressFromData = data.billingAddress
23
+ const shippingAddressFromData = data.shippingAddress
24
+
25
+ if (!secretKey) {
26
+ throw new Error('Stripe secret key is required.')
27
+ }
28
+
29
+ if (!currency) {
30
+ throw new Error('Currency is required.')
31
+ }
32
+
33
+ if (!cart || !cart.items || cart.items.length === 0) {
34
+ throw new Error('Cart is empty or not provided.')
35
+ }
36
+
37
+ if (!customerEmail || typeof customerEmail !== 'string') {
38
+ throw new Error('A valid customer email is required to make a purchase.')
39
+ }
40
+
41
+ if (!amount || typeof amount !== 'number' || amount <= 0) {
42
+ throw new Error('A valid amount is required to initiate a payment.')
43
+ }
44
+
45
+ const stripe = new Stripe(secretKey, {
46
+ // API version can only be the latest, stripe recommends ts ignoring it
47
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
48
+ // @ts-ignore - ignoring since possible versions are not type safe, only the latest version is recognised
49
+ apiVersion: apiVersion || '2025-06-30.preview',
50
+ appInfo: appInfo || {
51
+ name: 'Stripe Payload Plugin',
52
+ url: 'https://payloadcms.com',
53
+ },
54
+ })
55
+
56
+ try {
57
+ let customer = (
58
+ await stripe.customers.list({
59
+ email: customerEmail,
60
+ })
61
+ ).data[0]
62
+
63
+ if (!customer?.id) {
64
+ customer = await stripe.customers.create({
65
+ email: customerEmail,
66
+ })
67
+ }
68
+
69
+ const flattenedCart = cart.items.map((item) => {
70
+ const productID = typeof item.product === 'object' ? item.product.id : item.product
71
+ const variantID = item.variant
72
+ ? typeof item.variant === 'object'
73
+ ? item.variant.id
74
+ : item.variant
75
+ : undefined
76
+
77
+ return {
78
+ product: productID,
79
+ quantity: item.quantity,
80
+ variant: variantID,
81
+ }
82
+ })
83
+
84
+ const shippingAddressAsString = JSON.stringify(shippingAddressFromData)
85
+
86
+ const paymentIntent = await stripe.paymentIntents.create({
87
+ amount,
88
+ automatic_payment_methods: {
89
+ enabled: true,
90
+ },
91
+ currency,
92
+ customer: customer.id,
93
+ metadata: {
94
+ cartID: cart.id,
95
+ cartItemsSnapshot: JSON.stringify(flattenedCart),
96
+ shippingAddress: shippingAddressAsString,
97
+ },
98
+ })
99
+
100
+ // Create a transaction for the payment intent in the database
101
+ const transaction = await payload.create({
102
+ collection: transactionsSlug,
103
+ data: {
104
+ ...(req.user ? { customer: req.user.id } : { customerEmail }),
105
+ amount: paymentIntent.amount,
106
+ billingAddress: billingAddressFromData,
107
+ cart: cart.id,
108
+ currency: paymentIntent.currency.toUpperCase(),
109
+ items: flattenedCart,
110
+ paymentMethod: 'stripe',
111
+ status: 'pending',
112
+ stripe: {
113
+ customerID: customer.id,
114
+ paymentIntentID: paymentIntent.id,
115
+ },
116
+ },
117
+ })
118
+
119
+ const returnData: InitiatePaymentReturnType = {
120
+ clientSecret: paymentIntent.client_secret || '',
121
+ message: 'Payment initiated successfully',
122
+ paymentIntentID: paymentIntent.id,
123
+ }
124
+
125
+ return returnData
126
+ } catch (error) {
127
+ payload.logger.error(error, 'Error initiating payment with Stripe')
128
+
129
+ throw new Error(error instanceof Error ? error.message : 'Unknown error initiating payment')
130
+ }
131
+ }
@@ -0,0 +1,78 @@
1
+ import type { CollectionConfig, Field } from 'payload'
2
+
3
+ import type { CurrenciesConfig, FieldsOverride, InventoryConfig } from '../types.js'
4
+
5
+ import { inventoryField } from '../fields/inventoryField.js'
6
+ import { pricesField } from '../fields/pricesField.js'
7
+ import { variantsFields } from '../fields/variantsFields.js'
8
+
9
+ type Props = {
10
+ currenciesConfig: CurrenciesConfig
11
+ enableVariants?: boolean
12
+ /**
13
+ * Adds in an inventory field to the product and its variants. This is useful for tracking inventory levels.
14
+ * Defaults to true.
15
+ */
16
+ inventory?: boolean | InventoryConfig
17
+ overrides?: { fields?: FieldsOverride } & Partial<Omit<CollectionConfig, 'fields'>>
18
+ /**
19
+ * Slug of the variants collection, defaults to 'variants'.
20
+ */
21
+ variantsSlug?: string
22
+ /**
23
+ * Slug of the variant types collection, defaults to 'variantTypes'.
24
+ */
25
+ variantTypesSlug?: string
26
+ }
27
+
28
+ export const productsCollection: (props: Props) => CollectionConfig = (props) => {
29
+ const {
30
+ currenciesConfig,
31
+ enableVariants = false,
32
+ inventory = true,
33
+ overrides,
34
+ variantsSlug = 'variants',
35
+ variantTypesSlug = 'variantTypes',
36
+ } = props || {}
37
+ const fieldsOverride = overrides?.fields
38
+
39
+ const defaultFields: Field[] = [
40
+ ...(inventory
41
+ ? [
42
+ inventoryField({
43
+ overrides: {
44
+ admin: {
45
+ condition: ({ enableVariants }) => !enableVariants,
46
+ },
47
+ },
48
+ }),
49
+ ]
50
+ : []),
51
+ ]
52
+
53
+ const baseFields = [
54
+ ...defaultFields,
55
+ ...(enableVariants ? variantsFields({ variantsSlug, variantTypesSlug }) : []),
56
+ ...(currenciesConfig ? [...pricesField({ currenciesConfig })] : []),
57
+ ]
58
+
59
+ const fields =
60
+ fieldsOverride && typeof fieldsOverride === 'function'
61
+ ? fieldsOverride({ defaultFields: baseFields })
62
+ : baseFields
63
+
64
+ const baseConfig: CollectionConfig = {
65
+ slug: 'products',
66
+ ...overrides,
67
+ admin: {
68
+ defaultColumns: [
69
+ ...(currenciesConfig ? ['prices'] : []),
70
+ ...(enableVariants ? ['variants'] : []),
71
+ ],
72
+ ...overrides?.admin,
73
+ },
74
+ fields,
75
+ }
76
+
77
+ return { ...baseConfig }
78
+ }