feat: ecommerce plugin and template (#8297)
This PR adds an ecommerce plugin package with both a Payload plugin and React UI utilities for the frontend. It also adds a new ecommerce template and new ecommerce test suite. It also makes a change to the `cpa` package to accept a `--version` flag to install a specific version of Payload defaulting to the latest.
This commit is contained in:
2
test/plugin-ecommerce/.gitignore
vendored
Normal file
2
test/plugin-ecommerce/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
app/(payload)/admin/importMap.js
|
||||
/app/(payload)/admin/importMap.js
|
||||
@@ -0,0 +1,25 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, NotFoundPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const NotFound = ({ params, searchParams }: Args) =>
|
||||
NotFoundPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default NotFound
|
||||
@@ -0,0 +1,25 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { generatePageMetadata, RootPage } from '@payloadcms/next/views'
|
||||
|
||||
import { importMap } from '../importMap.js'
|
||||
|
||||
type Args = {
|
||||
params: Promise<{
|
||||
segments: string[]
|
||||
}>
|
||||
searchParams: Promise<{
|
||||
[key: string]: string | string[]
|
||||
}>
|
||||
}
|
||||
|
||||
export const generateMetadata = ({ params, searchParams }: Args): Promise<Metadata> =>
|
||||
generatePageMetadata({ config, params, searchParams })
|
||||
|
||||
const Page = ({ params, searchParams }: Args) =>
|
||||
RootPage({ config, importMap, params, searchParams })
|
||||
|
||||
export default Page
|
||||
10
test/plugin-ecommerce/app/(payload)/api/[...slug]/route.ts
Normal file
10
test/plugin-ecommerce/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { REST_DELETE, REST_GET, REST_OPTIONS, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = REST_GET(config)
|
||||
export const POST = REST_POST(config)
|
||||
export const DELETE = REST_DELETE(config)
|
||||
export const PATCH = REST_PATCH(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
@@ -0,0 +1,6 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes/index.js'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
8
test/plugin-ecommerce/app/(payload)/api/graphql/route.ts
Normal file
8
test/plugin-ecommerce/app/(payload)/api/graphql/route.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import config from '@payload-config'
|
||||
import { GRAPHQL_POST, REST_OPTIONS } from '@payloadcms/next/routes'
|
||||
|
||||
export const POST = GRAPHQL_POST(config)
|
||||
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
7
test/plugin-ecommerce/app/(payload)/custom.scss
Normal file
7
test/plugin-ecommerce/app/(payload)/custom.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
#custom-css {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#custom-css::after {
|
||||
content: 'custom-css';
|
||||
}
|
||||
31
test/plugin-ecommerce/app/(payload)/layout.tsx
Normal file
31
test/plugin-ecommerce/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
/* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. */
|
||||
/* DO NOT MODIFY IT BECAUSE IT COULD BE REWRITTEN AT ANY TIME. */
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { handleServerFunctions, RootLayout } from '@payloadcms/next/layouts'
|
||||
import React from 'react'
|
||||
|
||||
import { importMap } from './admin/importMap.js'
|
||||
import './custom.scss'
|
||||
|
||||
type Args = {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const serverFunction: ServerFunctionClient = async function (args) {
|
||||
'use server'
|
||||
return handleServerFunctions({
|
||||
...args,
|
||||
config,
|
||||
importMap,
|
||||
})
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Args) => (
|
||||
<RootLayout config={config} importMap={importMap} serverFunction={serverFunction}>
|
||||
{children}
|
||||
</RootLayout>
|
||||
)
|
||||
|
||||
export default Layout
|
||||
15
test/plugin-ecommerce/app/(shop)/shop/confirm-order/page.tsx
Normal file
15
test/plugin-ecommerce/app/(shop)/shop/confirm-order/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ConfirmOrder } from '@/components/ConfirmOrder.js'
|
||||
|
||||
export const ConfirmOrderPage = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<ConfirmOrder />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ConfirmOrderPage
|
||||
27
test/plugin-ecommerce/app/(shop)/shop/layout.tsx
Normal file
27
test/plugin-ecommerce/app/(shop)/shop/layout.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { currenciesConfig } from '@payload-config'
|
||||
import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/react'
|
||||
import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe'
|
||||
|
||||
export const metadata = {
|
||||
title: 'Next.js',
|
||||
description: 'Generated by Next.js',
|
||||
}
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<EcommerceProvider
|
||||
currenciesConfig={currenciesConfig}
|
||||
paymentMethods={[
|
||||
stripeAdapterClient({
|
||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || '',
|
||||
}),
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</EcommerceProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
38
test/plugin-ecommerce/app/(shop)/shop/order/[id]/page.tsx
Normal file
38
test/plugin-ecommerce/app/(shop)/shop/order/[id]/page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const Page = async ({ params: paramsPromise }: { params: Promise<{ id: string }> }) => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
const searchParams = await paramsPromise
|
||||
|
||||
const orderID = searchParams.id
|
||||
|
||||
const order = await payload.findByID({
|
||||
collection: 'orders',
|
||||
id: orderID,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
Order id: {searchParams.id}
|
||||
<div>
|
||||
<h1>Shop Page - {payload?.config?.collections?.length} collections</h1>
|
||||
|
||||
{order ? (
|
||||
<div>
|
||||
<h2>Order Details</h2>
|
||||
<pre>{JSON.stringify(order, null, 2)}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<p>No order found with ID {orderID}.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
45
test/plugin-ecommerce/app/(shop)/shop/page.tsx
Normal file
45
test/plugin-ecommerce/app/(shop)/shop/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import configPromise, { currenciesConfig } from '@payload-config'
|
||||
import { Cart } from '@/components/Cart.js'
|
||||
import { getPayload } from 'payload'
|
||||
import React from 'react'
|
||||
import { Product } from '@/components/Product.js'
|
||||
import { CurrencySelector } from '@/components/CurrencySelector.js'
|
||||
import { Payments } from '@/components/Payments.js'
|
||||
|
||||
export const Page = async () => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
const products = await payload.find({
|
||||
collection: 'products',
|
||||
depth: 2,
|
||||
limit: 10,
|
||||
})
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Shop Page - {payload?.config?.collections?.length} collections</h1>
|
||||
|
||||
{products?.docs?.length > 0 ? (
|
||||
<ul>
|
||||
{products.docs.map((product) => (
|
||||
<li key={product.id}>
|
||||
<Product product={product} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>No products found.</p>
|
||||
)}
|
||||
|
||||
<Cart />
|
||||
|
||||
<CurrencySelector currenciesConfig={currenciesConfig} />
|
||||
|
||||
<Payments currenciesConfig={currenciesConfig} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
58
test/plugin-ecommerce/app/components/Cart.tsx
Normal file
58
test/plugin-ecommerce/app/components/Cart.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client'
|
||||
|
||||
import { useCart, useCurrency } from '@payloadcms/plugin-ecommerce/react'
|
||||
|
||||
export const Cart = () => {
|
||||
const { cart, incrementItem, decrementItem, removeItem, subTotal, clearCart } = useCart()
|
||||
const { formatCurrency } = useCurrency()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Cart Component</h1>
|
||||
<p>This is a placeholder for the Cart component.</p>
|
||||
|
||||
<p>subTotal: {formatCurrency(subTotal)}</p>
|
||||
|
||||
{cart && cart.size > 0 ? (
|
||||
<ul>
|
||||
{Array.from(cart.values()).map((item, index) => {
|
||||
const id = item.variantID || item.productID
|
||||
|
||||
const options =
|
||||
item.variant?.options && item.variant.options.length > 0
|
||||
? item.variant.options
|
||||
.filter((option) => typeof option !== 'string')
|
||||
.map((option) => {
|
||||
return option.label
|
||||
})
|
||||
: []
|
||||
|
||||
return (
|
||||
<li key={id}>
|
||||
<h2>
|
||||
{item.product.name} {options.length > 0 ? `(${options.join(' – ')})` : ''}
|
||||
</h2>
|
||||
<p>Quantity: {item.quantity}</p>
|
||||
<button onClick={() => incrementItem(id)}>+</button>
|
||||
<button onClick={() => decrementItem(id)}>-</button>
|
||||
<button onClick={() => removeItem(id)}>Remove</button>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p>Your cart is empty.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
clearCart()
|
||||
}}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
|
||||
{/* <pre>{JSON.stringify(Array.from(cart.entries()), null, 2)}</pre> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
42
test/plugin-ecommerce/app/components/CheckoutStripe.tsx
Normal file
42
test/plugin-ecommerce/app/components/CheckoutStripe.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useStripe, useElements, PaymentElement } from '@stripe/react-stripe-js'
|
||||
|
||||
export const CheckoutStripe = () => {
|
||||
const stripe = useStripe()
|
||||
const elements = useElements()
|
||||
|
||||
const handleSubmit = async (event: any) => {
|
||||
// We don't want to let default form submission happen here,
|
||||
// which would refresh the page.
|
||||
event.preventDefault()
|
||||
|
||||
if (!stripe || !elements) {
|
||||
// Stripe.js hasn't yet loaded.
|
||||
// Make sure to disable form submission until Stripe.js has loaded.
|
||||
return
|
||||
}
|
||||
|
||||
const result = await stripe.confirmPayment({
|
||||
//`Elements` instance that was used to create the Payment Element
|
||||
elements,
|
||||
confirmParams: {
|
||||
return_url: 'http://localhost:3000/shop/confirm-order',
|
||||
},
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
// Show error to your customer (for example, payment details incomplete)
|
||||
console.log(result.error.message)
|
||||
} else {
|
||||
// Your customer will be redirected to your `return_url`. For some payment
|
||||
// methods like iDEAL, your customer will be redirected to an intermediate
|
||||
// site first to authorize the payment, then redirected to the `return_url`.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<PaymentElement />
|
||||
<button disabled={!stripe}>Submit</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
46
test/plugin-ecommerce/app/components/ConfirmOrder.tsx
Normal file
46
test/plugin-ecommerce/app/components/ConfirmOrder.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
'use client'
|
||||
|
||||
import { useCart, usePayments } from '@payloadcms/plugin-ecommerce/react'
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { useRouter, useSearchParams } from 'next/navigation.js'
|
||||
|
||||
export const ConfirmOrder: React.FC = () => {
|
||||
const { confirmOrder } = usePayments()
|
||||
const { cart } = useCart()
|
||||
const confirmedOrder = useRef(false)
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
if (!cart || cart.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const paymentIntentID = searchParams.get('payment_intent')
|
||||
|
||||
if (paymentIntentID) {
|
||||
confirmOrder('stripe', {
|
||||
additionalData: {
|
||||
paymentIntentID,
|
||||
},
|
||||
}).then((result) => {
|
||||
if (result && typeof result === 'object' && 'orderID' in result && result.orderID) {
|
||||
// Redirect to order confirmation page
|
||||
confirmedOrder.current = true
|
||||
router.push(`/shop/order/${result.orderID}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [cart, searchParams])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>Confirm Order</h2>
|
||||
<div>
|
||||
<strong>Order Summary:</strong>
|
||||
LOADING
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
test/plugin-ecommerce/app/components/CurrencySelector.tsx
Normal file
35
test/plugin-ecommerce/app/components/CurrencySelector.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client'
|
||||
import { useCurrency } from '@payloadcms/plugin-ecommerce/react'
|
||||
import React from 'react'
|
||||
import { CurrenciesConfig } from '@payloadcms/plugin-ecommerce/types'
|
||||
|
||||
type Props = {
|
||||
currenciesConfig: CurrenciesConfig
|
||||
}
|
||||
|
||||
export const CurrencySelector: React.FC<Props> = ({ currenciesConfig }) => {
|
||||
const { currency, setCurrency } = useCurrency()
|
||||
|
||||
return (
|
||||
<div>
|
||||
selected: {currency.label} ({currency.code})<br />
|
||||
<select
|
||||
value={currency.code}
|
||||
onChange={(e) => {
|
||||
const selectedCurrency = currenciesConfig.supportedCurrencies.find(
|
||||
(c) => c.code === e.target.value,
|
||||
)
|
||||
if (selectedCurrency) {
|
||||
setCurrency(selectedCurrency.code)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{currenciesConfig.supportedCurrencies.map((c) => (
|
||||
<option key={c.code} value={c.code}>
|
||||
{c.label} ({c.code})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
41
test/plugin-ecommerce/app/components/Payments.tsx
Normal file
41
test/plugin-ecommerce/app/components/Payments.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
'use client'
|
||||
import { usePayments } from '@payloadcms/plugin-ecommerce/react'
|
||||
import React from 'react'
|
||||
import { CurrenciesConfig } from '@payloadcms/plugin-ecommerce/types'
|
||||
import { loadStripe } from '@stripe/stripe-js'
|
||||
import { Elements } from '@stripe/react-stripe-js'
|
||||
import { CheckoutStripe } from '@/components/CheckoutStripe.js'
|
||||
|
||||
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!)
|
||||
|
||||
type Props = {
|
||||
currenciesConfig: CurrenciesConfig
|
||||
}
|
||||
|
||||
export const Payments: React.FC<Props> = ({ currenciesConfig }) => {
|
||||
const { selectedPaymentMethod, initiatePayment, confirmOrder, paymentData } = usePayments()
|
||||
|
||||
return (
|
||||
<div>
|
||||
selected: {selectedPaymentMethod}
|
||||
<br />
|
||||
<button
|
||||
onClick={async () => {
|
||||
await initiatePayment('stripe')
|
||||
}}
|
||||
>
|
||||
Pay with Stripe
|
||||
</button>
|
||||
{selectedPaymentMethod === 'stripe' &&
|
||||
paymentData &&
|
||||
'clientSecret' in paymentData &&
|
||||
typeof paymentData.clientSecret === 'string' && (
|
||||
<div>
|
||||
<Elements stripe={stripePromise} options={{ clientSecret: paymentData.clientSecret }}>
|
||||
<CheckoutStripe />
|
||||
</Elements>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
72
test/plugin-ecommerce/app/components/Product.tsx
Normal file
72
test/plugin-ecommerce/app/components/Product.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
'use client'
|
||||
import { Product as ProductType } from '@payload-types'
|
||||
import { useCart, useCurrency } from '@payloadcms/plugin-ecommerce/react'
|
||||
import React from 'react'
|
||||
|
||||
type Props = {
|
||||
product: ProductType
|
||||
}
|
||||
|
||||
export const Product: React.FC<Props> = ({ product }) => {
|
||||
const { addItem, removeItem } = useCart()
|
||||
const { formatCurrency, currency } = useCurrency()
|
||||
|
||||
const pricePath = `priceIn${currency.code.toUpperCase()}`
|
||||
// @ts-expect-error
|
||||
const productPrice = pricePath in product ? product[pricePath] : undefined
|
||||
|
||||
const hasVariants =
|
||||
product.enableVariants && product.variants?.docs?.length && product.variants?.docs?.length > 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>{product.name}</h2>
|
||||
|
||||
<div>Price: {formatCurrency(productPrice)}</div>
|
||||
|
||||
{!hasVariants && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
addItem({
|
||||
productID: product.id,
|
||||
quantity: 1,
|
||||
product: product,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Add to cart
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasVariants &&
|
||||
product.variants!.docs!.map((variant) => {
|
||||
if (typeof variant === 'string') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={variant.id}>
|
||||
<div>{variant.title}</div>
|
||||
<div>Price: {formatCurrency(variant.priceInUSD)}</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
addItem({
|
||||
productID: product.id,
|
||||
variantID: variant.id,
|
||||
quantity: 1,
|
||||
variant: variant,
|
||||
product: product,
|
||||
})
|
||||
}}
|
||||
>
|
||||
Add to cart
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
10
test/plugin-ecommerce/collections/Media.ts
Normal file
10
test/plugin-ecommerce/collections/Media.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Media: CollectionConfig = {
|
||||
slug: 'media',
|
||||
upload: true,
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [],
|
||||
}
|
||||
13
test/plugin-ecommerce/collections/Users.ts
Normal file
13
test/plugin-ecommerce/collections/Users.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { CollectionConfig } from 'payload'
|
||||
|
||||
export const Users: CollectionConfig = {
|
||||
slug: 'users',
|
||||
auth: true,
|
||||
admin: {
|
||||
useAsTitle: 'email',
|
||||
},
|
||||
access: {
|
||||
read: () => true,
|
||||
},
|
||||
fields: [],
|
||||
}
|
||||
99
test/plugin-ecommerce/config.ts
Normal file
99
test/plugin-ecommerce/config.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
import { ecommercePlugin, EUR, USD } from '@payloadcms/plugin-ecommerce'
|
||||
import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe'
|
||||
|
||||
import type { EcommercePluginConfig } from '../../packages/plugin-ecommerce/src/types.js'
|
||||
|
||||
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
|
||||
import { devUser } from '../credentials.js'
|
||||
import { Media } from './collections/Media.js'
|
||||
import { Users } from './collections/Users.js'
|
||||
import { seed } from './seed/index.js'
|
||||
|
||||
export const currenciesConfig: NonNullable<EcommercePluginConfig['currencies']> = {
|
||||
supportedCurrencies: [
|
||||
USD,
|
||||
EUR,
|
||||
{
|
||||
code: 'JPY',
|
||||
decimals: 0,
|
||||
label: 'Japanese Yen',
|
||||
symbol: '¥',
|
||||
},
|
||||
],
|
||||
defaultCurrency: 'USD',
|
||||
}
|
||||
|
||||
export default buildConfigWithDefaults({
|
||||
collections: [Users, Media],
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
maxDepth: 10,
|
||||
onInit: async (payload) => {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
},
|
||||
})
|
||||
|
||||
await seed(payload)
|
||||
},
|
||||
jobs: {
|
||||
autoRun: undefined,
|
||||
},
|
||||
plugins: [
|
||||
ecommercePlugin({
|
||||
access: {
|
||||
adminOnly: ({ req }) => Boolean(req.user),
|
||||
adminOnlyFieldAccess: ({ req }) => Boolean(req.user),
|
||||
adminOrCustomerOwner: ({ req }) => Boolean(req.user),
|
||||
customerOnlyFieldAccess: ({ req }) => Boolean(req.user),
|
||||
adminOrPublishedStatus: ({ req }) => {
|
||||
if (req.user) {
|
||||
return true
|
||||
}
|
||||
|
||||
return {
|
||||
_status: {
|
||||
equals: 'published',
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
customers: {
|
||||
slug: 'users',
|
||||
},
|
||||
products: {
|
||||
variants: true,
|
||||
},
|
||||
payments: {
|
||||
paymentMethods: [
|
||||
stripeAdapter({
|
||||
secretKey: process.env.STRIPE_SECRET_KEY!,
|
||||
publishableKey: process.env.STRIPE_PUBLISHABLE_KEY!,
|
||||
webhookSecret: process.env.STRIPE_WEBHOOKS_SECRET!,
|
||||
webhooks: {
|
||||
'payment_intent.succeeded': ({ event, req }) => {
|
||||
console.log({ event, data: event.data.object })
|
||||
req.payload.logger.info('Payment succeeded')
|
||||
},
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
currencies: currenciesConfig,
|
||||
}),
|
||||
],
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
61
test/plugin-ecommerce/int.spec.ts
Normal file
61
test/plugin-ecommerce/int.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import path from 'path'
|
||||
import { NotFound, type Payload } from 'payload'
|
||||
import { wait } from 'payload/shared'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import type { NextRESTClient } from '../helpers/NextRESTClient.js'
|
||||
|
||||
import { devUser } from '../credentials.js'
|
||||
import { initPayloadInt } from '../helpers/initPayloadInt.js'
|
||||
|
||||
let payload: Payload
|
||||
let restClient: NextRESTClient
|
||||
let token: string
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
describe('ecommerce', () => {
|
||||
beforeAll(async () => {
|
||||
;({ payload, restClient } = await initPayloadInt(dirname))
|
||||
|
||||
const data = await restClient
|
||||
.POST('/users/login', {
|
||||
body: JSON.stringify({
|
||||
email: devUser.email,
|
||||
password: devUser.password,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json())
|
||||
|
||||
token = data.token
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
// await payload.delete({
|
||||
// collection: 'search',
|
||||
// depth: 0,
|
||||
// where: {
|
||||
// id: {
|
||||
// exists: true,
|
||||
// },
|
||||
// },
|
||||
// })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
if (typeof payload.db.destroy === 'function') {
|
||||
await payload.db.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
it('should add a variants collection', async () => {
|
||||
const variants = await payload.find({
|
||||
collection: 'variants',
|
||||
depth: 0,
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
expect(variants).toBeTruthy()
|
||||
})
|
||||
})
|
||||
5
test/plugin-ecommerce/next-env.d.ts
vendored
Normal file
5
test/plugin-ecommerce/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
15
test/plugin-ecommerce/next.config.mjs
Normal file
15
test/plugin-ecommerce/next.config.mjs
Normal file
@@ -0,0 +1,15 @@
|
||||
import nextConfig from '../../next.config.mjs'
|
||||
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(__filename)
|
||||
|
||||
export default {
|
||||
...nextConfig,
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
},
|
||||
}
|
||||
559
test/plugin-ecommerce/payload-types.ts
Normal file
559
test/plugin-ecommerce/payload-types.ts
Normal file
@@ -0,0 +1,559 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* This file was automatically generated by Payload.
|
||||
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
|
||||
* and re-run `payload generate:types` to regenerate this file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "OrderStatus".
|
||||
*/
|
||||
export type OrderStatus = ('processing' | 'completed' | 'cancelled' | 'refunded') | null;
|
||||
/**
|
||||
* Supported timezones in IANA format.
|
||||
*
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "supportedTimezones".
|
||||
*/
|
||||
export type SupportedTimezones =
|
||||
| 'Pacific/Midway'
|
||||
| 'Pacific/Niue'
|
||||
| 'Pacific/Honolulu'
|
||||
| 'Pacific/Rarotonga'
|
||||
| 'America/Anchorage'
|
||||
| 'Pacific/Gambier'
|
||||
| 'America/Los_Angeles'
|
||||
| 'America/Tijuana'
|
||||
| 'America/Denver'
|
||||
| 'America/Phoenix'
|
||||
| 'America/Chicago'
|
||||
| 'America/Guatemala'
|
||||
| 'America/New_York'
|
||||
| 'America/Bogota'
|
||||
| 'America/Caracas'
|
||||
| 'America/Santiago'
|
||||
| 'America/Buenos_Aires'
|
||||
| 'America/Sao_Paulo'
|
||||
| 'Atlantic/South_Georgia'
|
||||
| 'Atlantic/Azores'
|
||||
| 'Atlantic/Cape_Verde'
|
||||
| 'Europe/London'
|
||||
| 'Europe/Berlin'
|
||||
| 'Africa/Lagos'
|
||||
| 'Europe/Athens'
|
||||
| 'Africa/Cairo'
|
||||
| 'Europe/Moscow'
|
||||
| 'Asia/Riyadh'
|
||||
| 'Asia/Dubai'
|
||||
| 'Asia/Baku'
|
||||
| 'Asia/Karachi'
|
||||
| 'Asia/Tashkent'
|
||||
| 'Asia/Calcutta'
|
||||
| 'Asia/Dhaka'
|
||||
| 'Asia/Almaty'
|
||||
| 'Asia/Jakarta'
|
||||
| 'Asia/Bangkok'
|
||||
| 'Asia/Shanghai'
|
||||
| 'Asia/Singapore'
|
||||
| 'Asia/Tokyo'
|
||||
| 'Asia/Seoul'
|
||||
| 'Australia/Brisbane'
|
||||
| 'Australia/Sydney'
|
||||
| 'Pacific/Guam'
|
||||
| 'Pacific/Noumea'
|
||||
| 'Pacific/Auckland'
|
||||
| 'Pacific/Fiji';
|
||||
|
||||
export interface Config {
|
||||
auth: {
|
||||
users: UserAuthOperations;
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
users: User;
|
||||
media: Media;
|
||||
variants: Variant;
|
||||
variantTypes: VariantType;
|
||||
variantOptions: VariantOption;
|
||||
products: Product;
|
||||
orders: Order;
|
||||
transactions: Transaction;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {
|
||||
variantTypes: {
|
||||
options: 'variantOptions';
|
||||
};
|
||||
products: {
|
||||
variants: 'variants';
|
||||
};
|
||||
};
|
||||
collectionsSelect: {
|
||||
users: UsersSelect<false> | UsersSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
variants: VariantsSelect<false> | VariantsSelect<true>;
|
||||
variantTypes: VariantTypesSelect<false> | VariantTypesSelect<true>;
|
||||
variantOptions: VariantOptionsSelect<false> | VariantOptionsSelect<true>;
|
||||
products: ProductsSelect<false> | ProductsSelect<true>;
|
||||
orders: OrdersSelect<false> | OrdersSelect<true>;
|
||||
transactions: TransactionsSelect<false> | TransactionsSelect<true>;
|
||||
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
|
||||
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
|
||||
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
|
||||
};
|
||||
db: {
|
||||
defaultIDType: string;
|
||||
};
|
||||
globals: {};
|
||||
globalsSelect: {};
|
||||
locale: null;
|
||||
user: User & {
|
||||
collection: 'users';
|
||||
};
|
||||
jobs: {
|
||||
tasks: unknown;
|
||||
workflows: unknown;
|
||||
};
|
||||
}
|
||||
export interface UserAuthOperations {
|
||||
forgotPassword: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
login: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
registerFirstUser: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
unlock: {
|
||||
email: string;
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users".
|
||||
*/
|
||||
export interface User {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
email: string;
|
||||
resetPasswordToken?: string | null;
|
||||
resetPasswordExpiration?: string | null;
|
||||
salt?: string | null;
|
||||
hash?: string | null;
|
||||
loginAttempts?: number | null;
|
||||
lockUntil?: string | null;
|
||||
password?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media".
|
||||
*/
|
||||
export interface Media {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
url?: string | null;
|
||||
thumbnailURL?: string | null;
|
||||
filename?: string | null;
|
||||
mimeType?: string | null;
|
||||
filesize?: number | null;
|
||||
width?: number | null;
|
||||
height?: number | null;
|
||||
focalX?: number | null;
|
||||
focalY?: number | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "variants".
|
||||
*/
|
||||
export interface Variant {
|
||||
id: string;
|
||||
/**
|
||||
* Used for administrative purposes, not shown to customers. This is populated by default.
|
||||
*/
|
||||
title?: string | null;
|
||||
/**
|
||||
* this should not be editable, or at least, should be able to be pre-filled via default
|
||||
*/
|
||||
product: string | Product;
|
||||
options: (string | VariantOption)[];
|
||||
inventory: number;
|
||||
priceInUSDEnabled?: boolean | null;
|
||||
priceInUSD?: number | null;
|
||||
priceInJPYEnabled?: boolean | null;
|
||||
priceInJPY?: number | null;
|
||||
priceInEUREnabled?: boolean | null;
|
||||
priceInEUR?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "products".
|
||||
*/
|
||||
export interface Product {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
enableVariants?: boolean | null;
|
||||
variantTypes?: (string | VariantType)[] | null;
|
||||
variants?: {
|
||||
docs?: (string | Variant)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
priceInUSDEnabled?: boolean | null;
|
||||
priceInUSD?: number | null;
|
||||
priceInJPYEnabled?: boolean | null;
|
||||
priceInJPY?: number | null;
|
||||
priceInEUREnabled?: boolean | null;
|
||||
priceInEUR?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "variantTypes".
|
||||
*/
|
||||
export interface VariantType {
|
||||
id: string;
|
||||
label: string;
|
||||
name: string;
|
||||
options?: {
|
||||
docs?: (string | VariantOption)[];
|
||||
hasNextPage?: boolean;
|
||||
totalDocs?: number;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "variantOptions".
|
||||
*/
|
||||
export interface VariantOption {
|
||||
id: string;
|
||||
variantType: string | VariantType;
|
||||
label: string;
|
||||
/**
|
||||
* should be defaulted or dynamic based on label
|
||||
*/
|
||||
value: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orders".
|
||||
*/
|
||||
export interface Order {
|
||||
id: string;
|
||||
customer?: (string | null) | User;
|
||||
customerEmail?: string | null;
|
||||
transactions?: (string | Transaction)[] | null;
|
||||
status?: OrderStatus;
|
||||
amount?: number | null;
|
||||
currency?: ('USD' | 'JPY' | 'EUR') | null;
|
||||
cart?:
|
||||
| {
|
||||
product?: (string | null) | Product;
|
||||
variant?: (string | null) | Variant;
|
||||
quantity: number;
|
||||
amount?: number | null;
|
||||
currency?: ('USD' | 'JPY' | 'EUR') | null;
|
||||
id?: string | null;
|
||||
}[]
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "transactions".
|
||||
*/
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
customer?: (string | null) | User;
|
||||
customerEmail?: string | null;
|
||||
order?: (string | null) | Order;
|
||||
status: 'pending' | 'succeeded' | 'failed' | 'cancelled' | 'expired' | 'refunded';
|
||||
paymentMethod?: 'stripe' | null;
|
||||
stripe?: {
|
||||
customerID?: string | null;
|
||||
paymentIntentID?: string | null;
|
||||
};
|
||||
currency?: ('USD' | 'JPY' | 'EUR') | null;
|
||||
amount?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'variants';
|
||||
value: string | Variant;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'variantTypes';
|
||||
value: string | VariantType;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'variantOptions';
|
||||
value: string | VariantOption;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'products';
|
||||
value: string | Product;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'orders';
|
||||
value: string | Order;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'transactions';
|
||||
value: string | Transaction;
|
||||
} | null);
|
||||
globalSlug?: string | null;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences".
|
||||
*/
|
||||
export interface PayloadPreference {
|
||||
id: string;
|
||||
user: {
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
};
|
||||
key?: string | null;
|
||||
value?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations".
|
||||
*/
|
||||
export interface PayloadMigration {
|
||||
id: string;
|
||||
name?: string | null;
|
||||
batch?: number | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "users_select".
|
||||
*/
|
||||
export interface UsersSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
email?: T;
|
||||
resetPasswordToken?: T;
|
||||
resetPasswordExpiration?: T;
|
||||
salt?: T;
|
||||
hash?: T;
|
||||
loginAttempts?: T;
|
||||
lockUntil?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "media_select".
|
||||
*/
|
||||
export interface MediaSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
url?: T;
|
||||
thumbnailURL?: T;
|
||||
filename?: T;
|
||||
mimeType?: T;
|
||||
filesize?: T;
|
||||
width?: T;
|
||||
height?: T;
|
||||
focalX?: T;
|
||||
focalY?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "variants_select".
|
||||
*/
|
||||
export interface VariantsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
product?: T;
|
||||
options?: T;
|
||||
inventory?: T;
|
||||
priceInUSDEnabled?: T;
|
||||
priceInUSD?: T;
|
||||
priceInJPYEnabled?: T;
|
||||
priceInJPY?: T;
|
||||
priceInEUREnabled?: T;
|
||||
priceInEUR?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "variantTypes_select".
|
||||
*/
|
||||
export interface VariantTypesSelect<T extends boolean = true> {
|
||||
label?: T;
|
||||
name?: T;
|
||||
options?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "variantOptions_select".
|
||||
*/
|
||||
export interface VariantOptionsSelect<T extends boolean = true> {
|
||||
variantType?: T;
|
||||
label?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "products_select".
|
||||
*/
|
||||
export interface ProductsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
enableVariants?: T;
|
||||
variantTypes?: T;
|
||||
variants?: T;
|
||||
priceInUSDEnabled?: T;
|
||||
priceInUSD?: T;
|
||||
priceInJPYEnabled?: T;
|
||||
priceInJPY?: T;
|
||||
priceInEUREnabled?: T;
|
||||
priceInEUR?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "orders_select".
|
||||
*/
|
||||
export interface OrdersSelect<T extends boolean = true> {
|
||||
customer?: T;
|
||||
customerEmail?: T;
|
||||
transactions?: T;
|
||||
status?: T;
|
||||
amount?: T;
|
||||
currency?: T;
|
||||
cart?:
|
||||
| T
|
||||
| {
|
||||
product?: T;
|
||||
variant?: T;
|
||||
quantity?: T;
|
||||
amount?: T;
|
||||
currency?: T;
|
||||
id?: T;
|
||||
};
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "transactions_select".
|
||||
*/
|
||||
export interface TransactionsSelect<T extends boolean = true> {
|
||||
customer?: T;
|
||||
customerEmail?: T;
|
||||
order?: T;
|
||||
status?: T;
|
||||
paymentMethod?: T;
|
||||
stripe?:
|
||||
| T
|
||||
| {
|
||||
customerID?: T;
|
||||
paymentIntentID?: T;
|
||||
};
|
||||
currency?: T;
|
||||
amount?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-locked-documents_select".
|
||||
*/
|
||||
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
|
||||
document?: T;
|
||||
globalSlug?: T;
|
||||
user?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-preferences_select".
|
||||
*/
|
||||
export interface PayloadPreferencesSelect<T extends boolean = true> {
|
||||
user?: T;
|
||||
key?: T;
|
||||
value?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "payload-migrations_select".
|
||||
*/
|
||||
export interface PayloadMigrationsSelect<T extends boolean = true> {
|
||||
name?: T;
|
||||
batch?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
*/
|
||||
export interface Auth {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
|
||||
declare module 'payload' {
|
||||
// @ts-ignore
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
144
test/plugin-ecommerce/seed/index.ts
Normal file
144
test/plugin-ecommerce/seed/index.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { Payload, PayloadRequest } from 'payload'
|
||||
|
||||
const sizeVariantOptions = [
|
||||
{ label: 'Small', value: 'small' },
|
||||
{ label: 'Medium', value: 'medium' },
|
||||
{ label: 'Large', value: 'large' },
|
||||
{ label: 'X Large', value: 'xlarge' },
|
||||
]
|
||||
|
||||
const colorVariantOptions = [
|
||||
{ label: 'Black', value: 'black' },
|
||||
{ label: 'White', value: 'white' },
|
||||
]
|
||||
|
||||
export const seed = async (payload: Payload): Promise<boolean> => {
|
||||
payload.logger.info('Seeding data for ecommerce...')
|
||||
const req = {} as PayloadRequest
|
||||
|
||||
try {
|
||||
const customer = await payload.create({
|
||||
collection: 'users',
|
||||
data: {
|
||||
email: 'customer@payloadcms.com',
|
||||
password: 'customer',
|
||||
},
|
||||
req,
|
||||
})
|
||||
|
||||
const sizeVariantType = await payload.create({
|
||||
collection: 'variantTypes',
|
||||
data: {
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
},
|
||||
})
|
||||
|
||||
const [small, medium, large, xlarge] = await Promise.all(
|
||||
sizeVariantOptions.map((option) => {
|
||||
return payload.create({
|
||||
collection: 'variantOptions',
|
||||
data: {
|
||||
...option,
|
||||
variantType: sizeVariantType.id,
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const colorVariantType = await payload.create({
|
||||
collection: 'variantTypes',
|
||||
data: {
|
||||
name: 'color',
|
||||
label: 'Color',
|
||||
},
|
||||
})
|
||||
|
||||
const [black, white] = await Promise.all(
|
||||
colorVariantOptions.map((option) => {
|
||||
return payload.create({
|
||||
collection: 'variantOptions',
|
||||
data: {
|
||||
...option,
|
||||
variantType: colorVariantType.id,
|
||||
},
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
const hoodieProduct = await payload.create({
|
||||
collection: 'products',
|
||||
data: {
|
||||
name: 'Hoodie',
|
||||
variantTypes: [sizeVariantType.id, colorVariantType.id],
|
||||
enableVariants: true,
|
||||
},
|
||||
})
|
||||
|
||||
const hoodieSmallWhite = await payload.create({
|
||||
collection: 'variants',
|
||||
data: {
|
||||
product: hoodieProduct.id,
|
||||
options: [small!.id, white!.id],
|
||||
inventory: 10,
|
||||
priceInUSDEnabled: true,
|
||||
priceInUSD: 1999,
|
||||
},
|
||||
})
|
||||
|
||||
const hoodieMediumWhite = await payload.create({
|
||||
collection: 'variants',
|
||||
data: {
|
||||
product: hoodieProduct.id,
|
||||
options: [white!.id, medium!.id],
|
||||
inventory: 492,
|
||||
priceInUSDEnabled: true,
|
||||
priceInUSD: 1999,
|
||||
},
|
||||
})
|
||||
|
||||
const hatProduct = await payload.create({
|
||||
collection: 'products',
|
||||
data: {
|
||||
name: 'Hat',
|
||||
priceInUSDEnabled: true,
|
||||
priceInUSD: 1999,
|
||||
priceInEUREnabled: true,
|
||||
priceInEUR: 2599,
|
||||
},
|
||||
})
|
||||
|
||||
const pendingPaymentRecord = await payload.create({
|
||||
collection: 'transactions',
|
||||
data: {
|
||||
currency: 'USD',
|
||||
customer: customer.id,
|
||||
paymentMethod: 'stripe',
|
||||
stripe: {
|
||||
customerID: 'cus_123',
|
||||
paymentIntentID: 'pi_123',
|
||||
},
|
||||
status: 'pending',
|
||||
},
|
||||
})
|
||||
|
||||
const succeededPaymentRecord = await payload.create({
|
||||
collection: 'transactions',
|
||||
data: {
|
||||
currency: 'USD',
|
||||
customer: customer.id,
|
||||
paymentMethod: 'stripe',
|
||||
stripe: {
|
||||
customerID: 'cus_123',
|
||||
paymentIntentID: 'pi_123',
|
||||
},
|
||||
status: 'succeeded',
|
||||
},
|
||||
})
|
||||
|
||||
return true
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return false
|
||||
}
|
||||
}
|
||||
0
test/plugin-ecommerce/shared.ts
Normal file
0
test/plugin-ecommerce/shared.ts
Normal file
13
test/plugin-ecommerce/tsconfig.eslint.json
Normal file
13
test/plugin-ecommerce/tsconfig.eslint.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
// extend your base config to share compilerOptions, etc
|
||||
//"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
// ensure that nobody can accidentally use this config for a build
|
||||
"noEmit": true
|
||||
},
|
||||
"include": [
|
||||
// whatever paths you intend to lint
|
||||
"./**/*.ts",
|
||||
"./**/*.tsx"
|
||||
]
|
||||
}
|
||||
30
test/plugin-ecommerce/tsconfig.json
Normal file
30
test/plugin-ecommerce/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@payload-config": ["./config.ts"],
|
||||
"@payload-types": ["./payload-types.ts"],
|
||||
"@/components/*": ["./app/components/*"],
|
||||
"@payloadcms/plugin-ecommerce": ["../../packages/plugin-ecommerce/exports/index.ts"],
|
||||
"@payloadcms/plugin-ecommerce/*": ["../../packages/plugin-ecommerce/exports/*"],
|
||||
"@payloadcms/ui/assets": ["../../packages/ui/src/assets/index.ts"],
|
||||
"@payloadcms/ui/elements/*": ["../../packages/ui/src/elements/*/index.tsx"],
|
||||
"@payloadcms/ui/fields/*": ["../../packages/ui/src/fields/*/index.tsx"],
|
||||
"@payloadcms/ui/forms/*": ["../../packages/ui/src/forms/*/index.tsx"],
|
||||
"@payloadcms/ui/graphics/*": ["../../packages/ui/src/graphics/*/index.tsx"],
|
||||
"@payloadcms/ui/hooks/*": ["../../packages/ui/src/hooks/*.ts"],
|
||||
"@payloadcms/ui/icons/*": ["../../packages/ui/src/icons/*/index.tsx"],
|
||||
"@payloadcms/ui/providers/*": ["../../packages/ui/src/providers/*/index.tsx"],
|
||||
"@payloadcms/ui/templates/*": ["../../packages/ui/src/templates/*/index.tsx"],
|
||||
"@payloadcms/ui/utilities/*": ["../../packages/ui/src/utilities/*.ts"],
|
||||
"@payloadcms/ui/scss": ["../../packages/ui/src/scss.scss"],
|
||||
"@payloadcms/ui/scss/app.scss": ["../../packages/ui/src/scss/app.scss"],
|
||||
"payload/types": ["../../packages/payload/src/exports/types.ts"],
|
||||
"@payloadcms/next/*": ["../../packages/next/src/*"],
|
||||
"@payloadcms/next": ["../../packages/next/src/exports/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user