mirror of
https://github.com/xtr-dev/payload-billing.git
synced 2025-12-10 19:03:23 +00:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f2ab50214b | |||
| 20030b435c | |||
| 1eb9d282b3 | |||
| 291ce255b4 | |||
| 2904d30a5c | |||
| bf6f546371 | |||
| 7e4ec86e00 | |||
| 79de7910d4 | |||
| bb5ba83bc3 | |||
| 79166f7edf | |||
| 6de405d07f | |||
| 7c0b42e35d | |||
| 25b340d818 | |||
| 46bec6bd2e | |||
| 4fde492e0f | |||
| a37757ffa1 | |||
| 1867bb2f96 | |||
| 89578aeba2 |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@xtr-dev/payload-billing",
|
"name": "@xtr-dev/payload-billing",
|
||||||
"version": "0.1.13",
|
"version": "0.1.28",
|
||||||
"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",
|
||||||
@@ -81,6 +81,7 @@
|
|||||||
"@playwright/test": "^1.52.0",
|
"@playwright/test": "^1.52.0",
|
||||||
"@swc-node/register": "1.10.9",
|
"@swc-node/register": "1.10.9",
|
||||||
"@swc/cli": "0.6.0",
|
"@swc/cli": "0.6.0",
|
||||||
|
"@swc/plugin-transform-imports": "^11.0.0",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
"@types/node": "^22.5.4",
|
"@types/node": "^22.5.4",
|
||||||
"@types/react": "19.1.8",
|
"@types/react": "19.1.8",
|
||||||
@@ -105,6 +106,7 @@
|
|||||||
"sort-package-json": "^2.10.0",
|
"sort-package-json": "^2.10.0",
|
||||||
"stripe": "^18.5.0",
|
"stripe": "^18.5.0",
|
||||||
"tailwindcss": "^4.1.17",
|
"tailwindcss": "^4.1.17",
|
||||||
|
"tsc-alias": "^1.8.16",
|
||||||
"typescript": "5.7.3",
|
"typescript": "5.7.3",
|
||||||
"vite-tsconfig-paths": "^5.1.4",
|
"vite-tsconfig-paths": "^5.1.4",
|
||||||
"vitest": "^3.1.2"
|
"vitest": "^3.1.2"
|
||||||
|
|||||||
54
pnpm-lock.yaml
generated
54
pnpm-lock.yaml
generated
@@ -51,6 +51,9 @@ importers:
|
|||||||
'@swc/cli':
|
'@swc/cli':
|
||||||
specifier: 0.6.0
|
specifier: 0.6.0
|
||||||
version: 0.6.0(@swc/core@1.13.5)
|
version: 0.6.0(@swc/core@1.13.5)
|
||||||
|
'@swc/plugin-transform-imports':
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.0.0
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
@@ -123,6 +126,9 @@ importers:
|
|||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.1.17
|
specifier: ^4.1.17
|
||||||
version: 4.1.17
|
version: 4.1.17
|
||||||
|
tsc-alias:
|
||||||
|
specifier: ^1.8.16
|
||||||
|
version: 1.8.16
|
||||||
typescript:
|
typescript:
|
||||||
specifier: 5.7.3
|
specifier: 5.7.3
|
||||||
version: 5.7.3
|
version: 5.7.3
|
||||||
@@ -1990,6 +1996,9 @@ packages:
|
|||||||
'@swc/helpers@0.5.15':
|
'@swc/helpers@0.5.15':
|
||||||
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
|
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':
|
'@swc/types@0.1.25':
|
||||||
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
|
resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
|
||||||
|
|
||||||
@@ -2820,6 +2829,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
|
commander@9.5.0:
|
||||||
|
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
|
||||||
|
engines: {node: ^12.20.0 || >=14}
|
||||||
|
|
||||||
comment-parser@1.4.1:
|
comment-parser@1.4.1:
|
||||||
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
|
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
@@ -4515,6 +4528,10 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
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:
|
nanoid@3.3.11:
|
||||||
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
|
||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
@@ -4838,6 +4855,10 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
plimit-lit@1.6.1:
|
||||||
|
resolution: {integrity: sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA==}
|
||||||
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
pluralize@8.0.0:
|
pluralize@8.0.0:
|
||||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -4944,6 +4965,10 @@ packages:
|
|||||||
quansync@0.2.11:
|
quansync@0.2.11:
|
||||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
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:
|
queue-microtask@1.2.3:
|
||||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||||
|
|
||||||
@@ -5535,6 +5560,11 @@ packages:
|
|||||||
ts-pattern@5.8.0:
|
ts-pattern@5.8.0:
|
||||||
resolution: {integrity: sha512-kIjN2qmWiHnhgr5DAkAafF9fwb0T5OhMVSWrm8XEdTFnX6+wfXwYOFjeF86UZ54vduqiR7BfqScFmXSzSaH8oA==}
|
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:
|
tsconfck@3.1.6:
|
||||||
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==}
|
||||||
engines: {node: ^18 || >=20}
|
engines: {node: ^18 || >=20}
|
||||||
@@ -7810,6 +7840,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
'@swc/plugin-transform-imports@11.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@swc/counter': 0.1.3
|
||||||
|
|
||||||
'@swc/types@0.1.25':
|
'@swc/types@0.1.25':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/counter': 0.1.3
|
'@swc/counter': 0.1.3
|
||||||
@@ -8774,6 +8808,8 @@ snapshots:
|
|||||||
|
|
||||||
commander@8.3.0: {}
|
commander@8.3.0: {}
|
||||||
|
|
||||||
|
commander@9.5.0: {}
|
||||||
|
|
||||||
comment-parser@1.4.1: {}
|
comment-parser@1.4.1: {}
|
||||||
|
|
||||||
commondir@1.0.1: {}
|
commondir@1.0.1: {}
|
||||||
@@ -10815,6 +10851,8 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
mylas@2.1.14: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
napi-postinstall@0.3.3: {}
|
napi-postinstall@0.3.3: {}
|
||||||
@@ -11194,6 +11232,10 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
|
|
||||||
|
plimit-lit@1.6.1:
|
||||||
|
dependencies:
|
||||||
|
queue-lit: 1.5.2
|
||||||
|
|
||||||
pluralize@8.0.0: {}
|
pluralize@8.0.0: {}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
@@ -11274,6 +11316,8 @@ snapshots:
|
|||||||
|
|
||||||
quansync@0.2.11: {}
|
quansync@0.2.11: {}
|
||||||
|
|
||||||
|
queue-lit@1.5.2: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
||||||
quick-format-unescaped@4.0.4: {}
|
quick-format-unescaped@4.0.4: {}
|
||||||
@@ -11976,6 +12020,16 @@ snapshots:
|
|||||||
|
|
||||||
ts-pattern@5.8.0: {}
|
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):
|
tsconfck@3.1.6(typescript@5.7.3):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
typescript: 5.7.3
|
typescript: 5.7.3
|
||||||
|
|||||||
@@ -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.`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ import type {
|
|||||||
CollectionSlug,
|
CollectionSlug,
|
||||||
Field,
|
Field,
|
||||||
} from 'payload'
|
} from 'payload'
|
||||||
import type { BillingPluginConfig} from '@/plugin/config';
|
import type { BillingPluginConfig} from '../plugin/config.js';
|
||||||
import { defaults } from '@/plugin/config'
|
import { defaults } from '../plugin/config.js'
|
||||||
import { extractSlug } from '@/plugin/utils'
|
import { extractSlug } from '../plugin/utils.js'
|
||||||
import { createContextLogger } from '@/utils/logger'
|
import { createContextLogger } from '../utils/logger.js'
|
||||||
import type { Invoice } from '@/plugin/types'
|
import type { Invoice } from '../plugin/types/index.js'
|
||||||
|
|
||||||
export function createInvoicesCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
export function createInvoicesCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
|
||||||
const {customerRelationSlug, customerInfoExtractor} = pluginConfig
|
const {customerRelationSlug, customerInfoExtractor} = pluginConfig
|
||||||
|
|||||||
@@ -78,6 +78,21 @@ 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: 'redirectUrl',
|
||||||
|
type: 'text',
|
||||||
|
admin: {
|
||||||
|
description: 'URL to redirect user after payment completion',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'invoice',
|
name: 'invoice',
|
||||||
type: 'relationship',
|
type: 'relationship',
|
||||||
@@ -136,6 +151,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,14 @@ 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;
|
||||||
|
/**
|
||||||
|
* URL to redirect user after payment completion
|
||||||
|
*/
|
||||||
|
redirectUrl?: 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]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,11 +85,15 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
|
|||||||
const status = mapMollieStatusToPaymentStatus(molliePayment.status)
|
const status = mapMollieStatusToPaymentStatus(molliePayment.status)
|
||||||
|
|
||||||
// Update the payment status and provider data
|
// Update the payment status and provider data
|
||||||
|
// Use toPlainObject if available, otherwise spread the object
|
||||||
|
const providerData = typeof molliePayment.toPlainObject === 'function'
|
||||||
|
? molliePayment.toPlainObject()
|
||||||
|
: { ...molliePayment }
|
||||||
const updateSuccess = await updatePaymentStatus(
|
const updateSuccess = await updatePaymentStatus(
|
||||||
payload,
|
payload,
|
||||||
payment.id,
|
payment.id,
|
||||||
status,
|
status,
|
||||||
molliePayment.toPlainObject(),
|
providerData,
|
||||||
pluginConfig
|
pluginConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,11 +138,24 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Setup URLs with development defaults
|
// Setup URLs with development defaults
|
||||||
|
// Only use localhost fallbacks in non-production environments
|
||||||
const isProduction = process.env.NODE_ENV === 'production'
|
const isProduction = process.env.NODE_ENV === 'production'
|
||||||
const redirectUrl = mollieConfig.redirectUrl ||
|
const serverUrl = process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || process.env.SERVER_URL
|
||||||
(!isProduction ? 'https://localhost:3000/payment/success' : undefined)
|
|
||||||
const webhookUrl = mollieConfig.webhookUrl ||
|
// Priority: payment.redirectUrl > config.redirectUrl > dev fallback
|
||||||
`${process.env.PAYLOAD_PUBLIC_SERVER_URL || (!isProduction ? 'https://localhost:3000' : '')}/api/payload-billing/mollie/webhook`
|
let redirectUrl = payment.redirectUrl || mollieConfig.redirectUrl
|
||||||
|
if (!redirectUrl && !isProduction) {
|
||||||
|
redirectUrl = 'https://localhost:3000/payment/success'
|
||||||
|
}
|
||||||
|
|
||||||
|
let webhookUrl = mollieConfig.webhookUrl
|
||||||
|
if (!webhookUrl) {
|
||||||
|
if (serverUrl) {
|
||||||
|
webhookUrl = `${serverUrl}/api/payload-billing/mollie/webhook`
|
||||||
|
} else if (!isProduction) {
|
||||||
|
webhookUrl = 'https://localhost:3000/api/payload-billing/mollie/webhook'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Validate URLs for production
|
// Validate URLs for production
|
||||||
validateProductionUrl(redirectUrl, 'Redirect')
|
validateProductionUrl(redirectUrl, 'Redirect')
|
||||||
@@ -154,7 +171,11 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
|
|||||||
webhookUrl,
|
webhookUrl,
|
||||||
});
|
});
|
||||||
payment.providerId = molliePayment.id
|
payment.providerId = molliePayment.id
|
||||||
payment.providerData = molliePayment.toPlainObject()
|
// Use toPlainObject if available, otherwise spread the object (for compatibility with different Mollie client versions)
|
||||||
|
payment.providerData = typeof molliePayment.toPlainObject === 'function'
|
||||||
|
? molliePayment.toPlainObject()
|
||||||
|
: { ...molliePayment }
|
||||||
|
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
|
||||||
@@ -234,6 +234,9 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
|
|||||||
|
|
||||||
const stripe = singleton.get(payload)
|
const stripe = singleton.get(payload)
|
||||||
|
|
||||||
|
// Priority: payment.redirectUrl > config.returnUrl
|
||||||
|
const returnUrl = payment.redirectUrl || stripeConfig.returnUrl
|
||||||
|
|
||||||
// Create a payment intent
|
// Create a payment intent
|
||||||
const paymentIntent = await stripe.paymentIntents.create({
|
const paymentIntent = await stripe.paymentIntents.create({
|
||||||
amount: payment.amount, // Stripe handles currency conversion internally
|
amount: payment.amount, // Stripe handles currency conversion internally
|
||||||
@@ -250,6 +253,7 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
|
|||||||
automatic_payment_methods: {
|
automatic_payment_methods: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
...(returnUrl && { return_url: returnUrl }),
|
||||||
})
|
})
|
||||||
|
|
||||||
payment.providerId = paymentIntent.id
|
payment.providerId = paymentIntent.id
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { Payment } from '../plugin/types/payments'
|
import type { Payment } from '../plugin/types/payments'
|
||||||
import type { PaymentProvider, ProviderData } from '../plugin/types/index'
|
import type { PaymentProvider, ProviderData } from '../plugin/types/index'
|
||||||
import type { BillingPluginConfig } from '../plugin/config'
|
import type { BillingPluginConfig } from '../plugin/config'
|
||||||
import type { Payload } from 'payload'
|
import type { CollectionSlug, Payload } from 'payload'
|
||||||
import { handleWebhookError, logWebhookEvent } from './utils'
|
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)
|
||||||
|
|
||||||
@@ -160,6 +160,7 @@ export interface TestPaymentSession {
|
|||||||
method?: PaymentMethod
|
method?: PaymentMethod
|
||||||
createdAt: Date
|
createdAt: Date
|
||||||
status: PaymentOutcome
|
status: PaymentOutcome
|
||||||
|
redirectUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the proper BillingPluginConfig type
|
// Use the proper BillingPluginConfig type
|
||||||
@@ -228,7 +229,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const scenarios = testConfig.scenarios || DEFAULT_SCENARIOS
|
const scenarios = testConfig.scenarios || DEFAULT_SCENARIOS
|
||||||
const baseUrl = testConfig.baseUrl || (process.env.PAYLOAD_PUBLIC_SERVER_URL || 'http://localhost:3000')
|
const baseUrl = testConfig.baseUrl || process.env.NEXT_PUBLIC_SERVER_URL || process.env.PAYLOAD_PUBLIC_SERVER_URL || process.env.SERVER_URL || 'http://localhost:3000'
|
||||||
const uiRoute = testConfig.customUiRoute || '/test-payment'
|
const uiRoute = testConfig.customUiRoute || '/test-payment'
|
||||||
|
|
||||||
// Test mode warnings will be logged in onInit when payload is available
|
// Test mode warnings will be logged in onInit when payload is available
|
||||||
@@ -242,10 +243,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 +269,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 as CollectionSlug,
|
||||||
|
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,
|
||||||
@@ -271,8 +311,11 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine redirect URL: session.redirectUrl > payment.redirectUrl > default
|
||||||
|
const redirectUrl = session.redirectUrl || (session.payment as Payment)?.redirectUrl || `${baseUrl}/payment/success`
|
||||||
|
|
||||||
// Generate test payment UI
|
// Generate test payment UI
|
||||||
const html = generateTestPaymentUI(session, scenarios, uiRoute, baseUrl, testConfig)
|
const html = generateTestPaymentUI(session, scenarios, uiRoute, baseUrl, testConfig, redirectUrl)
|
||||||
return new Response(html, {
|
return new Response(html, {
|
||||||
headers: { 'Content-Type': 'text/html' }
|
headers: { 'Content-Type': 'text/html' }
|
||||||
})
|
})
|
||||||
@@ -322,7 +365,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 as CollectionSlug,
|
||||||
|
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,
|
||||||
@@ -347,7 +424,8 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
processTestPayment(payload, session, pluginConfig).catch(async (error) => {
|
processTestPayment(payload, session, pluginConfig).catch(async (error) => {
|
||||||
const logger = createContextLogger(payload, 'Test Provider')
|
const logger = createContextLogger(payload, 'Test Provider')
|
||||||
logger.error('Failed to process payment:', error)
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
logger.error(`Failed to process payment: ${errorMessage}`)
|
||||||
|
|
||||||
// Ensure session status is updated consistently
|
// Ensure session status is updated consistently
|
||||||
session.status = 'failed'
|
session.status = 'failed'
|
||||||
@@ -398,10 +476,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 +502,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,
|
||||||
@@ -480,18 +597,23 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
// Generate unique test payment ID
|
// Generate unique test payment ID
|
||||||
const testPaymentId = `test_pay_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
const testPaymentId = `test_pay_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
||||||
|
|
||||||
// Create test payment session
|
// Create test payment session with redirect URL
|
||||||
const session = {
|
const session: TestPaymentSession = {
|
||||||
id: testPaymentId,
|
id: testPaymentId,
|
||||||
payment: { ...payment },
|
payment: { ...payment },
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
status: 'pending' as PaymentOutcome
|
status: 'pending' as PaymentOutcome,
|
||||||
|
redirectUrl: payment.redirectUrl || undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
testPaymentSessions.set(testPaymentId, session)
|
testPaymentSessions.set(testPaymentId, session)
|
||||||
|
|
||||||
// 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 +622,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 +634,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
|
|||||||
provider: 'test'
|
provider: 'test'
|
||||||
}
|
}
|
||||||
payment.providerData = providerData
|
payment.providerData = providerData
|
||||||
|
payment.checkoutUrl = paymentUrl
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
},
|
},
|
||||||
@@ -599,7 +722,8 @@ function generateTestPaymentUI(
|
|||||||
scenarios: PaymentScenario[],
|
scenarios: PaymentScenario[],
|
||||||
uiRoute: string,
|
uiRoute: string,
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
testConfig: TestProviderConfig
|
testConfig: TestProviderConfig,
|
||||||
|
redirectUrl: string
|
||||||
): string {
|
): string {
|
||||||
const payment = session.payment
|
const payment = session.payment
|
||||||
const testModeIndicators = testConfig.testModeIndicators || {}
|
const testModeIndicators = testConfig.testModeIndicators || {}
|
||||||
@@ -913,9 +1037,9 @@ function generateTestPaymentUI(
|
|||||||
|
|
||||||
if (result.status === 'paid') {
|
if (result.status === 'paid') {
|
||||||
status.className = 'status success';
|
status.className = 'status success';
|
||||||
status.textContent = '✅ Payment successful!';
|
status.textContent = '✅ Payment successful! Redirecting...';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = '${baseUrl}/success';
|
window.location.href = '${redirectUrl}';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
} else if (result.status === 'failed' || result.status === 'cancelled' || result.status === 'expired') {
|
} else if (result.status === 'failed' || result.status === 'cancelled' || result.status === 'expired') {
|
||||||
status.className = 'status error';
|
status.className = 'status error';
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const webhookResponses = {
|
|||||||
// Log error internally but don't expose details
|
// Log error internally but don't expose details
|
||||||
if (payload) {
|
if (payload) {
|
||||||
const logger = createContextLogger(payload, 'Webhook')
|
const logger = createContextLogger(payload, 'Webhook')
|
||||||
logger.error('Error:', message)
|
logger.error(`Error: ${message}`)
|
||||||
} else {
|
} else {
|
||||||
console.error('[Webhook] Error:', message)
|
console.error('[Webhook] Error:', message)
|
||||||
}
|
}
|
||||||
@@ -60,6 +60,7 @@ export async function updatePaymentStatus(
|
|||||||
pluginConfig: BillingPluginConfig
|
pluginConfig: BillingPluginConfig
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const paymentsCollection = extractSlug(pluginConfig.collections?.payments, defaults.paymentsCollection)
|
const paymentsCollection = extractSlug(pluginConfig.collections?.payments, defaults.paymentsCollection)
|
||||||
|
const logger = createContextLogger(payload, 'Payment Update')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First, fetch the current payment to get the current version
|
// First, fetch the current payment to get the current version
|
||||||
@@ -69,41 +70,65 @@ export async function updatePaymentStatus(
|
|||||||
}) as Payment
|
}) as Payment
|
||||||
|
|
||||||
if (!currentPayment) {
|
if (!currentPayment) {
|
||||||
const logger = createContextLogger(payload, 'Payment Update')
|
|
||||||
logger.error(`Payment ${paymentId} not found`)
|
logger.error(`Payment ${paymentId} not found`)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentVersion = currentPayment.version || 1
|
const currentVersion = currentPayment.version || 1
|
||||||
|
|
||||||
// Attempt to update with optimistic locking
|
// Try to use transactions if supported by the database adapter
|
||||||
// We'll use a transaction to ensure atomicity
|
let transactionID: string | number | null = null
|
||||||
const transactionID = await payload.db.beginTransaction()
|
try {
|
||||||
|
transactionID = await payload.db.beginTransaction()
|
||||||
if (!transactionID) {
|
} catch (error) {
|
||||||
const logger = createContextLogger(payload, 'Payment Update')
|
// Transaction support may not be available in all database adapters
|
||||||
logger.error('Failed to begin transaction')
|
logger.debug('Transactions not supported, falling back to direct update')
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (transactionID) {
|
||||||
// Re-fetch within transaction to ensure consistency
|
// Use transactional update with optimistic locking
|
||||||
const paymentInTransaction = await payload.findByID({
|
try {
|
||||||
collection: paymentsCollection,
|
// Re-fetch within transaction to ensure consistency
|
||||||
id: toPayloadId(paymentId),
|
const paymentInTransaction = await payload.findByID({
|
||||||
req: { transactionID }
|
collection: paymentsCollection,
|
||||||
}) as Payment
|
id: toPayloadId(paymentId),
|
||||||
|
req: { transactionID }
|
||||||
|
}) as Payment
|
||||||
|
|
||||||
// Check if version still matches
|
// Check if version still matches
|
||||||
if ((paymentInTransaction.version || 1) !== currentVersion) {
|
if ((paymentInTransaction.version || 1) !== currentVersion) {
|
||||||
// Version conflict detected - payment was modified by another process
|
// Version conflict detected - payment was modified by another process
|
||||||
const logger = createContextLogger(payload, 'Payment Update')
|
logger.warn(`Version conflict for payment ${paymentId} (expected version: ${currentVersion}, got: ${paymentInTransaction.version})`)
|
||||||
logger.warn(`Version conflict for payment ${paymentId} (expected version: ${currentVersion}, got: ${paymentInTransaction.version})`)
|
await payload.db.rollbackTransaction(transactionID)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update with new version
|
||||||
|
await payload.update({
|
||||||
|
collection: paymentsCollection,
|
||||||
|
id: toPayloadId(paymentId),
|
||||||
|
data: {
|
||||||
|
status,
|
||||||
|
providerData: {
|
||||||
|
...providerData,
|
||||||
|
webhookProcessedAt: new Date().toISOString()
|
||||||
|
},
|
||||||
|
version: currentVersion + 1
|
||||||
|
},
|
||||||
|
req: { transactionID }
|
||||||
|
})
|
||||||
|
|
||||||
|
await payload.db.commitTransaction(transactionID)
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
await payload.db.rollbackTransaction(transactionID)
|
await payload.db.rollbackTransaction(transactionID)
|
||||||
return false
|
throw error
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback: Direct update without transaction support
|
||||||
|
// This is less safe but allows payment updates on databases without transaction support
|
||||||
|
logger.debug('Using direct update without transaction')
|
||||||
|
|
||||||
// Update with new version
|
|
||||||
await payload.update({
|
await payload.update({
|
||||||
collection: paymentsCollection,
|
collection: paymentsCollection,
|
||||||
id: toPayloadId(paymentId),
|
id: toPayloadId(paymentId),
|
||||||
@@ -114,19 +139,18 @@ export async function updatePaymentStatus(
|
|||||||
webhookProcessedAt: new Date().toISOString()
|
webhookProcessedAt: new Date().toISOString()
|
||||||
},
|
},
|
||||||
version: currentVersion + 1
|
version: currentVersion + 1
|
||||||
},
|
}
|
||||||
req: { transactionID }
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await payload.db.commitTransaction(transactionID)
|
|
||||||
return true
|
return true
|
||||||
} catch (error) {
|
|
||||||
await payload.db.rollbackTransaction(transactionID)
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const logger = createContextLogger(payload, 'Payment Update')
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
logger.error(`Failed to update payment ${paymentId}:`, error)
|
const errorStack = error instanceof Error ? error.stack : undefined
|
||||||
|
logger.error(`Failed to update payment ${paymentId}: ${errorMessage}`)
|
||||||
|
if (errorStack) {
|
||||||
|
logger.error(`Stack trace: ${errorStack}`)
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,15 +189,22 @@ export function handleWebhookError(
|
|||||||
context?: string,
|
context?: string,
|
||||||
payload?: Payload
|
payload?: Payload
|
||||||
): Response {
|
): Response {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
const stack = error instanceof Error ? error.stack : undefined
|
||||||
const fullContext = context ? `${provider} Webhook - ${context}` : `${provider} Webhook`
|
const fullContext = context ? `${provider} Webhook - ${context}` : `${provider} Webhook`
|
||||||
|
|
||||||
// Log detailed error internally for debugging
|
// Log detailed error internally for debugging
|
||||||
if (payload) {
|
if (payload) {
|
||||||
const logger = createContextLogger(payload, fullContext)
|
const logger = createContextLogger(payload, fullContext)
|
||||||
logger.error('Error:', error)
|
logger.error(`Error: ${message}`)
|
||||||
|
if (stack) {
|
||||||
|
logger.error(`Stack trace: ${stack}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.error(`[${fullContext}] Error:`, error)
|
console.error(`[${fullContext}] Error: ${message}`)
|
||||||
|
if (stack) {
|
||||||
|
console.error(`[${fullContext}] Stack trace:`, stack)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return generic response to avoid information disclosure
|
// Return generic response to avoid information disclosure
|
||||||
|
|||||||
Reference in New Issue
Block a user