mirror of
https://github.com/xtr-dev/payload-billing.git
synced 2025-12-10 02:43:24 +00:00
feat: Add Mollie payment provider support
- Introduce `mollieProvider` for handling Mollie payments - Add configurable payment hooks for initialization and processing - Implement `initPayment` logic to create Mollie payments and update metadata - Include types for Mollie integration in payments and refunds - Update `package.json` to include `@mollie/api-client` dependency - Refactor existing payment-related types into modular files for better maintainability
This commit is contained in:
@@ -1,11 +1,13 @@
|
|||||||
import configPromise from '@payload-config'
|
import configPromise from '@payload-config'
|
||||||
import { getPayload } from 'payload'
|
import { getPayload } from 'payload'
|
||||||
|
import { useBillingPlugin } from '../../../src/plugin'
|
||||||
|
|
||||||
export const GET = async (request: Request) => {
|
export const GET = async (request: Request) => {
|
||||||
const payload = await getPayload({
|
const payload = await getPayload({
|
||||||
config: configPromise,
|
config: configPromise,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
return Response.json({
|
return Response.json({
|
||||||
message: 'This is an example of a custom route.',
|
message: 'This is an example of a custom route.',
|
||||||
})
|
})
|
||||||
|
|||||||
627
dev/payload-types.ts
Normal file
627
dev/payload-types.ts
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
/* 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;
|
||||||
|
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>;
|
||||||
|
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;
|
||||||
|
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` "invoices".
|
||||||
|
*/
|
||||||
|
export interface Invoice {
|
||||||
|
id: number;
|
||||||
|
/**
|
||||||
|
* Invoice number (e.g., INV-001)
|
||||||
|
*/
|
||||||
|
number: string;
|
||||||
|
/**
|
||||||
|
* Customer billing information
|
||||||
|
*/
|
||||||
|
customerInfo: {
|
||||||
|
/**
|
||||||
|
* Customer name
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
/**
|
||||||
|
* Customer email address
|
||||||
|
*/
|
||||||
|
email: string;
|
||||||
|
/**
|
||||||
|
* Customer phone number
|
||||||
|
*/
|
||||||
|
phone?: string | null;
|
||||||
|
/**
|
||||||
|
* Company name (optional)
|
||||||
|
*/
|
||||||
|
company?: string | null;
|
||||||
|
/**
|
||||||
|
* Tax ID or VAT number
|
||||||
|
*/
|
||||||
|
taxId?: string | null;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Billing address
|
||||||
|
*/
|
||||||
|
billingAddress: {
|
||||||
|
/**
|
||||||
|
* Address line 1
|
||||||
|
*/
|
||||||
|
line1: string;
|
||||||
|
/**
|
||||||
|
* Address line 2
|
||||||
|
*/
|
||||||
|
line2?: string | null;
|
||||||
|
city: string;
|
||||||
|
/**
|
||||||
|
* State or province
|
||||||
|
*/
|
||||||
|
state?: string | null;
|
||||||
|
/**
|
||||||
|
* Postal or ZIP code
|
||||||
|
*/
|
||||||
|
postalCode: string;
|
||||||
|
/**
|
||||||
|
* Country code (e.g., US, GB)
|
||||||
|
*/
|
||||||
|
country: string;
|
||||||
|
};
|
||||||
|
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: '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;
|
||||||
|
invoice?: T;
|
||||||
|
metadata?: T;
|
||||||
|
providerData?: T;
|
||||||
|
refunds?: 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;
|
||||||
|
customerInfo?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
name?: T;
|
||||||
|
email?: T;
|
||||||
|
phone?: T;
|
||||||
|
company?: T;
|
||||||
|
taxId?: T;
|
||||||
|
};
|
||||||
|
billingAddress?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
line1?: T;
|
||||||
|
line2?: T;
|
||||||
|
city?: T;
|
||||||
|
state?: T;
|
||||||
|
postalCode?: T;
|
||||||
|
country?: 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 {}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url'
|
|||||||
import { testEmailAdapter } from './helpers/testEmailAdapter'
|
import { testEmailAdapter } from './helpers/testEmailAdapter'
|
||||||
import { seed } from './seed'
|
import { seed } from './seed'
|
||||||
import billingPlugin from '../src/plugin'
|
import billingPlugin from '../src/plugin'
|
||||||
|
import { mollieProvider } from '../src/providers'
|
||||||
|
|
||||||
const filename = fileURLToPath(import.meta.url)
|
const filename = fileURLToPath(import.meta.url)
|
||||||
const dirname = path.dirname(filename)
|
const dirname = path.dirname(filename)
|
||||||
@@ -48,38 +49,16 @@ const buildConfigWithSQLite = () => {
|
|||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
billingPlugin({
|
billingPlugin({
|
||||||
providers: {
|
providers: [
|
||||||
test: {
|
mollieProvider({
|
||||||
enabled: true,
|
apiKey: process.env.MOLLIE_KEY!
|
||||||
autoComplete: true,
|
})
|
||||||
}
|
],
|
||||||
},
|
|
||||||
collections: {
|
collections: {
|
||||||
payments: 'payments',
|
payments: 'payments',
|
||||||
invoices: 'invoices',
|
invoices: 'invoices',
|
||||||
refunds: 'refunds',
|
refunds: 'refunds',
|
||||||
},
|
},
|
||||||
// // Customer relationship configuration
|
|
||||||
// customerRelationSlug: 'customers', // Use 'customers' collection for relationship
|
|
||||||
// // customerRelationSlug: false, // Or set to false to disable customer relationship
|
|
||||||
// // customerRelationSlug: 'clients', // Or use a custom collection slug
|
|
||||||
//
|
|
||||||
// // Provide an extractor for your customer collection structure:
|
|
||||||
// customerInfoExtractor: (customer) => ({
|
|
||||||
// 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,
|
|
||||||
// })
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
secret: process.env.PAYLOAD_SECRET || 'test-secret_key',
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ export const seed = async (payload: Payload) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Seed billing sample data
|
// Seed billing sample data
|
||||||
await seedBillingData(payload)
|
// await seedBillingData(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function seedBillingData(payload: Payload): Promise<void> {
|
// async function seedBillingData(payload: Payload): Promise<void> {
|
||||||
payload.logger.info('Seeding billing sample data...')
|
// payload.logger.info('Seeding billing sample data...')
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/cli": "^2.27.1",
|
"@changesets/cli": "^2.27.1",
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@mollie/api-client": "^3.7.0",
|
||||||
"@payloadcms/db-mongodb": "3.37.0",
|
"@payloadcms/db-mongodb": "3.37.0",
|
||||||
"@payloadcms/db-postgres": "3.37.0",
|
"@payloadcms/db-postgres": "3.37.0",
|
||||||
"@payloadcms/db-sqlite": "3.37.0",
|
"@payloadcms/db-sqlite": "3.37.0",
|
||||||
@@ -104,11 +105,11 @@
|
|||||||
"vitest": "^3.1.2"
|
"vitest": "^3.1.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
"@mollie/api-client": "^3.7.0",
|
||||||
"payload": "^3.37.0"
|
"payload": "^3.37.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"stripe": "^14.15.0",
|
"stripe": "^14.15.0",
|
||||||
"@mollie/api-client": "^3.7.0",
|
|
||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
12
pnpm-lock.yaml
generated
12
pnpm-lock.yaml
generated
@@ -8,9 +8,6 @@ importers:
|
|||||||
|
|
||||||
.:
|
.:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mollie/api-client':
|
|
||||||
specifier: ^3.7.0
|
|
||||||
version: 3.7.0
|
|
||||||
stripe:
|
stripe:
|
||||||
specifier: ^14.15.0
|
specifier: ^14.15.0
|
||||||
version: 14.25.0
|
version: 14.25.0
|
||||||
@@ -24,6 +21,9 @@ importers:
|
|||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3.2.0
|
specifier: ^3.2.0
|
||||||
version: 3.3.1
|
version: 3.3.1
|
||||||
|
'@mollie/api-client':
|
||||||
|
specifier: ^3.7.0
|
||||||
|
version: 3.7.0
|
||||||
'@payloadcms/db-mongodb':
|
'@payloadcms/db-mongodb':
|
||||||
specifier: 3.37.0
|
specifier: 3.37.0
|
||||||
version: 3.37.0(payload@3.37.0(graphql@16.11.0)(typescript@5.7.3))
|
version: 3.37.0(payload@3.37.0(graphql@16.11.0)(typescript@5.7.3))
|
||||||
@@ -8875,7 +8875,7 @@ snapshots:
|
|||||||
eslint: 9.35.0
|
eslint: 9.35.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.4.2(eslint@9.35.0)(typescript@5.7.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint@9.35.0))(eslint@9.35.0)
|
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import-x@4.4.2(eslint@9.35.0)(typescript@5.7.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint@9.35.0))(eslint@9.35.0)
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.4.2(eslint@9.35.0)(typescript@5.7.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint@9.35.0))(eslint@9.35.0))(eslint@9.35.0)
|
||||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0)
|
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.35.0)
|
||||||
eslint-plugin-react: 7.37.5(eslint@9.35.0)
|
eslint-plugin-react: 7.37.5(eslint@9.35.0)
|
||||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.35.0)
|
eslint-plugin-react-hooks: 5.2.0(eslint@9.35.0)
|
||||||
@@ -8909,7 +8909,7 @@ snapshots:
|
|||||||
tinyglobby: 0.2.15
|
tinyglobby: 0.2.15
|
||||||
unrs-resolver: 1.11.1
|
unrs-resolver: 1.11.1
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0)
|
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.4.2(eslint@9.35.0)(typescript@5.7.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint@9.35.0))(eslint@9.35.0))(eslint@9.35.0)
|
||||||
eslint-plugin-import-x: 4.4.2(eslint@9.35.0)(typescript@5.7.3)
|
eslint-plugin-import-x: 4.4.2(eslint@9.35.0)(typescript@5.7.3)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -8960,7 +8960,7 @@ snapshots:
|
|||||||
- typescript
|
- typescript
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.35.0):
|
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import-x@4.4.2(eslint@9.35.0)(typescript@5.7.3))(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.43.0(eslint@9.35.0)(typescript@5.7.3))(eslint@9.35.0))(eslint@9.35.0))(eslint@9.35.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rtsao/scc': 1.1.0
|
'@rtsao/scc': 1.1.0
|
||||||
array-includes: 3.1.9
|
array-includes: 3.1.9
|
||||||
|
|||||||
11
src/collections/hooks.ts
Normal file
11
src/collections/hooks.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { Payment } from '@/plugin/types'
|
||||||
|
import type { Payload } from 'payload'
|
||||||
|
import { useBillingPlugin } from '@/plugin'
|
||||||
|
|
||||||
|
export const initProviderPayment = (payload: Payload, payment: Partial<Payment>) => {
|
||||||
|
const billing = useBillingPlugin(payload)
|
||||||
|
if (!payment.provider || !billing.providerConfig[payment.provider]) {
|
||||||
|
throw new Error(`Provider ${payment.provider} not found.`)
|
||||||
|
}
|
||||||
|
return billing.providerConfig[payment.provider].initPayment(payload, payment)
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ import {
|
|||||||
CollectionBeforeValidateHook,
|
CollectionBeforeValidateHook,
|
||||||
CollectionConfig, Field,
|
CollectionConfig, Field,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
import { BillingPluginConfig, CustomerInfoExtractor, defaults } from '@/plugin/config'
|
import type { BillingPluginConfig} from '@/plugin/config';
|
||||||
import { Invoice } from '@/plugin/types'
|
import { defaults } from '@/plugin/config'
|
||||||
import { extractSlug } from '@/plugin/utils'
|
import { extractSlug } from '@/plugin/utils'
|
||||||
|
import type { Invoice } from '@/plugin/types/invoices'
|
||||||
|
|
||||||
export function createInvoicesCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
export function createInvoicesCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
||||||
const {customerRelationSlug, customerInfoExtractor} = pluginConfig
|
const {customerRelationSlug, customerInfoExtractor} = pluginConfig
|
||||||
@@ -31,7 +32,7 @@ export function createInvoicesCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
position: 'sidebar' as const,
|
position: 'sidebar' as const,
|
||||||
description: 'Link to customer record (optional)',
|
description: 'Link to customer record (optional)',
|
||||||
},
|
},
|
||||||
relationTo: pluginConfig.customerRelationSlug as never,
|
relationTo: extractSlug(customerRelationSlug),
|
||||||
required: false,
|
required: false,
|
||||||
}] : []),
|
}] : []),
|
||||||
// Basic customer info fields (embedded)
|
// Basic customer info fields (embedded)
|
||||||
@@ -275,7 +276,7 @@ export function createInvoicesCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
condition: (data) => data.status === 'paid',
|
condition: (data) => data.status === 'paid',
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
},
|
},
|
||||||
relationTo: 'payments',
|
relationTo: extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'notes',
|
name: 'notes',
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { AccessArgs, CollectionBeforeChangeHook, CollectionConfig, Field } from 'payload'
|
import type { AccessArgs, CollectionBeforeChangeHook, CollectionConfig, CollectionSlug, Field } from 'payload'
|
||||||
import type { Payment } from '@/plugin/types'
|
|
||||||
import type { BillingPluginConfig} from '@/plugin/config';
|
import type { BillingPluginConfig} from '@/plugin/config';
|
||||||
import { defaults } from '@/plugin/config'
|
import { defaults } from '@/plugin/config'
|
||||||
import { extractSlug } from '@/plugin/utils'
|
import { extractSlug } from '@/plugin/utils'
|
||||||
|
import { Payment } from '@/plugin/types/payments'
|
||||||
|
import { initProviderPayment } from '@/collections/hooks'
|
||||||
|
|
||||||
export function createPaymentsCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
export function createPaymentsCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
||||||
const overrides = typeof pluginConfig.collections?.payments === 'object' ? pluginConfig.collections?.payments : {}
|
const overrides = typeof pluginConfig.collections?.payments === 'object' ? pluginConfig.collections?.payments : {}
|
||||||
@@ -27,7 +28,6 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
description: 'The payment ID from the payment provider',
|
description: 'The payment ID from the payment provider',
|
||||||
},
|
},
|
||||||
label: 'Provider Payment ID',
|
label: 'Provider Payment ID',
|
||||||
required: true,
|
|
||||||
unique: true,
|
unique: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -78,7 +78,7 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
admin: {
|
admin: {
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
},
|
},
|
||||||
relationTo: 'invoices',
|
relationTo: extractSlug(pluginConfig.collections?.invoices || defaults.invoicesCollection) as CollectionSlug,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'metadata',
|
name: 'metadata',
|
||||||
@@ -103,7 +103,7 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
readOnly: true,
|
readOnly: true,
|
||||||
},
|
},
|
||||||
hasMany: true,
|
hasMany: true,
|
||||||
relationTo: 'refunds',
|
relationTo: extractSlug(pluginConfig.collections?.refunds || defaults.refundsCollection) as CollectionSlug,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
if (overrides?.fields) {
|
if (overrides?.fields) {
|
||||||
@@ -126,7 +126,7 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
fields,
|
fields,
|
||||||
hooks: {
|
hooks: {
|
||||||
beforeChange: [
|
beforeChange: [
|
||||||
({ data, operation }) => {
|
async ({ data, operation, req }) => {
|
||||||
if (operation === 'create') {
|
if (operation === 'create') {
|
||||||
// Validate amount format
|
// Validate amount format
|
||||||
if (data.amount && !Number.isInteger(data.amount)) {
|
if (data.amount && !Number.isInteger(data.amount)) {
|
||||||
@@ -140,6 +140,8 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
throw new Error('Currency must be a 3-letter ISO code')
|
throw new Error('Currency must be a 3-letter ISO code')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await initProviderPayment(req.payload, data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
] satisfies CollectionBeforeChangeHook<Payment>[],
|
] satisfies CollectionBeforeChangeHook<Payment>[],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { AccessArgs, CollectionConfig } from 'payload'
|
import type { AccessArgs, CollectionConfig } from 'payload'
|
||||||
import { BillingPluginConfig, defaults } from '@/plugin/config'
|
import { BillingPluginConfig, defaults } from '@/plugin/config'
|
||||||
import { extractSlug } from '@/plugin/utils'
|
import { extractSlug } from '@/plugin/utils'
|
||||||
|
import { Payment } from '@/plugin/types'
|
||||||
|
|
||||||
export function createRefundsCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
export function createRefundsCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
||||||
const overrides = typeof pluginConfig.collections?.invoices === 'object' ? pluginConfig.collections?.invoices : {}
|
|
||||||
// TODO: finish collection overrides
|
// TODO: finish collection overrides
|
||||||
return {
|
return {
|
||||||
slug: extractSlug(pluginConfig.collections?.refunds || defaults.refundsCollection),
|
slug: extractSlug(pluginConfig.collections?.refunds || defaults.refundsCollection),
|
||||||
@@ -35,7 +35,7 @@ export function createRefundsCollection(pluginConfig: BillingPluginConfig): Coll
|
|||||||
admin: {
|
admin: {
|
||||||
position: 'sidebar',
|
position: 'sidebar',
|
||||||
},
|
},
|
||||||
relationTo: 'payments',
|
relationTo: extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection),
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -117,13 +117,13 @@ export function createRefundsCollection(pluginConfig: BillingPluginConfig): Coll
|
|||||||
try {
|
try {
|
||||||
const payment = await req.payload.findByID({
|
const payment = await req.payload.findByID({
|
||||||
id: typeof doc.payment === 'string' ? doc.payment : doc.payment.id,
|
id: typeof doc.payment === 'string' ? doc.payment : doc.payment.id,
|
||||||
collection: 'payments',
|
collection: extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection),
|
||||||
})
|
}) as Payment
|
||||||
|
|
||||||
const refundIds = Array.isArray(payment.refunds) ? payment.refunds : []
|
const refundIds = Array.isArray(payment.refunds) ? payment.refunds : []
|
||||||
await req.payload.update({
|
await req.payload.update({
|
||||||
id: typeof doc.payment === 'string' ? doc.payment : doc.payment.id,
|
id: typeof doc.payment === 'string' ? doc.payment : doc.payment.id,
|
||||||
collection: 'payments',
|
collection: extractSlug(pluginConfig.collections?.payments || defaults.paymentsCollection),
|
||||||
data: {
|
data: {
|
||||||
refunds: [...refundIds, doc.id],
|
refunds: [...refundIds, doc.id],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
|
|
||||||
|
export { billingPlugin } from './plugin'
|
||||||
|
export type { BillingPluginConfig, CustomerInfoExtractor } from './plugin/config'
|
||||||
|
export type { Invoice, Payment, Refund } from './plugin/types'
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CollectionConfig } from 'payload'
|
import { CollectionConfig } from 'payload'
|
||||||
import { FieldsOverride } from '@/plugin/utils'
|
import { FieldsOverride } from '@/plugin/utils'
|
||||||
|
import { PaymentProvider } from '@/plugin/types'
|
||||||
|
|
||||||
export const defaults = {
|
export const defaults = {
|
||||||
paymentsCollection: 'payments',
|
paymentsCollection: 'payments',
|
||||||
@@ -63,11 +64,7 @@ export interface BillingPluginConfig {
|
|||||||
customerInfoExtractor?: CustomerInfoExtractor // Callback to extract customer info from relationship
|
customerInfoExtractor?: CustomerInfoExtractor // Callback to extract customer info from relationship
|
||||||
customerRelationSlug?: string // Customer collection slug for relationship
|
customerRelationSlug?: string // Customer collection slug for relationship
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
providers?: {
|
providers?: PaymentProvider[]
|
||||||
mollie?: MollieConfig
|
|
||||||
stripe?: StripeConfig
|
|
||||||
test?: TestProviderConfig
|
|
||||||
}
|
|
||||||
webhooks?: {
|
webhooks?: {
|
||||||
basePath?: string
|
basePath?: string
|
||||||
cors?: boolean
|
cors?: boolean
|
||||||
|
|||||||
@@ -1,62 +1,49 @@
|
|||||||
import type { Config } from 'payload'
|
|
||||||
import { createInvoicesCollection, createPaymentsCollection, createRefundsCollection } from '@/collections'
|
import { createInvoicesCollection, createPaymentsCollection, createRefundsCollection } from '@/collections'
|
||||||
import type { BillingPluginConfig } from '@/plugin/config'
|
import type { BillingPluginConfig } from '@/plugin/config'
|
||||||
|
import type { Config, Payload } from 'payload'
|
||||||
|
import { createSingleton } from '@/plugin/singleton'
|
||||||
|
import type { PaymentProvider } from '@/providers'
|
||||||
|
|
||||||
|
const singleton = createSingleton(Symbol('billingPlugin'))
|
||||||
|
|
||||||
|
type BillingPlugin = {
|
||||||
|
config: BillingPluginConfig
|
||||||
|
providerConfig: {
|
||||||
|
[key: string]: PaymentProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useBillingPlugin = (payload: Payload) => singleton.get(payload) as BillingPlugin
|
||||||
|
|
||||||
export const billingPlugin = (pluginConfig: BillingPluginConfig = {}) => (config: Config): Config => {
|
export const billingPlugin = (pluginConfig: BillingPluginConfig = {}) => (config: Config): Config => {
|
||||||
if (pluginConfig.disabled) {
|
if (pluginConfig.disabled) {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize collections
|
config.collections = [
|
||||||
if (!config.collections) {
|
...(config.collections || []),
|
||||||
config.collections = []
|
|
||||||
}
|
|
||||||
|
|
||||||
config.collections.push(
|
|
||||||
createPaymentsCollection(pluginConfig),
|
createPaymentsCollection(pluginConfig),
|
||||||
createInvoicesCollection(pluginConfig),
|
createInvoicesCollection(pluginConfig),
|
||||||
createRefundsCollection(pluginConfig),
|
createRefundsCollection(pluginConfig),
|
||||||
)
|
]
|
||||||
|
|
||||||
// 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
|
const incomingOnInit = config.onInit
|
||||||
|
|
||||||
config.onInit = async (payload) => {
|
config.onInit = async (payload) => {
|
||||||
// Execute any existing onInit functions first
|
|
||||||
if (incomingOnInit) {
|
if (incomingOnInit) {
|
||||||
await incomingOnInit(payload)
|
await incomingOnInit(payload)
|
||||||
}
|
}
|
||||||
|
singleton.set(payload, {
|
||||||
|
config: pluginConfig,
|
||||||
|
providerConfig: (pluginConfig.providers || []).reduce(
|
||||||
|
(acc, val) => {
|
||||||
|
acc[val.key] = val
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, PaymentProvider>
|
||||||
|
)
|
||||||
|
} satisfies BillingPlugin)
|
||||||
|
console.log('Billing plugin initialized', singleton.get(payload))
|
||||||
|
await Promise.all((pluginConfig.providers || []).map(p => p.onInit(payload)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|||||||
11
src/plugin/singleton.ts
Normal file
11
src/plugin/singleton.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export const createSingleton = <T>(s?: symbol | string) => {
|
||||||
|
const symbol = !s ? Symbol() : s
|
||||||
|
return {
|
||||||
|
get(container: any) {
|
||||||
|
return container[symbol] as T
|
||||||
|
},
|
||||||
|
set(container: any, value: T) {
|
||||||
|
container[symbol] = value
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/plugin/types/id.ts
Normal file
1
src/plugin/types/id.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export type Id = string | number
|
||||||
5
src/plugin/types/index.ts
Normal file
5
src/plugin/types/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export * from './id'
|
||||||
|
export * from './invoices'
|
||||||
|
export * from './payments'
|
||||||
|
export * from './refunds'
|
||||||
|
export * from '../../providers/types'
|
||||||
@@ -1,107 +1,6 @@
|
|||||||
|
import { Payment } from '@/plugin/types/payments'
|
||||||
|
|
||||||
export type Id = string | number
|
import { Id } from '@/plugin/types/id'
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
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<TCustomer = unknown> {
|
export interface Invoice<TCustomer = unknown> {
|
||||||
id: Id;
|
id: Id;
|
||||||
@@ -216,4 +115,3 @@ export interface Invoice<TCustomer = unknown> {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
53
src/plugin/types/payments.ts
Normal file
53
src/plugin/types/payments.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Refund } from '@/plugin/types/refunds'
|
||||||
|
import { Invoice } from '@/plugin/types/invoices'
|
||||||
|
import { Id } from '@/plugin/types/id'
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
53
src/plugin/types/refunds.ts
Normal file
53
src/plugin/types/refunds.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Payment } from '@/plugin/types/payments'
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { CollectionConfig, Field } from 'payload'
|
import type { CollectionConfig, CollectionSlug, Field } from 'payload'
|
||||||
|
|
||||||
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
|
export type FieldsOverride = (args: { defaultFields: Field[] }) => Field[]
|
||||||
|
|
||||||
export const extractSlug = (arg: string | Partial<CollectionConfig>) => typeof arg === 'string' ? arg : arg.slug!
|
export const extractSlug =
|
||||||
|
(arg: string | Partial<CollectionConfig>) => (typeof arg === 'string' ? arg : arg.slug!) as CollectionSlug
|
||||||
|
|||||||
2
src/providers/index.ts
Normal file
2
src/providers/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './mollie'
|
||||||
|
export * from './types'
|
||||||
40
src/providers/mollie.ts
Normal file
40
src/providers/mollie.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import type { Payment } from '@/plugin/types/payments'
|
||||||
|
import type { InitPayment, PaymentProvider } from '@/plugin/types'
|
||||||
|
import type { Payload } from 'payload'
|
||||||
|
import { createSingleton } from '@/plugin/singleton'
|
||||||
|
import type { createMollieClient, MollieClient } from '@mollie/api-client'
|
||||||
|
|
||||||
|
const symbol = Symbol('mollie')
|
||||||
|
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
|
||||||
|
|
||||||
|
export const mollieProvider = (config: MollieProviderConfig) => {
|
||||||
|
const singleton = createSingleton<MollieClient>(symbol)
|
||||||
|
return {
|
||||||
|
key: 'mollie',
|
||||||
|
onInit: async (payload: Payload) => {
|
||||||
|
const createMollieClient = (await import('@mollie/api-client')).default
|
||||||
|
const mollieClient = createMollieClient(config)
|
||||||
|
singleton.set(payload, mollieClient)
|
||||||
|
},
|
||||||
|
initPayment: async (payload, payment) => {
|
||||||
|
if (!payment.amount) {
|
||||||
|
throw new Error('Amount is required')
|
||||||
|
}
|
||||||
|
if (!payment.currency) {
|
||||||
|
throw new Error('Currency is required')
|
||||||
|
}
|
||||||
|
const molliePayment = await singleton.get(payload).payments.create({
|
||||||
|
amount: {
|
||||||
|
value: (payment.amount / 100).toFixed(2),
|
||||||
|
currency: payment.currency
|
||||||
|
},
|
||||||
|
description: payment.description || '',
|
||||||
|
redirectUrl: 'https://localhost:3000/payment/success',
|
||||||
|
webhookUrl: 'https://localhost:3000',
|
||||||
|
});
|
||||||
|
payment.providerId = molliePayment.id
|
||||||
|
payment.providerData = molliePayment.toPlainObject()
|
||||||
|
return payment
|
||||||
|
},
|
||||||
|
} satisfies PaymentProvider
|
||||||
|
}
|
||||||
10
src/providers/types.ts
Normal file
10
src/providers/types.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import type { Payment } from '@/plugin/types/payments'
|
||||||
|
import type { Payload } from 'payload'
|
||||||
|
|
||||||
|
export type InitPayment = (payload: Payload, payment: Partial<Payment>) => Promise<Partial<Payment>>
|
||||||
|
|
||||||
|
export type PaymentProvider = {
|
||||||
|
key: string
|
||||||
|
onInit: (payload: Payload) => Promise<void> | void
|
||||||
|
initPayment: InitPayment
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user