mirror of
https://github.com/xtr-dev/payload-billing.git
synced 2025-12-13 04:13: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,5 +1,6 @@
|
||||
import { CollectionConfig } from 'payload'
|
||||
import { FieldsOverride } from '@/plugin/utils'
|
||||
import { PaymentProvider } from '@/plugin/types'
|
||||
|
||||
export const defaults = {
|
||||
paymentsCollection: 'payments',
|
||||
@@ -63,11 +64,7 @@ export interface BillingPluginConfig {
|
||||
customerInfoExtractor?: CustomerInfoExtractor // Callback to extract customer info from relationship
|
||||
customerRelationSlug?: string // Customer collection slug for relationship
|
||||
disabled?: boolean
|
||||
providers?: {
|
||||
mollie?: MollieConfig
|
||||
stripe?: StripeConfig
|
||||
test?: TestProviderConfig
|
||||
}
|
||||
providers?: PaymentProvider[]
|
||||
webhooks?: {
|
||||
basePath?: string
|
||||
cors?: boolean
|
||||
|
||||
@@ -1,62 +1,49 @@
|
||||
import type { Config } from 'payload'
|
||||
import { createInvoicesCollection, createPaymentsCollection, createRefundsCollection } from '@/collections'
|
||||
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 => {
|
||||
if (pluginConfig.disabled) {
|
||||
return config
|
||||
}
|
||||
|
||||
// Initialize collections
|
||||
if (!config.collections) {
|
||||
config.collections = []
|
||||
}
|
||||
|
||||
config.collections.push(
|
||||
config.collections = [
|
||||
...(config.collections || []),
|
||||
createPaymentsCollection(pluginConfig),
|
||||
createInvoicesCollection(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
|
||||
|
||||
config.onInit = async (payload) => {
|
||||
// Execute any existing onInit functions first
|
||||
if (incomingOnInit) {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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;
|
||||
}
|
||||
import { Id } from '@/plugin/types/id'
|
||||
|
||||
export interface Invoice<TCustomer = unknown> {
|
||||
id: Id;
|
||||
@@ -216,4 +115,3 @@ export interface Invoice<TCustomer = unknown> {
|
||||
updatedAt: 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 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
|
||||
|
||||
Reference in New Issue
Block a user