Findings: Email Requirements vs Current Implementation What's Been Implemented Well 1. Financial Category Hierarchy -- All 14 header categories from the 14 emails are seeded with correct parent-child structure: Administrative Expenses, Professional & Consulting Fees, Staff Costs, Occupancy & Facilities Costs, Repairs & Maintenance, Information Technology, Marketing & Business Development, Motor Vehicle & Travel, Finance Costs, Insurance Expenses, Regulatory & Compliance Costs, Bad Debts & Adjustments, Non-Deductible Expenses, Depreciation & Amortisation 2. Subcategories -- 47 subcategories are seeded, closely matching what the emails specified. Each has definition, examples, boundary_rule, and user_guidance populated. 3. Expense Model + ExpenseLine -- Multi-line expense capture with per-line category assignment, VAT handling (gross/net/vat_amount in cents), is_vat_claimable, is_tax_deductible flags. 4. Conditional Context Fields -- Implemented via meta JSON on FinancialCategory + corresponding columns on ExpenseLine: counterparty_name/counterparty_purpose for Client Entertainment beneficiary_type/section_18a_obtained for Sponsorships & Donations vehicle_reference for Motor Vehicle categories travel_details for Travel & Accommodation 5. Expense Lifecycle -- OPEN → LOCKED → paid via allocations. Journal entries posted on lock (ExpenseService + JournalService). 6. Payment + Allocation System -- Full payment types (CLIENT_PAID, EXPENSE_PAYMENT, APPLICATION_FEE_PAYMENT, TRANSFER, CUSTOMER_CREDIT), with polymorphic allocations to Documents, ExpenseLines, and DocumentLines. Reversal logic is complete. 7. Journal Service -- Double-entry accounting for: expense recognition on lock, payment posting, payment reversals, payment adjustments. 8. AI Receipt Agent -- ExpenseReceiptAgent extracts structured data from receipt images, maps to financial categories, handles VAT logic, and populates context fields. 9. System-controlled Categories -- PAYE, UIF (employee), depreciation subcategories, and bad debt subcategories are all marked is_system_controlled = true. 10. Tax Deductibility -- Non-Deductible categories (Fines & Penalties, SARS Penalties & Interest) correctly set is_tax_deductible = false. Gaps & Missing Items 2. Asset Register / Fixed Assets Module (NOT implemented) Email 14 specifies a full asset register (category, description, acquisition date, cost excl VAT, useful life, NBV). No Asset/FixedAsset model exists. Only AssetStatus enum in Finance module. No depreciation calculation engine exists. No monthly auto-posting of depreciation journal entries. Depreciation subcategories are placeholders with no backing logic. 4. Vehicle Insurance Dual Categorization Email 8 lists Vehicle Insurance under Motor Vehicle & Travel Expenses. Email 10 also lists Vehicle Insurance under Insurance Expenses. Currently it exists under both categories in the seeder (6703 under Motor Vehicle and 6903 implied but actual seeder has separate insurance categories). The emails suggest it should only appear once but be cross-referenceable. Worth confirming the intended approach. 6. Loan/HP Capital vs Interest Split UI (NOT implemented) Email 9 requires the ability to split a loan/HP instalment into capital and interest portions. No UI for this exists. The finance cost categories are seeded but there's no mechanism to split payments. 7. Bad Debts & Adjustments Flow (NOT implemented) Email 12 requires: linking to original invoice, debtor selection, transaction type flag (BAD_DEBT/REFUND/CREDIT_NOTE), financial_impact mapping (EXPENSE vs CONTRA_INCOME), approval workflow. All 3 subcategories are marked is_system_controlled but no actual workflow or Filament pages exist for this process. 8. Payroll Integration / PAYE Prevention (NOT implemented) Email 3 specifies that PAYE/UIF/SDL should ideally flow from payroll, not manual capture. System should prevent users from manually posting PAYE as an expense. Categories are marked system-controlled but there's no payroll module or enforcement. 9. Capital vs Expense Threshold Setting (NOT implemented) Email 5 requests an admin-configurable threshold (e.g., R10,000) to flag potential capital expenditure. No such configuration exists. 10. Optional Flags from Emails (NOT implemented) "Related to Legal Matter?" flag on Professional & Consulting Fees (Email 2) "Capitalisable?" flag on advisory fees (Email 2) "Business-only trip" vs "Mixed business & personal" checkbox for travel (Email 8) Policy number / insurer fields for Insurance Expenses (Email 10) Linked Licence/Permit ID for Regulatory costs (Email 11) Invoice number field on expenses (mentioned in multiple emails) 11. Reporting (NOT assessed but likely incomplete) Every email specifies Income Statement line items with drill-down, per-supplier/per-vehicle/per-client filtering, audit pack exports. No evidence of financial statement reporting found, though this may be a future phase. What the AI still needs to work on Point 6: Loan/HP Capital vs Interest Split What Email 9 asks for: When a user captures a loan or HP instalment payment, they need to split the total instalment into: Interest portion → Finance Costs (expense on Income Statement) Capital portion → reduces Loan/HP liability (Balance Sheet movement, not an expense) Current state: The EXPENSE_PAYMENT payment type credits a bank account and debits Accounts Payable. There is no concept of a split payment where part goes to an expense account and part goes to a liability account. Recommended approach: This is fundamentally different from normal expense payments because one payment creates two journal effects. Two options: Option A: New PaymentType LOAN_INSTALMENT (recommended) Add a new PaymentType::LOAN_INSTALMENT case. When creating this payment type, the UI presents three fields: Total instalment, Interest amount (user enters from their statement), and Capital (auto-calculated: total - interest). The PaymentService creates a single Payment record with the total amount. The JournalService posts: Dr Finance Costs GL account (interest portion) Dr Loan/HP Liability account (capital portion) Cr Bank account (total instalment) This requires the user to select which Loan/HP liability account the capital portion reduces. This means we'd need a small set of liability accounts (e.g., "ABSA Term Loan", "Nedbank HP - GWM P500") that the franchise can configure. Option B: Two-line expense approach Capture the instalment as a normal expense with two lines: one line on a Finance Costs category (interest), one line on a liability category (capital). Less clean because it misuses the expense model for a balance-sheet movement. Option A is cleaner and more correct from an accounting perspective. It's a moderate effort: new enum case, new UI form variant in the Payment resource, and a new JournalService method. Point 9: Capital vs Expense Threshold Setting What Email 5 asks for: An admin-configurable monetary threshold (e.g., R10,000) where any single Repairs & Maintenance line exceeding it gets flagged as "Potential Capital Expenditure." Current state: No threshold configuration exists anywhere. Recommended approach: Add a settings JSON column (or a dedicated franchise_settings table if one doesn't exist) on the Franchise model, or use a simple settings table with key-value pairs scoped per franchise. Something like: key: 'capital_expense_threshold' value: 1000000 (R10,000 in cents) In the Expense form, when a user enters an amount_gross on a line that belongs to a Repairs & Maintenance subcategory (or any category flagged with a check_capital_threshold meta flag), compare the amount against the franchise threshold. If exceeded, show a Filament notification/warning similar to the keyword warnings: "This amount exceeds the capital expense threshold (R10,000). This may be a Capital Asset, not a Repair. Please confirm with management or your accountant." No hard block -- just a visible warning. The user can proceed. This is a small piece of work: one config value per franchise, one reactive check in the form's afterStateUpdated callback on the amount field. It should be tackled alongside the keyword guardrails (Point 1 from the gap list) since they share the same UI pattern -- warnings triggered by field values. Point 10: Optional Flags from Emails What the emails ask for: Various optional metadata fields scattered across categories: "Related to Legal Matter?" (Professional & Consulting Fees) "Capitalisable?" (advisory fees) "Business-only trip" vs "Mixed business & personal" (Travel) Policy number / insurer name (Insurance) Linked Licence/Permit ID (Regulatory) Invoice number (multiple categories) Current state: None of these exist. The ExpenseLine model has the 6 context fields (counterparty_name, counterparty_purpose, beneficiary_type, section_18a_obtained, vehicle_reference, travel_details) but nothing beyond that. Recommended approach: Rather than adding a dedicated column for every optional flag (which would bloat the expense_lines table with mostly-null columns), I'd recommend leveraging the same pattern already in place -- the meta JSON column on FinancialCategory + a corresponding meta JSON column on ExpenseLine. Add a meta JSON column to expense_lines -- this becomes a flexible bag for category-specific optional data. Define the optional fields in the category's meta using a convention like: { "requires_counterparty": true, "optional_fields": [ {"key": "related_to_legal_matter", "label": "Related to Legal Matter?", "type": "boolean"}, {"key": "capitalisable", "label": "Potentially Capitalisable?", "type": "boolean"} ] } The Expense form dynamically renders these optional fields based on the selected category's optional_fields meta, storing responses in expense_lines.meta. Priority order for implementation: Invoice number -- this is universal and important enough for its own column on expenses (not just meta). Add invoice_number to the expense table. Policy number / insurer -- relevant for Insurance categories. Good candidate for meta. Related to Legal Matter / Capitalisable -- simple booleans, good candidate for meta. Business vs Mixed travel -- good candidate for meta on Travel & Accommodation. Linked Licence/Permit ID -- this is a future-state item that depends on a Licence Register module existing. Park this one. The JSON meta approach keeps the schema stable, avoids dozens of nullable columns, and lets Jean request new optional fields on specific categories without migrations. The trade-off is that JSON fields aren't directly queryable/indexable, but for optional audit-trail fields that's acceptable.