mirror of
https://github.com/xtr-dev/payload-billing.git
synced 2025-12-10 19:03:23 +00:00
Compare commits
10 Commits
v0.1.14
...
79de7910d4
| Author | SHA1 | Date | |
|---|---|---|---|
| 79de7910d4 | |||
| bb5ba83bc3 | |||
| 79166f7edf | |||
| 6de405d07f | |||
| 7c0b42e35d | |||
| 25b340d818 | |||
| 46bec6bd2e | |||
| 4fde492e0f | |||
| a37757ffa1 | |||
| 1867bb2f96 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/payload-billing",
|
"name": "@xtr-dev/payload-billing",
|
||||||
"version": "0.1.14",
|
"version": "0.1.21",
|
||||||
"description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing",
|
"description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import { useBillingPlugin } from '../plugin/index'
|
|||||||
|
|
||||||
export const initProviderPayment = async (payload: Payload, payment: Partial<Payment>): Promise<Partial<Payment>> => {
|
export const initProviderPayment = async (payload: Payload, payment: Partial<Payment>): Promise<Partial<Payment>> => {
|
||||||
const billing = useBillingPlugin(payload)
|
const billing = useBillingPlugin(payload)
|
||||||
|
|
||||||
|
if (!billing) {
|
||||||
|
throw new Error(
|
||||||
|
'Billing plugin not initialized. Make sure the billingPlugin is properly configured in your Payload config and that Payload has finished initializing. ' +
|
||||||
|
'If you are calling this from a Next.js API route or Server Component, ensure you are using getPayload() with the same config instance used in your Payload configuration.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!payment.provider || !billing.providerConfig[payment.provider]) {
|
if (!payment.provider || !billing.providerConfig[payment.provider]) {
|
||||||
throw new Error(`Provider ${payment.provider} not found.`)
|
throw new Error(`Provider ${payment.provider} not found.`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
description: 'Payment description',
|
description: 'Payment description',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'checkoutUrl',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'Checkout URL where user can complete payment (if applicable)',
|
||||||
|
readOnly: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'invoice',
|
name: 'invoice',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
@@ -136,6 +144,18 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
|
|||||||
useAsTitle: 'id',
|
useAsTitle: 'id',
|
||||||
},
|
},
|
||||||
fields,
|
fields,
|
||||||
|
defaultPopulate: {
|
||||||
|
id: true,
|
||||||
|
provider: true,
|
||||||
|
status: true,
|
||||||
|
amount: true,
|
||||||
|
currency: true,
|
||||||
|
description: true,
|
||||||
|
checkoutUrl: true,
|
||||||
|
providerId: true,
|
||||||
|
metadata: true,
|
||||||
|
providerData: true,
|
||||||
|
},
|
||||||
hooks: {
|
hooks: {
|
||||||
afterChange: [
|
afterChange: [
|
||||||
async ({ doc, operation, req, previousDoc }) => {
|
async ({ doc, operation, req, previousDoc }) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Config, Payload } from 'payload'
|
|||||||
import { createSingleton } from './singleton'
|
import { createSingleton } from './singleton'
|
||||||
import type { PaymentProvider } from '../providers/index'
|
import type { PaymentProvider } from '../providers/index'
|
||||||
|
|
||||||
const singleton = createSingleton(Symbol('billingPlugin'))
|
const singleton = createSingleton(Symbol.for('@xtr-dev/payload-billing'))
|
||||||
|
|
||||||
type BillingPlugin = {
|
type BillingPlugin = {
|
||||||
config: BillingPluginConfig
|
config: BillingPluginConfig
|
||||||
@@ -13,7 +13,7 @@ type BillingPlugin = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBillingPlugin = (payload: Payload) => singleton.get(payload) as BillingPlugin
|
export const useBillingPlugin = (payload: Payload) => singleton.get(payload) as BillingPlugin | undefined
|
||||||
|
|
||||||
export const billingPlugin = (pluginConfig: BillingPluginConfig = {}) => (config: Config): Config => {
|
export const billingPlugin = (pluginConfig: BillingPluginConfig = {}) => (config: Config): Config => {
|
||||||
if (pluginConfig.disabled) {
|
if (pluginConfig.disabled) {
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ export interface Payment {
|
|||||||
* Payment description
|
* Payment description
|
||||||
*/
|
*/
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
/**
|
||||||
|
* Checkout URL where user can complete payment (if applicable)
|
||||||
|
*/
|
||||||
|
checkoutUrl?: string | null;
|
||||||
invoice?: (Id | null) | Invoice;
|
invoice?: (Id | null) | Invoice;
|
||||||
/**
|
/**
|
||||||
* Additional metadata for the payment
|
* Additional metadata for the payment
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency'
|
import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency'
|
||||||
import { createContextLogger } from '../utils/logger'
|
import { createContextLogger } from '../utils/logger'
|
||||||
|
|
||||||
const symbol = Symbol('mollie')
|
const symbol = Symbol.for('@xtr-dev/payload-billing/mollie')
|
||||||
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
|
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -155,6 +155,7 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
|
|||||||
});
|
});
|
||||||
payment.providerId = molliePayment.id
|
payment.providerId = molliePayment.id
|
||||||
payment.providerData = molliePayment.toPlainObject()
|
payment.providerData = molliePayment.toPlainObject()
|
||||||
|
payment.checkoutUrl = molliePayment._links?.checkout?.href || null
|
||||||
return payment
|
return payment
|
||||||
},
|
},
|
||||||
} satisfies PaymentProvider
|
} satisfies PaymentProvider
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
import { isValidAmount, isValidCurrencyCode } from './currency'
|
import { isValidAmount, isValidCurrencyCode } from './currency'
|
||||||
import { createContextLogger } from '../utils/logger'
|
import { createContextLogger } from '../utils/logger'
|
||||||
|
|
||||||
const symbol = Symbol('stripe')
|
const symbol = Symbol.for('@xtr-dev/payload-billing/stripe')
|
||||||
|
|
||||||
export interface StripeProviderConfig {
|
export interface StripeProviderConfig {
|
||||||
secretKey: string
|
secretKey: string
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { handleWebhookError, logWebhookEvent } from './utils'
|
|||||||
import { isValidAmount, isValidCurrencyCode } from './currency'
|
import { isValidAmount, isValidCurrencyCode } from './currency'
|
||||||
import { createContextLogger } from '../utils/logger'
|
import { createContextLogger } from '../utils/logger'
|
||||||
|
|
||||||
const TestModeWarningSymbol = Symbol('TestModeWarning')
|
const TestModeWarningSymbol = Symbol.for('@xtr-dev/payload-billing/test-mode-warning')
|
||||||
const hasGivenTestModeWarning = () => TestModeWarningSymbol in globalThis
|
const hasGivenTestModeWarning = () => TestModeWarningSymbol in globalThis
|
||||||
const setTestModeWarning = () => ((<any>globalThis)[TestModeWarningSymbol] = true)
|
const setTestModeWarning = () => ((<any>globalThis)[TestModeWarningSymbol] = true)
|
||||||
|
|
||||||
@@ -242,10 +242,15 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
{
|
{
|
||||||
path: '/payload-billing/test/payment/:id',
|
path: '/payload-billing/test/payment/:id',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
handler: (req) => {
|
handler: async (req) => {
|
||||||
// Extract payment ID from URL path
|
// Extract payment ID from URL path
|
||||||
const urlParts = req.url?.split('/') || []
|
const urlParts = req.url?.split('/') || []
|
||||||
const paymentId = urlParts[urlParts.length - 1]
|
let paymentId = urlParts[urlParts.length - 1]
|
||||||
|
|
||||||
|
// Remove query parameters if present
|
||||||
|
if (paymentId?.includes('?')) {
|
||||||
|
paymentId = paymentId.split('?')[0]
|
||||||
|
}
|
||||||
|
|
||||||
if (!paymentId) {
|
if (!paymentId) {
|
||||||
return new Response(JSON.stringify({ error: 'Payment ID required' }), {
|
return new Response(JSON.stringify({ error: 'Payment ID required' }), {
|
||||||
@@ -263,7 +268,41 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = testPaymentSessions.get(paymentId)
|
// Try to get session from memory first (for backward compatibility)
|
||||||
|
let session = testPaymentSessions.get(paymentId)
|
||||||
|
|
||||||
|
// If not in memory, fetch from database
|
||||||
|
if (!session && req.payload) {
|
||||||
|
try {
|
||||||
|
const paymentsConfig = pluginConfig.collections?.payments
|
||||||
|
const paymentSlug = typeof paymentsConfig === 'string' ? paymentsConfig : (paymentsConfig?.slug || 'payments')
|
||||||
|
const result = await req.payload.find({
|
||||||
|
collection: paymentSlug,
|
||||||
|
where: {
|
||||||
|
providerId: {
|
||||||
|
equals: paymentId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limit: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs && result.docs.length > 0) {
|
||||||
|
const payment = result.docs[0] as Payment
|
||||||
|
// Create session from database payment
|
||||||
|
session = {
|
||||||
|
id: paymentId,
|
||||||
|
payment: payment,
|
||||||
|
createdAt: new Date(payment.createdAt || Date.now()),
|
||||||
|
status: 'pending' as PaymentOutcome
|
||||||
|
}
|
||||||
|
// Store in memory for future requests
|
||||||
|
testPaymentSessions.set(paymentId, session)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching payment from database:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return new Response(JSON.stringify({ error: 'Payment session not found' }), {
|
return new Response(JSON.stringify({ error: 'Payment session not found' }), {
|
||||||
status: 404,
|
status: 404,
|
||||||
@@ -322,7 +361,41 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
|
|
||||||
const { paymentId, scenarioId, method } = validation.data!
|
const { paymentId, scenarioId, method } = validation.data!
|
||||||
|
|
||||||
const session = testPaymentSessions.get(paymentId)
|
// Try to get session from memory first
|
||||||
|
let session = testPaymentSessions.get(paymentId)
|
||||||
|
|
||||||
|
// If not in memory, fetch from database
|
||||||
|
if (!session && req.payload) {
|
||||||
|
try {
|
||||||
|
const paymentsConfig = pluginConfig.collections?.payments
|
||||||
|
const paymentSlug = typeof paymentsConfig === 'string' ? paymentsConfig : (paymentsConfig?.slug || 'payments')
|
||||||
|
const result = await req.payload.find({
|
||||||
|
collection: paymentSlug,
|
||||||
|
where: {
|
||||||
|
providerId: {
|
||||||
|
equals: paymentId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limit: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs && result.docs.length > 0) {
|
||||||
|
const payment = result.docs[0] as Payment
|
||||||
|
// Create session from database payment
|
||||||
|
session = {
|
||||||
|
id: paymentId,
|
||||||
|
payment: payment,
|
||||||
|
createdAt: new Date(payment.createdAt || Date.now()),
|
||||||
|
status: 'pending' as PaymentOutcome
|
||||||
|
}
|
||||||
|
// Store in memory for future requests
|
||||||
|
testPaymentSessions.set(paymentId, session)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching payment from database:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return new Response(JSON.stringify({ error: 'Payment session not found' }), {
|
return new Response(JSON.stringify({ error: 'Payment session not found' }), {
|
||||||
status: 404,
|
status: 404,
|
||||||
@@ -398,10 +471,15 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
{
|
{
|
||||||
path: '/payload-billing/test/status/:id',
|
path: '/payload-billing/test/status/:id',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
handler: (req) => {
|
handler: async (req) => {
|
||||||
// Extract payment ID from URL path
|
// Extract payment ID from URL path
|
||||||
const urlParts = req.url?.split('/') || []
|
const urlParts = req.url?.split('/') || []
|
||||||
const paymentId = urlParts[urlParts.length - 1]
|
let paymentId = urlParts[urlParts.length - 1]
|
||||||
|
|
||||||
|
// Remove query parameters if present
|
||||||
|
if (paymentId?.includes('?')) {
|
||||||
|
paymentId = paymentId.split('?')[0]
|
||||||
|
}
|
||||||
|
|
||||||
if (!paymentId) {
|
if (!paymentId) {
|
||||||
return new Response(JSON.stringify({ error: 'Payment ID required' }), {
|
return new Response(JSON.stringify({ error: 'Payment ID required' }), {
|
||||||
@@ -419,7 +497,41 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = testPaymentSessions.get(paymentId)
|
// Try to get session from memory first
|
||||||
|
let session = testPaymentSessions.get(paymentId)
|
||||||
|
|
||||||
|
// If not in memory, fetch from database
|
||||||
|
if (!session && req.payload) {
|
||||||
|
try {
|
||||||
|
const paymentsConfig = pluginConfig.collections?.payments
|
||||||
|
const paymentSlug = typeof paymentsConfig === 'string' ? paymentsConfig : (paymentsConfig?.slug || 'payments')
|
||||||
|
const result = await req.payload.find({
|
||||||
|
collection: paymentSlug,
|
||||||
|
where: {
|
||||||
|
providerId: {
|
||||||
|
equals: paymentId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
limit: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.docs && result.docs.length > 0) {
|
||||||
|
const payment = result.docs[0] as Payment
|
||||||
|
// Create session from database payment
|
||||||
|
session = {
|
||||||
|
id: paymentId,
|
||||||
|
payment: payment,
|
||||||
|
createdAt: new Date(payment.createdAt || Date.now()),
|
||||||
|
status: 'pending' as PaymentOutcome
|
||||||
|
}
|
||||||
|
// Store in memory for future requests
|
||||||
|
testPaymentSessions.set(paymentId, session)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching payment from database:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return new Response(JSON.stringify({ error: 'Payment session not found' }), {
|
return new Response(JSON.stringify({ error: 'Payment session not found' }), {
|
||||||
status: 404,
|
status: 404,
|
||||||
@@ -492,6 +604,10 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
|
|
||||||
// Set provider ID and data
|
// Set provider ID and data
|
||||||
payment.providerId = testPaymentId
|
payment.providerId = testPaymentId
|
||||||
|
// Use custom UI route if specified, otherwise use built-in UI endpoint
|
||||||
|
const paymentUrl = testConfig.customUiRoute
|
||||||
|
? `${baseUrl}${testConfig.customUiRoute}/${testPaymentId}`
|
||||||
|
: `${baseUrl}/api/payload-billing/test/payment/${testPaymentId}`
|
||||||
const providerData: ProviderData = {
|
const providerData: ProviderData = {
|
||||||
raw: {
|
raw: {
|
||||||
id: testPaymentId,
|
id: testPaymentId,
|
||||||
@@ -500,7 +616,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
description: payment.description,
|
description: payment.description,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
testMode: true,
|
testMode: true,
|
||||||
paymentUrl: `${baseUrl}/api/payload-billing/test/payment/${testPaymentId}`,
|
paymentUrl,
|
||||||
scenarios: scenarios.map(s => ({ id: s.id, name: s.name, description: s.description })),
|
scenarios: scenarios.map(s => ({ id: s.id, name: s.name, description: s.description })),
|
||||||
methods: Object.entries(PAYMENT_METHODS).map(([key, value]) => ({
|
methods: Object.entries(PAYMENT_METHODS).map(([key, value]) => ({
|
||||||
id: key,
|
id: key,
|
||||||
@@ -512,6 +628,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
provider: 'test'
|
provider: 'test'
|
||||||
}
|
}
|
||||||
payment.providerData = providerData
|
payment.providerData = providerData
|
||||||
|
payment.checkoutUrl = paymentUrl
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user