10 Commits

Author SHA1 Message Date
bb5ba83bc3 fix: use built-in UI when customUiRoute is not specified
In v0.1.19, the fix for customUiRoute made it always use the default
route '/test-payment' even when customUiRoute was not specified. This
caused 404 errors because users were unaware of this default behavior.

The plugin actually provides a built-in test payment UI at
/api/payload-billing/test/payment/:id that works out of the box.

This fix ensures the correct behavior:
- When customUiRoute IS specified: Use the custom route
- When customUiRoute is NOT specified: Use the built-in UI route

This allows the testProvider to work out of the box without requiring
users to implement a custom test payment page, while still supporting
custom implementations when needed.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 17:15:59 +01:00
79166f7edf fix: respect customUiRoute configuration in test provider
The test provider's customUiRoute parameter was being ignored when
generating checkout URLs. The checkout URL was always using the
hardcoded API endpoint instead of the configured custom UI route.

This fix ensures that when customUiRoute is configured, the generated
checkoutUrl will use the custom route (e.g., /test-payment/:id)
instead of the default API route.

Fixes issue where test provider checkout URLs returned 404 errors
because they pointed to /api/payload-billing/test/payment/:id instead
of the configured custom UI route.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 16:17:38 +01:00
6de405d07f 0.1.18 2025-11-21 15:39:40 +01:00
7c0b42e35d fix: resolve plugin initialization failure in Next.js API routes
Use Symbol.for() instead of Symbol() for plugin singleton storage to ensure
plugin state persists across different module loading contexts (admin panel,
API routes, server components).

This fixes the "Billing plugin not initialized" error that occurred when
calling payload.create() from Next.js API routes, server components, or
server actions.

Changes:
- Plugin singleton now uses Symbol.for('@xtr-dev/payload-billing')
- Provider singletons (stripe, mollie, test) use global symbols
- Enhanced error message with troubleshooting guidance

Fixes #1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 15:35:12 +01:00
25b340d818 0.1.17 2025-11-18 23:12:32 +01:00
46bec6bd2e feat: add defaultPopulate configuration to payments collection
- Include defaultPopulate fields to simplify API responses
- Ensure key payment details (amount, status, provider, etc.) are preloaded
2025-11-18 23:12:30 +01:00
4fde492e0f feat: add checkoutUrl field to payment collection
- Add checkoutUrl field to Payment type and collection
- Mollie provider now sets checkoutUrl from _links.checkout.href
- Test provider sets checkoutUrl to interactive payment UI
- Stripe provider doesn't use checkoutUrl (uses client_secret instead)
- Update README with checkoutUrl examples and clarifications
- Make it easier to redirect users to payment pages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 23:01:43 +01:00
a37757ffa1 fix: add better error handling for uninitialized billing plugin
- Fix TypeError when accessing providerConfig on undefined billing plugin
- Add proper type safety: useBillingPlugin now returns BillingPlugin | undefined
- Add clear error message when plugin hasn't been initialized
- Update README quickstart with concise provider response examples

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 22:31:51 +01:00
1867bb2f96 docs: restructure and update README for clarity
- Revise features list for precision and enhanced readability
- Expand and reorganize table of contents for better navigation
- Add detailed configurations and examples for Stripe, Mollie, and test providers
- Include new sections like customer management, payment flows, and webhook setup
- Refine descriptions of automatic behaviors and status synchronization
- Fix minor grammar inconsistencies and improve overall formatting

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-18 21:43:47 +01:00
89578aeba2 fix: replace path aliases with relative imports to fix published package
- Replace @/ path aliases with relative imports in invoices collection
- This fixes the 'Cannot find package @/plugin' error in published package
- Path aliases don't resolve correctly in the transpiled dist folder

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-08 16:35:21 +01:00
11 changed files with 1165 additions and 320 deletions

1367
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@xtr-dev/payload-billing",
"version": "0.1.13",
"version": "0.1.20",
"description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing",
"license": "MIT",
"type": "module",
@@ -81,6 +81,7 @@
"@playwright/test": "^1.52.0",
"@swc-node/register": "1.10.9",
"@swc/cli": "0.6.0",
"@swc/plugin-transform-imports": "^11.0.0",
"@tailwindcss/postcss": "^4.1.17",
"@types/node": "^22.5.4",
"@types/react": "19.1.8",
@@ -105,6 +106,7 @@
"sort-package-json": "^2.10.0",
"stripe": "^18.5.0",
"tailwindcss": "^4.1.17",
"tsc-alias": "^1.8.16",
"typescript": "5.7.3",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.1.2"

