Skip to content

Tax Module Documentation

Overview

The Tax Module is the centralized calculation engine for the ERP. Instead of scattering tax logic across Front Desk (Rooms), Restaurant (Food), and Accounting (Bills), a single Tax Engine handles all jurisdiction logic. It supports compound taxes, multiple jurisdictions (City/State/Federal), and granular reporting.

Key Features

  • Centralized Rules: Define "VAT" once, apply it to "Rooms", "Food", and "Services".
  • Jurisdiction Aware: Supports multi-layer taxes (e.g., 5% City Tax + 8% State Tax).
  • Granular Storage: Saves the exact tax breakdown for every single line item in the database (tax_calculations).
  • Audit Ready: Every calculated cent is traceable back to the specific Rate and Rule used.

Architecture

Domain Layer (app/Domain/Tax)

Models (4 Models)

TaxJurisdiction (TaxJurisdiction.php)
  • Table: tax_jurisdictions
  • Description: Geographic scope (e.g., "Mogadishu City", "Benadir Region", "Federal").
  • Key Fields:
    • name: Label.
    • type: CITY | STATE | FEDERAL.
TaxRate (TaxRate.php)
  • Table: tax_rates
  • Description: The numeric definition.
  • Key Fields:
    • code: Unique code (e.g., VAT-STD).
    • rate: Decimal format (0.18 for 18%).
    • is_compound: Boolean (Does it apply on top of other taxes?).
TaxRule (TaxRule.php)
  • Table: tax_rules
  • Description: The "Logic" that connects a Rate to an Item Type.
  • Key Fields:
    • applies_to: Target Enum (ROOM, FOOD, BEVERAGE, SERVICE, ALL).
    • tax_jurisdiction_id: Scope.
    • priority: Calculation order.
TaxCalculation (TaxCalculation.php)
  • Table: tax_calculations
  • Description: The persistent result of a calculation.
  • Key Fields:
    • source_table: Polymorphic (e.g., folio_lines).
    • source_line_id: ID of the item.
    • tax_rate_id: Which tax this chunk represents.
    • taxable_amount: Base.
    • tax_amount: Calculated Value.

Services

TaxEngine (TaxEngine.php)

Purpose: Stateless service processing all tax math.

Key Methods:

calculate(itemType, amount, jurisdictionId)

Returns a breakdown of applicable taxes.

  • Logic:
    1. Queries active TaxRules where applies_to == itemType.
    2. Loads linked TaxRates.
    3. Iterates: tax = amount * rate.
    4. Returns Collection: [ { rate_name: 'VAT', amount: 18.00 }, ... ].
    • Note: Currently implements simple additive tax. Compound logic is stubbed.
storeCalculations(sourceTable, sourceId, calculations)

Persists the math for audit trails.

  • Logic:
    1. Idempotency: Deletes ANY existing rows for this source_table/source_id pair.
    2. Insert: Bulk creates new TaxCalculation records.
    3. Context: Uses app('current_property_id') to scope data.

Integration Workflows

1. Room Charge Posting (Front Desk)

  1. Event: Night Audit runs room posting.
  2. Call: TaxEngine::calculate('ROOM', 100.00).
  3. Result: Engine returns [{'VAT', 10.00}, {'City', 2.00}].
  4. Action:
    • FrontDesk creates FolioLine (Amount: 100.00, Tax: 12.00).
    • FrontDesk calls TaxEngine::storeCalculations('folio_lines', lineId, result).

2. Restaurant Bill (POS)

  1. Event: Waiter prints check for Pasta ($20).
  2. Call: TaxEngine::calculate('FOOD', 20.00).
  3. Result: Engine returns [{'VAT', 2.00}].
  4. Action: POS displays "Tax: $2.00".

Audit Findings & Improvements

Strengths

  • Data Granularity: Unlike many systems that just store "Tax: $5.00", this module stores exactly which taxes made up that $5.00. This is crucial for filing returns where City and Federal taxes must be remitted separately.
  • Idempotent Storage: The storeCalculations method safely handles re-calculations (e.g., if a bill is edited) by clearing old records first.

Issues Identified

Major

  • Missing Compound Logic: The calculate method blindly iterates and multiplies amount * rate. It ignores the is_compound flag on TaxRate, meaning taxes that should be "Tax on Tax" are currently calculated as simple tax.
    • Impact: Potential under-collection of tax if compound rules differ by jurisdiction.
  • Unsafe Property Scope: storeCalculations falls back to app('current_property_id') ?? 1. In a queued job (e.g., Night Audit running in background), app() might not have the Tenant Context set, incorrectly assigning taxes to Property ID 1.

Minor

  • Testing Complexity: Because storeCalculations performs a Delete + Insert, customized "Audit Logs" on the tax_calculations table would be noisy (showing delete/create pairs).

Configuration

Config: None. Data-driven via tax_rules table.

Module Version: 1.0 Status: Beta (Compound Logic Pending)