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.
Data model
Section titled “Data model”TaxDefinition- a single tax rule:rate,type(goods|payment),calculation_method(percentage|fixed_amount),is_statutory/is_rate_lockedflags, and aneffective_from/effective_todate range used to version statutory rate changes without losing historical reporting.TaxProfile- a named, orderable group ofTaxDefinitions (e.g. “Standard VAT”). Either tenant-wide (store_idnull) or store-specific; one profile can be flaggedis_defaultper scope.TaxProfileItem- the pivot row (tax_profile_items) linking aTaxProfileto aTaxDefinition;TaxProfile::taxDefinitions()is abelongsToManythrough it.SaleTaxLine- a persisted goods-tax computation (e.g. VAT) attached to aSale.PaymentTaxLine- a persisted withholding-tax (WHT) computation attached to anInvoicePayment.
Definitions and profiles (setup)
Section titled “Definitions and profiles (setup)”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).
Sale tax computation
Section titled “Sale tax computation”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 fromsale_total + broker_totaland writessale.vat_total/tax_total/has_taxitself.writeSaleTaxLinesPostBreakdown(Sale, taxIds)is called afterSalesService::calculateVatBreakdownhas already run - it does not recompute VAT, it reads the already-computedsale.vat_totalfor the VAT line and only computes non-VAT taxes fresh from the pre-tax subtotal.
Payment (WHT) tax computation
Section titled “Payment (WHT) tax computation”computePaymentTaxLines / applyPaymentTaxLines mirror the sale-side helpers for a gross InvoicePayment amount: persist PaymentTaxLine rows and update invoice_payments.wht_amount.
Reporting
Section titled “Reporting”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.
Route surface
Section titled “Route surface”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 |
Response shape
Section titled “Response shape”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}Related
Section titled “Related”- Sales -
has_tax,vat_total,tax_totalonSale, and theSaleTaxLines this module writes - Accounting -
InvoicePaymentand thePaymentTaxLine(WHT) rows this module writes against it