2 Commits

Author SHA1 Message Date
f2ab50214b fix: add fallback for databases without transaction support
Some database adapters don't support transactions, causing payment
updates to fail completely. This change adds graceful fallback to
direct updates when transactions are unavailable.

Changes:
- Try to use transactions if supported
- Fall back to direct update if beginTransaction() fails or returns null
- Add debug logging to track which path is used
- Maintain backward compatibility with transaction-supporting databases

This fixes the "Failed to begin transaction" error in production.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 15:33:18 +01:00
20030b435c revert: remove testmode parameter from Mollie payment creation
Removed the testmode parameter as it was causing issues. Mollie will
automatically determine test/live mode based on the API key used.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 15:22:37 +01:00
3 changed files with 51 additions and 43 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@xtr-dev/payload-billing", "name": "@xtr-dev/payload-billing",
"version": "0.1.26", "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",

View File

@@ -17,13 +17,6 @@ import { createContextLogger } from '../utils/logger'
const symbol = Symbol.for('@xtr-dev/payload-billing/mollie') const symbol = Symbol.for('@xtr-dev/payload-billing/mollie')
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0] export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
/**
* Determine if testmode should be enabled based on API key prefix
*/
function isTestMode(apiKey: string): boolean {
return apiKey.startsWith('test_')
}
/** /**
* Type-safe mapping of Mollie payment status to internal status * Type-safe mapping of Mollie payment status to internal status
*/ */
@@ -168,9 +161,6 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
validateProductionUrl(redirectUrl, 'Redirect') validateProductionUrl(redirectUrl, 'Redirect')
validateProductionUrl(webhookUrl, 'Webhook') validateProductionUrl(webhookUrl, 'Webhook')
// Determine testmode from API key (test_ prefix = true)
const testmode = isTestMode(mollieConfig.apiKey)
const molliePayment = await singleton.get(payload).payments.create({ const molliePayment = await singleton.get(payload).payments.create({
amount: { amount: {
value: formatAmountForProvider(payment.amount, payment.currency), value: formatAmountForProvider(payment.amount, payment.currency),
@@ -179,8 +169,7 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
description: payment.description || '', description: payment.description || '',
redirectUrl, redirectUrl,
webhookUrl, webhookUrl,
testmode, });
} as any);
payment.providerId = molliePayment.id payment.providerId = molliePayment.id
// Use toPlainObject if available, otherwise spread the object (for compatibility with different Mollie client versions) // Use toPlainObject if available, otherwise spread the object (for compatibility with different Mollie client versions)
payment.providerData = typeof molliePayment.toPlainObject === 'function' payment.providerData = typeof molliePayment.toPlainObject === 'function'

View File

@@ -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,18 +139,12 @@ 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) const errorMessage = error instanceof Error ? error.message : String(error)
const errorStack = error instanceof Error ? error.stack : undefined const errorStack = error instanceof Error ? error.stack : undefined
logger.error(`Failed to update payment ${paymentId}: ${errorMessage}`) logger.error(`Failed to update payment ${paymentId}: ${errorMessage}`)