From f2ab50214bad5228aefcb9e5e9ce38e8b311e442 Mon Sep 17 00:00:00 2001 From: Bas van den Aakster Date: Fri, 5 Dec 2025 15:33:18 +0100 Subject: [PATCH] fix: add fallback for databases without transaction support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 2 +- src/providers/utils.ts | 79 ++++++++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/package.json b/package.json index 79f2d67..1d9e73d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@xtr-dev/payload-billing", - "version": "0.1.27", + "version": "0.1.28", "description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing", "license": "MIT", "type": "module", diff --git a/src/providers/utils.ts b/src/providers/utils.ts index ec27016..9aae574 100644 --- a/src/providers/utils.ts +++ b/src/providers/utils.ts @@ -60,6 +60,7 @@ export async function updatePaymentStatus( pluginConfig: BillingPluginConfig ): Promise { const paymentsCollection = extractSlug(pluginConfig.collections?.payments, defaults.paymentsCollection) + const logger = createContextLogger(payload, 'Payment Update') try { // First, fetch the current payment to get the current version @@ -69,41 +70,65 @@ export async function updatePaymentStatus( }) as Payment if (!currentPayment) { - const logger = createContextLogger(payload, 'Payment Update') logger.error(`Payment ${paymentId} not found`) return false } const currentVersion = currentPayment.version || 1 - // Attempt to update with optimistic locking - // We'll use a transaction to ensure atomicity - const transactionID = await payload.db.beginTransaction() - - if (!transactionID) { - const logger = createContextLogger(payload, 'Payment Update') - logger.error('Failed to begin transaction') - return false + // Try to use transactions if supported by the database adapter + let transactionID: string | number | null = null + try { + transactionID = await payload.db.beginTransaction() + } catch (error) { + // Transaction support may not be available in all database adapters + logger.debug('Transactions not supported, falling back to direct update') } - try { - // Re-fetch within transaction to ensure consistency - const paymentInTransaction = await payload.findByID({ - collection: paymentsCollection, - id: toPayloadId(paymentId), - req: { transactionID } - }) as Payment + if (transactionID) { + // Use transactional update with optimistic locking + try { + // Re-fetch within transaction to ensure consistency + const paymentInTransaction = await payload.findByID({ + collection: paymentsCollection, + id: toPayloadId(paymentId), + req: { transactionID } + }) as Payment - // Check if version still matches - if ((paymentInTransaction.version || 1) !== currentVersion) { - // 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})`) + // Check if version still matches + if ((paymentInTransaction.version || 1) !== currentVersion) { + // Version conflict detected - payment was modified by another process + 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) - 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({ collection: paymentsCollection, id: toPayloadId(paymentId), @@ -114,18 +139,12 @@ export async function updatePaymentStatus( webhookProcessedAt: new Date().toISOString() }, version: currentVersion + 1 - }, - req: { transactionID } + } }) - await payload.db.commitTransaction(transactionID) return true - } catch (error) { - await payload.db.rollbackTransaction(transactionID) - throw error } } catch (error) { - const logger = createContextLogger(payload, 'Payment Update') const errorMessage = error instanceof Error ? error.message : String(error) const errorStack = error instanceof Error ? error.stack : undefined logger.error(`Failed to update payment ${paymentId}: ${errorMessage}`)