54
pnpm-lock.yaml generated
View File

@@ -51,6 +51,9 @@ importers:
'@swc/cli':
specifier: 0.6.0
version: 0.6.0(@swc/core@1.13.5)
'@swc/plugin-transform-imports':
specifier: ^11.0.0
version: 11.0.0
'@tailwindcss/postcss':
specifier: ^4.1.17
version: 4.1.17
@@ -123,6 +126,9 @@ importers:
tailwindcss:
specifier: ^4.1.17
version: 4.1.17
tsc-alias:
specifier: ^1.8.16
version: 1.8.16
typescript:
specifier: 5.7.3
version: 5.7.3
@@ -1990,6 +1996,9 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@swc/plugin-transform-imports@11.0.0':
resolution: {integrity: sha512-vYxPeZd8GpsdO4RWu9h1sYUVj/3yMwdvZaHRTUjN+AcUKcTr+OMl4hK2iNk6n6UzMlpURcLvibfl1HkxZkCCLQ==}
'@swc/types@0.1.25':
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
@@ -2820,6 +2829,10 @@ packages:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
comment-parser@1.4.1:
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
engines: {node: '>= 12.0.0'}
@@ -4515,6 +4528,10 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mylas@2.1.14:
resolution: {integrity: sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog==}
engines: {node: '>=16.0.0'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -4838,6 +4855,10 @@ packages:
engines: {node: '>=18'}
hasBin: true
plimit-lit@1.6.1:
resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==}
engines: {node: '>=12'}
pluralize@8.0.0:
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
engines: {node: '>=4'}
@@ -4944,6 +4965,10 @@ packages:
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
queue-lit@1.5.2:
resolution: {integrity: sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw==}
engines: {node: '>=12'}
queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@@ -5535,6 +5560,11 @@ packages:
ts-pattern@5.8.0:
resolution: {integrity: sha512-kIjN2qmWiHnhgr5DAkAafF9fwb0T5OhMVSWrm8XEdTFnX6+wfXwYOFjeF86UZ54vduqiR7BfqScFmXSzSaH8oA==}
tsc-alias@1.8.16:
resolution: {integrity: sha512-QjCyu55NFyRSBAl6+MTFwplpFcnm2Pq01rR/uxfqJoLMm6X3O14KEGtaSDZpJYaE1bJBGDjD0eSuiIWPe2T58g==}
engines: {node: '>=16.20.2'}
hasBin: true
tsconfck@3.1.6:
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
engines: {node: ^18 || >=20}
@@ -7810,6 +7840,10 @@ snapshots:
dependencies:
tslib: 2.8.1
'@swc/plugin-transform-imports@11.0.0':
dependencies:
'@swc/counter': 0.1.3
'@swc/types@0.1.25':
dependencies:
'@swc/counter': 0.1.3
@@ -8774,6 +8808,8 @@ snapshots:
commander@8.3.0: {}
commander@9.5.0: {}
comment-parser@1.4.1: {}
commondir@1.0.1: {}
@@ -10815,6 +10851,8 @@ snapshots:
ms@2.1.3: {}
mylas@2.1.14: {}
nanoid@3.3.11: {}
napi-postinstall@0.3.3: {}
@@ -11194,6 +11232,10 @@ snapshots:
optionalDependencies:
fsevents: 2.3.2
plimit-lit@1.6.1:
dependencies:
queue-lit: 1.5.2
pluralize@8.0.0: {}
possible-typed-array-names@1.1.0: {}
@@ -11274,6 +11316,8 @@ snapshots:
quansync@0.2.11: {}
queue-lit@1.5.2: {}
queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {}
@@ -11976,6 +12020,16 @@ snapshots:
ts-pattern@5.8.0: {}
tsc-alias@1.8.16:
dependencies:
chokidar: 3.6.0
commander: 9.5.0
get-tsconfig: 4.10.1
globby: 11.1.0
mylas: 2.1.14
normalize-path: 3.0.0
plimit-lit: 1.6.1
tsconfck@3.1.6(typescript@5.7.3):
optionalDependencies:
typescript: 5.7.3

