mirror of
https://github.com/xtr-dev/payload-billing.git
synced 2025-12-10 02:43:24 +00:00
Add PayloadCMS type definitions, Prettier config, and PNPM lockfile
This commit is contained in:
16
dev/.env.example
Normal file
16
dev/.env.example
Normal file
@@ -0,0 +1,16 @@
|
||||
# PayloadCMS Configuration
|
||||
PAYLOAD_SECRET=your-secret-key-here
|
||||
|
||||
# Database Configuration
|
||||
DATABASE_URI=mongodb://localhost:27017/payload-billing-dev
|
||||
|
||||
# Payment Provider Configuration (Optional - for testing integrations)
|
||||
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
|
||||
STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key
|
||||
STRIPE_WEBHOOK_SECRET=whsec_your_webhook_secret
|
||||
|
||||
MOLLIE_API_KEY=test_your_mollie_api_key
|
||||
MOLLIE_WEBHOOK_URL=http://localhost:3000/api/billing/webhooks/mollie
|
||||
|
||||
# Development Settings
|
||||
NODE_ENV=development
|
||||
25
dev/app/(payload)/admin/[[...segments]]/not-found.tsx
Normal file
25
dev/app/(payload)/admin/[[...segments]]/not-found.tsx
Normal 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
|
||||
25
dev/app/(payload)/admin/[[...segments]]/page.tsx
Normal file
25
dev/app/(payload)/admin/[[...segments]]/page.tsx
Normal 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
|
||||
5
dev/app/(payload)/admin/importMap.js
Normal file
5
dev/app/(payload)/admin/importMap.js
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
export const importMap = {
|
||||
|
||||
}
|
||||
19
dev/app/(payload)/api/[...slug]/route.ts
Normal file
19
dev/app/(payload)/api/[...slug]/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/* 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 '@payloadcms/next/css'
|
||||
import {
|
||||
REST_DELETE,
|
||||
REST_GET,
|
||||
REST_OPTIONS,
|
||||
REST_PATCH,
|
||||
REST_POST,
|
||||
REST_PUT,
|
||||
} 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 PUT = REST_PUT(config)
|
||||
export const OPTIONS = REST_OPTIONS(config)
|
||||
7
dev/app/(payload)/api/graphql-playground/route.ts
Normal file
7
dev/app/(payload)/api/graphql-playground/route.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/* 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 '@payloadcms/next/css'
|
||||
import { GRAPHQL_PLAYGROUND_GET } from '@payloadcms/next/routes'
|
||||
|
||||
export const GET = GRAPHQL_PLAYGROUND_GET(config)
|
||||
8
dev/app/(payload)/api/graphql/route.ts
Normal file
8
dev/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)
|
||||
0
dev/app/(payload)/custom.scss
Normal file
0
dev/app/(payload)/custom.scss
Normal file
32
dev/app/(payload)/layout.tsx
Normal file
32
dev/app/(payload)/layout.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { ServerFunctionClient } from 'payload'
|
||||
|
||||
import '@payloadcms/next/css'
|
||||
/* 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 { 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
|
||||
12
dev/app/my-route/route.ts
Normal file
12
dev/app/my-route/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import configPromise from '@payload-config'
|
||||
import { getPayload } from 'payload'
|
||||
|
||||
export const GET = async (request: Request) => {
|
||||
const payload = await getPayload({
|
||||
config: configPromise,
|
||||
})
|
||||
|
||||
return Response.json({
|
||||
message: 'This is an example of a custom route.',
|
||||
})
|
||||
}
|
||||
15
dev/e2e.spec.ts
Normal file
15
dev/e2e.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
// this is an example Playwright e2e test
|
||||
test('should render admin panel logo', async ({ page }) => {
|
||||
await page.goto('/admin')
|
||||
|
||||
// login
|
||||
await page.fill('#field-email', 'dev@payloadcms.com')
|
||||
await page.fill('#field-password', 'test')
|
||||
await page.click('.form-submit button')
|
||||
|
||||
// should show dashboard
|
||||
await expect(page).toHaveTitle(/Dashboard/)
|
||||
await expect(page.locator('.graphic-icon')).toBeVisible()
|
||||
})
|
||||
4
dev/helpers/credentials.ts
Normal file
4
dev/helpers/credentials.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const devUser = {
|
||||
email: 'dev@payloadcms.com',
|
||||
password: 'test',
|
||||
}
|
||||
38
dev/helpers/testEmailAdapter.ts
Normal file
38
dev/helpers/testEmailAdapter.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { EmailAdapter, SendEmailOptions } from 'payload'
|
||||
|
||||
/**
|
||||
* Logs all emails to stdout
|
||||
*/
|
||||
export const testEmailAdapter: EmailAdapter<void> = ({ payload }) => ({
|
||||
name: 'test-email-adapter',
|
||||
defaultFromAddress: 'dev@payloadcms.com',
|
||||
defaultFromName: 'Payload Test',
|
||||
sendEmail: async (message) => {
|
||||
const stringifiedTo = getStringifiedToAddress(message)
|
||||
const res = `Test email to: '${stringifiedTo}', Subject: '${message.subject}'`
|
||||
payload.logger.info({ content: message, msg: res })
|
||||
return Promise.resolve()
|
||||
},
|
||||
})
|
||||
|
||||
function getStringifiedToAddress(message: SendEmailOptions): string | undefined {
|
||||
let stringifiedTo: string | undefined
|
||||
|
||||
if (typeof message.to === 'string') {
|
||||
stringifiedTo = message.to
|
||||
} else if (Array.isArray(message.to)) {
|
||||
stringifiedTo = message.to
|
||||
.map((to: { address: string } | string) => {
|
||||
if (typeof to === 'string') {
|
||||
return to
|
||||
} else if (to.address) {
|
||||
return to.address
|
||||
}
|
||||
return ''
|
||||
})
|
||||
.join(', ')
|
||||
} else if (message.to?.address) {
|
||||
stringifiedTo = message.to.address
|
||||
}
|
||||
return stringifiedTo
|
||||
}
|
||||
52
dev/int.spec.ts
Normal file
52
dev/int.spec.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import config from '@payload-config'
|
||||
import { createPayloadRequest, getPayload } from 'payload'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
|
||||
import { customEndpointHandler } from '../src/endpoints/customEndpointHandler.js'
|
||||
|
||||
let payload: Payload
|
||||
|
||||
afterAll(async () => {
|
||||
await payload.destroy()
|
||||
})
|
||||
|
||||
beforeAll(async () => {
|
||||
payload = await getPayload({ config })
|
||||
})
|
||||
|
||||
describe('Plugin integration tests', () => {
|
||||
test('should query custom endpoint added by plugin', async () => {
|
||||
const request = new Request('http://localhost:3000/api/my-plugin-endpoint', {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
const payloadRequest = await createPayloadRequest({ config, request })
|
||||
const response = await customEndpointHandler(payloadRequest)
|
||||
expect(response.status).toBe(200)
|
||||
|
||||
const data = await response.json()
|
||||
expect(data).toMatchObject({
|
||||
message: 'Hello from custom endpoint',
|
||||
})
|
||||
})
|
||||
|
||||
test('can create post with custom text field added by plugin', async () => {
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: {
|
||||
addedByPlugin: 'added by plugin',
|
||||
},
|
||||
})
|
||||
expect(post.addedByPlugin).toBe('added by plugin')
|
||||
})
|
||||
|
||||
test('plugin creates and seeds plugin-collection', async () => {
|
||||
expect(payload.collections['plugin-collection']).toBeDefined()
|
||||
|
||||
const { docs } = await payload.find({ collection: 'plugin-collection' })
|
||||
|
||||
expect(docs).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
5
dev/next-env.d.ts
vendored
Normal file
5
dev/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.
|
||||
21
dev/next.config.mjs
Normal file
21
dev/next.config.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { withPayload } from '@payloadcms/next/withPayload'
|
||||
import { fileURLToPath } from 'url'
|
||||
import path from 'path'
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
webpack: (webpackConfig) => {
|
||||
webpackConfig.resolve.extensionAlias = {
|
||||
'.cjs': ['.cts', '.cjs'],
|
||||
'.js': ['.ts', '.tsx', '.js', '.jsx'],
|
||||
'.mjs': ['.mts', '.mjs'],
|
||||
}
|
||||
|
||||
return webpackConfig
|
||||
},
|
||||
serverExternalPackages: ['mongodb-memory-server'],
|
||||
}
|
||||
|
||||
export default withPayload(nextConfig, { devBundleServerPackages: false })
|
||||
656
dev/payload-types.ts
Normal file
656
dev/payload-types.ts
Normal file
@@ -0,0 +1,656 @@
|
||||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 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: {
|
||||
posts: Post;
|
||||
media: Media;
|
||||
payments: Payment;
|
||||
customers: Customer;
|
||||
invoices: Invoice;
|
||||
refunds: Refund;
|
||||
users: User;
|
||||
'payload-locked-documents': PayloadLockedDocument;
|
||||
'payload-preferences': PayloadPreference;
|
||||
'payload-migrations': PayloadMigration;
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
media: MediaSelect<false> | MediaSelect<true>;
|
||||
payments: PaymentsSelect<false> | PaymentsSelect<true>;
|
||||
customers: CustomersSelect<false> | CustomersSelect<true>;
|
||||
invoices: InvoicesSelect<false> | InvoicesSelect<true>;
|
||||
refunds: RefundsSelect<false> | RefundsSelect<true>;
|
||||
users: UsersSelect<false> | UsersSelect<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` "posts".
|
||||
*/
|
||||
export interface Post {
|
||||
id: string;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* 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` "payments".
|
||||
*/
|
||||
export interface Payment {
|
||||
id: string;
|
||||
provider: 'stripe' | 'mollie' | 'test';
|
||||
/**
|
||||
* The payment ID from the payment provider
|
||||
*/
|
||||
providerId: string;
|
||||
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'canceled' | 'refunded' | 'partially_refunded';
|
||||
/**
|
||||
* Amount in cents (e.g., 2000 = $20.00)
|
||||
*/
|
||||
amount: number;
|
||||
/**
|
||||
* ISO 4217 currency code (e.g., USD, EUR)
|
||||
*/
|
||||
currency: string;
|
||||
/**
|
||||
* Payment description
|
||||
*/
|
||||
description?: string | null;
|
||||
customer?: (string | null) | Customer;
|
||||
invoice?: (string | null) | Invoice;
|
||||
/**
|
||||
* Additional metadata for the payment
|
||||
*/
|
||||
metadata?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* Raw data from the payment provider
|
||||
*/
|
||||
providerData?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
refunds?: (string | Refund)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "customers".
|
||||
*/
|
||||
export interface Customer {
|
||||
id: string;
|
||||
/**
|
||||
* Customer email address
|
||||
*/
|
||||
email?: string | null;
|
||||
/**
|
||||
* Customer full name
|
||||
*/
|
||||
name?: string | null;
|
||||
/**
|
||||
* Customer phone number
|
||||
*/
|
||||
phone?: string | null;
|
||||
address?: {
|
||||
line1?: string | null;
|
||||
line2?: string | null;
|
||||
city?: string | null;
|
||||
state?: string | null;
|
||||
postal_code?: string | null;
|
||||
/**
|
||||
* ISO 3166-1 alpha-2 country code
|
||||
*/
|
||||
country?: string | null;
|
||||
};
|
||||
/**
|
||||
* Customer IDs from payment providers
|
||||
*/
|
||||
providerIds?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* Additional customer metadata
|
||||
*/
|
||||
metadata?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* Customer payments
|
||||
*/
|
||||
payments?: (string | Payment)[] | null;
|
||||
/**
|
||||
* Customer invoices
|
||||
*/
|
||||
invoices?: (string | Invoice)[] | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "invoices".
|
||||
*/
|
||||
export interface Invoice {
|
||||
id: string;
|
||||
/**
|
||||
* Invoice number (e.g., INV-001)
|
||||
*/
|
||||
number: string;
|
||||
customer: string | Customer;
|
||||
status: 'draft' | 'open' | 'paid' | 'void' | 'uncollectible';
|
||||
/**
|
||||
* ISO 4217 currency code (e.g., USD, EUR)
|
||||
*/
|
||||
currency: string;
|
||||
items: {
|
||||
description: string;
|
||||
quantity: number;
|
||||
/**
|
||||
* Amount in cents
|
||||
*/
|
||||
unitAmount: number;
|
||||
/**
|
||||
* Calculated: quantity × unitAmount
|
||||
*/
|
||||
totalAmount?: number | null;
|
||||
id?: string | null;
|
||||
}[];
|
||||
/**
|
||||
* Sum of all line items
|
||||
*/
|
||||
subtotal?: number | null;
|
||||
/**
|
||||
* Tax amount in cents
|
||||
*/
|
||||
taxAmount?: number | null;
|
||||
/**
|
||||
* Total amount (subtotal + tax)
|
||||
*/
|
||||
amount?: number | null;
|
||||
dueDate?: string | null;
|
||||
paidAt?: string | null;
|
||||
payment?: (string | null) | Payment;
|
||||
/**
|
||||
* Internal notes
|
||||
*/
|
||||
notes?: string | null;
|
||||
/**
|
||||
* Additional invoice metadata
|
||||
*/
|
||||
metadata?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "refunds".
|
||||
*/
|
||||
export interface Refund {
|
||||
id: string;
|
||||
/**
|
||||
* The refund ID from the payment provider
|
||||
*/
|
||||
providerId: string;
|
||||
payment: string | Payment;
|
||||
status: 'pending' | 'processing' | 'succeeded' | 'failed' | 'canceled';
|
||||
/**
|
||||
* Refund amount in cents
|
||||
*/
|
||||
amount: number;
|
||||
/**
|
||||
* ISO 4217 currency code (e.g., USD, EUR)
|
||||
*/
|
||||
currency: string;
|
||||
/**
|
||||
* Reason for the refund
|
||||
*/
|
||||
reason?: ('duplicate' | 'fraudulent' | 'requested_by_customer' | 'other') | null;
|
||||
/**
|
||||
* Additional details about the refund
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
* Additional refund metadata
|
||||
*/
|
||||
metadata?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
/**
|
||||
* Raw data from the payment provider
|
||||
*/
|
||||
providerData?:
|
||||
| {
|
||||
[k: string]: unknown;
|
||||
}
|
||||
| unknown[]
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| null;
|
||||
updatedAt: string;
|
||||
createdAt: 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` "payload-locked-documents".
|
||||
*/
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'media';
|
||||
value: string | Media;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'payments';
|
||||
value: string | Payment;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'customers';
|
||||
value: string | Customer;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'invoices';
|
||||
value: string | Invoice;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'refunds';
|
||||
value: string | Refund;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'users';
|
||||
value: string | User;
|
||||
} | 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` "posts_select".
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: 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` "payments_select".
|
||||
*/
|
||||
export interface PaymentsSelect<T extends boolean = true> {
|
||||
provider?: T;
|
||||
providerId?: T;
|
||||
status?: T;
|
||||
amount?: T;
|
||||
currency?: T;
|
||||
description?: T;
|
||||
customer?: T;
|
||||
invoice?: T;
|
||||
metadata?: T;
|
||||
providerData?: T;
|
||||
refunds?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "customers_select".
|
||||
*/
|
||||
export interface CustomersSelect<T extends boolean = true> {
|
||||
email?: T;
|
||||
name?: T;
|
||||
phone?: T;
|
||||
address?:
|
||||
| T
|
||||
| {
|
||||
line1?: T;
|
||||
line2?: T;
|
||||
city?: T;
|
||||
state?: T;
|
||||
postal_code?: T;
|
||||
country?: T;
|
||||
};
|
||||
providerIds?: T;
|
||||
metadata?: T;
|
||||
payments?: T;
|
||||
invoices?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "invoices_select".
|
||||
*/
|
||||
export interface InvoicesSelect<T extends boolean = true> {
|
||||
number?: T;
|
||||
customer?: T;
|
||||
status?: T;
|
||||
currency?: T;
|
||||
items?:
|
||||
| T
|
||||
| {
|
||||
description?: T;
|
||||
quantity?: T;
|
||||
unitAmount?: T;
|
||||
totalAmount?: T;
|
||||
id?: T;
|
||||
};
|
||||
subtotal?: T;
|
||||
taxAmount?: T;
|
||||
amount?: T;
|
||||
dueDate?: T;
|
||||
paidAt?: T;
|
||||
payment?: T;
|
||||
notes?: T;
|
||||
metadata?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "refunds_select".
|
||||
*/
|
||||
export interface RefundsSelect<T extends boolean = true> {
|
||||
providerId?: T;
|
||||
payment?: T;
|
||||
status?: T;
|
||||
amount?: T;
|
||||
currency?: T;
|
||||
reason?: T;
|
||||
description?: T;
|
||||
metadata?: T;
|
||||
providerData?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* 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` "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' {
|
||||
export interface GeneratedTypes extends Config {}
|
||||
}
|
||||
84
dev/payload.config.ts
Normal file
84
dev/payload.config.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { mongooseAdapter } from '@payloadcms/db-mongodb'
|
||||
import { lexicalEditor } from '@payloadcms/richtext-lexical'
|
||||
import { MongoMemoryReplSet } from 'mongodb-memory-server'
|
||||
import path from 'path'
|
||||
import { buildConfig } from 'payload'
|
||||
import { billingPlugin } from '../src/index.js'
|
||||
import sharp from 'sharp'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { testEmailAdapter } from './helpers/testEmailAdapter.js'
|
||||
import { seed } from './seed.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
if (!process.env.ROOT_DIR) {
|
||||
process.env.ROOT_DIR = dirname
|
||||
}
|
||||
|
||||
const buildConfigWithMemoryDB = async () => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
const memoryDB = await MongoMemoryReplSet.create({
|
||||
replSet: {
|
||||
count: 3,
|
||||
dbName: 'payloadmemory',
|
||||
},
|
||||
})
|
||||
|
||||
process.env.DATABASE_URI = `${memoryDB.getUri()}&retryWrites=true`
|
||||
}
|
||||
|
||||
return buildConfig({
|
||||
admin: {
|
||||
importMap: {
|
||||
baseDir: path.resolve(dirname),
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'posts',
|
||||
fields: [],
|
||||
},
|
||||
{
|
||||
slug: 'media',
|
||||
fields: [],
|
||||
upload: {
|
||||
staticDir: path.resolve(dirname, 'media'),
|
||||
},
|
||||
},
|
||||
],
|
||||
db: mongooseAdapter({
|
||||
ensureIndexes: true,
|
||||
url: process.env.DATABASE_URI || '',
|
||||
}),
|
||||
editor: lexicalEditor(),
|
||||
email: testEmailAdapter,
|
||||
onInit: async (payload) => {
|
||||
await seed(payload)
|
||||
},
|
||||
plugins: [
|
||||
billingPlugin({
|
||||
providers: {
|
||||
test: {
|
||||
enabled: true,
|
||||
autoComplete: true,
|
||||
}
|
||||
},
|
||||
collections: {
|
||||
payments: 'payments',
|
||||
customers: 'customers',
|
||||
invoices: 'invoices',
|
||||
refunds: 'refunds',
|
||||
}
|
||||
}),
|
||||
],
|
||||
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
||||
sharp,
|
||||
typescript: {
|
||||
outputFile: path.resolve(dirname, 'payload-types.ts'),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default buildConfigWithMemoryDB()
|
||||
149
dev/seed.ts
Normal file
149
dev/seed.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
import { devUser } from './helpers/credentials.js'
|
||||
|
||||
export const seed = async (payload: Payload) => {
|
||||
// Seed default user first
|
||||
const { totalDocs } = await payload.count({
|
||||
collection: 'users',
|
||||
where: {
|
||||
email: {
|
||||
equals: devUser.email,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (!totalDocs) {
|
||||
await payload.create({
|
||||
collection: 'users',
|
||||
data: devUser,
|
||||
})
|
||||
}
|
||||
|
||||
// Seed billing sample data
|
||||
await seedBillingData(payload)
|
||||
}
|
||||
|
||||
async function seedBillingData(payload: Payload): Promise<void> {
|
||||
payload.logger.info('Seeding billing sample data...')
|
||||
|
||||
try {
|
||||
// Check if we already have sample data
|
||||
const existingCustomers = await payload.count({
|
||||
collection: 'customers',
|
||||
where: {
|
||||
email: {
|
||||
equals: 'john.doe@example.com',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
if (existingCustomers.totalDocs > 0) {
|
||||
payload.logger.info('Sample billing data already exists, skipping seed')
|
||||
return
|
||||
}
|
||||
|
||||
// Create a sample customer
|
||||
const customer = await payload.create({
|
||||
collection: 'customers',
|
||||
data: {
|
||||
email: 'john.doe@example.com',
|
||||
name: 'John Doe',
|
||||
phone: '+1-555-0123',
|
||||
address: {
|
||||
line1: '123 Main St',
|
||||
city: 'New York',
|
||||
state: 'NY',
|
||||
postal_code: '10001',
|
||||
country: 'US'
|
||||
},
|
||||
metadata: {
|
||||
source: 'seed',
|
||||
created_by: 'system'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
payload.logger.info(`Created sample customer: ${customer.id}`)
|
||||
|
||||
// Create a sample invoice
|
||||
const invoice = await payload.create({
|
||||
collection: 'invoices',
|
||||
data: {
|
||||
number: 'INV-001-SAMPLE',
|
||||
customer: customer.id,
|
||||
currency: 'USD',
|
||||
items: [
|
||||
{
|
||||
description: 'Web Development Services',
|
||||
quantity: 10,
|
||||
unitAmount: 5000, // $50.00 per hour
|
||||
totalAmount: 50000 // $500.00 total
|
||||
},
|
||||
{
|
||||
description: 'Design Consultation',
|
||||
quantity: 2,
|
||||
unitAmount: 7500, // $75.00 per hour
|
||||
totalAmount: 15000 // $150.00 total
|
||||
}
|
||||
],
|
||||
subtotal: 65000, // $650.00
|
||||
taxAmount: 5200, // $52.00 (8% tax)
|
||||
amount: 70200, // $702.00 total
|
||||
status: 'open',
|
||||
dueDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(), // 30 days from now
|
||||
notes: 'Payment terms: Net 30 days. This is sample data for development.',
|
||||
metadata: {
|
||||
project: 'website-redesign',
|
||||
billable_hours: 12,
|
||||
sample: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
payload.logger.info(`Created sample invoice: ${invoice.number}`)
|
||||
|
||||
// Create a sample payment using test provider
|
||||
const payment = await payload.create({
|
||||
collection: 'payments',
|
||||
data: {
|
||||
provider: 'test',
|
||||
providerId: `test_pay_sample_${Date.now()}`,
|
||||
status: 'succeeded',
|
||||
amount: 70200, // $702.00
|
||||
currency: 'USD',
|
||||
description: `Sample payment for invoice ${invoice.number}`,
|
||||
customer: customer.id,
|
||||
invoice: invoice.id,
|
||||
metadata: {
|
||||
invoice_number: invoice.number,
|
||||
payment_method: 'test_card',
|
||||
sample: true
|
||||
},
|
||||
providerData: {
|
||||
testMode: true,
|
||||
simulatedPayment: true,
|
||||
autoCompleted: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
payload.logger.info(`Created sample payment: ${payment.id}`)
|
||||
|
||||
// Update invoice status to paid
|
||||
await payload.update({
|
||||
collection: 'invoices',
|
||||
id: invoice.id,
|
||||
data: {
|
||||
status: 'paid',
|
||||
payment: payment.id,
|
||||
paidAt: new Date().toISOString()
|
||||
}
|
||||
})
|
||||
|
||||
payload.logger.info('Billing sample data seeded successfully!')
|
||||
|
||||
} catch (error) {
|
||||
payload.logger.error('Error seeding billing data:', error)
|
||||
}
|
||||
}
|
||||
35
dev/tsconfig.json
Normal file
35
dev/tsconfig.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"exclude": [],
|
||||
"include": [
|
||||
"**/*.js",
|
||||
"**/*.jsx",
|
||||
"**/*.mjs",
|
||||
"**/*.cjs",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"../src/**/*.ts",
|
||||
"../src/**/*.tsx",
|
||||
"next.config.mjs",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@payload-config": [
|
||||
"./payload.config.ts"
|
||||
],
|
||||
"temp-plugin": [
|
||||
"../src/index.ts"
|
||||
],
|
||||
"temp-plugin/client": [
|
||||
"../src/exports/client.ts"
|
||||
],
|
||||
"temp-plugin/rsc": [
|
||||
"../src/exports/rsc.ts"
|
||||
]
|
||||
},
|
||||
"noEmit": true,
|
||||
"emitDeclarationOnly": false,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user