6 Commits

Author SHA1 Message Date
Bas
7d069e5cf1 Merge pull request #27 from xtr-dev/dev
chore: bump package version to 0.1.11
2025-09-30 21:00:10 +02:00
f7d6066d9a chore: bump package version to 0.1.11 2025-09-30 20:59:53 +02:00
Bas
c5442f9ce2 Merge pull request #26 from xtr-dev/dev
feat: implement structured logging system throughout the codebase
2025-09-20 21:24:59 +02:00
b27b5806b1 fix: resolve inconsistent console usage in logging implementation
- Move Stripe provider webhook warning to onInit where payload is available
- Fix client-side logging in test provider UI generation
- Replace server-side logger calls with browser-compatible console in generated HTML
- Maintain proper logging context separation between server and client code

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 21:21:35 +02:00
da96a0a838 chore: bump package version to 0.1.10 2025-09-20 21:18:40 +02:00
2374dbcec8 feat: implement structured logging system throughout the codebase
- Add logger utility adapted from payload-mailing pattern
- Use PAYLOAD_BILLING_LOG_LEVEL environment variable for configuration
- Replace console.* calls with contextual loggers across providers
- Update webhook utilities to support proper logging
- Export logging utilities for external use
- Maintain fallback console logging for compatibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-20 21:16:55 +02:00
9 changed files with 149 additions and 47 deletions

View File

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

@@ -8,6 +8,7 @@ import {
import type { BillingPluginConfig} from '../plugin/config'; import type { BillingPluginConfig} from '../plugin/config';
import { defaults } from '../plugin/config' import { defaults } from '../plugin/config'
import { extractSlug } from '../plugin/utils' import { extractSlug } from '../plugin/utils'
import { createContextLogger } from '../utils/logger'
import type { Invoice } from '../plugin/types/invoices' import type { Invoice } from '../plugin/types/invoices'
export function createInvoicesCollection(pluginConfig: BillingPluginConfig): CollectionConfig { export function createInvoicesCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
@@ -314,7 +315,8 @@ export function createInvoicesCollection(pluginConfig: BillingPluginConfig): Col
afterChange: [ afterChange: [
({ doc, operation, req }) => { ({ doc, operation, req }) => {
if (operation === 'create') { if (operation === 'create') {
req.payload.logger.info(`Invoice created: ${doc.number}`) const logger = createContextLogger(req.payload, 'Invoices Collection')
logger.info(`Invoice created: ${doc.number}`)
} }
}, },
] satisfies CollectionAfterChangeHook<Invoice>[], ] satisfies CollectionAfterChangeHook<Invoice>[],
@@ -350,7 +352,8 @@ export function createInvoicesCollection(pluginConfig: BillingPluginConfig): Col
data.billingAddress = extractedInfo.billingAddress data.billingAddress = extractedInfo.billingAddress
} }
} catch (error) { } catch (error) {
req.payload.logger.error(`Failed to extract customer info: ${error}`) const logger = createContextLogger(req.payload, 'Invoices Collection')
logger.error(`Failed to extract customer info: ${error}`)
throw new Error('Failed to extract customer information') throw new Error('Failed to extract customer information')
} }
} }

View File

@@ -2,6 +2,7 @@ import type { AccessArgs, CollectionConfig } from 'payload'
import { BillingPluginConfig, defaults } from '../plugin/config' import { BillingPluginConfig, defaults } from '../plugin/config'
import { extractSlug } from '../plugin/utils' import { extractSlug } from '../plugin/utils'
import { Payment } from '../plugin/types/index' import { Payment } from '../plugin/types/index'
import { createContextLogger } from '../utils/logger'
export function createRefundsCollection(pluginConfig: BillingPluginConfig): CollectionConfig { export function createRefundsCollection(pluginConfig: BillingPluginConfig): CollectionConfig {
// TODO: finish collection overrides // TODO: finish collection overrides
@@ -111,7 +112,8 @@ export function createRefundsCollection(pluginConfig: BillingPluginConfig): Coll
afterChange: [ afterChange: [
async ({ doc, operation, req }) => { async ({ doc, operation, req }) => {
if (operation === 'create') { if (operation === 'create') {
req.payload.logger.info(`Refund created: ${doc.id} for payment: ${doc.payment}`) const logger = createContextLogger(req.payload, 'Refunds Collection')
logger.info(`Refund created: ${doc.id} for payment: ${doc.payment}`)
// Update the related payment's refund relationship // Update the related payment's refund relationship
try { try {
@@ -129,7 +131,8 @@ export function createRefundsCollection(pluginConfig: BillingPluginConfig): Coll
}, },
}) })
} catch (error) { } catch (error) {
req.payload.logger.error(`Failed to update payment refunds: ${error}`) const logger = createContextLogger(req.payload, 'Refunds Collection')
logger.error(`Failed to update payment refunds: ${error}`)
} }
} }
}, },

