From f17b4c064e181b998f42ddf1bc25c34190a1479e Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Mon, 15 Sep 2025 23:14:25 +0200 Subject: [PATCH] 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 --- dev/payload-types.ts | 656 ----------------------------------- eslint.config.js | 1 + src/collections/customers.ts | 149 -------- src/collections/invoices.ts | 37 +- src/collections/payments.ts | 26 +- src/index.ts | 95 ----- src/plugin/config.ts | 76 ++++ src/plugin/index.ts | 70 ++++ src/plugin/types.ts | 168 +++++++++ src/types/index.ts | 260 -------------- src/types/payload.ts | 178 ---------- 11 files changed, 338 insertions(+), 1378 deletions(-) delete mode 100644 dev/payload-types.ts delete mode 100644 src/collections/customers.ts create mode 100644 src/plugin/config.ts create mode 100644 src/plugin/index.ts create mode 100644 src/plugin/types.ts delete mode 100644 src/types/index.ts delete mode 100644 src/types/payload.ts diff --git a/dev/payload-types.ts b/dev/payload-types.ts deleted file mode 100644 index 3c141b2..0000000 --- a/dev/payload-types.ts +++ /dev/null @@ -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 | PostsSelect; - media: MediaSelect | MediaSelect; - payments: PaymentsSelect | PaymentsSelect; - customers: CustomersSelect | CustomersSelect; - invoices: InvoicesSelect | InvoicesSelect; - refunds: RefundsSelect | RefundsSelect; - users: UsersSelect | UsersSelect; - 'payload-locked-documents': PayloadLockedDocumentsSelect | PayloadLockedDocumentsSelect; - 'payload-preferences': PayloadPreferencesSelect | PayloadPreferencesSelect; - 'payload-migrations': PayloadMigrationsSelect | PayloadMigrationsSelect; - }; - 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 { - updatedAt?: T; - createdAt?: T; -} -/** - * This interface was referenced by `Config`'s JSON-Schema - * via the `definition` "media_select". - */ -export interface MediaSelect { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 { - 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 {} -} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index aacea9f..b831856 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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', }, }, { diff --git a/src/collections/customers.ts b/src/collections/customers.ts deleted file mode 100644 index 6bb55f2..0000000 --- a/src/collections/customers.ts +++ /dev/null @@ -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) => { - if (operation === 'create') { - req.payload.logger.info(`Customer created: ${doc.id} (${doc.email})`) - } - }, - ], - beforeChange: [ - ({ data, operation }: CollectionBeforeChangeHook) => { - 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, - } -} \ No newline at end of file diff --git a/src/collections/invoices.ts b/src/collections/invoices.ts index fdaf804..647c09d 100644 --- a/src/collections/invoices.ts +++ b/src/collections/invoices.ts @@ -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) => { + ({ doc, operation, req }) => { if (operation === 'create') { req.payload.logger.info(`Invoice created: ${doc.number}`) } }, - ], + ] satisfies CollectionAfterChangeHook[], beforeChange: [ - async ({ data, operation, req, originalDoc }: CollectionBeforeChangeHook) => { + 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[], beforeValidate: [ - ({ data }: CollectionBeforeValidateHook) => { + ({ 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[], }, timestamps: true, } -} \ No newline at end of file +} diff --git a/src/collections/payments.ts b/src/collections/payments.ts index 7aff5cc..4d9d8bf 100644 --- a/src/collections/payments.ts +++ b/src/collections/payments.ts @@ -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) => { - if (operation === 'create') { - req.payload.logger.info(`Payment created: ${doc.id} (${doc.provider})`) - } - }, - ], beforeChange: [ - ({ data, operation }: CollectionBeforeChangeHook) => { + ({ 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[], }, timestamps: true, } -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 303a57d..509a567 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 diff --git a/src/plugin/config.ts b/src/plugin/config.ts new file mode 100644 index 0000000..ace743d --- /dev/null +++ b/src/plugin/config.ts @@ -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 +} diff --git a/src/plugin/index.ts b/src/plugin/index.ts new file mode 100644 index 0000000..e644aec --- /dev/null +++ b/src/plugin/index.ts @@ -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 diff --git a/src/plugin/types.ts b/src/plugin/types.ts new file mode 100644 index 0000000..d7ae2ee --- /dev/null +++ b/src/plugin/types.ts @@ -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; +} diff --git a/src/types/index.ts b/src/types/index.ts deleted file mode 100644 index 55f264d..0000000 --- a/src/types/index.ts +++ /dev/null @@ -1,260 +0,0 @@ -import type { Config } from 'payload' - -// Base payment provider interface -export interface PaymentProvider { - cancelPayment(id: string): Promise - createPayment(options: CreatePaymentOptions): Promise - handleWebhook(request: Request, signature?: string): Promise - name: string - refundPayment(id: string, amount?: number): Promise - retrievePayment(id: string): Promise -} - -// Payment types -export interface CreatePaymentOptions { - amount: number - cancelUrl?: string - currency: string - customer?: string - description?: string - metadata?: Record - returnUrl?: string -} - -export interface Payment { - amount: number - createdAt: string - currency: string - customer?: string - description?: string - id: string - metadata?: Record - provider: string - providerData?: Record - status: PaymentStatus - updatedAt: string -} - -export interface Refund { - amount: number - createdAt: string - currency: string - id: string - paymentId: string - providerData?: Record - reason?: string - status: RefundStatus -} - -export interface WebhookEvent { - data: Record - 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 - provider: string - providerData?: Record - 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 - name?: string - phone?: string - providerIds?: Record - 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 - 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 - ) { - super(message) - this.name = 'BillingError' - } -} - -export class PaymentProviderError extends BillingError { - constructor( - message: string, - provider: string, - code?: string, - details?: Record - ) { - super(message, code || 'PROVIDER_ERROR', provider, details) - this.name = 'PaymentProviderError' - } -} - -export class WebhookError extends BillingError { - constructor( - message: string, - provider: string, - code?: string, - details?: Record - ) { - super(message, code || 'WEBHOOK_ERROR', provider, details) - this.name = 'WebhookError' - } -} \ No newline at end of file diff --git a/src/types/payload.ts b/src/types/payload.ts deleted file mode 100644 index a4f2582..0000000 --- a/src/types/payload.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * PayloadCMS type definitions for hooks and handlers - */ - -import type { PayloadRequest, User } from 'payload' - -// Collection hook types -export interface CollectionBeforeChangeHook> { - data: T - operation: 'create' | 'delete' | 'update' - originalDoc?: T - req: PayloadRequest -} - -export interface CollectionAfterChangeHook> { - doc: T - operation: 'create' | 'delete' | 'update' - previousDoc?: T - req: PayloadRequest -} - -export interface CollectionBeforeValidateHook> { - data?: T - operation: 'create' | 'update' - originalDoc?: T - req: PayloadRequest -} - -// Access control types -export interface AccessArgs { - 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 - 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 - provider?: string - providerData?: Record - 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 - name?: string - phone?: string - providerIds?: Record -} - -// Refund data type for hooks -export interface RefundData { - amount?: number - currency?: string - description?: string - metadata?: Record - payment?: { id: string | number } | string - providerData?: Record - 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 -}