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:
Paul
2025-09-30 01:05:16 +01:00
committed by GitHub
parent 92a5f075b6
commit ef4874b9a0
372 changed files with 30874 additions and 331 deletions

2
test/plugin-ecommerce/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
app/(payload)/admin/importMap.js
/app/(payload)/admin/importMap.js

View File

@@ -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

View File

@@ -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

View 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)

View File

@@ -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)

View 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)

View File

@@ -0,0 +1,7 @@
#custom-css {
font-family: monospace;
}
#custom-css::after {
content: 'custom-css';
}

View 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

View 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

View 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>
)
}

View 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

View 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

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -0,0 +1,10 @@
import type { CollectionConfig } from 'payload'
export const Media: CollectionConfig = {
slug: 'media',
upload: true,
access: {
read: () => true,
},
fields: [],
}

View 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: [],
}

View 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'),
},
})

View 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
View 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.

View 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),
},
}

View 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 {}
}

View 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
}
}

View File

View 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"
]
}

View 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"]
}