View File

@@ -4,6 +4,14 @@ import { useBillingPlugin } from '../plugin/index'
export const initProviderPayment = async (payload: Payload, payment: Partial<Payment>): Promise<Partial<Payment>> => {
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]) {
throw new Error(`Provider ${payment.provider} not found.`)
}

View File

@@ -7,11 +7,11 @@ import type {
CollectionSlug,
Field,
} from 'payload'
import type { BillingPluginConfig} from '@/plugin/config';
import { defaults } from '@/plugin/config'
import { extractSlug } from '@/plugin/utils'
import { createContextLogger } from '@/utils/logger'
import type { Invoice } from '@/plugin/types'
import type { BillingPluginConfig} from '../plugin/config.js';
import { defaults } from '../plugin/config.js'
import { extractSlug } from '../plugin/utils.js'
import { createContextLogger } from '../utils/logger.js'
import type { Invoice } from '../plugin/types/index.js'
export function createInvoicesCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
const {customerRelationSlug, customerInfoExtractor} = pluginConfig

View File

@@ -78,6 +78,14 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
description: 'Payment description',
},
},
{
name: 'checkoutUrl',
type: 'text',
admin: {
description: 'Checkout URL where user can complete payment (if applicable)',
readOnly: true,
},
},
{
name: 'invoice',
type: 'relationship',
@@ -136,6 +144,18 @@ export function createPaymentsCollection(pluginConfig: BillingPluginConfig): Col
useAsTitle: 'id',
},
fields,
defaultPopulate: {
id: true,
provider: true,
status: true,
amount: true,
currency: true,
description: true,
checkoutUrl: true,
providerId: true,
metadata: true,
providerData: true,
},
hooks: {
afterChange: [
async ({ doc, operation, req, previousDoc }) => {

View File

@@ -4,7 +4,7 @@ import type { Config, Payload } from 'payload'
import { createSingleton } from './singleton'
import type { PaymentProvider } from '../providers/index'
const singleton = createSingleton(Symbol('billingPlugin'))
const singleton = createSingleton(Symbol.for('@xtr-dev/payload-billing'))
type BillingPlugin = {
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 => {
if (pluginConfig.disabled) {

View File

@@ -22,6 +22,10 @@ export interface Payment {
* Payment description
*/
description?: string | null;
/**
* Checkout URL where user can complete payment (if applicable)
*/
checkoutUrl?: string | null;
invoice?: (Id | null) | Invoice;
/**
* Additional metadata for the payment

View File

@@ -14,7 +14,7 @@ import {
import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency'
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]
/**
@@ -155,6 +155,7 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
});
payment.providerId = molliePayment.id
payment.providerData = molliePayment.toPlainObject()
payment.checkoutUrl = molliePayment._links?.checkout?.href || null
return payment
},
} satisfies PaymentProvider

View File

@@ -14,7 +14,7 @@ import {
import { isValidAmount, isValidCurrencyCode } from './currency'
import { createContextLogger } from '../utils/logger'
const symbol = Symbol('stripe')
const symbol = Symbol.for('@xtr-dev/payload-billing/stripe')
export interface StripeProviderConfig {
secretKey: string

View File

@@ -6,7 +6,7 @@ import { handleWebhookError, logWebhookEvent } from './utils'
import { isValidAmount, isValidCurrencyCode } from './currency'
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 setTestModeWarning = () => ((<any>globalThis)[TestModeWarningSymbol] = true)
@@ -492,6 +492,10 @@ export const testProvider = (testConfig: TestProviderConfig) => {
// Set provider ID and data
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 = {
raw: {
id: testPaymentId,
@@ -500,7 +504,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
description: payment.description,
status: 'pending',
testMode: true,
paymentUrl: `${baseUrl}/api/payload-billing/test/payment/${testPaymentId}`,
paymentUrl,
scenarios: scenarios.map(s => ({ id: s.id, name: s.name, description: s.description })),
methods: Object.entries(PAYMENT_METHODS).map(([key, value]) => ({
id: key,
@@ -512,6 +516,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
provider: 'test'
}
payment.providerData = providerData
payment.checkoutUrl = paymentUrl
return payment
},