chore: Remove unused billing-related collections, types, and utility modules

- Drop `customers` collection and associated types (`types/index.ts`, `payload.ts`)
- Remove generated `payload-types.ts` file
- Clean up unused exports and dependencies across modules
- Streamline codebase by eliminating redundant billing logic
This commit is contained in:
2025-09-15 23:14:25 +02:00
parent 28e9e8d208
commit f17b4c064e
11 changed files with 338 additions and 1378 deletions

View File

@@ -1,656 +0,0 @@
/* 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: number;
};
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: number;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "media".
*/
export interface Media {
id: number;
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: number;
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?: (number | null) | Customer;
invoice?: (number | 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?: (number | Refund)[] | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "customers".
*/
export interface Customer {
id: number;
/**
* 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?: (number | Payment)[] | null;
/**
* Customer invoices
*/
invoices?: (number | Invoice)[] | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "invoices".
*/
export interface Invoice {
id: number;
/**
* Invoice number (e.g., INV-001)
*/
number: string;
customer: number | 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?: (number | 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: number;
/**
* The refund ID from the payment provider
*/
providerId: string;
payment: number | 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: number;
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: number;
document?:
| ({
relationTo: 'posts';
value: number | Post;
} | null)
| ({
relationTo: 'media';
value: number | Media;
} | null)
| ({
relationTo: 'payments';
value: number | Payment;
} | null)
| ({
relationTo: 'customers';
value: number | Customer;
} | null)
| ({
relationTo: 'invoices';
value: number | Invoice;
} | null)
| ({
relationTo: 'refunds';
value: number | Refund;
} | null)
| ({
relationTo: 'users';
value: number | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: number | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: number;
user: {
relationTo: 'users';
value: number | 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: number;
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 {}
}

View File

@@ -44,6 +44,7 @@ export default [
'perfectionist/sort-switch-case': 'off',
'perfectionist/sort-union-types': 'off',
'perfectionist/sort-variable-declarations': 'off',
'perfectionist/sort-intersection-types': 'off',
},
},
{

View File

@@ -1,149 +0,0 @@
import type { CollectionConfig } from 'payload'
import type {
AccessArgs,
CollectionAfterChangeHook,
CollectionBeforeChangeHook,
CustomerData,
CustomerDocument
} from '../types/payload'
export function createCustomersCollection(slug: string = 'customers'): CollectionConfig {
return {
slug,
access: {
create: ({ req: { user } }: AccessArgs) => !!user,
delete: ({ req: { user } }: AccessArgs) => !!user,
read: ({ req: { user } }: AccessArgs) => !!user,
update: ({ req: { user } }: AccessArgs) => !!user,
},
admin: {
defaultColumns: ['email', 'name', 'createdAt'],
group: 'Billing',
useAsTitle: 'email',
},
fields: [
{
name: 'email',
type: 'email',
admin: {
description: 'Customer email address',
},
index: true,
unique: true,
},
{
name: 'name',
type: 'text',
admin: {
description: 'Customer full name',
},
},
{
name: 'phone',
type: 'text',
admin: {
description: 'Customer phone number',
},
},
{
name: 'address',
type: 'group',
fields: [
{
name: 'line1',
type: 'text',
label: 'Address Line 1',
},
{
name: 'line2',
type: 'text',
label: 'Address Line 2',
},
{
name: 'city',
type: 'text',
label: 'City',
},
{
name: 'state',
type: 'text',
label: 'State/Province',
},
{
name: 'postalCode',
type: 'text',
label: 'Postal Code',
},
{
name: 'country',
type: 'text',
admin: {
description: 'ISO 3166-1 alpha-2 country code',
},
label: 'Country',
maxLength: 2,
},
],
},
{
name: 'providerIds',
type: 'json',
admin: {
description: 'Customer IDs from payment providers',
readOnly: true,
},
},
{
name: 'metadata',
type: 'json',
admin: {
description: 'Additional customer metadata',
},
},
{
name: 'payments',
type: 'relationship',
admin: {
description: 'Customer payments',
readOnly: true,
},
hasMany: true,
relationTo: 'payments',
},
{
name: 'invoices',
type: 'relationship',
admin: {
description: 'Customer invoices',
readOnly: true,
},
hasMany: true,
relationTo: 'invoices',
},
],
hooks: {
afterChange: [
({ doc, operation, req }: CollectionAfterChangeHook<CustomerDocument>) => {
if (operation === 'create') {
req.payload.logger.info(`Customer created: ${doc.id} (${doc.email})`)
}
},
],
beforeChange: [
({ data, operation }: CollectionBeforeChangeHook<CustomerData>) => {
if (operation === 'create' || operation === 'update') {
// Normalize country code
if (data.address?.country) {
data.address.country = data.address.country.toUpperCase()
if (!/^[A-Z]{2}$/.test(data.address.country)) {
throw new Error('Country must be a 2-letter ISO code')
}
}
}
},
],
},
timestamps: true,
}
}

View File

@@ -1,15 +1,12 @@
import type { CollectionConfig } from 'payload'
import type {
import {
AccessArgs,
CollectionAfterChangeHook,
CollectionBeforeChangeHook,
CollectionBeforeValidateHook,
InvoiceData,
InvoiceDocument,
InvoiceItemData
} from '../types/payload'
import type { CustomerInfoExtractor } from '../types'
CollectionConfig,
} from 'payload'
import { CustomerInfoExtractor } from '@/plugin/config'
import { Invoice } from '@/plugin/types'
export function createInvoicesCollection(
slug: string = 'invoices',
@@ -281,7 +278,7 @@ export function createInvoicesCollection(
name: 'paidAt',
type: 'date',
admin: {
condition: (data: InvoiceData) => data.status === 'paid',
condition: (data) => data.status === 'paid',
readOnly: true,
},
},
@@ -289,7 +286,7 @@ export function createInvoicesCollection(
name: 'payment',
type: 'relationship',
admin: {
condition: (data: InvoiceData) => data.status === 'paid',
condition: (data) => data.status === 'paid',
position: 'sidebar',
},
relationTo: 'payments',
@@ -311,14 +308,14 @@ export function createInvoicesCollection(
],
hooks: {
afterChange: [
({ doc, operation, req }: CollectionAfterChangeHook<InvoiceDocument>) => {
({ doc, operation, req }) => {
if (operation === 'create') {
req.payload.logger.info(`Invoice created: ${doc.number}`)
}
},
],
] satisfies CollectionAfterChangeHook<Invoice>[],
beforeChange: [
async ({ data, operation, req, originalDoc }: CollectionBeforeChangeHook<InvoiceData>) => {
async ({ data, operation, req, originalDoc }) => {
// Sync customer info from relationship if extractor is provided
if (customerCollectionSlug && customerInfoExtractor && data.customer) {
// Check if customer changed or this is a new invoice
@@ -329,7 +326,7 @@ export function createInvoicesCollection(
try {
// Fetch the customer data
const customer = await req.payload.findByID({
collection: customerCollectionSlug,
collection: customerCollectionSlug as any,
id: data.customer,
})
@@ -383,9 +380,9 @@ export function createInvoicesCollection(
data.paidAt = new Date().toISOString()
}
},
],
] satisfies CollectionBeforeChangeHook<Invoice>[],
beforeValidate: [
({ data }: CollectionBeforeValidateHook<InvoiceData>) => {
({ data }) => {
if (!data) return
// If using extractor, customer relationship is required
@@ -406,14 +403,14 @@ export function createInvoicesCollection(
if (data && data.items && Array.isArray(data.items)) {
// Calculate totals for each line item
data.items = data.items.map((item: InvoiceItemData) => ({
data.items = data.items.map((item) => ({
...item,
totalAmount: (item.quantity || 0) * (item.unitAmount || 0),
}))
// Calculate subtotal
data.subtotal = data.items.reduce(
(sum: number, item: InvoiceItemData) => sum + (item.totalAmount || 0),
(sum: number, item) => sum + (item.totalAmount || 0),
0
)
@@ -421,8 +418,8 @@ export function createInvoicesCollection(
data.amount = (data.subtotal || 0) + (data.taxAmount || 0)
}
},
],
] satisfies CollectionBeforeValidateHook<Invoice>[],
},
timestamps: true,
}
}
}

View File

@@ -1,12 +1,5 @@
import type { CollectionConfig } from 'payload'
import type {
AccessArgs,
CollectionAfterChangeHook,
CollectionBeforeChangeHook,
PaymentData,
PaymentDocument
} from '../types/payload'
import { AccessArgs, CollectionAfterChangeHook, CollectionBeforeChangeHook, CollectionConfig } from 'payload'
import { Payment } from '@/plugin/types'
export function createPaymentsCollection(slug: string = 'payments'): CollectionConfig {
return {
@@ -131,21 +124,14 @@ export function createPaymentsCollection(slug: string = 'payments'): CollectionC
},
],
hooks: {
afterChange: [
({ doc, operation, req }: CollectionAfterChangeHook<PaymentDocument>) => {
if (operation === 'create') {
req.payload.logger.info(`Payment created: ${doc.id} (${doc.provider})`)
}
},
],
beforeChange: [
({ data, operation }: CollectionBeforeChangeHook<PaymentData>) => {
({ data, operation }) => {
if (operation === 'create') {
// Validate amount format
if (data.amount && !Number.isInteger(data.amount)) {
throw new Error('Amount must be an integer (in cents)')
}
// Validate currency format
if (data.currency) {
data.currency = data.currency.toUpperCase()
@@ -155,8 +141,8 @@ export function createPaymentsCollection(slug: string = 'payments'): CollectionC
}
}
},
],
] satisfies CollectionBeforeChangeHook<Payment>[],
},
timestamps: true,
}
}
}

View File

@@ -1,98 +1,3 @@
import type { Config } from 'payload'
import type { BillingPluginConfig, CustomerInfoExtractor } from './types'
import { createCustomersCollection } from './collections/customers'
import { createInvoicesCollection } from './collections/invoices'
import { createPaymentsCollection } from './collections/payments'
import { createRefundsCollection } from './collections/refunds'
export * from './types'
// Default customer info extractor for the built-in customer collection
export const defaultCustomerInfoExtractor: CustomerInfoExtractor = (customer) => {
return {
name: customer.name || '',
email: customer.email || '',
phone: customer.phone,
company: customer.company,
taxId: customer.taxId,
billingAddress: customer.address ? {
line1: customer.address.line1 || '',
line2: customer.address.line2,
city: customer.address.city || '',
state: customer.address.state,
postalCode: customer.address.postalCode || '',
country: customer.address.country || '',
} : undefined,
}
}
export const billingPlugin = (pluginConfig: BillingPluginConfig = {}) => (config: Config): Config => {
if (pluginConfig.disabled) {
return config
}
// Initialize collections
if (!config.collections) {
config.collections = []
}
const customerSlug = pluginConfig.collections?.customers || 'customers'
config.collections.push(
createPaymentsCollection(pluginConfig.collections?.payments || 'payments'),
createCustomersCollection(customerSlug),
createInvoicesCollection(
pluginConfig.collections?.invoices || 'invoices',
pluginConfig.collections?.customerRelation !== false ? customerSlug : undefined,
pluginConfig.customerInfoExtractor
),
createRefundsCollection(pluginConfig.collections?.refunds || 'refunds'),
)
// Initialize endpoints
if (!config.endpoints) {
config.endpoints = []
}
config.endpoints?.push(
// Webhook endpoints
{
handler: (_req) => {
try {
const provider = null
if (!provider) {
return Response.json({ error: 'Provider not found' }, { status: 404 })
}
// TODO: Process webhook event and update database
return Response.json({ received: true })
} catch (error) {
// TODO: Use proper logger instead of console
// eslint-disable-next-line no-console
console.error('[BILLING] Webhook error:', error)
return Response.json({ error: 'Webhook processing failed' }, { status: 400 })
}
},
method: 'post',
path: '/billing/webhooks/:provider'
},
)
// Initialize providers and onInit hook
const incomingOnInit = config.onInit
config.onInit = async (payload) => {
// Execute any existing onInit functions first
if (incomingOnInit) {
await incomingOnInit(payload)
}
}
return config
}
export default billingPlugin

76
src/plugin/config.ts Normal file
View File

@@ -0,0 +1,76 @@
export const defaults = {
paymentsCollection: 'payments'
}
// Provider configurations
export interface StripeConfig {
apiVersion?: string
publishableKey: string
secretKey: string
webhookEndpointSecret: string
}
export interface MollieConfig {
apiKey: string
testMode?: boolean
webhookUrl: string
}
export interface TestProviderConfig {
autoComplete?: boolean
defaultDelay?: number
enabled: boolean
failureRate?: number
simulateFailures?: boolean
}
// Customer info extractor callback type
export interface CustomerInfoExtractor {
(customer: any): {
name: string
email: string
phone?: string
company?: string
taxId?: string
billingAddress?: {
line1: string
line2?: string
city: string
state?: string
postalCode: string
country: string
}
}
}
// Plugin configuration
export interface BillingPluginConfig {
admin?: {
customComponents?: boolean
dashboard?: boolean
}
collections?: {
customerRelation?: boolean | string // false to disable, string for custom collection slug
customers?: string
invoices?: string
payments?: string
refunds?: string
}
customerInfoExtractor?: CustomerInfoExtractor // Callback to extract customer info from relationship
disabled?: boolean
providers?: {
mollie?: MollieConfig
stripe?: StripeConfig
test?: TestProviderConfig
}
webhooks?: {
basePath?: string
cors?: boolean
}
}
// Plugin type
export interface BillingPluginOptions extends BillingPluginConfig {
disabled?: boolean
}

70
src/plugin/index.ts Normal file
View File

@@ -0,0 +1,70 @@
import type { Config } from 'payload'
import { createInvoicesCollection, createPaymentsCollection, createRefundsCollection } from '@/collections'
import { BillingPluginConfig } from '@/plugin/config'
export const billingPlugin = (pluginConfig: BillingPluginConfig = {}) => (config: Config): Config => {
if (pluginConfig.disabled) {
return config
}
// Initialize collections
if (!config.collections) {
config.collections = []
}
const customerSlug = pluginConfig.collections?.customers || 'customers'
config.collections.push(
createPaymentsCollection(pluginConfig.collections?.payments || 'payments'),
createInvoicesCollection(
pluginConfig.collections?.invoices || 'invoices',
pluginConfig.collections?.customerRelation !== false ? customerSlug : undefined,
pluginConfig.customerInfoExtractor
),
createRefundsCollection(pluginConfig.collections?.refunds || 'refunds'),
)
// Initialize endpoints
if (!config.endpoints) {
config.endpoints = []
}
config.endpoints?.push(
// Webhook endpoints
{
handler: (_req) => {
try {
const provider = null
if (!provider) {
return Response.json({ error: 'Provider not found' }, { status: 404 })
}
// TODO: Process webhook event and update database
return Response.json({ received: true })
} catch (error) {
// TODO: Use proper logger instead of console
// eslint-disable-next-line no-console
console.error('[BILLING] Webhook error:', error)
return Response.json({ error: 'Webhook processing failed' }, { status: 400 })
}
},
method: 'post',
path: '/billing/webhooks/:provider'
},
)
// Initialize providers and onInit hook
const incomingOnInit = config.onInit
config.onInit = async (payload) => {
// Execute any existing onInit functions first
if (incomingOnInit) {
await incomingOnInit(payload)
}
}
return config
}
export default billingPlugin

168
src/plugin/types.ts Normal file
View File

@@ -0,0 +1,168 @@
import { Customer, Refund } from '../../dev/payload-types'
export type Id = string | number
export interface Payment {
id: Id;
provider: 'stripe' | 'mollie' | 'test';
/**
* The payment ID from the payment provider
*/
providerId: Id;
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?: (Id | null) | Customer;
invoice?: (Id | 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?: (number | Refund)[] | null;
updatedAt: string;
createdAt: string;
}
export interface Invoice {
id: Id;
/**
* Invoice number (e.g., INV-001)
*/
number: string;
/**
* Link to customer record (optional)
*/
customer?: (Id | null) | Customer;
/**
* Customer billing information (auto-populated from customer relationship)
*/
customerInfo?: {
/**
* Customer name
*/
name?: string | null;
/**
* Customer email address
*/
email?: string | null;
/**
* Customer phone number
*/
phone?: string | null;
/**
* Company name (optional)
*/
company?: string | null;
/**
* Tax ID or VAT number
*/
taxId?: string | null;
};
/**
* Billing address (auto-populated from customer relationship)
*/
billingAddress?: {
/**
* Address line 1
*/
line1?: string | null;
/**
* Address line 2
*/
line2?: string | null;
city?: string | null;
/**
* State or province
*/
state?: string | null;
/**
* Postal or ZIP code
*/
postalCode?: string | null;
/**
* Country code (e.g., US, GB)
*/
country?: string | null;
};
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?: Id | 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?: (number | null) | Payment;
/**
* Internal notes
*/
notes?: string | null;
/**
* Additional invoice metadata
*/
metadata?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}

View File

@@ -1,260 +0,0 @@
import type { Config } from 'payload'
// Base payment provider interface
export interface PaymentProvider {
cancelPayment(id: string): Promise<Payment>
createPayment(options: CreatePaymentOptions): Promise<Payment>
handleWebhook(request: Request, signature?: string): Promise<WebhookEvent>
name: string
refundPayment(id: string, amount?: number): Promise<Refund>
retrievePayment(id: string): Promise<Payment>
}
// Payment types
export interface CreatePaymentOptions {
amount: number
cancelUrl?: string
currency: string
customer?: string
description?: string
metadata?: Record<string, unknown>
returnUrl?: string
}
export interface Payment {
amount: number
createdAt: string
currency: string
customer?: string
description?: string
id: string
metadata?: Record<string, unknown>
provider: string
providerData?: Record<string, unknown>
status: PaymentStatus
updatedAt: string
}
export interface Refund {
amount: number
createdAt: string
currency: string
id: string
paymentId: string
providerData?: Record<string, unknown>
reason?: string
status: RefundStatus
}
export interface WebhookEvent {
data: Record<string, unknown>
id: string
provider: string
type: string
verified: boolean
}
// Status enums
export type PaymentStatus =
| 'canceled'
| 'failed'
| 'partially_refunded'
| 'pending'
| 'processing'
| 'refunded'
| 'succeeded'
export type RefundStatus =
| 'canceled'
| 'failed'
| 'pending'
| 'processing'
| 'succeeded'
// Provider configurations
export interface StripeConfig {
apiVersion?: string
publishableKey: string
secretKey: string
webhookEndpointSecret: string
}
export interface MollieConfig {
apiKey: string
testMode?: boolean
webhookUrl: string
}
export interface TestProviderConfig {
autoComplete?: boolean
defaultDelay?: number
enabled: boolean
failureRate?: number
simulateFailures?: boolean
}
// Customer info extractor callback type
export interface CustomerInfoExtractor {
(customer: any): {
name: string
email: string
phone?: string
company?: string
taxId?: string
billingAddress?: {
line1: string
line2?: string
city: string
state?: string
postalCode: string
country: string
}
}
}
// Plugin configuration
export interface BillingPluginConfig {
admin?: {
customComponents?: boolean
dashboard?: boolean
}
collections?: {
customerRelation?: boolean | string // false to disable, string for custom collection slug
customers?: string
invoices?: string
payments?: string
refunds?: string
}
customerInfoExtractor?: CustomerInfoExtractor // Callback to extract customer info from relationship
disabled?: boolean
providers?: {
mollie?: MollieConfig
stripe?: StripeConfig
test?: TestProviderConfig
}
webhooks?: {
basePath?: string
cors?: boolean
}
}
// Collection types
export interface PaymentRecord {
amount: number
createdAt: string
currency: string
customer?: string
description?: string
id: string
metadata?: Record<string, unknown>
provider: string
providerData?: Record<string, unknown>
providerId: string
status: PaymentStatus
updatedAt: string
}
export interface CustomerRecord {
address?: {
city?: string
country?: string
line1?: string
line2?: string
postalCode?: string
state?: string
}
createdAt: string
email?: string
id: string
metadata?: Record<string, unknown>
name?: string
phone?: string
providerIds?: Record<string, string>
updatedAt: string
}
export interface InvoiceRecord {
amount: number
billingAddress?: {
city: string
country: string
line1: string
line2?: string
postalCode: string
state?: string
}
createdAt: string
currency: string
customer?: string // Optional relationship to customer collection
customerInfo?: {
company?: string
email: string
name: string
phone?: string
taxId?: string
}
dueDate?: string
id: string
items: InvoiceItem[]
metadata?: Record<string, unknown>
number: string
paidAt?: string
status: InvoiceStatus
updatedAt: string
}
export interface InvoiceItem {
description: string
quantity: number
totalAmount: number
unitAmount: number
}
export type InvoiceStatus =
| 'draft'
| 'open'
| 'paid'
| 'uncollectible'
| 'void'
// Plugin type
export interface BillingPluginOptions extends BillingPluginConfig {
disabled?: boolean
}
// Error types
export class BillingError extends Error {
constructor(
message: string,
public code: string,
public provider?: string,
public details?: Record<string, unknown>
) {
super(message)
this.name = 'BillingError'
}
}
export class PaymentProviderError extends BillingError {
constructor(
message: string,
provider: string,
code?: string,
details?: Record<string, unknown>
) {
super(message, code || 'PROVIDER_ERROR', provider, details)
this.name = 'PaymentProviderError'
}
}
export class WebhookError extends BillingError {
constructor(
message: string,
provider: string,
code?: string,
details?: Record<string, unknown>
) {
super(message, code || 'WEBHOOK_ERROR', provider, details)
this.name = 'WebhookError'
}
}

View File

@@ -1,178 +0,0 @@
/**
* PayloadCMS type definitions for hooks and handlers
*/
import type { PayloadRequest, User } from 'payload'
// Collection hook types
export interface CollectionBeforeChangeHook<T = Record<string, unknown>> {
data: T
operation: 'create' | 'delete' | 'update'
originalDoc?: T
req: PayloadRequest
}
export interface CollectionAfterChangeHook<T = Record<string, unknown>> {
doc: T
operation: 'create' | 'delete' | 'update'
previousDoc?: T
req: PayloadRequest
}
export interface CollectionBeforeValidateHook<T = Record<string, unknown>> {
data?: T
operation: 'create' | 'update'
originalDoc?: T
req: PayloadRequest
}
// Access control types
export interface AccessArgs<T = unknown> {
data?: T
id?: number | string
req: {
payload: unknown
user: null | User
}
}
// Invoice item type for hooks
export interface InvoiceItemData {
description: string
quantity: number
totalAmount?: number
unitAmount: number
}
// Invoice data type for hooks
export interface InvoiceData {
amount?: number
billingAddress?: {
city?: string
country?: string
line1?: string
line2?: string
postalCode?: string
state?: string
}
currency?: string
customer?: string // Optional relationship
customerInfo?: {
company?: string
email?: string
name?: string
phone?: string
taxId?: string
}
dueDate?: string
items?: InvoiceItemData[]
metadata?: Record<string, unknown>
notes?: string
number?: string
paidAt?: string
payment?: string
status?: string
subtotal?: number
taxAmount?: number
}
// Payment data type for hooks
export interface PaymentData {
amount?: number
currency?: string
customer?: string
description?: string
invoice?: string
metadata?: Record<string, unknown>
provider?: string
providerData?: Record<string, unknown>
providerId?: string | number
status?: string
}
// Customer data type for hooks
export interface CustomerData {
address?: {
city?: string
country?: string
line1?: string
line2?: string
postalCode?: string
state?: string
}
email?: string
metadata?: Record<string, unknown>
name?: string
phone?: string
providerIds?: Record<string, string | number>
}
// Refund data type for hooks
export interface RefundData {
amount?: number
currency?: string
description?: string
metadata?: Record<string, unknown>
payment?: { id: string | number } | string
providerData?: Record<string, unknown>
providerId?: string | number
reason?: string
status?: string
}
// Document types with required fields after creation
export interface PaymentDocument extends PaymentData {
amount: number
createdAt: string
currency: string
id: string | number
provider: string
providerId: string | number
status: string
updatedAt: string
}
export interface CustomerDocument extends CustomerData {
createdAt: string
id: string | number
updatedAt: string
}
export interface InvoiceDocument extends InvoiceData {
amount: number
createdAt: string
currency: string
customer?: string // Optional relationship
customerInfo?: { // Optional when customer relationship exists
company?: string
email: string
name: string
phone?: string
taxId?: string
}
billingAddress?: { // Optional when customer relationship exists
city: string
country: string
line1: string
line2?: string
postalCode: string
state?: string
}
id: string | number
items: InvoiceItemData[]
number: string
status: string
updatedAt: string
}
export interface RefundDocument extends RefundData {
amount: number
createdAt: string
currency: string
id: string | number
payment: { id: string } | string
providerId: string
refunds?: string[]
status: string
updatedAt: string
}