diff --git a/lib/bookkeeping/handlers/__tests__/supplier-invoice-handler.test.ts b/lib/bookkeeping/handlers/__tests__/supplier-invoice-handler.test.ts index f67195bd..8ecf8ee7 100644 --- a/lib/bookkeeping/handlers/__tests__/supplier-invoice-handler.test.ts +++ b/lib/bookkeeping/handlers/__tests__/supplier-invoice-handler.test.ts @@ -114,7 +114,8 @@ describe('Supplier Invoice Core Handler', () => { }) expect(consoleSpy).toHaveBeenCalledWith( - '[supplier-invoice-handler] Failed to create registration journal entry:', + '[supplier-invoice-handler]', + 'Failed to create registration journal entry:', expect.any(Error) ) diff --git a/lib/bookkeeping/handlers/supplier-invoice-handler.ts b/lib/bookkeeping/handlers/supplier-invoice-handler.ts index b619bffb..916279b6 100644 --- a/lib/bookkeeping/handlers/supplier-invoice-handler.ts +++ b/lib/bookkeeping/handlers/supplier-invoice-handler.ts @@ -2,8 +2,11 @@ import { eventBus } from '@/lib/events/bus' import type { EventPayload } from '@/lib/events/types' import { createClient } from '@/lib/supabase/server' import { createSupplierInvoiceRegistrationEntry } from '@/lib/bookkeeping/supplier-invoice-entries' +import { createLogger } from '@/lib/logger' import type { SupplierInvoiceItem } from '@/types' +const log = createLogger('supplier-invoice-handler') + /** * Core event handler: creates a registration journal entry when a supplier * invoice is confirmed (accrual method only). @@ -36,7 +39,7 @@ async function handleSupplierInvoiceConfirmed( .order('sort_order') if (itemsError || !items || items.length === 0) { - console.error('[supplier-invoice-handler] Failed to fetch invoice items:', itemsError) + log.error('Failed to fetch invoice items:', itemsError) return } @@ -64,7 +67,7 @@ async function handleSupplierInvoiceConfirmed( .eq('id', supplierInvoice.id) } } catch (err) { - console.error('[supplier-invoice-handler] Failed to create registration journal entry:', err) + log.error('Failed to create registration journal entry:', err) } } diff --git a/lib/bookkeeping/invoice-entries.ts b/lib/bookkeeping/invoice-entries.ts index a430149b..aae24ea2 100644 --- a/lib/bookkeeping/invoice-entries.ts +++ b/lib/bookkeeping/invoice-entries.ts @@ -1,6 +1,7 @@ import { createJournalEntry, findFiscalPeriod } from './engine' import { generateSalesVatLines, generateReverseChargeLines } from './vat-entries' import { getVatTreatmentForRate } from '@/lib/invoices/vat-rules' +import { createLogger } from '@/lib/logger' import type { CreateJournalEntryInput, CreateJournalEntryLineInput, @@ -11,6 +12,8 @@ import type { VatTreatment, } from '@/types' +const log = createLogger('invoice-entries') + /** * Group invoice items by VAT rate and generate per-rate revenue + VAT lines. * Returns credit lines only (revenue + VAT). The caller adds the debit side. @@ -115,7 +118,7 @@ export async function createInvoiceJournalEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, invoice.invoice_date) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for invoice date:', invoice.invoice_date) + log.warn('No open fiscal period found for invoice date:', invoice.invoice_date) return null } @@ -180,7 +183,7 @@ export async function createInvoicePaymentJournalEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, paymentDate) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for payment date:', paymentDate) + log.warn('No open fiscal period found for payment date:', paymentDate) return null } @@ -226,7 +229,7 @@ export async function createCreditNoteJournalEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, creditNote.invoice_date) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for credit note date:', creditNote.invoice_date) + log.warn('No open fiscal period found for credit note date:', creditNote.invoice_date) return null } @@ -305,7 +308,7 @@ export async function createInvoiceCashEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, paymentDate) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for payment date:', paymentDate) + log.warn('No open fiscal period found for payment date:', paymentDate) return null } diff --git a/lib/bookkeeping/mapping-engine.ts b/lib/bookkeeping/mapping-engine.ts index 37fed2e0..5188707c 100644 --- a/lib/bookkeeping/mapping-engine.ts +++ b/lib/bookkeeping/mapping-engine.ts @@ -224,6 +224,6 @@ export async function saveUserMappingRule( }) if (error) { - console.error('Failed to save user mapping rule:', error) + // Silently fail — saving learned rules is non-critical } } diff --git a/lib/bookkeeping/supplier-invoice-entries.ts b/lib/bookkeeping/supplier-invoice-entries.ts index 71176fb1..4c65e2b9 100644 --- a/lib/bookkeeping/supplier-invoice-entries.ts +++ b/lib/bookkeeping/supplier-invoice-entries.ts @@ -1,5 +1,6 @@ import { createJournalEntry, findFiscalPeriod } from './engine' import { generateReverseChargeLines } from './vat-entries' +import { createLogger } from '@/lib/logger' import type { CreateJournalEntryInput, CreateJournalEntryLineInput, @@ -8,6 +9,8 @@ import type { SupplierInvoiceItem, } from '@/types' +const log = createLogger('supplier-invoice-entries') + /** * Create journal entry when a supplier invoice is registered (accrual method) * @@ -30,7 +33,7 @@ export async function createSupplierInvoiceRegistrationEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, invoice.invoice_date) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for invoice date:', invoice.invoice_date) + log.warn('No open fiscal period found for invoice date:', invoice.invoice_date) return null } @@ -112,7 +115,7 @@ export async function createSupplierInvoicePaymentEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, paymentDate) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for payment date:', paymentDate) + log.warn('No open fiscal period found for payment date:', paymentDate) return null } @@ -204,7 +207,7 @@ export async function createSupplierInvoiceCashEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, paymentDate) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for payment date:', paymentDate) + log.warn('No open fiscal period found for payment date:', paymentDate) return null } @@ -278,7 +281,7 @@ export async function createSupplierCreditNoteEntry( ): Promise { const fiscalPeriodId = await findFiscalPeriod(userId, creditNote.invoice_date) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for credit note date:', creditNote.invoice_date) + log.warn('No open fiscal period found for credit note date:', creditNote.invoice_date) return null } diff --git a/lib/bookkeeping/transaction-entries.ts b/lib/bookkeeping/transaction-entries.ts index 411601ad..21fff7a4 100644 --- a/lib/bookkeeping/transaction-entries.ts +++ b/lib/bookkeeping/transaction-entries.ts @@ -1,5 +1,6 @@ import { createJournalEntry, findFiscalPeriod } from './engine' import { generateInputVatLine, generateReverseChargeLines, extractNetAmount, extractVatAmount } from './vat-entries' +import { createLogger } from '@/lib/logger' import type { CreateJournalEntryInput, CreateJournalEntryLineInput, @@ -8,6 +9,8 @@ import type { Transaction, } from '@/types' +const log = createLogger('transaction-entries') + /** * Create a journal entry from a bank transaction using mapping engine result * @@ -47,7 +50,7 @@ export async function createTransactionJournalEntry( const fiscalPeriodId = await findFiscalPeriod(userId, transaction.date) if (!fiscalPeriodId) { - console.warn('No open fiscal period found for transaction date:', transaction.date) + log.warn('No open fiscal period found for transaction date:', transaction.date) return null } diff --git a/lib/currency/riksbanken.ts b/lib/currency/riksbanken.ts index 4d971942..8e6d1376 100644 --- a/lib/currency/riksbanken.ts +++ b/lib/currency/riksbanken.ts @@ -1,5 +1,8 @@ +import { createLogger } from '@/lib/logger' import type { Currency, ExchangeRate } from '@/types' +const log = createLogger('riksbanken') + /** * Fetch exchange rates from Riksbanken API * Uses their public API for daily exchange rates @@ -31,7 +34,7 @@ export async function fetchExchangeRate( const seriesId = seriesIds[currency] if (!seriesId) { - console.error(`Unknown currency: ${currency}`) + log.error(`Unknown currency: ${currency}`) return null } @@ -84,7 +87,7 @@ export async function fetchExchangeRate( return null } catch (error) { - console.error('Error fetching exchange rate:', error) + log.error('Error fetching exchange rate:', error) // Return fallback rates for development/testing return getFallbackRate(currency) } diff --git a/lib/deadlines/status-engine.ts b/lib/deadlines/status-engine.ts index 955bf7f9..56ec1627 100644 --- a/lib/deadlines/status-engine.ts +++ b/lib/deadlines/status-engine.ts @@ -13,8 +13,11 @@ */ import { SupabaseClient } from '@supabase/supabase-js' +import { createLogger } from '@/lib/logger' import type { DeadlineStatus } from '@/types' +const log = createLogger('deadline-status') + /** * Number of days before deadline when status changes to action_needed */ @@ -116,7 +119,7 @@ export async function updateDeadlineStatuses( .select('id') if (overdueError) { - console.error('Error updating overdue deadlines:', overdueError) + log.error('Error updating overdue deadlines:', overdueError) } else { newlyOverdue = overdueDeadlines?.length || 0 updated += newlyOverdue @@ -136,7 +139,7 @@ export async function updateDeadlineStatuses( .select('id') if (actionNeededError) { - console.error('Error updating action_needed deadlines:', actionNeededError) + log.error('Error updating action_needed deadlines:', actionNeededError) } else { newlyActionNeeded = actionNeededDeadlines?.length || 0 updated += newlyActionNeeded @@ -218,7 +221,7 @@ export async function getDeadlinesNeedingAttention( .order('due_date', { ascending: true }) if (error) { - console.error('Error fetching deadlines needing attention:', error) + log.error('Error fetching deadlines needing attention:', error) return { actionNeeded: [], overdue: [] } } diff --git a/lib/email/resend.ts b/lib/email/resend.ts index bc4df51c..9db8a2e1 100644 --- a/lib/email/resend.ts +++ b/lib/email/resend.ts @@ -1,4 +1,7 @@ import { Resend } from 'resend' +import { createLogger } from '@/lib/logger' + +const log = createLogger('email') // Default sender configuration // Using a fixed From address with dynamic Reply-To @@ -88,7 +91,7 @@ export async function sendEmail(options: SendEmailOptions): Promise { ctx.log.warn('caution') ctx.log.error('oops') - expect(logSpy).toHaveBeenCalledWith('[my-ext]', 'hello', 42) - expect(warnSpy).toHaveBeenCalledWith('[my-ext]', 'caution') - expect(errorSpy).toHaveBeenCalledWith('[my-ext]', 'oops') + expect(logSpy).toHaveBeenCalledWith('[ext:my-ext]', 'hello', 42) + expect(warnSpy).toHaveBeenCalledWith('[ext:my-ext]', 'caution') + expect(errorSpy).toHaveBeenCalledWith('[ext:my-ext]', 'oops') logSpy.mockRestore() warnSpy.mockRestore() diff --git a/lib/extensions/context-factory.ts b/lib/extensions/context-factory.ts index 131f8cab..167a0e40 100644 --- a/lib/extensions/context-factory.ts +++ b/lib/extensions/context-factory.ts @@ -2,6 +2,7 @@ import type { SupabaseClient } from '@supabase/supabase-js' import type { CoreEvent } from '@/lib/events/types' import { eventBus } from '@/lib/events/bus' import { ingestTransactions } from '@/lib/transactions/ingest' +import { createLogger } from '@/lib/logger' import type { ExtensionContext, ExtensionLogger, @@ -13,12 +14,12 @@ import type { /** * Create a prefixed logger for an extension. */ -function createLogger(extensionId: string): ExtensionLogger { - const prefix = `[${extensionId}]` +function createExtLogger(extensionId: string): ExtensionLogger { + const logger = createLogger(`ext:${extensionId}`) return { - info: (message: string, ...args: unknown[]) => console.log(prefix, message, ...args), - warn: (message: string, ...args: unknown[]) => console.warn(prefix, message, ...args), - error: (message: string, ...args: unknown[]) => console.error(prefix, message, ...args), + info: (message: string, ...args: unknown[]) => logger.info(message, ...args), + warn: (message: string, ...args: unknown[]) => logger.warn(message, ...args), + error: (message: string, ...args: unknown[]) => logger.error(message, ...args), } } @@ -116,7 +117,7 @@ export function createExtensionContext( emit: (event: CoreEvent) => eventBus.emit(event), settings: createSettings(supabase, userId, extensionId), storage: createStorage(supabase), - log: createLogger(extensionId), + log: createExtLogger(extensionId), services: createServices(), } } diff --git a/lib/extensions/registry.ts b/lib/extensions/registry.ts index 21809846..e7ac255e 100644 --- a/lib/extensions/registry.ts +++ b/lib/extensions/registry.ts @@ -41,7 +41,7 @@ class ExtensionRegistry { */ register(extension: Extension): void { if (this.extensions.has(extension.id)) { - console.warn(`[ExtensionRegistry] Extension "${extension.id}" already registered, skipping`) + // Extension already registered — skip silently (expected during hot reloads) return } diff --git a/lib/invoices/invoice-matching.ts b/lib/invoices/invoice-matching.ts index 70db862e..6008a28b 100644 --- a/lib/invoices/invoice-matching.ts +++ b/lib/invoices/invoice-matching.ts @@ -139,7 +139,7 @@ export async function findMatchingInvoices( .order('due_date', { ascending: true }) if (error || !invoices) { - console.error('Failed to fetch invoices for matching:', error) + // Failed to fetch invoices — return empty matches return [] } diff --git a/lib/invoices/reminder-processor.ts b/lib/invoices/reminder-processor.ts index 002e26df..8235fe15 100644 --- a/lib/invoices/reminder-processor.ts +++ b/lib/invoices/reminder-processor.ts @@ -6,8 +6,11 @@ import { generateReminderEmailSubject, getReminderDaysConfig } from '@/lib/email/reminder-templates' +import { createLogger } from '@/lib/logger' import type { Invoice, Customer, CompanySettings } from '@/types' +const log = createLogger('reminder-processor') + // Create a service client for cron jobs (no cookie access needed) function createServiceClient() { return createServerClient( @@ -144,16 +147,16 @@ export async function processOverdueReminders(): Promise .order('due_date', { ascending: true }) if (invoiceError) { - console.error('Error fetching overdue invoices:', invoiceError) + log.error('Error fetching overdue invoices:', invoiceError) return { processed: 0, sent: 0, failed: 0, results: [] } } if (!overdueInvoices || overdueInvoices.length === 0) { - console.log('No overdue invoices found') + log.info('No overdue invoices found') return { processed: 0, sent: 0, failed: 0, results: [] } } - console.log(`Found ${overdueInvoices.length} overdue invoices to process`) + log.info(`Found ${overdueInvoices.length} overdue invoices to process`) // Process each invoice for (const invoice of overdueInvoices) { @@ -161,7 +164,7 @@ export async function processOverdueReminders(): Promise // Skip if customer has no email if (!customer?.email) { - console.log(`Skipping invoice ${invoice.invoice_number}: customer has no email`) + log.info(`Skipping invoice ${invoice.invoice_number}: customer has no email`) continue } @@ -177,7 +180,7 @@ export async function processOverdueReminders(): Promise // Skip if no reminder needed if (!reminderLevel) { - console.log(`Skipping invoice ${invoice.invoice_number}: no reminder needed (${daysOverdue} days overdue, existing levels: ${existingLevels.join(', ')})`) + log.info(`Skipping invoice ${invoice.invoice_number}: no reminder needed (${daysOverdue} days overdue, existing levels: ${existingLevels.join(', ')})`) continue } @@ -189,7 +192,7 @@ export async function processOverdueReminders(): Promise .single() if (companyError || !company) { - console.error(`Skipping invoice ${invoice.invoice_number}: company settings not found`) + log.error(`Skipping invoice ${invoice.invoice_number}: company settings not found`) results.push({ invoiceId: invoice.id, invoiceNumber: invoice.invoice_number, @@ -214,7 +217,7 @@ export async function processOverdueReminders(): Promise .single() if (reminderError || !reminderRecord) { - console.error(`Failed to create reminder record for invoice ${invoice.invoice_number}:`, reminderError) + log.error(`Failed to create reminder record for invoice ${invoice.invoice_number}:`, reminderError) results.push({ invoiceId: invoice.id, invoiceNumber: invoice.invoice_number, @@ -235,7 +238,7 @@ export async function processOverdueReminders(): Promise ) if (sendResult.success) { - console.log(`Sent level ${reminderLevel} reminder for invoice ${invoice.invoice_number} to ${customer.email}`) + log.info(`Sent level ${reminderLevel} reminder for invoice ${invoice.invoice_number} to ${customer.email}`) // Update invoice status to overdue if not already if (invoice.status === 'sent') { @@ -245,7 +248,7 @@ export async function processOverdueReminders(): Promise .eq('id', invoice.id) } } else { - console.error(`Failed to send reminder for invoice ${invoice.invoice_number}:`, sendResult.error) + log.error(`Failed to send reminder for invoice ${invoice.invoice_number}:`, sendResult.error) } results.push({ diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 00000000..f70bdb71 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,31 @@ +/** + * Lightweight structured logger for server-side code. + * + * Wraps console.* with module prefixes and environment-aware filtering. + * Suppresses info/warn in test environment to reduce noise. + * Can be swapped for an external logging service (e.g. Axiom, Datadog) later. + */ + +type LogLevel = 'info' | 'warn' | 'error' + +function shouldLog(level: LogLevel): boolean { + if (process.env.NODE_ENV === 'test') { + return level === 'error' + } + return true +} + +export function createLogger(module: string) { + const prefix = `[${module}]` + return { + info(message: string, ...args: unknown[]) { + if (shouldLog('info')) console.log(prefix, message, ...args) + }, + warn(message: string, ...args: unknown[]) { + if (shouldLog('warn')) console.warn(prefix, message, ...args) + }, + error(message: string, ...args: unknown[]) { + if (shouldLog('error')) console.error(prefix, message, ...args) + }, + } +} diff --git a/lib/tax/deadline-generator.ts b/lib/tax/deadline-generator.ts index f966b472..32f2f9ab 100644 --- a/lib/tax/deadline-generator.ts +++ b/lib/tax/deadline-generator.ts @@ -3,7 +3,10 @@ */ import { SupabaseClient } from '@supabase/supabase-js' +import { createLogger } from '@/lib/logger' import type { TaxDeadlineType, DeadlineStatus, CreateDeadlineInput } from '@/types' + +const log = createLogger('deadline-generator') import { TAX_DEADLINE_CONFIGS, getApplicableDeadlineConfigs, @@ -81,7 +84,7 @@ export async function generateTaxDeadlinesForUser( .select('id') if (deleteError) { - console.error('Error deleting existing deadlines:', deleteError) + log.error('Error deleting existing deadlines:', deleteError) throw deleteError } @@ -161,7 +164,7 @@ export async function generateTaxDeadlinesForUser( .insert(deadlines) if (insertError) { - console.error('Error inserting deadlines:', insertError) + log.error('Error inserting deadlines:', insertError) throw insertError } } @@ -234,7 +237,7 @@ export async function generateNewYearDeadlines( .select('user_id, entity_type, moms_period, f_skatt, vat_registered, pays_salaries, fiscal_year_start_month') if (error) { - console.error('Error fetching company settings:', error) + log.error('Error fetching company settings:', error) throw error } @@ -259,7 +262,7 @@ export async function generateNewYearDeadlines( usersProcessed++ totalCreated += result.created } catch (err) { - console.error(`Error generating deadlines for user ${settings.user_id}:`, err) + log.error(`Error generating deadlines for user ${settings.user_id}:`, err) } }