View File

@@ -5,6 +5,9 @@ export type { BillingPluginConfig, CustomerInfoExtractor, AdvancedTestProviderCo
export type { Invoice, Payment, Refund } from './plugin/types/index.js' export type { Invoice, Payment, Refund } from './plugin/types/index.js'
export type { PaymentProvider, ProviderData } from './providers/types.js' export type { PaymentProvider, ProviderData } from './providers/types.js'
// Export logging utilities
export { getPluginLogger, createContextLogger } from './utils/logger.js'
// Export all providers // Export all providers
export { testProvider } from './providers/test.js' export { testProvider } from './providers/test.js'
export type { export type {

View File

@@ -12,6 +12,7 @@ import {
validateProductionUrl validateProductionUrl
} from './utils' } from './utils'
import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency' import { formatAmountForProvider, isValidAmount, isValidCurrencyCode } from './currency'
import { createContextLogger } from '../utils/logger'
const symbol = Symbol('mollie') const symbol = Symbol('mollie')
export type MollieProviderConfig = Parameters<typeof createMollieClient>[0] export type MollieProviderConfig = Parameters<typeof createMollieClient>[0]
@@ -96,12 +97,13 @@ export const mollieProvider = (mollieConfig: MollieProviderConfig & {
if (status === 'succeeded' && updateSuccess) { if (status === 'succeeded' && updateSuccess) {
await updateInvoiceOnPaymentSuccess(payload, payment, pluginConfig) await updateInvoiceOnPaymentSuccess(payload, payment, pluginConfig)
} else if (!updateSuccess) { } else if (!updateSuccess) {
console.warn(`[Mollie Webhook] Failed to update payment ${payment.id}, skipping invoice update`) const logger = createContextLogger(payload, 'Mollie Webhook')
logger.warn(`Failed to update payment ${payment.id}, skipping invoice update`)
} }
return webhookResponses.success() return webhookResponses.success()
} catch (error) { } catch (error) {
return handleWebhookError('Mollie', error) return handleWebhookError('Mollie', error, undefined, req.payload)
} }
} }
} }

View File

