Assets & equipment.
A school owns 200 tablets, 30 projectors, 50 desks per classroom, and a storeroom of consumables. The bursar's nightmare is knowing what's where, whose hands it's in, when it broke, when it was thrown out, and what it's worth on the balance sheet today. YESS gives Inventory & Assets the same atomic-finance discipline the rest of Operations runs on: every breakage approval, every wastage event, every depreciation row writes its own journal entry inside the same transaction that the operator's click commits.
- 11tables across 5 migrations
- 5atomic finance triggers
- 1depreciation gap closed this turn
- EN/FR/ARevery surface
Prologue
What an asset's life looks like#
A tablet arrives at the school. The IT manager opens /dashboard/assets, picks the category, assigns it a tag, prints the barcode. It goes to a classroom; the bursar files an acquisition batch via /dashboard/inventory/receipts — the receipt posts DR 1400 Library/Assets / CR 1100 Cash automatically.
The tablet's life now has six writeable surfaces. A teacher borrows it for the afternoon (asset booking). A student drops it on the floor (breakage event → bursar approves → write-off journal). The biology lab pulls a marker pen from the storeroom (inventory transfer). Half a box of expired paper gets discarded (wastage event → loss journal). The monthly depreciation schedule runs (depreciation row → DR 5900 / CR 1800 journal — automated as of migration 00657). Every step posts to the ledger inside the same transaction the operator's tap committed in. The bursar never reconciles a spreadsheet against the journal.
Eleven tables, five atomic fanouts, one register. Read this one first.
Chapter 1
The eleven tables#
Two halves of the same picture: durable assets that get tagged and tracked individually (durable equipment with serial numbers + depreciation schedules); and consumable inventory that gets bought, moved, used, and written off in bulk.
- Asset register (00012) —
asset_categories(hierarchical taxonomy),assets(per-physical with tag, serial, location, purchase + warranty info),asset_assignments(long-term to staff member),asset_maintenance(scheduled + ad-hoc repairs),asset_depreciation(per-asset per-month schedule, now auto-posts via 00657),asset_bookings(00578 — short-term checkout with deposit + return condition). - Consumables (00012 + 00571) —
inventory_items(catalogue with sku + min_stock for reorder alerts),location_inventory(per-location stock for multi-storeroom schools). - Movement events (00571 + 00584 + 00586) —
breakage_events(report + approve write-off, triggers the DR Loss / CR Inventory journal),inventory_wastage_events(00584 — expired / spoiled / spilled, posts a loss journal),inventory_transfers(00586 — between locations / sub-stores, atomic via RPC),inventory_count_sessions(periodic counts that reconcile location_inventory against reality).
Every table has RLS scoped to public.get_school_id() and an updated_at trigger. The recent extensions (00571 / 00578 / 00584 / 00586 / 00657) all share the cluster's atomic-finance discipline.
Chapter 2
Short-term bookings#
A teacher needs the school's nicest projector for Wednesday's parent-teacher conference. They open /dashboard/inventory/bookings, pick the asset, set the window, write the purpose. The booking row INSERTs with status='requested'. The ops manager taps Approve; the RPC asset_booking_approve moves it to 'approved'. The teacher picks up the projector — RPC asset_booking_check_out stamps deposit + sets status='active'. After conferences end, they return it — RPC asset_booking_return records condition (excellent / good / fair / damaged / lost) + returns the deposit. If the condition is 'damaged' or 'lost', the deposit is partially or fully retained.
Chapter 3
Breakage + wastage write-offs#
Breakage and wastage are two flavours of the same accounting reality: stock you owned but no longer have. Both post to the finance ledger automatically.
Breakage (00571): a student drops a beaker. Anyone with assets.create reports it via /dashboard/assets/by-location. The row INSERTs with approval_status='pending'. The bursar reviews; on Approve the trigger breakage_post_writeoff fires, calls post_inventory_breakage_write_off RPC, posts the journal entry (DR 5700 Inventory loss / CR 1500 Stock), and stamps the journal_entry_id back on the row. The location_inventory count also decrements via the breakage_decrement_location_count trigger — two separate effects, one approval.
Wastage (00584): /dashboard/inventory/wastage logs expired / spoiled / spilled stock. Kinds: expired, spoiled, damaged, spilled, theft, other. The trg_wastage_apply trigger fires AFTER INSERT, calls inventory_wastage_apply RPC, posts the loss journal, decrements the relevant stock count. No approval step — wastage is recorded as it happens (the school controls who can log via the assets.edit permission).
Chapter 4
Inventory transfers#
Multi-storeroom schools move stock between locations daily — kitchen pulls from the main store, biology lab borrows from the chemistry lab, a sports trophy moves from one campus to another. The transfer RPC inventory_transfer_apply (00586) is atomic: decrements from_location, increments to_location, writes one transfer row recording both sides. No double-bookkeeping to maintain; no half-applied transfer state. Surfaced at /dashboard/inventory/transfers.
Chapter 5
The depreciation journal#
Asset depreciation is the slowest-moving piece of school finance — the bursar reduces each asset's book value monthly by a fraction of its purchase cost, posting a depreciation expense to the ledger and increasing accumulated depreciation on the asset side. Schools that don't post depreciation get balance sheets that overstate asset value year after year.
The schema (asset_depreciation, 00012:717) has the per-asset per-period rows: year, month, depreciation_amount, accumulated_depreciation, book_value, method. But until migration 00657, no trigger posted them — the row landed in the table and the bursar had to manually compute + manually post the matching journal entry. Most schools quietly stopped doing it.
00657 wires the missing posting. Two new columns: journal_entry_id + posted_at. New RPC post_asset_depreciation(p_id) (idempotent — re-call is a no-op) finds-or-creates the ASSET-DEPR expense category, posts the journal entry (DR 5900 Depreciation expense / CR 1800 Accumulated depreciation) via c4_post_journal_entry, stamps back the journal_entry_id + posted_at. A trigger asset_depreciation_auto_post on INSERT fires it automatically — so a manual or scheduled depreciation run lands in the ledger the same transaction the calculation INSERTs the row.
Chapter 6
What travels outward#
Five atomic finance fanouts make this sub-domain integrate cleanly into the school's books:
- Acquisition → Finance.
post_inventory_acquisition_batchRPC (00571) posts the purchase as DR 1400 Inventory / CR 1100 Cash on receipt entry. - Breakage approval → Finance.
breakage_post_writeofftrigger →post_inventory_breakage_write_offRPC (00571) posts the write-off journal when the bursar approves. - Wastage event → Finance.
trg_wastage_applytrigger →inventory_wastage_applyRPC (00584) posts the loss journal on every wastage INSERT. - Transfer → both sides atomic.
inventory_transfer_applyRPC (00586) writes both location balance updates in one transaction. (No ledger post — moving stock between locations isn't a P&L event; only acquisition, write-off, and depreciation are.) - Depreciation → Finance.
asset_depreciation_auto_posttrigger →post_asset_depreciationRPC (NEW in 00657) posts DR 5900 / CR 1800 on every depreciation row INSERT. Idempotent. Was missing entirely; now correct.
Chapter 7 · prologue
The school store runs on the same discipline#
The same bursar who watches asset depreciation also runs the school store. Uniforms, textbooks, stationery, sports kit, school-branded merchandise — products + variants + stock + a real cart + checkout. The shop ships on the same atomic-finance discipline: every paid order posts revenue to the ledger inside the transaction the parent's payment committed in.
Open the shop admin at /dashboard/shop. Five sub-pages: catalog management split across /dashboard/shop/categories + /dashboard/shop/products; orders queue at /dashboard/shop/orders; returns review at /dashboard/shop/returns. Parents shop from the portal side; their cart converts to an order on checkout.
Chapter 8
Eight shop tables#
- Catalog —
shop_product_categories,shop_products(with variants_json + reserved_stock + min_stock). - Pre-checkout —
shop_carts+shop_cart_items(each parent / staff has one open cart per school). - Orders + items —
shop_orders(with payment_status + fulfillment_status state machine) +shop_order_items(price + variant snapshots so historical reports survive catalog edits). - Delivery + returns —
shop_delivery_logs(every state change of a fulfilment) +shop_returns(returned items lifecycle with restock decision).
The 00563 schema landed with two bugs the audit caught later: the SELECT RLS policies on shop_orders and shop_order_items referenced parent_student_links.parent_user_id — a column that does not exist on parent_student_links. Parents have been unable to see their own child's orders in the portal since 00563 shipped; only staff with shop.view could see anything via the other OR branch. Migration 00653 rewrote both policies with the canonical multi-hop walk (parent_student_links → parents → user_profiles → auth.uid()). Parents now see their child's orders.
Chapter 9
Two parallel state machines#
A shop order doesn't have one status — it has two, tracked independently: payment_status (pending / paid / cancelled / refunded / failed) and fulfillment_status (pending / preparing / ready / delivered / collected). The order is "done" only when both reach paid + delivered/collected.
The 00565 trigger shop_handle_payment_status_change reacts to payment_status transitions. On transition into 'paid', it walks every line item and reserves stock (for variant-less products it bumps shop_products.reserved_stock; for variants it decrements the matching variants_json[].stock via a precise JSONB rewrite). On transition out of 'paid' to 'cancelled' / 'refunded' / 'failed', it releases the reserved stock. The revenue journal posts via post_shop_order_revenue (00564) when both payment_status='paid' AND fulfillment_status='delivered' have landed — whichever transition completes the pair fires the post. Idempotent via a journal_entry_id stamp on the order row.
Chapter 10
Returns + restock#
A parent or student requests a return via the portal. shop_returns rows lifecycle through requested → approved / rejected → refunded. On approval, the bursar marks the restock decision: items in good condition go back to shop_products.reserved_stock; damaged items get a wastage event recorded separately. On refund, the payment_status flips to 'refunded' which fires the 00565 trigger to release any remaining reservation and post a counter-entry to the revenue journal.
Chapter 11
Cashier shifts at every counter#
The canteen, the shop, the library (for fines), and the transport desk all collect cash. Each cashier opens a shift at the start of their day with an opening_float declared, runs their counter through the day, and closes the shift with declared_cash. The system computes the expected_total (opening float + expected takings from the till's payment events) and stamps the variance. Non-zero variance lands the shift on the accountant's reconcile queue at /dashboard/finance/cashier-shifts.
Three SECURITY DEFINER RPCs cover the till lifecycle: cashier_shift_open(location_kind, location_id, opening_float) — opens the shift, returns the row; cashier_shift_close(shift_id, declared_cash, notes) — stamps closed_at + variance and routes to reconcile or done; cashier_shift_reconcile(shift_id, notes, status) — accountant signs off with reconciled or disputed. A helper cashier_shift_expected_takings(shift_id) live- computes the expected total by walking every payment event tagged to the shift.
Permissions
Who can do what#
assets.view— read the register, locations, depreciation schedules, bookings.assets.create— request a booking, report a breakage, record an asset acquisition. Default for any staff member.assets.edit— approve a booking (with the deposit), check it out, return it, cancel it; approve a breakage write-off; log a wastage event; apply a transfer. The bursar's day-to-day surface. (All 9 of these mutations were ungated before Pass 1 — now gated.)assets.manage— manage categories, edit the inventory item catalogue, configure reorder thresholds, run depreciation schedules.shop.view / shop.create / shop.edit / shop.delete— mirror of the assets.* set, scoped to the shop catalog and order surfaces. Parents have shop.view via parent_guardian role; RLS scopes to their own child's orders via the parent_student_links → parents → user_profile_id walk (corrected in 00653).finance.create / edit / manage— cashier shift open / close / reconcile. Cashier role gets finance.create + finance.edit; accountant gets the reconcile gate.assets.delete— hard delete categories / inventory items. Default for school admins only.
What makes this elite
- 01
Five atomic ledger fanouts atomic
Acquisition, breakage write-off, wastage loss, transfer (no journal — internal), depreciation. Every state transition writes the journal entry in the same transaction the operator's click committed in.
- 02
Depreciation that actually posts actually
asset_depreciation table existed since 00012 but had zero posting logic — bursar manually reconciled monthly. Migration 00657 adds the journal_entry_id + posted_at columns + the auto-post trigger. Idempotent RPC for retry safety. Now atomic with the rest of the cluster.
- 03
Bookings + assignments — separate surfaces, same row same row
Long-term assignments (asset_assignments) and short-term checkouts (asset_bookings 00578) co-exist on the same asset_id. Asset.status reflects whichever is current — 'booked', 'assigned', 'maintenance', 'retired'. No data duplication.
- 04
Multi-storeroom from day one multi-storeroom
location_inventory (00571) tracks per-location stock. inventory_transfer_apply RPC (00586) is atomic across both sides. Schools with a kitchen + chemistry lab + main store + sports cage stop double-bookkeeping.
- 05
Breakage write-off requires explicit approval explicit
Anyone with assets.create reports a breakage. Only assets.edit approves the write-off — which is the moment the journal posts. Approval is the only place authority over money happens. Tight authorisation chain.
- 06
9 permission gates added in one audit 9 gates
Pass 1 caught the worst gate coverage in the Operations cluster: 9 ungated mutations across 4 hook files (use-asset-bookings, use-inventory-transfers, use-inventory-wastage, use-inventory-locations). All gated with assets.create or assets.edit per their cluster's authorisation chain.