ECOBANK026 / WASTE BANK
A waste-bank ledger with guarded Excel imports and protected stock.

A Laravel operations system for a community waste bank that unites deposit, withdrawal, and collector-sale records under one ledger of record. It handles dual pricing, bulk Excel import, cumulative stock control, and a multi-role audit trail.
Orders were simple on paper.
Fulfillment was fragmented.
Waste-bank operations ran on dozens of date-separated Excel spreadsheets, so every deposit had to be reconciled by hand and was prone to duplication or mistyping. Field material labels were inconsistent: Paper, Used Paper, and Crumpled Paper referred to the same item, which risked duplicate categories and inconsistent reports. Customer savings balances and the waste bank's cash were tracked separately from the transaction data, leaving no single source of truth when figures diverged. Aggregate sales to collectors could also exceed the stock that actually existed, particularly when entries were backdated. Without a reliable audit trail, investigating a discrepancy or a suspected misappropriation was extremely difficult.
Deposit records were scattered across many date-based Excel sheets, and material labels were inconsistent (Paper, Used Paper, and Crumpled Paper all meant the same item), inviting duplicate categories and reports that contradicted each other.
Customer savings balances and the waste bank's cash were calculated by hand outside the transaction data, so there was no single source of truth when the two disagreed.
Aggregate sales to collectors could exceed the stock that actually existed, especially when entries were backdated, and there was no audit trail to trace the amounts or the people behind them.
One order. Multiple fulfillment paths.
Explore how the system splits each line item into independent operational branches while enforcing strict transactional convergence before dispatch.
Direct Processing: Daily Deposits
The primary path that runs every day when an operator records a waste deposit directly at the waste bank.
Select Customer & Material
The deposit form loads active customers and waste categories; the customer buy price (member_price) is filled automatically and can be overridden manually.
Validate & Save Atomically
DB::transaction writes the Deposit and each DepositDetail with its subtotal, rejecting any row without a waste_category_id or material_label.
Record Credit Ledger
Writes a savings_ledgers entry of type credit with reference_type/reference_id to the Deposit as the source of truth for the customer balance.
Audit Trail After Commit
A deposit.create activity log is written outside the transaction, carrying customer_name and total_amount so it is still recorded even if an exception occurs after commit.
LEDGER AS SINGLE SOURCE OF TRUTH
Do not store a directly mutable balance column; customer savings and cash are derived from the append-only savings_ledgers and waste_bank_cash_ledgers.
A mutable balance column opens a path to desynchronization when transactions are edited or deleted, and it makes reconciliation hard. With a ledger, every change is explained as a new entry and the balance can be recomputed at any time. The trade-off is that every write must consistently create both the primary transaction and the ledger entry within one DB::transaction.
DUAL PRICING WITH HISTORICAL SNAPSHOTS
Store member_price and margin per sale_details row, separate from the WastePrice master, so the master price is never reused to reconstruct past data.
Storing only a reference to the master price lets a retroactive price change rewrite historical financial reports. A snapshot keeps history stable, while WastePriceHistory tracks price changes. The trade-off is duplicated values on detail rows and the need to keep them consistent during explicit overrides.
IDEMPOTENT EXCEL IMPORT WITH CROSS-VALIDATION
Bulk import never inserts directly: rows are parsed, normalized, mapped, recomputed, cross-checked through a recap, and only then committed atomically with an anti-reimport fingerprint.
Raw spreadsheets are full of inconsistent labels, empty prices, and duplicate sheets. Without a validation stage, a single import could permanently pollute the master data and the financial history. The staged approach allows human review of ambiguous rows, at the cost of a longer, stateful import flow.
ATOMIC TRANSACTIONS WITH A FULL-TIMELINE STOCK GUARD
A sale locks the stock row with lockForUpdate, aggregates weight per category, and validates remaining stock across the entire timeline through assertCumulativeStockNotNegative before writing.
Two concurrent sales, or a single backdated sale, can push cumulative stock negative without being detected. Scanning the timeline ensures no date point drops below zero, not just the ending balance. The cost is a heavier query and a longer lock on the sold category.
Engineered for relational integrity.
At the data layer, deposit, deposit_details, savings_ledgers, and waste_bank_cash_ledgers record every value movement explicitly; all writes are wrapped in DB::transaction, and balances are restored through deterministic recalculation. The business layer enforces validation boundaries: a withdrawal is allowed only after two deposits and never exceeds the balance, while a sale may not exceed cumulative stock up to the transaction date. Concurrency is protected with lockForUpdate on the stock row, while assertCumulativeStockNotNegative scans the whole timeline to prevent negative stock on backdated transactions. The activity log is append-only and immutable, bound with a correlation_id per operation sequence. A Blade UI with Alpine.js and Tailwind presents the result without leaking transactional complexity to operators.
CLIENT LAYER
- Blade Templates (server-rendered)
- Alpine.js for interactive state
- Tailwind CSS + custom UI components
- ApexCharts for dashboard and report visualization
BUSINESS & DOMAIN
- Service layer: BankSampahService, DepositImportService, MaterialMappingService
- State transitions and validation: dual pricing, margin, balance, and cumulative stock
- Authorization: Spatie Laravel Permission + admin device binding
- Logging: append-only ActivityLogService with correlation_id
DATA & STORAGE
- MySQL as the production database
- Eloquent ORM with ledger and transaction-detail relations
- Local storage for expense proofs (proof_path)
- DB::transaction + lockForUpdate + balance recalculation
The system turns manual spreadsheet reconciliation into a deterministic operation: every rupiah that enters or leaves can be traced from the transaction to the ledger entry. Layered validation on balances, cumulative stock, and recap consistency closes the gaps that allowed over-selling and hard-to-detect data discrepancies. Historical price snapshots keep old reports stable even when master prices change. With an append-only audit trail and strict role separation, the system can carry a growing volume of waste-bank transactions without losing accountability.
More systems worth exploring.
Explore another project where software, business processes, and practical engineering meet.