@@ -12,6 +12,7 @@ import {
logWebhookEvent logWebhookEvent
} from './utils' } from './utils'
import { isValidAmount, isValidCurrencyCode } from './currency' import { isValidAmount, isValidCurrencyCode } from './currency'
import { createContextLogger } from '../utils/logger'
const symbol = Symbol('stripe') const symbol = Symbol('stripe')
@@ -60,13 +61,13 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
return webhookResponses.missingBody() return webhookResponses.missingBody()
} }
} catch (error) { } catch (error) {
return handleWebhookError('Stripe', error, 'Failed to read request body') return handleWebhookError('Stripe', error, 'Failed to read request body', req.payload)
} }
const signature = req.headers.get('stripe-signature') const signature = req.headers.get('stripe-signature')
if (!signature) { if (!signature) {
return webhookResponses.error('Missing webhook signature', 400) return webhookResponses.error('Missing webhook signature', 400, req.payload)
} }
// webhookSecret is guaranteed to exist since we only register this endpoint when it's configured // webhookSecret is guaranteed to exist since we only register this endpoint when it's configured
@@ -76,7 +77,7 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
try { try {
event = stripe.webhooks.constructEvent(body, signature, stripeConfig.webhookSecret!) event = stripe.webhooks.constructEvent(body, signature, stripeConfig.webhookSecret!)
} catch (err) { } catch (err) {
return handleWebhookError('Stripe', err, 'Signature verification failed') return handleWebhookError('Stripe', err, 'Signature verification failed', req.payload)
} }
// Handle different event types // Handle different event types
@@ -90,7 +91,7 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
const payment = await findPaymentByProviderId(payload, paymentIntent.id, pluginConfig) const payment = await findPaymentByProviderId(payload, paymentIntent.id, pluginConfig)
if (!payment) { if (!payment) {
logWebhookEvent('Stripe', `Payment not found for intent: ${paymentIntent.id}`) logWebhookEvent('Stripe', `Payment not found for intent: ${paymentIntent.id}`, undefined, req.payload)
return webhookResponses.success() // Still return 200 to acknowledge receipt return webhookResponses.success() // Still return 200 to acknowledge receipt
} }
@@ -129,7 +130,8 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
if (status === 'succeeded' && updateSuccess) { if (status === 'succeeded' && updateSuccess) {
await updateInvoiceOnPaymentSuccess(payload, payment, pluginConfig) await updateInvoiceOnPaymentSuccess(payload, payment, pluginConfig)
} else if (!updateSuccess) { } else if (!updateSuccess) {
console.warn(`[Stripe Webhook] Failed to update payment ${payment.id}, skipping invoice update`) const logger = createContextLogger(payload, 'Stripe Webhook')
logger.warn(`Failed to update payment ${payment.id}, skipping invoice update`)
} }
break break
} }
@@ -172,7 +174,8 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
) )
if (!updateSuccess) { if (!updateSuccess) {
console.warn(`[Stripe Webhook] Failed to update refund status for payment ${payment.id}`) const logger = createContextLogger(payload, 'Stripe Webhook')
logger.warn(`Failed to update refund status for payment ${payment.id}`)
} }
} }
break break
@@ -180,19 +183,16 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
default: default:
// Unhandled event type // Unhandled event type
logWebhookEvent('Stripe', `Unhandled event type: ${event.type}`) logWebhookEvent('Stripe', `Unhandled event type: ${event.type}`, undefined, req.payload)
} }
return webhookResponses.success() return webhookResponses.success()
} catch (error) { } catch (error) {
return handleWebhookError('Stripe', error) return handleWebhookError('Stripe', error, undefined, req.payload)
} }
} }
} }
] ]
} else {
// Log that webhook endpoint is not registered
console.warn('[Stripe Provider] Webhook endpoint not registered - webhookSecret not configured')
} }
}, },
onInit: async (payload: Payload) => { onInit: async (payload: Payload) => {
@@ -201,6 +201,12 @@ export const stripeProvider = (stripeConfig: StripeProviderConfig) => {
apiVersion: stripeConfig.apiVersion || DEFAULT_API_VERSION, apiVersion: stripeConfig.apiVersion || DEFAULT_API_VERSION,
}) })
singleton.set(payload, stripe) singleton.set(payload, stripe)
// Log webhook registration status
if (!stripeConfig.webhookSecret) {
const logger = createContextLogger(payload, 'Stripe Provider')
logger.warn('Webhook endpoint not registered - webhookSecret not configured')
}
}, },
initPayment: async (payload, payment) => { initPayment: async (payload, payment) => {
// Validate required fields // Validate required fields

View File

@@ -4,6 +4,12 @@ import type { BillingPluginConfig } from '../plugin/config'
import type { Payload } from 'payload' import type { 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'
const TestModeWarningSymbol = Symbol('TestModeWarning')
const hasGivenTestModeWarning = () => TestModeWarningSymbol in globalThis
const setTestModeWarning = () => ((<any>globalThis)[TestModeWarningSymbol] = true)
// Request validation schemas // Request validation schemas
interface ProcessPaymentRequest { interface ProcessPaymentRequest {
@@ -97,7 +103,8 @@ async function updatePaymentInDatabase(
return { success: true } return { success: true }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown database error' const errorMessage = error instanceof Error ? error.message : 'Unknown database error'
console.error('[Test Provider] Database update failed:', errorMessage) const logger = createContextLogger(payload, 'Test Provider')
logger.error('Database update failed:', errorMessage)
return { success: false, error: errorMessage } return { success: false, error: errorMessage }
} }
} }
@@ -217,17 +224,14 @@ const testPaymentSessions = new Map<string, TestPaymentSession>()
export const testProvider = (testConfig: TestProviderConfig) => { export const testProvider = (testConfig: TestProviderConfig) => {
if (!testConfig.enabled) { if (!testConfig.enabled) {
throw new Error('Test provider is disabled') return
} }
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.PAYLOAD_PUBLIC_SERVER_URL || 'http://localhost:3000')
const uiRoute = testConfig.customUiRoute || '/test-payment' const uiRoute = testConfig.customUiRoute || '/test-payment'
// Log test mode warnings if enabled // Test mode warnings will be logged in onInit when payload is available
if (testConfig.testModeIndicators?.consoleWarnings !== false) {
console.warn('🧪 [TEST PROVIDER] Payment system is running in test mode')
}
return { return {
key: 'test', key: 'test',
@@ -238,7 +242,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
{ {
path: '/payload-billing/test/payment/:id', path: '/payload-billing/test/payment/:id',
method: 'get', method: 'get',
handler: async (req) => { handler: (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] const paymentId = urlParts[urlParts.length - 1]
@@ -342,7 +346,8 @@ export const testProvider = (testConfig: TestProviderConfig) => {
// Process payment after delay // Process payment after delay
setTimeout(() => { setTimeout(() => {
processTestPayment(payload, session, pluginConfig).catch(async (error) => { processTestPayment(payload, session, pluginConfig).catch(async (error) => {
console.error('[Test Provider] Failed to process payment:', error) const logger = createContextLogger(payload, 'Test Provider')
logger.error('Failed to process payment:', error)
// Ensure session status is updated consistently // Ensure session status is updated consistently
session.status = 'failed' session.status = 'failed'
@@ -368,10 +373,11 @@ export const testProvider = (testConfig: TestProviderConfig) => {
) )
if (!dbResult.success) { if (!dbResult.success) {
console.error('[Test Provider] Database error during failure handling:', dbResult.error) const logger = createContextLogger(payload, 'Test Provider')
logger.error('Database error during failure handling:', dbResult.error)
// Even if database update fails, we maintain session consistency // Even if database update fails, we maintain session consistency
} else { } else {
logWebhookEvent('Test Provider', `Payment ${session.id} marked as failed after processing error`) logWebhookEvent('Test Provider', `Payment ${session.id} marked as failed after processing error`, undefined, req.payload)
} }
}) })
}, scenario.delay || testConfig.defaultDelay || 1000) }, scenario.delay || testConfig.defaultDelay || 1000)
@@ -385,7 +391,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
headers: { 'Content-Type': 'application/json' } headers: { 'Content-Type': 'application/json' }
}) })
} catch (error) { } catch (error) {
return handleWebhookError('Test Provider', error, 'Failed to process test payment') return handleWebhookError('Test Provider', error, 'Failed to process test payment', req.payload)
} }
} }
}, },
@@ -433,7 +439,14 @@ export const testProvider = (testConfig: TestProviderConfig) => {
] ]
}, },
onInit: (payload: Payload) => { onInit: (payload: Payload) => {
logWebhookEvent('Test Provider', 'Test payment provider initialized') logWebhookEvent('Test Provider', 'Test payment provider initialized', undefined, payload)
// Log test mode warnings if enabled
if (testConfig.testModeIndicators?.consoleWarnings !== false && !hasGivenTestModeWarning()) {
setTestModeWarning()
const logger = createContextLogger(payload, 'Test Provider')
logger.warn('🧪 Payment system is running in test mode')
}
// Clean up old sessions periodically (older than 1 hour) // Clean up old sessions periodically (older than 1 hour)
setInterval(() => { setInterval(() => {
@@ -445,7 +458,7 @@ export const testProvider = (testConfig: TestProviderConfig) => {
}) })
}, 10 * 60 * 1000) // Clean every 10 minutes }, 10 * 60 * 1000) // Clean every 10 minutes
}, },
initPayment: async (payload, payment) => { initPayment: (payload, payment) => {
// Validate required fields // Validate required fields
if (!payment.amount) { if (!payment.amount) {
throw new Error('Amount is required') throw new Error('Amount is required')
@@ -562,9 +575,10 @@ async function processTestPayment(
) )
if (dbResult.success) { if (dbResult.success) {
logWebhookEvent('Test Provider', `Payment ${session.id} processed with outcome: ${session.scenario.outcome}`) logWebhookEvent('Test Provider', `Payment ${session.id} processed with outcome: ${session.scenario.outcome}`, undefined, payload)
} else { } else {
console.error('[Test Provider] Failed to update payment in database:', dbResult.error) const logger = createContextLogger(payload, 'Test Provider')
logger.error('Failed to update payment in database:', dbResult.error)
// Update session status to indicate database error, but don't throw // Update session status to indicate database error, but don't throw
// This allows the UI to still show the intended test result // This allows the UI to still show the intended test result
session.status = 'failed' session.status = 'failed'
@@ -572,7 +586,8 @@ async function processTestPayment(
} }
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown processing error' const errorMessage = error instanceof Error ? error.message : 'Unknown processing error'
console.error('[Test Provider] Failed to process payment:', errorMessage) const logger = createContextLogger(payload, 'Test Provider')
logger.error('Failed to process payment:', errorMessage)
session.status = 'failed' session.status = 'failed'
throw error // Re-throw to be handled by the caller throw error // Re-throw to be handled by the caller
} }
@@ -913,12 +928,12 @@ function generateTestPaymentUI(
setTimeout(() => pollStatus(), 2000); setTimeout(() => pollStatus(), 2000);
} }
} catch (error) { } catch (error) {
console.error('Failed to poll status:', error); console.error('[Test Provider] Failed to poll status:', error);
} }
} }
${testModeIndicators.consoleWarnings !== false ? ` ${testModeIndicators.consoleWarnings !== false ? `
console.warn('🧪 TEST MODE: This is a simulated payment interface for development purposes'); console.warn('[Test Provider] 🧪 TEST MODE: This is a simulated payment interface for development purposes');
` : ''} ` : ''}
</script> </script>
</body> </body>

