@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,312 @@
1
+ import { addDataAndFileToRequest, type DefaultDocumentIDType, type Endpoint } from 'payload'
2
+
3
+ import type { CurrenciesConfig, PaymentAdapter, ProductsValidation } from '../types.js'
4
+
5
+ import { defaultProductsValidation } from '../utilities/defaultProductsValidation.js'
6
+
7
+ type Args = {
8
+ /**
9
+ * The slug of the carts collection, defaults to 'carts'.
10
+ */
11
+ cartsSlug?: string
12
+ currenciesConfig: CurrenciesConfig
13
+ /**
14
+ * The slug of the customers collection, defaults to 'users'.
15
+ */
16
+ customersSlug?: string
17
+ /**
18
+ * The slug of the orders collection, defaults to 'orders'.
19
+ */
20
+ ordersSlug?: string
21
+ paymentMethod: PaymentAdapter
22
+ /**
23
+ * The slug of the products collection, defaults to 'products'.
24
+ */
25
+ productsSlug?: string
26
+ /**
27
+ * Customise the validation used for checking products or variants before a transaction is created.
28
+ */
29
+ productsValidation?: ProductsValidation
30
+ /**
31
+ * The slug of the transactions collection, defaults to 'transactions'.
32
+ */
33
+ transactionsSlug?: string
34
+ /**
35
+ * The slug of the variants collection, defaults to 'variants'.
36
+ */
37
+ variantsSlug?: string
38
+ }
39
+
40
+ type ConfirmOrderHandler = (args: Args) => Endpoint['handler']
41
+
42
+ /**
43
+ * Handles the endpoint for initiating payments. We will handle checking the amount and product and variant prices here before it is sent to the payment provider.
44
+ * This is the first step in the payment process.
45
+ */
46
+ export const confirmOrderHandler: ConfirmOrderHandler =
47
+ ({
48
+ cartsSlug = 'carts',
49
+ currenciesConfig,
50
+ customersSlug = 'users',
51
+ ordersSlug = 'orders',
52
+ paymentMethod,
53
+ productsSlug = 'products',
54
+ productsValidation,
55
+ transactionsSlug = 'transactions',
56
+ variantsSlug = 'variants',
57
+ }) =>
58
+ async (req) => {
59
+ await addDataAndFileToRequest(req)
60
+
61
+ const data = req.data
62
+ const payload = req.payload
63
+ const user = req.user
64
+
65
+ let currency: string = currenciesConfig.defaultCurrency
66
+ let cartID: DefaultDocumentIDType = data?.cartID
67
+ let cart = undefined
68
+ let customerEmail: string = user?.email ?? ''
69
+
70
+ if (user) {
71
+ if (user.cart?.docs && Array.isArray(user.cart.docs) && user.cart.docs.length > 0) {
72
+ if (!cartID && user.cart.docs[0]) {
73
+ // Use the user's cart instead
74
+ if (typeof user.cart.docs[0] === 'object') {
75
+ cartID = user.cart.docs[0].id
76
+ cart = user.cart.docs[0]
77
+ } else {
78
+ cartID = user.cart.docs[0]
79
+ }
80
+ }
81
+ }
82
+ } else {
83
+ // Get the email from the data if user is not available
84
+ if (data?.customerEmail && typeof data.customerEmail === 'string') {
85
+ customerEmail = data.customerEmail
86
+ } else {
87
+ return Response.json(
88
+ {
89
+ message: 'A customer email is required to make a purchase.',
90
+ },
91
+ {
92
+ status: 400,
93
+ },
94
+ )
95
+ }
96
+ }
97
+
98
+ if (!cart) {
99
+ if (cartID) {
100
+ cart = await payload.findByID({
101
+ id: cartID,
102
+ collection: cartsSlug,
103
+ depth: 2,
104
+ overrideAccess: false,
105
+ select: {
106
+ id: true,
107
+ currency: true,
108
+ customerEmail: true,
109
+ items: true,
110
+ subtotal: true,
111
+ },
112
+ user,
113
+ })
114
+
115
+ if (!cart) {
116
+ return Response.json(
117
+ {
118
+ message: `Cart with ID ${cartID} not found.`,
119
+ },
120
+ {
121
+ status: 404,
122
+ },
123
+ )
124
+ }
125
+ } else {
126
+ return Response.json(
127
+ {
128
+ message: 'Cart ID is required.',
129
+ },
130
+ {
131
+ status: 400,
132
+ },
133
+ )
134
+ }
135
+ }
136
+
137
+ if (cart.currency && typeof cart.currency === 'string') {
138
+ currency = cart.currency
139
+ }
140
+
141
+ // Ensure the currency is provided or inferred in some way
142
+ if (!currency) {
143
+ return Response.json(
144
+ {
145
+ message: 'Currency is required.',
146
+ },
147
+ {
148
+ status: 400,
149
+ },
150
+ )
151
+ }
152
+
153
+ try {
154
+ if (Array.isArray(cart.items) && cart.items.length > 0) {
155
+ for (const item of cart.items) {
156
+ // Target field to check the price based on the currency so we can validate the total
157
+ const priceField = `priceIn${currency.toUpperCase()}`
158
+ const quantity = item.quantity || 1
159
+
160
+ // If the item has a product but no variant, we assume the product has a price in the specified currency
161
+ if (item.product) {
162
+ const id = typeof item.product === 'object' ? item.product.id : item.product
163
+
164
+ const product = await payload.findByID({
165
+ id,
166
+ collection: productsSlug,
167
+ depth: 0,
168
+ select: {
169
+ [priceField]: true,
170
+ },
171
+ })
172
+
173
+ if (!product) {
174
+ payload.logger.error(
175
+ `Product with ID ${item.product} not found.`,
176
+ 'Error validating product',
177
+ )
178
+
179
+ return Response.json(
180
+ {
181
+ message: `Product with ID ${item.product} not found.`,
182
+ },
183
+ {
184
+ status: 404,
185
+ },
186
+ )
187
+ }
188
+
189
+ // Run product validation only if the item does not have a variant, each variant will have its own inventory and price
190
+ if (!item.variant) {
191
+ try {
192
+ if (productsValidation) {
193
+ await productsValidation({
194
+ currenciesConfig,
195
+ currency,
196
+ product,
197
+ quantity,
198
+ })
199
+ } else {
200
+ await defaultProductsValidation({
201
+ currenciesConfig,
202
+ currency,
203
+ product,
204
+ quantity,
205
+ })
206
+ }
207
+ } catch (error) {
208
+ payload.logger.error(error, 'Error validating product.')
209
+ return Response.json(
210
+ {
211
+ message: error,
212
+ },
213
+ {
214
+ status: 400,
215
+ },
216
+ )
217
+ }
218
+ }
219
+
220
+ if (item.variant) {
221
+ const id = typeof item.variant === 'object' ? item.variant.id : item.variant
222
+
223
+ const variant = await payload.findByID({
224
+ id,
225
+ collection: variantsSlug,
226
+ depth: 0,
227
+ select: {
228
+ inventory: true,
229
+ [priceField]: true,
230
+ },
231
+ })
232
+
233
+ if (!variant) {
234
+ payload.logger.error(
235
+ `Variant with ID ${item.variant} not found.`,
236
+ 'Error validating variant',
237
+ )
238
+
239
+ return Response.json(
240
+ {
241
+ message: `Variant with ID ${item.variant} not found.`,
242
+ },
243
+ {
244
+ status: 404,
245
+ },
246
+ )
247
+ }
248
+
249
+ try {
250
+ if (productsValidation) {
251
+ await productsValidation({
252
+ currenciesConfig,
253
+ currency,
254
+ product,
255
+ quantity,
256
+ variant,
257
+ })
258
+ } else {
259
+ await defaultProductsValidation({
260
+ currenciesConfig,
261
+ currency,
262
+ product,
263
+ quantity,
264
+ variant,
265
+ })
266
+ }
267
+ } catch (error) {
268
+ payload.logger.error(error, 'Error validating product or variant.')
269
+
270
+ return Response.json(
271
+ {
272
+ message: error,
273
+ },
274
+ {
275
+ status: 400,
276
+ },
277
+ )
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
283
+
284
+ const paymentResponse = await paymentMethod.confirmOrder({
285
+ customersSlug,
286
+ data: {
287
+ ...data,
288
+ customerEmail,
289
+ },
290
+ ordersSlug,
291
+ req,
292
+ transactionsSlug,
293
+ })
294
+
295
+ if (paymentResponse) {
296
+ // Start decrementing the inventory for each product and variant in the cart
297
+ }
298
+
299
+ return Response.json(paymentResponse)
300
+ } catch (error) {
301
+ payload.logger.error(error, 'Error initiating payment')
302
+
303
+ return Response.json(
304
+ {
305
+ message: 'Error confirming order.',
306
+ },
307
+ {
308
+ status: 500,
309
+ },
310
+ )
311
+ }
312
+ }
@@ -0,0 +1,322 @@
1
+ import { addDataAndFileToRequest, type DefaultDocumentIDType, type Endpoint } from 'payload'
2
+
3
+ import type {
4
+ CurrenciesConfig,
5
+ PaymentAdapter,
6
+ ProductsValidation,
7
+ SanitizedEcommercePluginConfig,
8
+ } from '../types.js'
9
+
10
+ import { defaultProductsValidation } from '../utilities/defaultProductsValidation.js'
11
+
12
+ type Args = {
13
+ /**
14
+ * The slug of the carts collection, defaults to 'carts'.
15
+ */
16
+ cartsSlug?: string
17
+ currenciesConfig: CurrenciesConfig
18
+ /**
19
+ * The slug of the customers collection, defaults to 'users'.
20
+ */
21
+ customersSlug?: string
22
+ /**
23
+ * Track inventory stock for the products and variants.
24
+ * Accepts an object to override the default field name.
25
+ */
26
+ inventory?: SanitizedEcommercePluginConfig['inventory']
27
+ paymentMethod: PaymentAdapter
28
+ /**
29
+ * The slug of the products collection, defaults to 'products'.
30
+ */
31
+ productsSlug?: string
32
+ /**
33
+ * Customise the validation used for checking products or variants before a transaction is created.
34
+ */
35
+ productsValidation?: ProductsValidation
36
+ /**
37
+ * The slug of the transactions collection, defaults to 'transactions'.
38
+ */
39
+ transactionsSlug?: string
40
+ /**
41
+ * The slug of the variants collection, defaults to 'variants'.
42
+ */
43
+ variantsSlug?: string
44
+ }
45
+
46
+ type InitiatePayment = (args: Args) => Endpoint['handler']
47
+
48
+ /**
49
+ * Handles the endpoint for initiating payments. We will handle checking the amount and product and variant prices here before it is sent to the payment provider.
50
+ * This is the first step in the payment process.
51
+ */
52
+ export const initiatePaymentHandler: InitiatePayment =
53
+ ({
54
+ cartsSlug = 'carts',
55
+ currenciesConfig,
56
+ customersSlug = 'users',
57
+ paymentMethod,
58
+ productsSlug = 'products',
59
+ productsValidation,
60
+ transactionsSlug = 'transactions',
61
+ variantsSlug = 'variants',
62
+ }) =>
63
+ async (req) => {
64
+ await addDataAndFileToRequest(req)
65
+ const data = req.data
66
+ const payload = req.payload
67
+ const user = req.user
68
+
69
+ let currency: string = currenciesConfig.defaultCurrency
70
+ let cartID: DefaultDocumentIDType = data?.cartID
71
+ let cart = undefined
72
+ const billingAddress = data?.billingAddress
73
+ const shippingAddress = data?.shippingAddress
74
+
75
+ let customerEmail: string = user?.email ?? ''
76
+
77
+ if (user) {
78
+ if (user.cart?.docs && Array.isArray(user.cart.docs) && user.cart.docs.length > 0) {
79
+ if (!cartID && user.cart.docs[0]) {
80
+ // Use the user's cart instead
81
+ if (typeof user.cart.docs[0] === 'object') {
82
+ cartID = user.cart.docs[0].id
83
+ cart = user.cart.docs[0]
84
+ } else {
85
+ cartID = user.cart.docs[0]
86
+ }
87
+ }
88
+ }
89
+ } else {
90
+ // Get the email from the data if user is not available
91
+ if (data?.customerEmail && typeof data.customerEmail === 'string') {
92
+ customerEmail = data.customerEmail
93
+ } else {
94
+ return Response.json(
95
+ {
96
+ message: 'A customer email is required to make a purchase.',
97
+ },
98
+ {
99
+ status: 400,
100
+ },
101
+ )
102
+ }
103
+ }
104
+
105
+ if (!cart) {
106
+ if (cartID) {
107
+ cart = await payload.findByID({
108
+ id: cartID,
109
+ collection: cartsSlug,
110
+ depth: 2,
111
+ overrideAccess: false,
112
+ select: {
113
+ id: true,
114
+ currency: true,
115
+ customerEmail: true,
116
+ items: true,
117
+ subtotal: true,
118
+ },
119
+ user,
120
+ })
121
+
122
+ if (!cart) {
123
+ return Response.json(
124
+ {
125
+ message: `Cart with ID ${cartID} not found.`,
126
+ },
127
+ {
128
+ status: 404,
129
+ },
130
+ )
131
+ }
132
+ } else {
133
+ return Response.json(
134
+ {
135
+ message: 'Cart ID is required.',
136
+ },
137
+ {
138
+ status: 400,
139
+ },
140
+ )
141
+ }
142
+ }
143
+
144
+ if (cart.currency && typeof cart.currency === 'string') {
145
+ currency = cart.currency
146
+ }
147
+
148
+ // Ensure the currency is provided or inferred in some way
149
+ if (!currency) {
150
+ return Response.json(
151
+ {
152
+ message: 'Currency is required.',
153
+ },
154
+ {
155
+ status: 400,
156
+ },
157
+ )
158
+ }
159
+
160
+ // Ensure the selected currency is supported
161
+ if (
162
+ !currenciesConfig.supportedCurrencies.find(
163
+ (c) => c.code.toLocaleLowerCase() === currency.toLocaleLowerCase(),
164
+ )
165
+ ) {
166
+ return Response.json(
167
+ {
168
+ message: `Currency ${currency} is not supported.`,
169
+ },
170
+ {
171
+ status: 400,
172
+ },
173
+ )
174
+ }
175
+
176
+ // Verify the cart is available and items are present in an array
177
+ if (!cart || !cart.items || !Array.isArray(cart.items) || cart.items.length === 0) {
178
+ return Response.json(
179
+ {
180
+ message: 'Cart is required and must contain at least one item.',
181
+ },
182
+ {
183
+ status: 400,
184
+ },
185
+ )
186
+ }
187
+
188
+ for (const item of cart.items) {
189
+ // Target field to check the price based on the currency so we can validate the total
190
+ const priceField = `priceIn${currency.toUpperCase()}`
191
+ const quantity = item.quantity || 1
192
+
193
+ // If the item has a product but no variant, we assume the product has a price in the specified currency
194
+ if (item.product && !item.variant) {
195
+ const id = typeof item.product === 'object' ? item.product.id : item.product
196
+
197
+ const product = await payload.findByID({
198
+ id,
199
+ collection: productsSlug,
200
+ depth: 0,
201
+ select: {
202
+ [priceField]: true,
203
+ },
204
+ })
205
+
206
+ if (!product) {
207
+ return Response.json(
208
+ {
209
+ message: `Product with ID ${item.product} not found.`,
210
+ },
211
+ {
212
+ status: 404,
213
+ },
214
+ )
215
+ }
216
+
217
+ try {
218
+ if (productsValidation) {
219
+ await productsValidation({ currenciesConfig, currency, product, quantity })
220
+ } else {
221
+ await defaultProductsValidation({
222
+ currenciesConfig,
223
+ currency,
224
+ product,
225
+ quantity,
226
+ })
227
+ }
228
+ } catch (error) {
229
+ return Response.json(
230
+ {
231
+ message: error,
232
+ },
233
+ {
234
+ status: 400,
235
+ },
236
+ )
237
+ }
238
+
239
+ if (item.variant) {
240
+ const id = typeof item.variant === 'object' ? item.variant.id : item.variant
241
+
242
+ const variant = await payload.findByID({
243
+ id,
244
+ collection: variantsSlug,
245
+ depth: 0,
246
+ select: {
247
+ inventory: true,
248
+ [priceField]: true,
249
+ },
250
+ })
251
+
252
+ if (!variant) {
253
+ return Response.json(
254
+ {
255
+ message: `Variant with ID ${item.variant} not found.`,
256
+ },
257
+ {
258
+ status: 404,
259
+ },
260
+ )
261
+ }
262
+
263
+ try {
264
+ if (productsValidation) {
265
+ await productsValidation({
266
+ currenciesConfig,
267
+ currency,
268
+ product: item.product,
269
+ quantity,
270
+ variant,
271
+ })
272
+ } else {
273
+ await defaultProductsValidation({
274
+ currenciesConfig,
275
+ currency,
276
+ product: item.product,
277
+ quantity,
278
+ variant,
279
+ })
280
+ }
281
+ } catch (error) {
282
+ return Response.json(
283
+ {
284
+ message: error,
285
+ },
286
+ {
287
+ status: 400,
288
+ },
289
+ )
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ try {
296
+ const paymentResponse = await paymentMethod.initiatePayment({
297
+ customersSlug,
298
+ data: {
299
+ billingAddress,
300
+ cart,
301
+ currency,
302
+ customerEmail,
303
+ shippingAddress,
304
+ },
305
+ req,
306
+ transactionsSlug,
307
+ })
308
+
309
+ return Response.json(paymentResponse)
310
+ } catch (error) {
311
+ payload.logger.error(error, 'Error initiating payment.')
312
+
313
+ return Response.json(
314
+ {
315
+ message: 'Error initiating payment.',
316
+ },
317
+ {
318
+ status: 500,
319
+ },
320
+ )
321
+ }
322
+ }
@@ -0,0 +1,2 @@
1
+ export { addressesCollection } from '../addresses/addressesCollection.js'
2
+ export { defaultCountries } from '../addresses/defaultCountries.js'
@@ -0,0 +1 @@
1
+ export { EUR, GBP, JPY, USD } from '../currencies/index.js'
@@ -0,0 +1,5 @@
1
+ export { amountField } from '../fields/amountField.js'
2
+ export { currencyField } from '../fields/currencyField.js'
3
+ export { pricesField } from '../fields/pricesField.js'
4
+ export { statusField } from '../fields/statusField.js'
5
+ export { variantsFields } from '../fields/variantsFields.js'
@@ -0,0 +1 @@
1
+ export { ordersCollection } from '../orders/ordersCollection.js'
@@ -0,0 +1 @@
1
+ export { stripeAdapter, stripeAdapterClient } from '../../payments/adapters/stripe/index.js'
@@ -0,0 +1 @@
1
+ export { ecommercePlugin } from '../index.js'
@@ -0,0 +1 @@
1
+ export { productsCollection } from '../products/productsCollection.js'
@@ -0,0 +1,8 @@
1
+ export {
2
+ EcommerceProvider,
3
+ useAddresses,
4
+ useCart,
5
+ useCurrency,
6
+ useEcommerce,
7
+ usePayments,
8
+ } from '../react/provider/index.js'
@@ -0,0 +1 @@
1
+ export { transactionsCollection } from '../transactions/transactionsCollection.js'
@@ -0,0 +1 @@
1
+ export { en } from '../translations/en.js'
@@ -0,0 +1,7 @@
1
+ export type {
2
+ Cart,
3
+ CollectionOverride,
4
+ CurrenciesConfig,
5
+ EcommercePluginConfig,
6
+ FieldsOverride,
7
+ } from '../types.js'
@@ -0,0 +1,3 @@
1
+ export { PriceInput } from '../ui/PriceInput/index.js'
2
+ export { PriceRowLabel } from '../ui/PriceRowLabel/index.js'
3
+ export { VariantOptionsSelector } from '../ui/VariantOptionsSelector/index.js'
@@ -0,0 +1,5 @@
1
+ export { variantOptionsCollection } from '../variants/variantOptionsCollection.js'
2
+
3
+ export { variantsCollection } from '../variants/variantsCollection/index.js'
4
+
5
+ export { variantTypesCollection } from '../variants/variantTypesCollection.js'