Book 5, Chapter 19 — Fuel Management¶
Static-analysis documentation of the Bizwiz Laravel ERP. All claims are cited to file:line from the code at /Users/melaniefayne/Desktop/retail-pay/bizwiz. Nothing here was produced by running the application.
1. Purpose (plain language)¶
Fuel is one of the largest recurring operating costs for a distribution business that runs a delivery fleet, so Bizwiz treats it as a full sub-ledger of its own rather than a simple expense line. The Fuel Management module answers three practical questions: what did we authorise a vehicle to burn on a route, what did the driver actually pump at the station, and what did the fuel supplier actually bill us — and then it forces those three numbers to agree before any money is paid.
The lifecycle starts with a Fuel LPO (Local Purchase Order). A dispatcher creates an LPO that ties a vehicle, a route, a driver, and a fuelling station together and estimates how much fuel that trip should need (distance estimate × consumption rate). The driver fuels up; the entry moves from pending to fueled. Later, the fuel supplier sends an electronic statement (a spreadsheet of every card swipe at their pumps). Bizwiz imports that statement and tries to match each pump transaction to an LPO/fuel entry. Matched entries get verified; if the driver over-pumped versus the estimate, a driver fuel penalty can be raised. Verified entries are approved, and finally rolled up into a Fuel Invoice that posts to Accounts Payable so the supplier can be paid.
The workflow is deliberately split across two areas of the app. The operational half (creating LPOs, confirming fuelling, watching pending/confirmed/expired entries) lives under the Fleet navigation. The finance half (uploading supplier statements, reconciling/verifying, approving, invoicing) lives under the Logistics navigation. This mirrors an operational-vs-finance separation of duties: fleet staff raise and fuel LPOs; logistics/finance staff reconcile statements and cut invoices.
2. Users & roles (permissions)¶
Access is gated by per-model permission strings of the form <model>___<action>, checked either against $my_permissions[...] directly in the sidebar or via the can('action','model') helper. role_id == 1 (superadmin) bypasses all checks.
Permissions observed in the two nav files:
| Permission slug | Grants | Nav | Cite |
|---|---|---|---|
fuel-suppliers___view |
Fuel Suppliers list; also gates Fuelling Stations in fleet nav | both | fleet.blade.php:783, 791; logistics.blade.php:208 |
fueling-stations___view |
Fuelling Stations list | logistics | logistics.blade.php:216 |
fuel-lpos___view |
Fuel LPOs list | fleet | fleet.blade.php:799 |
pending-fuel-lpos___view (+ can('create'/'view','pending-fuel-lpos')) |
Pending fuel entries; create LPO | both | fleet.blade.php:813; logistics.blade.php:90, 99 |
confirmed-fuel-lpos___view |
Confirmed fuel entries | fleet | fleet.blade.php:821 |
expired-fuel-lpos___view |
Expired fuel entries | fleet | fleet.blade.php:836 |
verified-fuel-lpos___view, approved-fuel-lpos___view |
Verified / approved LPO lists (commented out in logistics) | logistics | logistics.blade.php:122, 130 |
fuel-verification___view |
Statement upload page + verification listing | logistics | logistics.blade.php:146, 155 |
fuel-statements___view |
Statements listing | logistics | logistics.blade.php:163 |
fuel-statements___unknown |
Unknown/unmatched statements listing | logistics | logistics.blade.php:171 |
fuel-approval___view |
Fuel approval index | logistics | logistics.blade.php:181 |
unverified-fuel-approval___view |
Unverified fuel approval index | logistics | logistics.blade.php:188 |
fuel-invoices___view |
Fuel Invoices (AP hand-off) | logistics | logistics.blade.php:196 |
Note the accidental duplication/typos in the fleet nav: the "Approved" fuel-entries item is guarded by pending-fuel-lpos___view and links to fuel-lpos.pending, and its $model string is 'Approved-fuel-lpos' (capital A), so it will never highlight (fleet.blade.php:829-831). These are code-proven navigation bugs, not intended behaviour.
3. Processes (trigger → states → routes → outcome)¶
3.1 The status model¶
The lifecycle status is not stored on fuel_lpos. It lives on the fuel-entry row (table new_fuel_entries, later refactored / aliased as fuel_entries) in the column entry_status, whose allowed values come from the FuelEntryStatus enum (app/Enums/FuelEntryStatus.php:5-15):
pending → fueled_incomplete → fueled → verified → approved → processed, plus the off-ramp states expired and reactivated. A transient draft string is also referenced in the controller (FuelLPOController.php:384).
3.2 Fuel LPO lifecycle (Fleet nav)¶
- Create —
GET fuel-lpos/create→FuelLPOController@create; storePOST fuel-lpos/store(fleet.php:39-40). An LPO ties branch/route/vehicle/deliveryman and an estimate (seefuel_lposschema §4). A companion fuel entry is created and startspending. - Fuel — the driver fuels; entry becomes
fueled('entry_status' => "fueled", FuelLPOController.php:253) orfueled_incomplete. - Pending list —
GET fuel-purchase-orders/pending→pendingEntries, detail at.../pending/details/{id}(fleet.php:56-57). - Confirm —
GET fuel-purchase-orders/confirm/{id}→confirmLposetsentry_status = Processed(FuelLPOController.php:639, 648); bulkconfirm-all→confirmSelected(fleet.php:53-54). Confirmed list at.../confirmed→confirmedEntries(fleet.php:49). - Verify / Approve —
fuel-purchase-orders/verified→verifiedEntries,.../approved→approvedEntries(fleet.php:58-59).approveLposetsentry_status = Approved(FuelLPOController.php:528, 540). - Expire —
GET fuel-lpos/expire/{id}→expireLposetsentry_status = Expired(fleet.php:63; FuelLPOController.php:499, 503). This is automated by the scheduled commandapp/Console/Commands/Telematics/ExpireFuelLpos.php. - Reactivate —
POST fuel-purchase-orders/reactivate→reactivateExpiredLpo(fleet.php:51) brings an expired LPO back (reactivatedstatus). Onlypending/expiredentries are editable (FuelLPOController.php:429-430).
3.3 Reconciliation & finance lifecycle (Logistics nav)¶
- Upload statement —
GET fuel-statements/upload→showUploadPage;POST fuel-statements/upload→upload;POST fuel-statements/save→save(logistics.php:65-67). Import usesapp/Imports/FuelReceiptImport.phpand the supplier-specificapp/Services/FuelStatementGalanaImporter.php. - Run verification —
POST fuel-verification/verify→FuelVerificationRecordController@runVerification(logistics.php:73) matches statement lines to fuel entries and creates afuel_verification_recordsrow. Missing / unknown / unfueled / unutilised buckets are exposed atfuel-verification/{missing,unknown,unfueled,unutilized}(logistics.php:76-83). - Resolve exceptions — unknown payments and unfueled routes can be resolved or reset (
.../unknown/resolve,.../unknown/reset,.../unfueled/resolve,.../unfueled/reset, logistics.php:78-82). A statement line with no matching entry can spawn one:POST fuel-statements/{receipt}/create-missing-lpo→createMissingLpo(logistics.php:62). - Penalties —
POST fuel-verification/{lpo}/apply_penalty→applyPenalty(logistics.php:72) creates adriver_fuel_penaltiesrow (over-pumping vs standard qty). - Approve —
fuel-approval.index→FuelEntryApprovalController@showVerificationPage(logistics.php:85); pending/approved/penalty buckets atfuel-approval/{pending,approved,approved-penalties,pending-penalties}(logistics.php:89-92). A parallel unverified approval path exists viaUnverifiedFuelEntryApprovalController(unverified-fuel-approval.index, logistics.php:21) for entries approved without a supplier statement. - Invoice — approved entries are rolled into a Fuel Invoice (
fuel-invoices.index→FuelInvoiceController), which posts to AP (§5).
3.4 Reconciliation flow (Mermaid)¶
flowchart TD
A[Create Fuel LPO<br/>vehicle+route+driver+station<br/>estimate = distance x rate] --> B[Fuel entry: pending]
B --> C{Driver fuels?}
C -->|yes| D[fueled / fueled_incomplete]
C -->|no, timed out| Z[expired<br/>ExpireFuelLpos cmd]
Z -.reactivate.-> B
D --> E[Confirm entry<br/>confirmLpo]
S[Supplier statement<br/>Galana / CSV upload] --> T[fuel_statements rows<br/>receipt lines]
T --> U[runVerification: match<br/>statement line to fuel entry]
E --> U
U -->|matched| V[fuel_verification_records<br/>entry -> verified]
U -->|no match| W[Unknown / Unmatched<br/>resolve or create-missing-lpo]
V --> P{over-pumped vs<br/>estimate?}
P -->|yes| PEN[driver_fuel_penalties]
P -->|no| X
PEN --> X[Approve<br/>entry -> approved]
W -.resolved.-> V
X --> I[Fuel Invoice<br/>group approved entries by supplier/branch/period]
I --> AP[Accounts Payable<br/>wa_supp_trans + wa_gl_trans]
AP --> PV[PaymentVoucherItem<br/>payable_type = 'fuel']
4. Tables touched & key data¶
Verbatim column names (including misspellings/inconsistencies) from the migrations.
fuel_lpos (2023_10_25_133318_create_fuel_lpos_table.php:17-27): id, lpo_number, branch_id, route_id, vehicle_id, deliveryman_id, pre_mileage, pre_fuel, distance_estimate, fuel_estimate, timestamps. Later added: fueled status (2023_12_20), shift_id (2024_06_24_155609), last-shift details (2024_06_25_123722), backdated flag (2025_06_18_125429). Note the LPO row carries no lifecycle status column of its own — status lives on the fuel entry.
new_fuel_entries / refactored fuel_entries (2023_10_25_140958:16-30; refactor 2024_07_07_122247): id, fuel_lpo_id, distance_estimate, distance_covered, fuel_estimate, fuel_consumed, consumption_rate, fuel_price, fuel_type, pre_mileage, current_mileage, image. Grown over time with: multiple (2024_01_02_075253), invoice details (2024_02_04_101733), receipt details + receipt_number (2024_06_24_183148, 2024_08_19), fueling_station (2024_06_25_092800), status/entry_status (2024_06_25_133452), photos (2024_08_08), resolution_comments (2024_08_29_085107), approval (2024_08_29_155257), created_by (2025_03_26), branch + backfill (2025_04_30), odometer (2025_07_21), fueling_branch_id + FK + index (2025_08_14 / 2026_07_28). Note the misspelled table token fueling throughout.
fuel_stations (2023_10_25_121430:17-25): id, branch_id (FK → restaurants), name, slug, location_name, lat, lng. Added: fuel_price (2024_01_02_051944), supplier (2024_06_24_112725), verify_fuel_entries flag (2025_04_11_142421), card_number (2025_05_07_140459).
fuel_suppliers (2024_06_20_143034; columns in 2024_06_24_091344): supplier profile linked to a wa_suppliers account.
fuel_statements (2024_08_25_165805:15-25): id, timestamp, receipt_number, branch_id, matched_fuel_entry_id (nullable — null = unmatched/unknown), verification_record_id (nullable), quantity, terminal_price, discount, narrative. resolution_comments added 2024_08_29_065027; Galana upload tracking added 2026_05_06. A second fuel_statements migration exists (2026_02_17_152917_add_fuel_statements_table.php).
fuel_verification_records (2024_08_26_071732:14-17): id, branch_id, verification_date, fueling_date. This is the reconciliation header linking a fuelling day/branch to matched statement lines and entries.
fuel_invoices (2025_03_15_180900:16-33): id, invoice_number, wa_supplier_id, cu_invoice_number, supplier_invoice_number, invoice_date, restaurant_id, from, to, memo, vat_amount, withholding_amount, amount, created_by, status enum ['pending','paid'], payment_voucher_id (nullable), file_name. Plus fuel_expense_gl_account (nullable-false), invoice_type (2025_04_13, e.g. 'lpo'), VAT breakdown columns (2026_06_19: vat_rate, vatable_amount, non_vatable_amount), withholding backfill (2025_08_16).
fuel_invoice_items (2025_03_15_180900:37-42): id, fuel_invoice_id, fuel_entry_id, quantity, unit_price, amount — one line per approved fuel entry rolled into the invoice.
driver_fuel_penalties (2025_06_30_085918:14-20): id, driver_id, created_by, fuel_lpo_id, std_fuel_qty, actual_fuel_qty, penalty_per_liter. status added 2025_07_01_124116.
fuel_entry_images (2025_07_22_142110) with backfill from existing photos.
AP tables (written to, not owned): wa_supp_trans, wa_gl_trans (via WaSuppTran/WaGlTran), financial_notes (fuel_invoice_id FK added 2026_04_16), and voucher_items / PaymentVoucherItem.
5. Interactions with other modules¶
5.1 Accounts Payable seam (Book 3) — the money hand-off¶
This is the load-bearing integration. When a Fuel Invoice is posted (FuelInvoiceController@store, FuelInvoiceController.php:113), the controller writes directly into the AP/GL ledgers:
- Supplier transaction — a
WaSuppTran(wa_supp_trans) row is created withdocument_no = invoice_number,supplier_no = supplier->supplier_code, VAT breakdown, andtotal_amount_inc_vat(FuelInvoiceController.php:210-222). The GRN/series type comes fromWaNumerSeriesCodewheremodule = 'FUEL_INVOICES'(line 208). - GL postings — three
WaGlTran(wa_gl_trans) rows: a credit to creditors (amount * -1, account fromSupplierChartOfAccountService::getCreditorsAccountCodeForPosting, lines 224-240), a debit to the fuel expense GL (amount - vat_amount,companyPreference->fuelExpenseGlAccount, lines 242-258), and a VAT input line whenvat_amount > 0(taxVat->getOutputGlAccount, lines 260-278). All three carrytransaction_no = invoice_numberandwa_supp_tran_id. - Payment linkage —
FuelInvoice::payment()/payments()is aPaymentVoucherItemwithpayable_type = 'fuel'(FuelInvoice.php:47-66); the AP payment-voucher engine settles the supplier through this polymorphic payable. The listing query filtersvoucher_items.payable_type = 'fuel'(FuelInvoiceController.php:56). This confirms the guide's earlier note that fuel is a distinct payable type. - Credit/debit notes —
FinancialNoterows link back viafuel_invoice_id(FuelInvoice.php:68-76; migration 2026_04_16). - Reversal — deleting an invoice removes its
WaSuppTran(where document_no = invoice_number) andWaGlTran(where transaction_no = invoice_number) rows (FuelInvoiceController.php:568-570).
The model relations confirm the shape: supplier() → WaSupplier on wa_supplier_id, suppTran() → WaSuppTran on document_no↔invoice_number, glTransactions() → WaGlTran on transaction_no↔invoice_number (FuelInvoice.php:27-55).
5.2 Vehicle register (Book 5, Ch18)¶
Fuel LPOs reference vehicle_id, route_id, deliveryman_id (fuel_lpos schema). Consumption estimation uses a per-vehicle rate: migration 2026_01_30_165949_add_fuel_consumption_rate_to_vehicles_table.php adds fuel_consumption_rate to vehicles, and app/Services/FuelEstimationService.php computes fuel_estimate from distance × rate. FuelEntryVehicleFinancialsService ties fuel spend back to vehicle financials. Cross-reference Ch18 for the vehicle master; this chapter only consumes it.
5.3 Branches / stations¶
Every LPO, statement, and invoice is scoped to a branch_id / restaurant_id (→ restaurants). Stations belong to a branch and to a supplier, and can carry a verify_fuel_entries flag (2025_04_11) that governs whether that station's pumps go through verification.
5.4 Telematics / scheduling¶
Scheduled commands drive automation: ExpireFuelLpos (auto-expiry), FuelVerificationCommand (batch verification), CreateMissingFuelVerifications, and SetFuelInvoiceVatRate — all under app/Console/Commands/.
6. Alternatives & variants (flags / tenant)¶
- Two navs, one lifecycle — the operational surface (fleet.blade.php:777-840) and the finance surface (logistics.blade.php:77-224) are alternate entry points into the same underlying entities. Much of the fleet-nav "Fuel Entries" tree and the logistics-nav "Fuel LPOs" tree are Blade-commented-out (fleet.blade.php:777; logistics.blade.php:108-138), indicating an in-progress migration of the LPO views from fleet into logistics.
- Invoice type —
fuel_invoices.invoice_type('lpo'perisLpo(), FuelInvoice.php:57-59) distinguishes LPO-backed invoices from other fuel invoice types (2025_04_13 migration). - Verified vs unverified approval — two approval controllers exist:
FuelEntryApprovalController(statement-verified path) andUnverifiedFuelEntryApprovalController(approve without a matching supplier statement). This is a deliberate fallback for suppliers that don't send statements. - Supplier-specific import —
FuelStatementGalanaImporteris a named importer for the Galana supplier format, alongside the genericFuelReceiptImport. Galana upload tracking columns (2026_05_06) suggest additional supplier importers may follow. - Station verification flag —
verify_fuel_entriesper station (2025_04_11) toggles whether a station's entries require verification. - Withholding tax —
withholding_amounton invoices with a backfill migration (2025_08_16) implies WHT handling that may be tenant/config dependent.
7. Open questions — CODE-PROVEN vs INFERENCE¶
CODE-PROVEN¶
- Lifecycle status is stored on the fuel-entry row (
entry_status), not onfuel_lpos; enum valuespending/fueled_incomplete/fueled/verified/approved/processed/expired/reactivated(FuelEntryStatus.php:5-15; FuelLPOController.php:253, 503, 540, 648). confirmLposets status toProcessed, notConfirmed— there is noConfirmedenum case (FuelLPOController.php:648; enum lacks it).- Fuel invoices post to AP through
WaSuppTran+ threeWaGlTranlegs and settle viaPaymentVoucherItemwithpayable_type = 'fuel'(FuelInvoiceController.php:210-278; FuelInvoice.php:47-66). - Statement lines with
matched_fuel_entry_id = NULLare the "unknown/unmatched" bucket (fuel_statements.php:19; routefuel-statements.list.unknown, logistics.php:63). - Fleet nav has a mislabeled "Approved" item guarded by
pending-fuel-lpos___view, linking tofuel-lpos.pending, model string'Approved-fuel-lpos'(fleet.blade.php:829-838) — a UI bug. - Two
fuel_statementscreate migrations exist (2024_08_25 and 2026_02_17_152917).
INFERENCE (needs runtime/data confirmation)¶
- The exact matching algorithm in
runVerification(by receipt_number? by station+date+quantity tolerance?) was not read line-by-line; I infer it matches statement lines to fuel entries on branch/date/receipt and quantity, based on thefuel_statementscolumns and the unknown/unfueled/unutilised buckets. Not verified against controller body. - I infer that a driver penalty is raised when
actual_fuel_qty > std_fuel_qtybeyond a threshold, fromdriver_fuel_penaltiescolumns (std_fuel_qty,actual_fuel_qty,penalty_per_liter); the threshold/trigger logic inapplyPenaltywas not read. - I infer the fleet→logistics split reflects operational-vs-finance separation of duties; the commented-out Blade blocks support an in-progress consolidation, but no code comment states the rationale explicitly.
- The precise grouping key that turns approved entries into a single
fuel_invoice(supplier + branch + from/to period) is inferred from thefuel_invoicescolumns (wa_supplier_id,restaurant_id,from,to); the aggregation code inFuelLpoInvoiceController/FuelInvoiceController@storeupstream of line 200 was not fully traced. - Whether
withholding_amountis always computed or tenant-gated is inferred from the backfill migration; not confirmed in config.
8. Source references (file:line)¶
Navigation
- resources/views/admin/includes/sidebar_includes/fleet.blade.php:777-840 — Fuel Management + Fuel Entries tree, permission guards, the 'Approved-fuel-lpos' bug (829-838).
- resources/views/admin/includes/sidebar_includes/logistics.blade.php:77-224 — Fuel Management, Fuel Statements, approval, invoices, configuration; can()-guarded create/view (90, 99); commented-out LPO subtree (108-138).
Routes
- routes/modules/fleet.php:31-63 — stations, entries, suppliers, LPO lifecycle (create/store/confirm/verify/approve/expire/reactivate, reconcile-excel).
- routes/modules/logistics.php:50-93 — reports, statement upload/save/show/unknown, verification (verify, records, unknown/unfueled/unutilized resolve/reset, penalty), approval, invoices.
Controllers
- app/Http/Controllers/Admin/FuelLPOController.php:253, 384, 429-430, 499-540, 639-666 — status transitions.
- app/Http/Controllers/FuelInvoiceController.php:56, 113, 200-278, 568-570 — AP/GL posting and reversal.
- app/Http/Controllers/FuelStatementController.php, FuelVerificationRecordController.php, FuelEntryApprovalController.php, Admin/UnverifiedFuelEntryApprovalController.php, Admin/FuelLpoInvoiceController.php — statement, verification, approval, invoicing surfaces.
Models / enums / services
- app/Models/FuelInvoice.php:27-76 — AP relations (WaSupplier, WaSuppTran, WaGlTran, PaymentVoucherItem payable_type='fuel', FinancialNote).
- app/Models/FuelStatement.php:15-22; app/Models/FuelVerificationRecord.php; app/Models/FuelInvoiceItem.php; app/Models/DriverFuelPenalty.php; app/FuelLpo.php; app/Model/Fuelentry.php; app/NewFuelEntry.php.
- app/Enums/FuelEntryStatus.php:5-15; app/Enums/FuelEntryParentTypes.php.
- app/Services/FuelEstimationService.php, FuelEntryVehicleFinancialsService.php, FuelStatementGalanaImporter.php; app/Imports/FuelReceiptImport.php.
- app/Console/Commands/Telematics/ExpireFuelLpos.php, FuelVerificationCommand.php, CreateMissingFuelVerifications.php; app/Console/Commands/SetFuelInvoiceVatRate.php.
Migrations (database/migrations/)
- 2023_10_25_133318_create_fuel_lpos_table.php:17-27
- 2023_10_25_140958_create_new_fuel_entries_table.php:16-30 (+ many alters through 2026_07_28)
- 2023_10_25_121430_create_fuel_stations_table.php:17-25 (+ 2024_01_02_051944, 2024_06_24_112725, 2025_04_11_142421, 2025_05_07_140459)
- 2024_06_20_143034_create_fuel_suppliers_table.php
- 2024_08_25_165805_create_fuel_statements_table.php:15-25; 2026_02_17_152917_add_fuel_statements_table.php; 2026_05_06_120000_add_galana_upload_tracking_to_fuel_statements.php
- 2024_08_26_071732_create_fuel_verification_records_table.php:14-17
- 2025_03_15_180900_create_fuel_invoices_table.php:16-42; 2025_04_13_180753_add_invoice_type; 2026_06_19_140000_add_vat_breakdown; 2025_08_16_103306_backfill_withholding; 2026_04_16_100000_add_fuel_invoice_id_to_financial_notes_table.php
- 2025_06_30_085918_create_driver_fuel_penalties_table.php:14-20 (+ 2025_07_01_124116 status)
- 2026_01_30_165949_add_fuel_consumption_rate_to_vehicles_table.php
Cross-references: Ch18 (Vehicle register — vehicle_id, fuel_consumption_rate); Book 3 Accounts Payable (wa_supp_trans, wa_gl_trans, payment vouchers). Not covered here: Tyres (Ch20), Service (Ch21), Inbound logistics (Ch22).