Skip to content

Tax

Location: dg_smart_pos_api/modules/Tax. Owns tax rules (definitions and named profiles grouping them) and the computed tax lines attached to sales and payments. It does not own the sale or payment records themselves - Modules\Sales\Models\Sale and App\Models\InvoicePayment (used by Accounting) are updated by this module’s service, not defined by it.

  • TaxDefinition - a single tax rule: rate, type (goods|payment), calculation_method (percentage|fixed_amount), is_statutory/is_rate_locked flags, and an effective_from/effective_to date range used to version statutory rate changes without losing historical reporting.
  • TaxProfile - a named, orderable group of TaxDefinitions (e.g. “Standard VAT”). Either tenant-wide (store_id null) or store-specific; one profile can be flagged is_default per scope.
  • TaxProfileItem - the pivot row (tax_profile_items) linking a TaxProfile to a TaxDefinition; TaxProfile::taxDefinitions() is a belongsToMany through it.
  • SaleTaxLine - a persisted goods-tax computation (e.g. VAT) attached to a Sale.
  • PaymentTaxLine - a persisted withholding-tax (WHT) computation attached to an InvoicePayment.

TaxDefinitionController: index (filter by active_only, type), store (create a custom tax - always forced non-statutory and non-rate-locked), update (rate changes on a rate-locked definition are blocked unless the caller isSuperAdmin()), destroy (a statutory definition can’t be deleted, only deactivated; a non-statutory one is deactivated then hard-deleted), newRateVersion (super-admin only - closes the current record’s effective_to at today and creates a new versioned record starting tomorrow, so a known-in-advance rate change doesn’t retroactively change historical tax lines).

TaxProfileController: index (profiles visible to a store: tenant-wide or that store’s own), store (create profile, sync tax_definition_ids), update, destroy (blocks deleting a tenant-wide default profile), setDefault (clears any other default in the same scope, then sets this one default and pins it to the calling store).

TaxService is injected elsewhere (not a controller) and has two separate entry points for writing SaleTaxLines, used at different points in the Sales flow:

  • applySaleTaxLines(Sale, taxIds) computes tax fresh from sale_total + broker_total and writes sale.vat_total/tax_total/has_tax itself.
  • writeSaleTaxLinesPostBreakdown(Sale, taxIds) is called after SalesService::calculateVatBreakdown has already run - it does not recompute VAT, it reads the already-computed sale.vat_total for the VAT line and only computes non-VAT taxes fresh from the pre-tax subtotal.

computePaymentTaxLines / applyPaymentTaxLines mirror the sale-side helpers for a gross InvoicePayment amount: persist PaymentTaxLine rows and update invoice_payments.wht_amount.

TaxReportController: report - a unified, paginated ledger that unions SaleTaxLine and PaymentTaxLine rows (filterable by from/to, type, code); summary - aggregate total_taxable/total_tax/transaction_count grouped by tax_code, split by goods vs payment.

Registered as a tenant route module (Tax is in ModulesServiceProvider::TENANT_ROUTE_MODULES), required once from routes/tenant.php. All routes: auth:sanctum, prefix v1_tax/stores/{store} (from modules/Tax/Routes/tax.php):

Method Path Controller method
GET tax-definitions TaxDefinitionController::index
POST tax-definitions TaxDefinitionController::store
PUT tax-definitions/{taxDefinition} TaxDefinitionController::update
DELETE tax-definitions/{taxDefinition} TaxDefinitionController::destroy
POST tax-definitions/{taxDefinition}/new-rate TaxDefinitionController::newRateVersion
GET tax-profiles TaxProfileController::index
POST tax-profiles TaxProfileController::store
PUT tax-profiles/{taxProfile} TaxProfileController::update
DELETE tax-profiles/{taxProfile} TaxProfileController::destroy
PUT tax-profiles/{taxProfile}/set-default TaxProfileController::setDefault
GET tax-report TaxReportController::report
GET tax-summary TaxReportController::summary

None of the three controllers use API Resource classes - they return the Eloquent model/collection directly under a data key, so the JSON is exactly the model’s fillable plus casts. A tax definition:

{
"id": 1,
"name": "VAT",
"code": "VAT",
"rate": "16.0000",
"type": "goods",
"calculation_method": "percentage",
"application_order": 0,
"is_statutory": true,
"is_rate_locked": true,
"is_active": true,
"description": null,
"effective_from": "2026-01-01",
"effective_to": null
}
  • Sales - has_tax, vat_total, tax_total on Sale, and the SaleTaxLines this module writes
  • Accounting - InvoicePayment and the PaymentTaxLine (WHT) rows this module writes against it