View File

@@ -4,6 +4,7 @@ import type { BillingPluginConfig } from '../plugin/config'
import type { ProviderData } from './types' import type { ProviderData } from './types'
import { defaults } from '../plugin/config' import { defaults } from '../plugin/config'
import { extractSlug, toPayloadId } from '../plugin/utils' import { extractSlug, toPayloadId } from '../plugin/utils'
import { createContextLogger } from '../utils/logger'
/** /**
* Common webhook response utilities * Common webhook response utilities
@@ -11,9 +12,14 @@ import { extractSlug, toPayloadId } from '../plugin/utils'
*/ */
export const webhookResponses = { export const webhookResponses = {
success: () => Response.json({ received: true }, { status: 200 }), success: () => Response.json({ received: true }, { status: 200 }),
error: (message: string, status = 400) => { error: (message: string, status = 400, payload?: Payload) => {
// Log error internally but don't expose details // Log error internally but don't expose details
if (payload) {
const logger = createContextLogger(payload, 'Webhook')
logger.error('Error:', message)
} else {
console.error('[Webhook] Error:', message) console.error('[Webhook] Error:', message)
}
return Response.json({ error: 'Invalid request' }, { status }) return Response.json({ error: 'Invalid request' }, { status })
}, },
missingBody: () => Response.json({ received: true }, { status: 200 }), missingBody: () => Response.json({ received: true }, { status: 200 }),
@@ -63,7 +69,8 @@ export async function updatePaymentStatus(
}) as Payment }) as Payment
if (!currentPayment) { if (!currentPayment) {
console.error(`[Payment Update] Payment ${paymentId} not found`) const logger = createContextLogger(payload, 'Payment Update')
logger.error(`Payment ${paymentId} not found`)
return false return false
} }
@@ -74,7 +81,8 @@ export async function updatePaymentStatus(
const transactionID = await payload.db.beginTransaction() const transactionID = await payload.db.beginTransaction()
if (!transactionID) { if (!transactionID) {
console.error(`[Payment Update] Failed to begin transaction`) const logger = createContextLogger(payload, 'Payment Update')
logger.error('Failed to begin transaction')
return false return false
} }
@@ -89,7 +97,8 @@ export async function updatePaymentStatus(
// 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
console.warn(`[Payment Update] Version conflict for payment ${paymentId} (expected version: ${currentVersion}, got: ${paymentInTransaction.version})`) const logger = createContextLogger(payload, 'Payment Update')
logger.warn(`Version conflict for payment ${paymentId} (expected version: ${currentVersion}, got: ${paymentInTransaction.version})`)
await payload.db.rollbackTransaction(transactionID) await payload.db.rollbackTransaction(transactionID)
return false return false
} }
@@ -116,7 +125,8 @@ export async function updatePaymentStatus(
throw error throw error
} }
} catch (error) { } catch (error) {
console.error(`[Payment Update] Failed to update payment ${paymentId}:`, error) const logger = createContextLogger(payload, 'Payment Update')
logger.error(`Failed to update payment ${paymentId}:`, error)
return false return false
} }
} }
@@ -152,13 +162,19 @@ export async function updateInvoiceOnPaymentSuccess(
export function handleWebhookError( export function handleWebhookError(
provider: string, provider: string,
error: unknown, error: unknown,
context?: string context?: string,
payload?: Payload
): Response { ): Response {
const message = error instanceof Error ? error.message : 'Unknown error' const message = error instanceof Error ? error.message : 'Unknown error'
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
console.error(`${fullContext} Error:`, error) if (payload) {
const logger = createContextLogger(payload, fullContext)
logger.error('Error:', error)
} else {
console.error(`[${fullContext}] Error:`, error)
}
// Return generic response to avoid information disclosure // Return generic response to avoid information disclosure
return Response.json({ return Response.json({
@@ -173,9 +189,15 @@ export function handleWebhookError(
export function logWebhookEvent( export function logWebhookEvent(
provider: string, provider: string,
event: string, event: string,
details?: any details?: any,
payload?: Payload
): void { ): void {
if (payload) {
const logger = createContextLogger(payload, `${provider} Webhook`)
logger.info(event, details ? JSON.stringify(details) : '')
} else {
console.log(`[${provider} Webhook] ${event}`, details ? JSON.stringify(details) : '') console.log(`[${provider} Webhook] ${event}`, details ? JSON.stringify(details) : '')
}
} }
/** /**

48
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,48 @@
import type { Payload } from 'payload'
let pluginLogger: any = null
/**
* Get or create the plugin logger instance
* Uses PAYLOAD_BILLING_LOG_LEVEL environment variable to configure log level
* Defaults to 'info' if not set
*/
export function getPluginLogger(payload: Payload) {
if (!pluginLogger && payload.logger) {
const logLevel = process.env.PAYLOAD_BILLING_LOG_LEVEL || 'info'
pluginLogger = payload.logger.child({
level: logLevel,
plugin: '@xtr-dev/payload-billing'
})
// Log the configured log level on first initialization
pluginLogger.info(`Logger initialized with level: ${logLevel}`)
}
// Fallback to console if logger not available (shouldn't happen in normal operation)
if (!pluginLogger) {
return {
debug: (...args: any[]) => console.log('[BILLING DEBUG]', ...args),
info: (...args: any[]) => console.log('[BILLING INFO]', ...args),
warn: (...args: any[]) => console.warn('[BILLING WARN]', ...args),
error: (...args: any[]) => console.error('[BILLING ERROR]', ...args),
}
}
return pluginLogger
}
/**
* Create a context-specific logger for a particular operation
*/
export function createContextLogger(payload: Payload, context: string) {
const logger = getPluginLogger(payload)
return {
debug: (message: string, ...args: any[]) => logger.debug(`[${context}] ${message}`, ...args),
info: (message: string, ...args: any[]) => logger.info(`[${context}] ${message}`, ...args),
warn: (message: string, ...args: any[]) => logger.warn(`[${context}] ${message}`, ...args),
error: (message: string, ...args: any[]) => logger.error(`[${context}] ${message}`, ...args),
}
}