# Financial Module TypeORM Migration Guide ## Overview This guide documents the migration of the Financial module from raw SQL queries to TypeORM. The migration maintains backwards compatibility while introducing modern ORM patterns. ## Completed Tasks ### 1. Entity Creation ✅ All TypeORM entities have been created in `/src/modules/financial/entities/`: - **account-type.entity.ts** - Chart of account types catalog - **account.entity.ts** - Accounts with hierarchy support - **journal.entity.ts** - Accounting journals - **journal-entry.entity.ts** - Journal entries (header) - **journal-entry-line.entity.ts** - Journal entry lines (detail) - **invoice.entity.ts** - Customer and supplier invoices - **invoice-line.entity.ts** - Invoice line items - **payment.entity.ts** - Payment transactions - **tax.entity.ts** - Tax configuration - **fiscal-year.entity.ts** - Fiscal years - **fiscal-period.entity.ts** - Fiscal periods (months/quarters) - **index.ts** - Barrel export file ### 2. Entity Registration ✅ All financial entities have been registered in `/src/config/typeorm.ts`: - Import statements added - Entities added to the `entities` array in AppDataSource configuration ### 3. Service Refactoring ✅ #### accounts.service.ts - COMPLETED The accounts service has been fully migrated to TypeORM with the following features: #### journals.service.ts - COMPLETED (2025-01-04) The journals service has been fully migrated to TypeORM with the following features: **Key Changes:** - Uses `Repository` from TypeORM - Implements QueryBuilder for complex queries with joins (company, defaultAccount) - Uses camelCase properties matching entity definitions (companyId, journalType, etc.) - Maintains all original functionality including: - Unique code validation per company - Journal entry existence check before delete - Soft delete with deletedAt/deletedBy - Full CRUD operations with pagination **API Changes (DTOs now use camelCase):** - `company_id` -> `companyId` - `journal_type` -> `journalType` - `default_account_id` -> `defaultAccountId` - `sequence_id` -> `sequenceId` - `currency_id` -> `currencyId` #### taxes.service.ts - COMPLETED (2025-01-04) The taxes service has been fully migrated to TypeORM with the following features: **Key Changes:** - Uses `Repository` from TypeORM - Uses `In()` operator for batch tax lookups in calculateTaxes() - Implements QueryBuilder for complex queries with company join - Uses TaxType enum from entity for type safety - Maintains all original functionality including: - Unique code validation per tenant - Tax usage check before delete - **PRESERVED: calculateTaxes() and calculateDocumentTaxes() logic unchanged** **API Changes (DTOs now use camelCase):** - `company_id` -> `companyId` - `tax_type` -> `taxType` - `included_in_price` -> `includedInPrice` **Critical Preserved Logic:** - Tax calculation algorithms (lines 321-423 in new file) - Odoo-style VAT calculation for included/excluded prices - Document tax consolidation **Key Changes:** - Uses `Repository` and `Repository` - Implements QueryBuilder for complex queries with joins - Supports both snake_case (DB) and camelCase (TS) through decorators - Maintains all original functionality including: - Account hierarchy with cycle detection - Soft delete with validation - Balance calculations - Full CRUD operations **Pattern to Follow:** ```typescript import { Repository, IsNull } from 'typeorm'; import { AppDataSource } from '../../config/typeorm.js'; import { Entity } from './entities/index.js'; class MyService { private repository: Repository; constructor() { this.repository = AppDataSource.getRepository(Entity); } async findAll(tenantId: string, filters = {}) { const queryBuilder = this.repository .createQueryBuilder('alias') .leftJoin('alias.relation', 'relation') .addSelect(['relation.field']) .where('alias.tenantId = :tenantId', { tenantId }); // Apply filters // Get count and results return { data, total }; } } ``` ## Remaining Tasks ### Services to Migrate #### 1. journals.service.ts - ✅ COMPLETED (2025-01-04) ~~**Current State:** Uses raw SQL queries~~ **Status:** Migrated to TypeORM Repository pattern --- #### 2. taxes.service.ts - ✅ COMPLETED (2025-01-04) ~~**Current State:** Uses raw SQL queries~~ **Status:** Migrated to TypeORM Repository pattern **Note:** Tax calculation logic preserved exactly as specified --- #### 3. journal-entries.service.ts - COMPLETED (2025-01-04) ~~**Current State:** Uses raw SQL with transactions~~ **Status:** Migrated to TypeORM Repository pattern with QueryRunner transactions **Key Changes:** - Uses `Repository` and `Repository` from TypeORM - Uses `QueryRunner` for transaction management (create, update operations) - Implements QueryBuilder for complex queries with joins (company, journal) - Uses camelCase properties matching entity definitions **Critical Preserved Logic:** - Double-entry balance validation (debits must equal credits with 0.01 tolerance) - Minimum 2 lines validation - Status transitions (draft -> posted -> cancelled) - Only draft entries can be modified/deleted - Multi-tenant security with tenantId on all operations **API Changes (DTOs now use camelCase):** - `company_id` -> `companyId` - `journal_id` -> `journalId` - `account_id` -> `accountId` - `partner_id` -> `partnerId` - `date_from` -> `dateFrom` - `date_to` -> `dateTo` - `total_debit` -> `totalDebit` - `total_credit` -> `totalCredit` **Transaction Pattern Used:** ```typescript const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // Operations using queryRunner.manager await queryRunner.manager.save(JournalEntry, entry); await queryRunner.manager.save(JournalEntryLine, line); await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } ``` --- #### 4. invoices.service.ts - ✅ COMPLETED (2025-01-04) ~~**Current State:** Uses raw SQL with complex line management~~ **Status:** Migrated to TypeORM Repository pattern **Key Changes:** - Uses `Repository` and `Repository` from TypeORM - Implements QueryBuilder for main queries, raw SQL for cross-schema joins - Uses camelCase properties matching entity definitions - Preserved all critical business logic: - Tax calculation integration with taxesService.calculateTaxes() - Invoice status workflow (draft -> open -> paid/cancelled) - Sequential number generation (INV-XXXXXX / BILL-XXXXXX) - Line management with automatic total recalculation - Only draft invoices can be modified/deleted - Payment amount tracking (amountPaid, amountResidual) **API Changes (DTOs now use camelCase):** - `company_id` -> `companyId` - `partner_id` -> `partnerId` - `invoice_type` -> `invoiceType` - `invoice_date` -> `invoiceDate` - `due_date` -> `dueDate` - `currency_id` -> `currencyId` - `payment_term_id` -> `paymentTermId` - `journal_id` -> `journalId` - `journal_entry_id` -> `journalEntryId` - `amount_untaxed` -> `amountUntaxed` - `amount_tax` -> `amountTax` - `amount_total` -> `amountTotal` - `amount_paid` -> `amountPaid` - `amount_residual` -> `amountResidual` - `product_id` -> `productId` - `price_unit` -> `priceUnit` - `tax_ids` -> `taxIds` - `uom_id` -> `uomId` - `account_id` -> `accountId` - `date_from` -> `dateFrom` - `date_to` -> `dateTo` **Critical Preserved Logic:** - Tax calculation for invoice lines using taxesService - Invoice totals recalculation from lines (updateTotals method) - Transaction type determination (sales vs purchase) for tax applicability - Cascade delete for invoice lines - Multi-tenant security with tenantId on all operations **Cross-Schema Joins:** - Used raw SQL queries for joins with core.partners, core.currencies, inventory.products - TypeORM QueryBuilder used for financial schema relations only --- #### 5. payments.service.ts - ✅ COMPLETED (2025-01-04) ~~**Current State:** Uses raw SQL with invoice reconciliation~~ **Status:** Migrated to TypeORM Repository pattern with QueryRunner transactions **Key Changes:** - Uses `Repository`, `Repository`, and `Repository` from TypeORM - Created `PaymentInvoice` entity for payment-invoice junction table - Uses `QueryRunner` for transaction management (reconcile, cancel operations) - Implements QueryBuilder for complex queries with joins (company, journal) - Uses raw SQL for cross-schema joins (partners, currencies) - Uses camelCase properties matching entity definitions **Critical Preserved Logic:** - Payment reconciliation workflow with invoice validation - Invoice amount updates (amountPaid, amountResidual, status) - Partner validation (invoice must belong to same partner as payment) - Amount validation (reconciled amount cannot exceed payment amount or invoice residual) - Transaction rollback on errors - Status transitions (draft -> posted -> reconciled -> cancelled) - Only draft payments can be modified/deleted - Reverse reconciliations on cancel - Multi-tenant security with tenantId on all operations **API Changes (DTOs now use camelCase):** - `company_id` -> `companyId` - `partner_id` -> `partnerId` - `payment_type` -> `paymentType` - `payment_method` -> `paymentMethod` - `currency_id` -> `currencyId` - `payment_date` -> `paymentDate` - `journal_id` -> `journalId` - `journal_entry_id` -> `journalEntryId` - `date_from` -> `dateFrom` - `date_to` -> `dateTo` - `invoice_id` -> `invoiceId` - `invoice_number` -> `invoiceNumber` **Transaction Pattern Used:** ```typescript const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // Remove existing payment-invoice links await queryRunner.manager.delete(PaymentInvoice, { paymentId: id }); // Create new payment-invoice links await queryRunner.manager.save(PaymentInvoice, paymentInvoice); // Update invoice amounts await queryRunner.manager.update(Invoice, { id }, { amountPaid, amountResidual, status }); // Update payment status await queryRunner.manager.update(Payment, { id }, { status }); await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } ``` --- #### 6. fiscalPeriods.service.ts - COMPLETED (2025-01-04) ~~**Current State:** Uses raw SQL + database functions~~ **Status:** Migrated to TypeORM Repository pattern **Key Changes:** - Uses `Repository` and `Repository` from TypeORM - Implements QueryBuilder for complex queries with joins (fiscalYear, company) - Uses camelCase properties matching entity definitions - Maintains all original functionality including: - Date overlap validation for years and periods - Database function calls for close/reopen operations (preserved as raw SQL) - Monthly period generation - Period statistics calculation - User name lookup for closedBy field **API Changes (DTOs now use camelCase):** - `company_id` -> `companyId` - `fiscal_year_id` -> `fiscalYearId` - `date_from` -> `dateFrom` - `date_to` -> `dateTo` - `closed_at` -> `closedAt` - `closed_by` -> `closedBy` **Critical Preserved Logic:** - Database functions for close/reopen (lines 443-499): ```typescript await this.fiscalPeriodRepository.query( 'SELECT * FROM financial.close_fiscal_period($1, $2)', [periodId, userId] ); ``` - PostgreSQL OVERLAPS operator for date range validation - Monthly period generation algorithm - Period statistics using raw SQL (fiscal_period_id reference) - Manual snake_case to camelCase mapping for DB function results --- ## Controller Updates ### Accept Both snake_case and camelCase The controller currently only accepts snake_case. Update to support both: **Current:** ```typescript const createAccountSchema = z.object({ company_id: z.string().uuid(), code: z.string(), // ... }); ``` **Updated:** ```typescript const createAccountSchema = z.object({ companyId: z.string().uuid().optional(), company_id: z.string().uuid().optional(), code: z.string(), // ... }).refine( (data) => data.companyId || data.company_id, { message: "Either companyId or company_id is required" } ); // Then normalize before service call: const dto = { companyId: parseResult.data.companyId || parseResult.data.company_id, // ... rest of fields }; ``` **Simpler Approach:** Transform incoming data before validation: ```typescript // Add utility function function toCamelCase(obj: any): any { const camelObj: any = {}; for (const key in obj) { const camelKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase()); camelObj[camelKey] = obj[key]; } return camelObj; } // Use in controller const normalizedBody = toCamelCase(req.body); const parseResult = createAccountSchema.safeParse(normalizedBody); ``` --- ## Migration Patterns ### 1. Repository Setup ```typescript import { Repository, IsNull } from 'typeorm'; import { AppDataSource } from '../../config/typeorm.js'; import { MyEntity } from './entities/index.js'; class MyService { private repository: Repository; constructor() { this.repository = AppDataSource.getRepository(MyEntity); } } ``` ### 2. Simple Find Operations **Before (Raw SQL):** ```typescript const result = await queryOne( `SELECT * FROM schema.table WHERE id = $1 AND tenant_id = $2`, [id, tenantId] ); ``` **After (TypeORM):** ```typescript const result = await this.repository.findOne({ where: { id, tenantId, deletedAt: IsNull() } }); ``` ### 3. Complex Queries with Joins **Before:** ```typescript const data = await query( `SELECT e.*, r.name as relation_name FROM schema.entities e LEFT JOIN schema.relations r ON e.relation_id = r.id WHERE e.tenant_id = $1`, [tenantId] ); ``` **After:** ```typescript const data = await this.repository .createQueryBuilder('entity') .leftJoin('entity.relation', 'relation') .addSelect(['relation.name']) .where('entity.tenantId = :tenantId', { tenantId }) .getMany(); ``` ### 4. Transactions **Before:** ```typescript const client = await getClient(); try { await client.query('BEGIN'); // operations await client.query('COMMIT'); } catch (error) { await client.query('ROLLBACK'); throw error; } finally { client.release(); } ``` **After:** ```typescript const queryRunner = AppDataSource.createQueryRunner(); await queryRunner.connect(); await queryRunner.startTransaction(); try { // operations using queryRunner.manager await queryRunner.manager.save(entity); await queryRunner.commitTransaction(); } catch (error) { await queryRunner.rollbackTransaction(); throw error; } finally { await queryRunner.release(); } ``` ### 5. Soft Deletes **Pattern:** ```typescript await this.repository.update( { id, tenantId }, { deletedAt: new Date(), deletedBy: userId, } ); ``` ### 6. Pagination ```typescript const skip = (page - 1) * limit; const [data, total] = await this.repository.findAndCount({ where: { tenantId, deletedAt: IsNull() }, skip, take: limit, order: { createdAt: 'DESC' }, }); return { data, total }; ``` --- ## Testing Strategy ### 1. Unit Tests For each refactored service: ```typescript describe('AccountsService', () => { let service: AccountsService; let repository: Repository; beforeEach(() => { repository = AppDataSource.getRepository(Account); service = new AccountsService(); }); it('should create account with valid data', async () => { const dto = { /* ... */ }; const result = await service.create(dto, tenantId, userId); expect(result.id).toBeDefined(); expect(result.code).toBe(dto.code); }); }); ``` ### 2. Integration Tests Test with actual database: ```bash # Run tests npm test src/modules/financial/__tests__/ ``` ### 3. API Tests Test HTTP endpoints: ```bash # Test accounts endpoints curl -X GET http://localhost:3000/api/financial/accounts?companyId=xxx curl -X POST http://localhost:3000/api/financial/accounts -d '{"companyId":"xxx",...}' ``` --- ## Rollback Plan If migration causes issues: 1. **Restore Old Services:** ```bash cd src/modules/financial mv accounts.service.ts accounts.service.new.ts mv accounts.service.old.ts accounts.service.ts ``` 2. **Remove Entity Imports:** Edit `/src/config/typeorm.ts` and remove financial entity imports 3. **Restart Application:** ```bash npm run dev ``` --- ## Database Schema Notes ### Schema: `financial` All tables use the `financial` schema as specified in entities. ### Important Columns: - **tenant_id**: Multi-tenancy isolation (UUID, NOT NULL) - **company_id**: Company isolation (UUID, NOT NULL) - **deleted_at**: Soft delete timestamp (NULL = active) - **created_at**: Audit timestamp - **created_by**: User ID who created (UUID) - **updated_at**: Audit timestamp - **updated_by**: User ID who updated (UUID) ### Decimal Precision: - **Amounts**: DECIMAL(15, 2) - invoices, payments - **Quantity**: DECIMAL(15, 4) - invoice lines - **Tax Rate**: DECIMAL(5, 2) - tax percentage --- ## Common Issues and Solutions ### Issue 1: Column Name Mismatch **Error:** `column "companyId" does not exist` **Solution:** Entity decorators map camelCase to snake_case: ```typescript @Column({ name: 'company_id' }) companyId: string; ``` ### Issue 2: Soft Deletes Not Working **Solution:** Always include `deletedAt: IsNull()` in where clauses: ```typescript where: { id, tenantId, deletedAt: IsNull() } ``` ### Issue 3: Transaction Not Rolling Back **Solution:** Always use try-catch-finally with queryRunner: ```typescript finally { await queryRunner.release(); // MUST release } ``` ### Issue 4: Relations Not Loading **Solution:** Use leftJoin or relations option: ```typescript // Option 1: Query Builder .leftJoin('entity.relation', 'relation') .addSelect(['relation.field']) // Option 2: Find options findOne({ where: { id }, relations: ['relation'], }) ``` --- ## Performance Considerations ### 1. Query Optimization - Use `leftJoin` + `addSelect` instead of `relations` option for better control - Add indexes on frequently queried columns (already in entities) - Use pagination for large result sets ### 2. Connection Pooling TypeORM pool configuration (in typeorm.ts): ```typescript extra: { max: 10, // Conservative to not compete with pg pool min: 2, idleTimeoutMillis: 30000, } ``` ### 3. Caching Currently disabled: ```typescript cache: false ``` Can enable later for read-heavy operations. --- ## Next Steps 1. **Complete service migrations** in this order: - ~~taxes.service.ts (High priority, simple)~~ ✅ DONE - ~~journals.service.ts (High priority, simple)~~ ✅ DONE - ~~journal-entries.service.ts (Medium, complex transactions)~~ ✅ DONE - ~~payments.service.ts (Medium, reconciliation)~~ ✅ DONE - ~~invoices.service.ts (Medium, tax integration)~~ ✅ DONE - ~~fiscalPeriods.service.ts (Low, DB functions)~~ ✅ DONE 2. **Update controller** to accept both snake_case and camelCase 3. **Write tests** for each migrated service 4. **Update API documentation** to reflect camelCase support 5. **Monitor performance** after deployment --- ## Support and Questions For questions about this migration: - Check existing patterns in `accounts.service.ts` - Review TypeORM documentation: https://typeorm.io - Check entity definitions in `/entities/` folder --- ## Changelog ### 2025-01-04 - Completed fiscalPeriods.service.ts migration to TypeORM - Replaced raw SQL with Repository pattern for FiscalYear and FiscalPeriod - Implemented QueryBuilder for complex queries with joins (fiscalYear, company) - Preserved database function calls for close/reopen operations using repository.query() - Preserved all critical business logic: - Date overlap validation using PostgreSQL OVERLAPS operator - Monthly period generation algorithm - Period statistics calculation - User name lookup for closedBy field - Manual snake_case to camelCase mapping for database function results - Converted DTOs to camelCase - Added comprehensive logging - Completed payments.service.ts migration to TypeORM - Created PaymentInvoice entity for payment-invoice junction table - Replaced raw SQL with Repository pattern for Payment, PaymentInvoice, and Invoice - Used QueryRunner for transaction management (reconcile, cancel operations) - Used QueryBuilder for main queries, raw SQL for cross-schema joins (partners, currencies) - Preserved all critical business logic: - Payment reconciliation workflow with invoice validation - Invoice amount updates (amountPaid, amountResidual, status) - Partner validation (invoice must belong to same partner as payment) - Amount validation (reconciled amount cannot exceed payment or invoice residual) - Status transitions (draft -> posted -> reconciled -> cancelled) - Only draft payments can be modified/deleted - Reverse reconciliations on cancel - Converted DTOs to camelCase - Added comprehensive logging - Completed invoices.service.ts migration to TypeORM - Replaced raw SQL with Repository pattern for Invoice and InvoiceLine - Used QueryBuilder for main queries, raw SQL for cross-schema joins - Preserved all critical business logic: - Tax calculation integration with taxesService - Invoice status workflow (draft -> open -> paid/cancelled) - Sequential number generation (INV-XXXXXX / BILL-XXXXXX) - Line management with automatic total recalculation - Payment tracking (amountPaid, amountResidual) - Converted DTOs to camelCase - Added comprehensive logging - Completed journal-entries.service.ts migration to TypeORM - Replaced raw SQL with Repository pattern - Used QueryRunner for transaction management (create, update) - Implemented QueryBuilder for complex queries with joins - Preserved all accounting logic: - Double-entry balance validation (debits = credits) - Minimum 2 lines validation - Status transitions (draft -> posted -> cancelled) - Only draft entries can be modified/deleted - Converted DTOs to camelCase - Completed journals.service.ts migration to TypeORM - Replaced raw SQL with Repository pattern - Implemented QueryBuilder for joins - Converted DTOs to camelCase - Completed taxes.service.ts migration to TypeORM - Replaced raw SQL CRUD with Repository pattern - Used In() operator for batch tax lookups - Preserved calculateTaxes() and calculateDocumentTaxes() logic exactly - Converted DTOs to camelCase - Updated MIGRATION_GUIDE.md with progress ### 2024-12-14 - Created all TypeORM entities - Registered entities in AppDataSource - Completed accounts.service.ts migration - Created this migration guide