6 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
1eb9d282b3 fix: use testmode boolean parameter for Mollie payments
Changed from mode: 'test' | 'live' to testmode: boolean as per Mollie
API requirements. The testmode parameter is set to true when the API
key starts with 'test_', false otherwise.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 15:12:09 +01:00
291ce255b4 feat: add automatic mode detection for Mollie payments
Automatically set Mollie payment mode to 'test' or 'live' based on
the API key prefix (test_ or live_). This ensures payments are
created in the correct mode and helps prevent configuration errors.

Changes:
- Add getMollieMode() helper to detect mode from API key
- Include mode parameter in payment creation
- Use type assertion for Mollie client compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 14:45:04 +01:00
2904d30a5c fix: improve error logging with detailed messages and stack traces
Previously, error objects were passed directly to the logger without
proper serialization, resulting in empty error messages like "Error:"
with no details. This made debugging production issues impossible.

Changes:
- Extract error message and stack trace before logging
- Format errors consistently across all providers
- Add stack trace logging for better debugging
- Update test provider error handling

This fixes the issue where webhook and payment update errors showed
no useful information in production logs.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 14:39:52 +01:00
bf6f546371 fix: handle missing toPlainObject in Mollie client responses
Add fallback for Mollie API responses that may not have toPlainObject method
depending on client version or response type.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 21:16:45 +01:00
4 changed files with 78 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@xtr-dev/payload-billing",
"version": "0.1.22",
"version": "0.1.28",
"description": "PayloadCMS plugin for billing and payment provider integrations with tracking and local testing",
"license": "MIT",
"type": "module",

View File

@@ -85,11 +85,15 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
const status = mapMollieStatusToPaymentStatus(molliePayment.status)
// 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(
payload,
payment.id,
status,
molliePayment.toPlainObject(),
providerData,
pluginConfig
)
@@ -167,7 +171,10 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
webhookUrl,
});
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
},

View File

@@ -424,7 +424,8 @@ export const testProvider = (testConfig: TestProviderConfig) => {
setTimeout(() => {
processTestPayment(payload, session, pluginConfig).catch(async (error) => {
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
session.status = 'failed'

View File

@@ -16,7 +16,7 @@ export const webhookResponses = {
// Log error internally but don't expose details
if (payload) {
const logger = createContextLogger(payload, 'Webhook')
logger.error('Error:', message)
logger.error(`Error: ${message}`)
} else {
console.error('[Webhook] Error:', message)
}
@@ -60,6 +60,7 @@ export async function updatePaymentStatus(
pluginConfig: BillingPluginConfig
): Promise<boolean> {
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,19 +139,18 @@ 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')
logger.error(`Failed to update payment ${paymentId}:`, error)
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}`)
if (errorStack) {
logger.error(`Stack trace: ${errorStack}`)
}
return false
}
}
@@ -165,15 +189,22 @@ export function handleWebhookError(
context?: string,
payload?: Payload
): 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`
// Log detailed error internally for debugging
if (payload) {
const logger = createContextLogger(payload, fullContext)
logger.error('Error:', error)
logger.error(`Error: ${message}`)
if (stack) {
logger.error(`Stack trace: ${stack}`)
}
} 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