Open
Conversation
- Add database migrations for accounts, journals, invoices, invoice_items, and wallets - Implement models with full relationships and traits - Create LedgerService, WalletService, and InvoiceService for business logic - Add controllers for accounts, invoices, wallets, and transactions - Create API resources for all models - Implement events and observers for invoice lifecycle - Configure routes and service provider - Support double-entry bookkeeping with journal entries - Enable system-wide transaction viewing - Implement wallet functionality for driver/entity payments
feat: Complete backend implementation for Ledger module
M1.1 — Fix WalletService accounting direction
- Deposit: DEBIT Cash (asset+), CREDIT Wallet Liability (liability+)
- Withdrawal: DEBIT Wallet Liability (liability-), CREDIT Cash (asset-)
- Added currency propagation from wallet to journal entry
- Replaced getDefaultExpenseAccount with getDefaultCashAccount for withdrawals
(expense account is used for driver payouts in M3, not raw withdrawals)
M1.2 — Add HasPublicId trait to Journal model
- Added use HasPublicId trait
- Set publicIdPrefix = 'journal'
- Added public_id to $fillable and $appends
M1.3 — Migration: add public_id to ledger_journals table
- New migration: 2024_01_01_000006_add_public_id_to_ledger_journals_table.php
- Adds nullable unique string column after _key
M1.4 — Enrich LedgerService::createJournalEntry
- Transaction payload now populates: subject_uuid, subject_type, gateway_uuid,
notes, gateway_transaction_id from $options
- currency falls back to debit account currency before defaulting to USD
- Added getTrialBalance() method (debit/credit totals across all accounts)
- Improved getGeneralLedger() with eager-loaded relations and secondary sort
- Improved getBalanceAtDate() with explicit int cast
- Enriched docblocks on all public methods
M1.5 — Improve InvoiceService::createItemsFromOrder
- Three-strategy item resolution:
1. FleetOps payload entities (native order items)
2. Order meta 'items' array (storefront-style)
3. Fallback single summary line item
- Separate line items for delivery_fee and service_fee from order meta
- Currency resolved from options > order meta > default USD
- recordPayment now passes subject_uuid/subject_type to journal entry
M1.6 — Add LedgerSeeder
- New file: server/seeds/LedgerSeeder.php
- Seeds 24 default system accounts (assets, liabilities, equity, revenue, expenses)
- Idempotent via firstOrCreate — safe to run multiple times
- runForCompany() method for use in company provisioning hooks
- Covers: Cash, Bank, AR, AP, Wallet Pool, Driver Payable, Tax Payable,
Stripe Clearing, Gateway Clearing, Delivery Revenue, Service Fee Revenue,
Driver Payout Expense, Gateway Fees, Refunds, and more
M1.7 — Add journal entry routes and reporting routes
- GET/POST/DELETE ledger/int/v1/journals (JournalController)
- GET ledger/int/v1/accounts/{id}/ledger (general ledger per account)
- GET ledger/int/v1/reports/trial-balance (ReportController)
- POST ledger/int/v1/invoices/{id}/send (InvoiceController::send)
- New controllers: JournalController, ReportController
- AccountController: injected LedgerService, added generalLedger() method
- InvoiceController: added send() method with customer email validation
…yer (M2)
## Overview
Complete refactor and re-imagination of the payment gateway system as a
first-class, interface-driven, extensible architecture. Replaces the
ad-hoc Storefront gateway code with a clean, driver-based system that
makes adding new payment gateways a 3-step process.
## M2.1 — Core Abstraction Layer
- Add GatewayDriverInterface (Contracts/) — the contract every driver must implement
Methods: purchase(), refund(), handleWebhook(), createPaymentMethod(),
getCapabilities(), getConfigSchema(), initialize()
- Add PurchaseRequest DTO — typed, immutable purchase request
- Add RefundRequest DTO — typed, immutable refund request
- Add GatewayResponse DTO — normalized response from any gateway
Includes: status, successful, gatewayTransactionId, eventType,
amount, currency, message, data, rawResponse, errorCode
- Add AbstractGatewayDriver — base class with shared helpers (logInfo,
logError, hasCapability, config, formatAmount)
- Add WebhookSignatureException — thrown on HMAC/signature failures
- Add Gateway model (ledger_gateways) — replaces storefront.gateways
Features: encrypted:array config cast, HasPublicId, HasUuid,
decryptedConfig(), getWebhookUrl(), capabilities JSON column
- Add GatewayTransaction model (ledger_gateway_transactions) — new audit
and idempotency log linking gateway events to the core transactions table
Features: alreadyProcessed(), isProcessed(), markAsProcessed()
- Add PaymentGatewayManager — extends Laravel Manager, resolves drivers
by name, injects credentials, returns ready-to-use driver instances
Registers: stripe, qpay, cash drivers
Exposes: getDriverManifest() for frontend dynamic form rendering
- Migration 000007: create ledger_gateways table
- Migration 000008: create ledger_gateway_transactions table
## M2.2 — Stripe Driver
- Full StripeDriver implementation using stripe/stripe-php SDK
- purchase(): creates PaymentIntent with automatic_payment_methods
- refund(): creates Stripe Refund via Refunds::create()
- handleWebhook(): verifies Stripe-Signature header using constructEvent()
Maps Stripe events to normalized GatewayResponse event types:
payment_intent.succeeded → EVENT_PAYMENT_SUCCEEDED
payment_intent.payment_failed → EVENT_PAYMENT_FAILED
charge.refunded → EVENT_REFUND_PROCESSED
- createPaymentMethod(): creates SetupIntent for card tokenization
- getConfigSchema(): returns publishable_key, secret_key, webhook_secret fields
- getCapabilities(): purchase, refund, webhooks, tokenization, setup_intents
## M2.3 — QPay Driver
- Full QPayDriver implementation (refactored from Storefront QPay support class)
- Retains all QPay business logic: token auth, invoice creation, deep links
- purchase(): authenticates, creates QPay invoice, returns payment URL + deep links
- refund(): calls QPay refund API with original transaction reference
- handleWebhook(): verifies QPay callback signature, maps to normalized events
- getConfigSchema(): username, password, invoice_code, merchant_id, terminal_id
- getCapabilities(): purchase, refund, webhooks, redirect
## M2.4 — Cash Driver
- CashDriver for cash on delivery and manual payment scenarios
- purchase(): immediately marks as succeeded, generates local reference ID
- refund(): records manual refund, no external API call
- getConfigSchema(): label, instructions (operator-configurable display text)
- getCapabilities(): purchase, refund (no webhooks needed)
## M2.5 — Webhook System
- Add WebhookController (POST /ledger/webhooks/{driver})
Flow: identify gateway → verify signature → idempotency check →
persist GatewayTransaction → dispatch normalized event → return 200
Always returns 200 to prevent gateway retries on internal errors
Returns 400 only for signature verification failures
- Add PaymentSucceeded event
- Add PaymentFailed event
- Add RefundProcessed event
- Add HandleSuccessfulPayment listener (ShouldQueue, 3 retries, 30s backoff)
Actions: mark invoice paid, create revenue journal entry, seal transaction
- Add HandleFailedPayment listener (ShouldQueue)
Actions: mark invoice overdue, seal transaction
- Add HandleProcessedRefund listener (ShouldQueue)
Actions: mark invoice refunded, create reversal journal entry, seal transaction
## M2.6 — PaymentService & GatewayController
- Add PaymentService — single orchestration entry point for all payments
charge(): resolve gateway → call driver → persist → dispatch event
refund(): resolve gateway → call driver → persist → dispatch event
createPaymentMethod(): tokenize card via driver
getDriverManifest(): return driver list for frontend
- Add GatewayController with full CRUD + payment operations:
GET /ledger/int/v1/gateways → list all gateways
POST /ledger/int/v1/gateways → create gateway
GET /ledger/int/v1/gateways/{id} → get gateway
PUT /ledger/int/v1/gateways/{id} → update gateway config
DELETE /ledger/int/v1/gateways/{id} → delete gateway
GET /ledger/int/v1/gateways/drivers → driver manifest (dynamic forms)
POST /ledger/int/v1/gateways/{id}/charge → initiate payment
POST /ledger/int/v1/gateways/{id}/refund → refund transaction
POST /ledger/int/v1/gateways/{id}/setup-intent → tokenize card
GET /ledger/int/v1/gateways/{id}/transactions → transaction history
- Add Gateway API Resource (excludes credentials from responses)
- Add POST /ledger/webhooks/{driver} (public, no auth) to routes.php
## M2.7 — Wiring & Dependencies
- Update LedgerServiceProvider: register PaymentGatewayManager singleton,
alias as 'ledger.gateway', register PaymentService, bind all 3 event-
listener pairs via Event::listen()
- Update composer.json: add stripe/stripe-php ^13.0, guzzlehttp/guzzle ^7.0
…stomers
## Stripe SDK
- Fix stripe/stripe-php version to ^17.0 in composer.json
- Update StripeDriver to use StripeClient instance methods (v17 API)
- paymentIntents->create(), refunds->create(), setupIntents->create()
- Remove static Stripe::setApiKey() call (deprecated in v17)
## M3.1 — WalletTransaction model + migration
- New model: WalletTransaction with full type/direction/status constants
- Types: deposit, withdrawal, transfer_in, transfer_out, payout, fee, refund, adjustment, earning
- Directions: credit, debit
- Statuses: pending, completed, failed, reversed
- Polymorphic 'subject' relationship (driver/customer/order/invoice)
- Scopes: credits(), debits(), completed(), ofType()
- Helpers: isCredit(), isDebit(), isCompleted(), getFormattedAmountAttribute()
- New migration: ledger_wallet_transactions with composite indexes for
common query patterns (wallet+type, wallet+direction, wallet+status, wallet+date)
## M3.2 — Wallet model enriched
- Added transactions(), completedTransactions(), credits(), debits() HasMany relationships
- Added getTypeAttribute() — infers 'driver'/'customer'/'company' from subject_type
- Added getFormattedBalanceAttribute() — cents to decimal string
- Added canDebit() / canCredit() guards (frozen wallets accept credits, not debits)
- Added close() state transition
- Added credit()/debit() low-level balance methods with atomic increment/decrement
- Added Wallet::forSubject() static factory (findOrCreate by subject)
- Added STATUS_ACTIVE/FROZEN/CLOSED constants
- Added 'type' and 'formatted_balance' to $appends
## M3.3 — WalletService fully enriched
- deposit() now creates WalletTransaction audit record + uses wallet->credit()
- withdraw() now creates WalletTransaction audit record + uses wallet->debit()
- transfer() now creates paired WalletTransaction records (transfer_in + transfer_out)
- New: topUp() — charges a gateway via PaymentService, credits wallet on sync success
- New: creditEarnings() — credits driver earnings with TYPE_EARNING transaction
- New: processPayout() — debits driver wallet with TYPE_PAYOUT transaction
- New: provisionBatch() — bulk wallet provisioning for existing drivers/customers
- New: recalculateBalance() — reconciliation utility from transaction history
- Lazy PaymentService resolution to avoid circular dependency
## M3.4 — WalletController fully enriched
- Constructor injection of WalletService
- deposit()/withdraw() now return {wallet, transaction} JSON (not just wallet)
- transfer() now returns {from_wallet, to_wallet, from_transaction, to_transaction}
- New: topUp() endpoint — POST /wallets/{id}/topup
- New: payout() endpoint — POST /wallets/{id}/payout
- New: getTransactions() — GET /wallets/{id}/transactions with full filtering
- New: freeze() — POST /wallets/{id}/freeze
- New: unfreeze() — POST /wallets/{id}/unfreeze
- New: recalculate() — POST /wallets/{id}/recalculate (reconciliation)
- Private resolveWallet() helper for DRY wallet lookup
## M3.5 — Public API (Customer/Driver facing)
- New controller: Api/v1/WalletApiController
- GET /ledger/v1/wallet — get own wallet (auto-provisions)
- GET /ledger/v1/wallet/balance — get balance + formatted_balance
- GET /ledger/v1/wallet/transactions — paginated transaction history
- POST /ledger/v1/wallet/topup — top up via gateway
- New resource: Http/Resources/v1/WalletTransaction (safe public serialization)
- Wallet resource enriched with 'type' and 'formatted_balance' fields
## M3.6 — Routes updated
- Added all new internal wallet routes (topup, payout, freeze, unfreeze, recalculate, transactions)
- Added public API route group (/ledger/v1/...) with fleetbase.api middleware
- Added /ledger/int/v1/wallet-transactions standalone query endpoint
- Added /ledger/int/v1/reports/wallet-summary endpoint
- New: WalletTransactionController (standalone cross-wallet query + find)
- New: ReportController::walletSummary() — wallet counts, period stats, top driver wallets
M4.1 — LedgerService: new financial statement methods - getBalanceSheet(): generates Balance Sheet (Assets = Liabilities + Equity) with per-account rows, section totals, and equation verification - getIncomeStatement(): generates P&L for a period using journal activity (not running balances), with revenue/expense line items and net income - getCashFlowSummary(): derives cash flows from WalletTransactions grouped into Operating / Financing / Investing activities; cross-validates against journal Cash account (code 1000) opening/closing balance - getArAging(): buckets outstanding invoices by days overdue into 5 buckets: current, 1-30, 31-60, 61-90, 90+; includes per-invoice detail rows - getDashboardMetrics(): comprehensive KPI set with period-over-period comparison (% change), outstanding AR, wallet totals by currency, daily revenue trend, invoice status counts, and last 10 journal entries - computeNetFlow(): internal helper for cash flow direction calculation - percentageChange(): internal helper for period-over-period KPI deltas - Refactored getBalanceAtDate() to use Account::TYPE_* constants - Refactored getTrialBalance() to use Account::TYPE_* constants + orderBy code M4.2 — ReportController: full suite of report endpoints - dashboard() GET /ledger/int/v1/reports/dashboard - trialBalance() GET /ledger/int/v1/reports/trial-balance - balanceSheet() GET /ledger/int/v1/reports/balance-sheet - incomeStatement() GET /ledger/int/v1/reports/income-statement - cashFlow() GET /ledger/int/v1/reports/cash-flow - arAging() GET /ledger/int/v1/reports/ar-aging - walletSummary() GET /ledger/int/v1/reports/wallet-summary All endpoints validate date inputs and return structured JSON with status/data envelope M4.3 — routes.php: added 5 new report routes - reports/dashboard - reports/balance-sheet - reports/income-statement - reports/cash-flow - reports/ar-aging
## Overview Full Ember engine frontend for the Ledger module. Implements all 8 navigation sections with declarative sidebar (EmberWormhole pattern from iam-engine), 7 Ember Data models, 16 route controllers, 16 templates, 26 components, and 80 app/ re-export files. ## Sidebar Navigation (declarative HBS, not universe menu service) - Dashboard (home route) - Billing > Invoices, Transactions - Wallets - Accounting > Journal Entries, Chart of Accounts - Reports - Settings > Payment Gateways ## Ember Data Models (addon/models/) - account, invoice, transaction, wallet, wallet-transaction, journal, gateway ## Routes & Controllers - home (dashboard) - billing/invoices/index + details (tabs: details, line-items, transactions) - billing/transactions/index + details (tabs: details) - wallets/index + details (tabs: details, transactions) - accounting/journal/index + details (tabs: details) - accounting/accounts/index + details (tabs: details, general-ledger) - reports/index (tab-based: trial-balance, balance-sheet, income-statement, cash-flow, ar-aging) - settings/gateways/index + details (tabs: configuration, webhook-events) ## Components (26 total across 8 namespaces) dashboard/: kpi-metric, revenue-chart, invoice-summary, wallet-balances, activity-feed invoice/: panel-header, details, line-items, transactions transaction/: panel-header, details wallet/: panel-header, details, transaction-history journal/: panel-header, details account/: panel-header, details, general-ledger report/: balance-sheet, income-statement, cash-flow, ar-aging gateway/: panel-header, details, webhook-events, form (dynamic config schema renderer) ## engine.js / extension.js / routes.js - engine.js: clean Ember engine with correct dependencies - extension.js: registers header menu item and dashboard widget only - routes.js: full nested route tree matching all 8 sections ## app/ Re-exports (80 files) All routes, controllers, models, and components re-exported from app/ directory under the @fleetbase/ledger-engine namespace for Ember resolver compatibility.
…blic_id with id/uuid
Frontend fixes:
- addon/routes.js: all details route path params changed from /:public_id to /:id
- All 6 detail routes: model({ public_id }) -> model({ id }), findRecord uses id
- All 7 Ember Data models: removed @attr('string') public_id (Ember uses id automatically)
- All list controllers: transitionTo calls use .id not .public_id
- billing/invoices/index/details controller: fetch calls use namespace option
- wallets/index and wallets/index/details controllers: fetch calls use namespace option
- reports/index controller: endpointMap paths corrected, namespace option added
- settings/gateways/index controller: gateways/drivers fetch uses namespace option
- routes/home.js: dashboard fetch uses namespace option
- components/invoice/transactions.js: guard uses id, fetch uses namespace option
- components/wallet/transaction-history.js: guard uses id, fetch uses namespace option
- components/account/general-ledger.js: guard uses id, fetch uses namespace option
- components/gateway/webhook-events.js: guard uses id, fetch uses namespace option
Backend fixes:
- server/src/Http/Resources/v1/Gateway.php: now extends FleetbaseResource,
id field returns uuid for internal requests and public_id for public API requests,
consistent with all other Ledger resources (Account, Invoice, Wallet, etc.)
- All other controllers already resolve {id} via orWhere(uuid, id) — no changes needed
… per group Replace single Panel with Layout::Sidebar::Section wrappers with the correct pattern of one Panel per navigation group, matching the pallet/iam-engine/dev-engine pattern. Each group (Ledger, Billing, Wallets, Accounting, Reports, Settings) is now its own collapsible Panel. Also added missing <ContextPanel /> to application.hbs.
…ebar panels
Adapters:
- Add addon/adapters/ledger.js base adapter with namespace 'ledger/int/v1'
- Add per-model adapters for account, invoice, transaction, wallet,
wallet-transaction, journal, gateway — each re-exporting ledger base adapter
- Add app/adapters/ re-exports for all adapters
Dashboard widget system (correct implementation):
- Rewrite extension.js: use universe/menu-service and universe/widget-service,
call widgetService.registerDashboard('ledger') and
widgetService.registerWidgets('ledger', widgets) with 7 widget definitions
(5 default: overview, revenue-chart, invoice-summary, wallet-balances,
activity-feed; 2 optional: ar-aging, top-wallets)
- Rewrite home.hbs: use <Dashboard @defaultDashboardId='ledger'
@defaultDashboardName='Ledger Dashboard' @extension='ledger' /> inside
<Layout::Section::Body> with <Spacer> and {{outlet}}
- Remove old addon/components/dashboard/ namespace (kpi-metric, revenue-chart,
invoice-summary, wallet-balances, activity-feed) — replaced by widget/
- Create addon/components/widget/ with 7 widget components (JS + HBS each):
overview, revenue-chart, invoice-summary, wallet-balances, activity-feed,
ar-aging, top-wallets — each fetches its own data via fetch service
- Add app/components/widget/ re-exports for all 7 widget components
- Simplify home route — Dashboard component handles all data fetching via widgets
…i, params, options)
All fetch.get calls were incorrectly passing namespace in the params argument:
fetch.get('url', { namespace: 'ledger/int/v1' })
Fixed to use the correct 3-argument signature:
fetch.get('url', {}, { namespace: 'ledger/int/v1' })
Files fixed:
- addon/components/widget/overview.js
- addon/components/widget/revenue-chart.js
- addon/components/widget/invoice-summary.js
- addon/components/widget/wallet-balances.js
- addon/components/widget/activity-feed.js
- addon/components/widget/ar-aging.js
- addon/components/widget/top-wallets.js
- addon/controllers/reports/index.js (params now passed as 2nd arg)
- addon/controllers/settings/gateways/index.js
…nt error in LedgerService)
…alls (namespace belongs in adapter, not params)
…trollers - Add LedgerController base class extending FleetbaseController (sets namespace) - Remove all hand-rolled query/find/create/update/delete methods from every internal v1 controller; CRUD is now handled by HasApiControllerBehavior (queryRecord, findRecord, createRecord, updateRecord, deleteRecord) - Add Filter classes for every resource (Account, Invoice, Journal, Wallet, WalletTransaction, Gateway, GatewayTransaction) — auto-resolved by Resolve::httpFilterForModel(); each filter implements queryForInternal() for company scoping and individual param methods (type, status, query, etc.) - Add missing resources: Journal, GatewayTransaction, Transaction - Rewrite routes.php to use fleetbaseRoutes() for all resources; only custom action routes (charge, refund, transfer, freeze, etc.) are declared manually inside the fleetbaseRoutes callback - TransactionController overrides findRecord() to append journal entry data - JournalController keeps createManual() as a custom action (service-layer orchestration required); standard createRecord() still available for simple creates
…verride
- Add Fleetbase\Ledger\Models\Transaction extending core-api Transaction
with a journal() hasOne relationship — journal data is now available on
the model itself, no controller override needed
- Add TransactionFilter with queryForInternal() eager-loading journal entries
- Update Transaction resource to use whenLoaded('journal') for clean serialization
- Simplify TransactionController to a pure LedgerController stub (no overrides)
- Update Journal model to reference Fleetbase\Ledger\Models\Transaction instead
of the core-api Transaction so the inverse relationship resolves correctly
Fixes: Declaration of findRecord() must be compatible with FleetbaseController
Every migration declared ->unique() inline on the uuid/public_id column AND then repeated $table->unique(['uuid']) as a standalone call at the bottom of the same Schema::create block. MySQL creates the index on the first declaration and then throws: SQLSTATE[42000]: Duplicate key name 'ledger_accounts_uuid_unique' when the second call tries to add an identical index. Removed the redundant standalone $table->unique(['uuid']) lines from: - 2024_01_01_000001_create_ledger_accounts_table - 2024_01_01_000002_create_ledger_journals_table - 2024_01_01_000003_create_ledger_invoices_table - 2024_01_01_000004_create_ledger_invoice_items_table - 2024_01_01_000005_create_ledger_wallets_table - 2024_01_01_000007_create_ledger_gateways_table - 2024_01_01_000008_create_ledger_gateway_transactions_table Also removed redundant ->index() chained after ->unique() on public_id columns (a UNIQUE constraint already implies an index in MySQL).
Migrations missing softDeletes() (deleted_at column):
- ledger_journals — caused 'Unknown column deleted_at' when eager-loading
journal entries via the Transaction -> journal() hasOne relationship
- ledger_invoice_items
Models missing SoftDeletes trait (import + use):
- Gateway
- GatewayTransaction
- InvoiceItem
- Journal
Fleetbase\Ledger\Models\Transaction extends BaseTransaction which already
uses SoftDeletes via the core-api model — no change needed there.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.