# TOBAS decision log

Each entry records an assumption or choice, why it was made, and who confirmed it.
Source brief: `TOBAS_Platform_Master_Build_Prompt.docx` (v1.0, 18 Oct 2026).

## D-001 Build TOBAS by evolving Ecclesia Ledger (confirmed by owner, 18 Oct 2026)

The brief describes a fresh Laravel 11 + Livewire build. This repository already holds a working
church accounting system (ledger and posting service, giving, budgets, fixed assets, remittances,
licensing/billing, member portal). TOBAS is built on top of it rather than from scratch.

- Kept: Laravel 12, Inertia + Vue 3, Tailwind 4. The brief's Livewire/Alpine choice is not adopted,
  because rewriting about 90 working Vue pages adds risk without adding features.
- Added: the public Website/CMS surface and the TOBAS Directs surface, plus the brief's missing
  modules, phase by phase.
- PHP: XAMPP runs 8.2. Laravel 12 supports it; the brief's 8.3 target applies to the production server.

## D-002 Visual direction: "Hymnal" (confirmed by owner, 18 Oct 2026)

Oxblood `#5E1A28`, burnished gold `#B8893B`, parchment `#F6F1E7`, ink `#1E1A17`.
Fraunces (display), Inter (interface), IBM Plex Mono (document numbers).
Signature motifs: the arch (church window and gateway to "the old paths"), letterpress rules and
dotted leaders, accounting single/double total rules, and a "posted" seal on final entries.
Branch sidebar is oxblood; Directs sidebar is ink, so HQ users always know which surface they are on.

Church name: Old Path Revival Church (TOPRC). Platform: TOBAS.

## D-003 Design is approved as a static prototype before code (confirmed by owner, 18 Oct 2026)

Prototype lives in `public/prototype/` (served by XAMPP at
`http://localhost/church_acct_app/public/prototype/`). Once approved, its tokens move into
`resources/css/app.css` and the prototype folder is deleted before going live.

## D-004 Chart colours are validated, not eyeballed

Series colours were checked for colour-blind separation and contrast against the card surface:
light `#963247` / `#A2772E`, dark `#CF5A70` / `#B38533`. The dark pair sits in the 6 to 8 CVD band, so
charts always add a second cue (bars versus line with markers, legend, direct label).

## D-005 Phase 0 plan approved as written (confirmed by owner, 18 Oct 2026)

See `docs/phases/phase-0-plan.md`. Baseline before any Phase 0 change: 93 tests passing (983 assertions).

## D-006 Roles use spatie/laravel-permission with branch "teams" (confirmed by owner, 18 Oct 2026)

Roles are stored in the database and scoped per branch, so one person can hold different roles in
different branches. Existing users' `role` column is migrated into role assignments.

## D-007 Branch users see their own branch only (confirmed by owner, 18 Oct 2026)

This replaces today's "own branch and everything below it". Multi-branch visibility is reserved for HQ
roles, auditors (an assigned branch set) and a future regional role.

## D-008 Git before code (confirmed by owner, 18 Oct 2026)

The owner installs Git for Windows. The current app is committed as the baseline before any Phase 0
change, and Phase 0 is built on a `phase-0-foundation` branch.

Note: on this PC the installed program is GitHub Desktop, which bundles git
(`%LOCALAPPDATA%\GitHubDesktop\app-<version>\resources\app\git\cmd\git.exe`). It was used for Phase 0.
Installing Git for Windows would put `git` on the PATH for terminals.

## D-009 Role keys and the users.role column (Phase 0, developer decision)

- Existing role keys are kept so working screens and tests are unchanged: `admin` is the brief's HQ
  Administrator and `auditor` is the External Auditor. New roles: `hq_finance`, `branch_admin`,
  `cashier`, `pastor`, `content_editor`, `ministry_leader`. The older specialist roles (counter, welfare
  officer, unit manager, trustee, secretary, department head, financial secretary, senior pastor) stay.
- spatie/laravel-permission holds every assignment (role + branch). `users.role` + `users.branch_id` are
  kept as a readable mirror of the role in the user's home branch, and saving them re-syncs the
  assignment. This replaces the plan's "drop the column next phase": the mirror keeps 2FA settings,
  screens and exports simple, and the tests guard that both stay in step.
- Network roles (see every branch) are `admin`, `hq_finance` and `auditor`, and only when held in the HQ
  branch. Held in a branch, they see that branch only.
- A user with no branch belongs to the HQ branch. A Treasurer at HQ sees HQ's books only; HQ-wide finance
  work needs the HQ Finance Officer role. (This changes behaviour for existing HQ treasurers in
  multi-branch churches; it follows D-007.)
- `users.manage` is a new permission so branch admins can manage their own users without
  `settings.manage` (church-wide settings).

## D-010 Branch isolation design (Phase 0)

- 19 tables with `branch_id` use a global scope (`BelongsToBranch`); 23 child tables without one follow
  their parent (`BelongsToBranchThrough`). A test fails if a new `branch_id` table is not scoped.
- Events with no branch are network-wide and visible to every branch. Bank accounts, meetings and report
  schedules with no branch are HQ-level and hidden from branch users.
- Remittances are visible to both the remitting and the receiving branch.
- Console commands, queued jobs and webhooks run without a branch restriction (the church scope still
  applies). Services that must write into another branch use `BranchContext::unrestricted()`.
- The controllers' older hand-written branch filters remain as a second layer; they now read the same
  branch set, and can be removed module by module.
- Tenant-level records not scoped by branch for now: depreciation runs and statutory remittances (they
  span the whole church), online gifts (branch routing arrives in Phase 9), and reference data
  (accounts, funds, categories, vendors).

## D-011 Audit chain (Phase 0)

One SHA-256 chain per church (and one for platform events). Each row hashes its content plus the
previous row's hash; `audit_chain_heads` stores the latest link so deleting the newest rows is detected.
JSON values are hashed in a canonical form because MySQL rewrites JSON text. Existing rows were linked
by the migration. `php artisan audit:verify` checks the chain and runs in CI on MariaDB.

## D-012 Quality tooling (Phase 0)

- Larastan level 6. 1,391 findings in older code are frozen in `phpstan-baseline.neon`; Core is clean.
- Pest is **not** installed. Pest 3 is the last version for PHP 8.2 and requires a PHPUnit release
  with a published security advisory. The PHPUnit tests stay; Pest 4 can be added with the PHP 8.3
  upgrade (the brief's production target).
- Laravel Pint is enforced in CI. The whole codebase was formatted once, in its own commit.

## D-013 Fonts and product name (Phase 0)

Fonts are self-hosted from npm (@fontsource): Fraunces (optical size axis), Inter, IBM Plex Mono.
The product is called TOBAS in the interface, emails and PDFs. Two identifiers keep the old name on
purpose: the backup file signature (old backups must still restore) and the default seller name in
`config/billing.php` (a legal entity name to confirm).

## D-014 Staff app moves to /app (confirmed by owner, 18 Oct 2026)

The public website takes the root addresses. Staff screens move under `/app`, old addresses redirect
permanently, and `TOBAS_APP_DOMAIN` can later put the staff app on its own subdomain.

## D-015 Public site is server-rendered Blade with Alpine.js (confirmed by owner, 18 Oct 2026)

For speed on weak networks and for search engines. CMS editing screens stay in the Vue staff app.

## D-016 Branch locator uses OpenStreetMap via Leaflet (confirmed by owner, 18 Oct 2026)

No API key or billing; loaded only on the Branches page, with a searchable list as fallback.

## D-017 Phase 1 in four increments, 1A to 1D (confirmed by owner, 18 Oct 2026)

Each increment stops for the owner's review. Plan: `docs/phases/phase-1-plan.md`.

## D-018 Content Editor is an HQ-wide role; 1A site screens are Headquarters' (Phase 1A, developer decision)

`content_editor` joins the network roles: held at Headquarters it manages every branch's website content
(brief section 6.3, "HQ editors manage all content"). It carries no finance permissions, so seeing every
branch exposes no financial data. Held in a branch, it sees that branch only.
In 1A the site-wide screens (pages, menus, banners, messages, settings) are HQ-only; branch editors can
use the media library (their own files, plus placing HQ files). Branch pages arrive in 1B.

## D-019 Public website technical choices (Phase 1A, developer decision)

- **Forms without CSRF tokens.** Public site pages are cached, so they cannot carry a per-visitor token.
  Public forms (contact now; prayer and others later) have no signed-in privileges to protect, so they
  use a hidden honeypot field and per-address rate limits instead. No reCAPTCHA by default: it sends
  visitor data to Google. Staff forms keep full CSRF protection.
- **Alpine.js CSP build.** The site's Content-Security-Policy forbids `eval`, so the site uses Alpine's
  CSP build: templates only reference named properties and methods.
- **Page cache.** Whole pages are cached for visitors who are not signed in (10 minutes, cleared when
  any website content is saved, and automatically on each new build). The security nonce is swapped per
  request.
- **Rich text** is cleaned server-side with HTML Purifier (paragraphs, two heading levels, emphasis,
  lists, quotes, http/https/mailto/tel links). Headings use `*asterisks*` for the italic emphasis.
- **Uploads**: JPEG, PNG, WebP, GIF (10 MB) and PDF (20 MB). The type is read from the content, and
  pictures must decode before anything is stored. SVG is refused (it can carry scripts). Every picture
  gets WebP sizes (320 to 1600 px) and a 1200x630 sharing image.
- **Embeds** are limited to YouTube (privacy mode), Vimeo, Facebook video, Google Maps embed and
  OpenStreetMap; the CSP allows only those frames plus OpenStreetMap tiles.

## D-020 Sermon media (Phase 1B, developer decision)

A sermon can carry a video link (YouTube, Vimeo or Facebook, shown through the D-019 embeds), an audio
file uploaded to the media library (MP3 or M4A, up to 100 MB or the server's PHP upload limit if lower),
or a direct link to an MP3 hosted elsewhere, plus optional notes (PDF). Large audio is better hosted
elsewhere, so the direct link exists for churches on small hosting plans. The CSP gains
`media-src 'self' https:` so linked audio can play. Views are counted by a small beacon, once per visitor
address per 6 hours, so cached pages still count.

## D-021 Live page (Phase 1B, developer decision)

`/live` shows the stream when a service from Site settings is running (start time plus the configured
duration, 150 minutes by default), or when an HQ editor presses **We are live now** (which lasts the same
duration, or until **Stop**). Otherwise it shows the next service with a countdown and the last replay.
`/live` is never page-cached, because its state changes by the minute. Prayer requests sent from the
Live page go to the CMS Messages inbox until the prayer wall arrives in 1C.

## D-022 Content collections and who edits them (Phase 1B, developer decision)

Sermons, series, ministries, news, gallery albums, testimonies and branch pages share one generic CMS
screen driven by a field list per collection (`CollectionController`), so later content types cost little.
Branch content editors manage their own branch's sermons, ministries, news, albums and branch page;
HQ editors manage all of it. The live stream and testimonies are HQ-only because they speak for the
whole church. Testimonies sent from the website wait for approval and are never shown before it.

## D-023 Events on the website (Phase 1C, developer decision)

The website's event pages (`/events`, list and month calendar) use the existing event bookings, so
capacity, waiting list and door check-in behave exactly as in the staff app. Events gain an address,
a picture and a "show on the website" switch (on by default, as open events were already public at
`/e/`). Old `/e/` links redirect for the website's church; other churches on the platform keep them.
- **Tickets**: after booking, a signed link (also in the confirmation email) opens the ticket: QR code,
  add-to-calendar (.ics and Google Calendar) and cancel. The QR code holds only the ticket code. Anyone
  holding the link can see or cancel that booking, as with the earlier emailed cancel link.
- **Repeating events** are created as separate events (weekly, fortnightly or monthly on the same date,
  2 to 52 times) sharing a series key, so each date has its own capacity and bookings. Edits can be
  copied to later dates; times are never copied.
- **Door scanner** (`/app/events/{id}/scan`): the phone camera reads tickets with the browser's own
  barcode reader, or the small jsQR library where there is none (iPhone Safari, Firefox). Typing the
  code always works. The camera needs an https address.
- Event pages are page-cached; every booking and cancellation clears the cache so places left are right.

## D-024 Prayer requests (Phase 1C, developer decision)

Prayer requests are sensitive personal data (religious belief, often health). They go to a new
`prayer.manage` permission held by a new **Prayer Team** role (a network role: held at HQ it sees every
branch), branch pastors and senior pastors. Content editors do not see them. Opening one is logged.
A request reaches the public wall only when the sender ticked "others may pray too" and the team
switched it on; the wall shows a first name only, never contact details. "I prayed" counts once per
browser using a random id in a cookie (not personal data, so not encrypted). Requests from the Live
page (D-021) now go here instead of Messages.

## D-025 Newsletter (Phase 1C, developer decision)

Double opt-in: nothing is sent until the address's owner opens the confirmation link (valid 7 days).
The form answers the same whatever the address's state, so it cannot reveal who is subscribed.
Confirm and unsubscribe links are signed URLs (no tokens stored). Unsubscribing takes one button
(mail link scanners must not unsubscribe people), and newsletters carry RFC 8058 one-click
unsubscribe headers, which Gmail and Yahoo require for bulk mail. Staff may add someone by hand only
after ticking that the person agreed. Subscribers are church-wide: only Headquarters writes to them.

## D-026 Broadcasts (Phase 1C, developer decision)

One message to members (all, by status, a branch, or groups) by SMS or email, or to newsletter
subscribers by email. Members receive it only on a channel they consented to (NDPA); the screen shows
how many can be reached before sending, and asks for confirmation. Each broadcast is recorded, and each
person's message is in the existing Messages log with its delivery status. WhatsApp is not offered for
broadcasts: Meta allows only pre-approved templates, which the statement and receipt messages use.
At most 20,000 recipients per broadcast, queued and sent in the background.

## D-027 Birthdays and anniversaries (Phase 1C, developer decision)

Members gain a wedding anniversary date. Every morning at 07:00 (if the church switches it on) the
chosen staff receive a list of the next seven days, each seeing only the branches they may see, and
members can receive a greeting by SMS or email, with consent, once a year. 29 February birthdays are
remembered on 28 February in other years. Both are off until an administrator turns them on.

## D-028 Member portal additions (Phase 1C, developer decision)

The portal's new "Church life" page shows the member's event bookings (bookings made with their email
or phone are linked to them), ministries they can ask to join with one click, their prayer requests
(with an option to stay anonymous) and documents to download. Downloads are media-library files,
which anyone holding a file's unguessable address can open: the portal list is a convenience, not
a vault, so confidential papers do not belong there.

## D-029 Starter website for a real church (Phase 1D, developer decision)

`php artisan site:install {admin email}` builds a church's first website (settings, menus, Home, About,
Contact, Privacy) with its own name and clearly bracketed "[Replace…]" text, and nothing invented: no
sample people, testimonies, address or phone. The demo seeders keep the Old Path Revival Church sample
content for testing and must not run on a live server. Both use the same StarterSiteService and are
safe to run again (existing pages, menus and settings are left alone).

## D-030 Church overview and release checks (Phase 1D, developer decision)

- **Church overview** (`/app/overview`) replaces a separate "CMS dashboard": one page of the last twelve
  weeks (giving, attendance, bookings, new members, sermon views, website and prayer counts), where each
  panel appears only for roles that cover it and every figure respects branch scope. Charts are CSS bars
  with a table view (and a screen-reader table), no chart library.
- **Accessibility**: `npm run a11y` runs axe-core (WCAG 2.1 AA and best-practice rules) over the public
  pages of a running site; a PHPUnit test checks the same pages' structure on every run. Colour contrast
  is measured in a real browser in light and dark mode (jsdom cannot); the muted text colour was darkened
  slightly to pass on the footer background.
- **Speed**: hashed build files are cached for a year, uploaded pictures for 30 days, text compressed
  where the server allows (`public/.htaccess`). OPcache is a launch requirement. Lighthouse is measured on
  the live server, not on XAMPP without OPcache, where timings would mislead.

## D-031 Five-digit hierarchical chart of accounts with branch sub-accounts (Phase 2A, confirmed by owner, 18 Oct 2026)

- **Codes** `X0000` class (1 assets … 5 expenditure), `XY000` group, `XYZZ0` account (steps of 10),
  `XYZZn` a branch's own sub-account (n = 1 to 9). Children always share their parent's type; only
  accounts and sub-accounts post. The rules live in one place, `ChartService`.
- **Universal vs branch.** Headquarters (`accounts.manage` at HQ) owns the universal chart. Branches add
  sub-accounts only under accounts HQ marks *open to branches* (`is_extendable`), with the new
  `accounts.extend` permission (branch admins and treasurers) or `accounts.manage` held in a branch.
  Each branch numbers its own sub-accounts (two branches can both have 52051), a sub-account posts only
  in its branch, and reports across several branches count it inside its parent so figures compare.
- **Locked accounts** (system accounts and every class and group heading) keep their code, name, type
  and place. System accounts cannot be switched off; an account with branch sub-accounts stays open.
- **Renumbering.** Existing 4-digit charts were moved once by migration: account ids are unchanged,
  so no posted line was touched; `account_code_map` keeps every old and new code. Accounts the template
  did not know (bank accounts, business-unit accounts) were placed in the group their kind belongs to.
- Modules find where to add accounts by group key (`group_cash_bank`, `group_unit_income`…), never by a
  hard-coded code. A branch's new bank account becomes its sub-account under the main bank account.
- Versioned chart templates and their controlled rollout to branches remain Directs work (Phase 5).

## D-032 Cost centre and ministry dimensions (Phase 2A, confirmed by owner, 18 Oct 2026)

Journal lines can carry a cost centre (new `cost_centres`, church-wide or one branch's) and a ministry
(the website's ministry list, one list for the web page and the accounts). The posting service refuses a
cost centre or ministry belonging to another branch, and reversals carry both. The outlet dimension is
the existing business unit. Trial balance, income and expenditure, financial position, changes in funds
and the general ledger can be filtered by either; the filter is named under the report title.

## D-033 Reporting framework and stored times (confirmed by owner, 18 Oct 2026)

- Statements follow **IFRS for SMEs as adopted in Nigeria**, with a not-for-profit "funds and reserves"
  section for equity. The church's auditor is to confirm (brief section 5).
- Times stay stored as **Lagos time** rather than UTC (brief section 3 suggests UTC): Nigeria has no
  daylight saving, so stored times are unambiguous, and converting every existing record carries risk
  for no gain. Displays are unchanged.

## D-034 The database enforces the ledger rules; documents numbered per branch (Phase 2B, developer decision)

- Database triggers, on MySQL/MariaDB and SQLite alike (so the automated tests prove them), refuse:
  posting a journal with fewer than two lines, unbalanced debits and credits (in total or within any
  fund), a total that does not match its lines, a date in a closed month, or a reversal without a posted
  original; any change to a posted journal except recording its reversal once; any change, deletion or
  addition of lines on a posted journal; a line that is negative or both a debit and a credit. The posting
  service checks the same first and gives the friendly message; the database is the backstop for raw
  queries, imports and manual edits. Checked on the local MariaDB: every attempt refused.
- Accounting documents (journals, giving batches, expenses, payment vouchers, bills, remittances, sales,
  payroll) are numbered per branch, per type, per year, gap-free: `JV-BR002-2026-000041` (the branch code
  without punctuation). Registers (members, staff, assets, projects, welfare cases, resolutions, leave)
  stay church-wide. Numbers issued earlier keep their old form.

## D-035 Periods: Headquarters Finance reopens, older years opened on purpose (Phase 2B, developer decision)

- Closing a month stays with accountants (`periods.manage`). Reopening needs the new `periods.reopen`
  permission (HQ Finance and administrators, brief section 5), a reason of at least 10 characters kept on
  the period (`reopened_by`, `reopened_at`, `reopen_reason`) and in the audit trail, and is refused for a
  month inside a closed fiscal year (reopen the year, which reverses its closing entry).
- Last year, this year and next year open on first use. Any other year must be opened on purpose
  (Setup › Periods, with a reason), so a mistyped date is refused instead of silently creating a year.

## D-036 Traceability and the nightly ledger check (Phase 2B, developer decision)

- Each journal records the module that made it (`source_module`), and the IP address and device
  (`X-Device-Id`) that created and posted it, in addition to the hash-chained audit log.
- `php artisan ledger:verify`, scheduled at 03:30, checks every church read-only: every posted journal
  has two or more lines, balances in total and per fund, and matches its total; posted journals have only
  posted lines and the reverse; the whole ledger balances; each number series runs 1..n without gaps or
  repeats; every reversal and its original point at each other. Results are kept (`ledger_checks`); a
  failure emails HQ Finance and administrators and shows a red notice on the dashboard.
- The trial balance is checked for the whole church, not per branch: a journal may legitimately carry
  lines in two branches (remittances), so a single branch's lines need not balance on their own.

## D-037 Supporting documents (Phase 2B, developer decision)

PDFs and pictures (JPEG, PNG, WebP; type read from the content; up to 10 MB) can be attached to any
journal. They are stored on the private disk (never the public media library), opened only through a
permission-checked link within the user's branches, and fingerprinted (SHA-256). On a posted journal files
can be added but not removed; on a draft, the uploader or a reviewer can remove one. Additions and
removals are written to the audit trail.

## D-038 Timestamps are never rewritten by the database; Phase 2 proved on both databases (Phase 2C, developer decision)

MySQL and MariaDB quietly give the first non-null TIMESTAMP column of a table an "update to now whenever
the row changes" rule. Running the whole test suite on MariaDB showed the harm: one wrong member-portal
code reset the code's expiry to "now", so the right code was then refused. The same rule had caught
approval, sign-off, case-note and ledger-check times. Migration `2026_10_22_000003` removes it from
every column except `updated_at`, where it is wanted. SQLite never had the rule.

Phase 2 is proved by the full automated suite on SQLite and on MariaDB (CI job `tests-mariadb`), by a
whole-ledger proof test that builds the demo church through every module and checks every journal,
every number series and the trial balance directly, and by `ledger:verify` on the seeded demo church in
CI. The newest migrations also roll back and forward again on MariaDB in CI. Evidence:
`docs/phases/phase-2-exit-evidence.md`.

Working rule learnt the hard way: while `bootstrap/cache/config.php` exists, artisan ignores DB_* set in
the shell, so a command meant for the test database runs on the live one. Run `php artisan config:clear`
before pointing artisan at another database, or reach the test database only through phpunit (whose
config cache is a separate file).

## D-039 Headquarters Finance reopens a closed year (owner request, 18 Oct 2026)

Reopening a closed fiscal year needed `settings.manage` (the HQ administrator only). It now needs
`periods.reopen`, the same permission as reopening a closed month (D-035), held by HQ Finance and the
HQ administrator; brief section 7.10 asks for HQ Finance approval with a recorded reason. Branch staff,
accountants and treasurers can still close a year (`periods.manage`) but cannot reopen one. The reason
(at least 10 characters) goes to the audit trail, and reopening reverses the closing journal as before.

## D-040 Imprest floats and cash counts (Phase 3A; plan decision 1 confirmed by the owner, 18 Oct 2026)

- **A float per branch and custodian**, held in its own branch sub-account under 11020 Petty Cash / Imprest,
  with a limit. Cash is issued to it from a bank account of the same branch (or one not tied to a branch)
  and can never take it above the limit.
- **Petty cash vouchers** are expenses only, numbered per branch (`PCV-BR002-2026-000001`), posted at once
  (Dr expense with fund, cost centre and ministry; Cr the float) and refused if the float would go below
  ₦0.00. The custodian records them; someone with `imprest.manage` may record on their behalf. Receipts
  are attached on the voucher's journal (D-037).
- **Top-ups** (`IMP-…`) cover exactly the vouchers since the last request, one open request per float. They
  are decided by someone who is neither the requester nor the custodian (`imprest.approve`); approval posts
  Dr float / Cr bank **fund by fund**, so each fund's share of the float returns to where it was. A
  returned request releases its vouchers into the next one.
- **Cash counts** (`CC-…`) for any cash account, by denomination, outside giving batches. Changing the
  sheet clears its signatures; the church's `count_signoffs` setting (at least 2) of different people must
  sign. The last signature takes the book balance at the count date and posts any difference to Cash Count
  Excess or Shortage.
- **Cash book** report: the ledger of one cash account with the running balance and who entered each line;
  its closing balance is the general ledger's.
- Permissions: `imprest.manage` (open, edit, close floats; issue cash), `imprest.spend` (custodian),
  `imprest.approve`, `cash.count`. Branch admins and treasurers hold all four; accountants manage, approve
  and count; cashiers spend and count. Viewing uses `banking.view`.

## D-041 Bank book: transfers, alerts, Excel import, sign-off and lock, fixed deposits (Phase 3B; plan decisions 3 and 6 confirmed, 18 Oct 2026)

- **Account kinds and minimums.** Bank accounts are current, savings, deposit/call, fixed deposit or
  domiciliary, with an optional minimum balance. After every posting that touches a bank account, and
  nightly (`bank:daily`, 06:00), an account below its minimum emails its branch's treasurer, accountant
  and branch admin and Headquarters Finance and administrators, once, and shows on the dashboard until it
  is back above. (A user with no branch works at Headquarters.)
- **Transfers** (`TRF-…`, `banking.transfer`) between two accounts of the same branch (an account not
  tied to a branch may pair with any), same currency only: Dr destination, Cr source for amount plus fee,
  Dr Bank Charges for the fee. Money between branches stays with remittances.
- **Statement import** from CSV or Excel (PhpSpreadsheet, decision 3). Columns are taken from the user's
  choice, then from the names chosen at the account's last import, then from the usual bank headings; if
  none fits, the user picks the heading row and columns from a preview, and the choice is remembered. Excel
  dates are read from the cell values, never from their display format.
- **Auto-match** needs the same amount and is within 5 days, or within 60 days when the references agree
  (the journal's reference or number appears in the statement's reference or narration, four characters or
  more). Agreeing references win first, then the nearest date.
- **Sign-off and lock.** A completed reconciliation keeps the items behind its totals (snapshot), so its
  printed statement never changes. Someone other than its preparer signs it off (`banking.reconcile`).
  After that, statement lines dated on or before it cannot be unmatched, older lines are left out of
  imports, and no reconciliation can be completed for an earlier date. Previously unmatched items may still
  be matched or booked later: that is how outstanding items clear. (This refines the plan's wording, which
  would have stopped old items from ever clearing.) Year-end close now asks for signed-off reconciliations
  (brief 7.10).
- **Fixed deposits** (`FD-…`, decision 6): placement Dr Fixed Deposits / Cr bank; simple interest,
  actual/365, drafted at each month end before maturity as Dr Accrued Interest Receivable / Cr Bank
  Interest in the placer's name, so someone else posts it; at maturity what the bank paid goes to the bank
  account (or into a new deposit when rolled over), clearing the principal and posted accruals, with any
  difference to Bank Interest. Interest drafts must be posted or discarded first. Reminder 14 days before
  maturity. New system accounts: 11200 Fixed Deposits, 12050 Accrued Interest Receivable; 43010 Bank
  Interest is tagged.
- **Bank Book** report: one bank account's ledger with who entered each line and the statement date it
  cleared.
- **Lines from another branch's journal** (found while checking 3B in the browser): a remittance is the
  paying branch's journal with a line in the parent's bank account. The parent's staff see that line, and
  now also its journal's header (number, memo); before, the bank page and the ledger reports failed on it.

## D-042 Receipts, pledges, the review step and cheques (Phase 3C; plan decisions 2, 5 and 7 confirmed, 18 Oct 2026)

- **Receipts** (`RCT-…`, per branch) are issued when a giving batch is posted, inside its transaction so
  numbers stay gap-free: one per identified giver for everything they gave at that service, and one for the
  plate and unidentified gifts. A giver from another branch still gets a receipt in their own name, whoever
  posts the batch. Receipts are printable (A5 PDF with the branch's name and address) and emailed as a PDF
  only to members who agreed to email (NDPA); gifts made online are not emailed twice. Batches posted before
  3C have no receipts. Other income (journals) does not get receipts.
- **Pledges** may belong to a campaign (which fixes category, fund and project; optional target) and be paid
  once, monthly or quarterly in up to 60 instalments from a first due date; the last instalment's date is
  the pledge's due date. Gifts against the pledge in posted batches count; it becomes "fulfilled" when they
  reach the amount. `pledges:remind` (08:00 daily) reminds members N days (church setting
  `pledge_reminder_days`, default 3) before an instalment when they are behind, by SMS and email with their
  consent, once per instalment and channel. Campaigns are opened by `giving.post`; pledges are still added on
  the member's page (`members.manage`).
- **Requisitions** go prepared → **reviewed** → approved → paid. The reviewer (`expenses.review`: accountant,
  treasurer, branch admin) checks coding, fund and documents and may reject with a reason. Nobody does two
  steps on the same requisition: the preparer cannot review, approve or pay; the reviewer cannot approve or
  pay; an approver cannot pay. With two approvals a requisition therefore needs five people; requisitions
  approved before 3C are not reviewed retroactively. Awaiting review counts as a budget commitment.
  The payment voucher prints with every step's name and date.
- **Cheques**: cheque books are registered per bank account (first and last leaf, at most 1,000; no
  overlaps). Paying by cheque (requisitions, supplier bills, welfare) takes the next leaf of the account's
  oldest open book, refused when there is none; the payment reference becomes `CHQ 000123`, which statement
  matching recognises. A spoilt leaf is recorded with a reason. A cheque is "presented" when its payment is
  matched to a statement line and "issued" again if unmatched. Welfare cheques show the case number, never
  the beneficiary. Cancelling an issued cheque means reversing its payment (Phase 3D).
- **Reports**: Receipt Register (ties to giving posted), Pledge Status (by campaign, as at a date), Payment
  Voucher Register (requisitions, bills, welfare), Cheque Register (with cheques issued but not presented).

## D-043 Every journal has a checker; reversal wizard; recurring and auto-reversing journals (Phase 3D; plan decision 4 confirmed, 18 Oct 2026)

- **Reversals asked for by hand are drafts.** The Reverse button drafts the reversal with its reason; a
  second authorised person posts it, and posting is what marks the original reversed. Only one reversal may
  wait for a journal; a draft is refused at posting if the journal was reversed another way meanwhile.
  Reversals that are part of an approved process (cancelling a remittance, reopening a year, undoing a
  depreciation run…) still post at once inside that process.
- **Reversal wizard.** The date offered is the journal's own if its month is open, otherwise the first open
  day after it. "Then enter the corrected journal" opens a new journal with the original's lines filled in.
- **Auto-reversing accruals.** A manual journal can be marked as an accrual; on the 1st of the next month
  `journals:automate` (05:30 daily) drafts its reversal in the preparer's name, so another person posts it.
- **Recurring journals** are made from a posted journal (monthly, quarterly or yearly, optional end date,
  optionally each one an accrual). Each run drafts the journal in the name of whoever set it up; missed
  runs are caught up; a run in a closed month is reported, not posted. Nothing is ever posted automatically.
- **Source module for every posting** (brief section 5). Module postings that did not name their module
  were recorded as "manual"; `LedgerService::record()` now labels them with the calling service (a bill
  payment "bill", a depreciation run "fixed_asset"…). "manual" now means only journals made by hand.
  Journals posted before this change keep their label: posted journals never change.
- **Phase 3 proof**: `BranchModulesProofTest` and `docs/phases/phase-3-exit-evidence.md`.

## D-044 Fixed assets: conventions, improvements, write-offs, transfers, counts, history (Phase 4A; plan decisions 1 and 2 confirmed, 19 Oct 2026)

- **First month of depreciation** is set per category: a full month in the month acquired (the default, as
  before), from the following month, or only the days owned (pro rata, the remaining life spreading the
  rest). It is copied onto each asset when registered, so changing a category never rewrites history.
  Nothing is charged in the month of disposal or write-off.
- **Depreciation stops** at cost less residual value (straight line after its life; reducing balance never
  below the residual), and on disposal or write-off. Proved by `FixedAssetLifecycleTest` (exit criterion).
- **Improvements** add to cost after depreciation is brought up to date; the remaining (optionally longer)
  life spreads the new carrying amount. **Write-offs** are disposals with no proceeds and their own status.
- **Transfers between branches** are requested by the branch (to any branch of the church, even one the
  requester cannot open) and posted only when HQ Finance approves (`assets.approve_transfer`: HQ Finance and
  administrators); the requester cannot approve their own. Returned requests keep the reason.
- **Photos** are kept privately (JPEG, PNG or WebP, type read from the content, up to 10 MB) and shown only
  to people who may see the asset. **QR labels** open the asset's page in TOBAS (staff sign-in needed).
- **Verification counts** cover a branch or one location; each asset is marked found, poor or missing, and
  items not on the register are noted. Closing marks every unseen asset missing and records a verification
  event (with condition) in each asset's history; the closed count is the variance report.
- **History for "at any date"**: every change of cost or depreciation now keeps its before and after figures,
  so the Fixed Asset Register can be shown as at any date and ties to the ledger at that date. Revaluations
  made before this change kept no "before" figures; the report says so and shows their current cost.
- **Depreciation Schedule** report: each asset's charge month by month for a year, from where it stood on
  1 January (actual for months run, forecast after).

## D-045 Budgets by cost centre, utilisation alerts; custom roles, sessions, password policy (Phase 4B; plan decision 3 confirmed, 19 Oct 2026)

- **Budget lines by cost centre.** An expense or income account is budgeted either as a whole or split by
  cost centre (shared HQ centres or the branch's own), never both: the two would count the same spending
  twice. Lines are unique per budget, account and cost centre (`cost_centre_key` = centre id or 0).
- **Commitment control by cost centre.** A requisition may name a cost centre. When the budget has a line
  for that account and centre, control uses that line, its spending and its open commitments. Otherwise it
  uses the whole account (the sum of its lines). The cost centre flows onto the expense line when it is paid.
- **Budget holder** (optional, per budget) is emailed once when a line first reaches 80% used and once at
  100%. "Used" means spent plus committed. Without a holder, the preparer is told. The check runs nightly at
  06:30 (`budgets:alerts`). The dashboard lists the lines at 80% or more for anyone with `budgets.view`. A
  requester is told the line's new utilisation when they create a requisition.
- **Copy forward** makes next year's draft from any budget, as is or with a percentage added to every
  line, once per branch, fund and year. It is a draft and goes through the usual maker-checker approval.
- **Budget Variance report** for any run of whole months in one year (month, quarter, year buttons),
  optionally for one cost centre. Variance is shown as favourable when positive.
- **Custom roles** (plan decision 3). Anyone who manages users (e.g. a branch admin) can define a role under
  Setup › Roles; only its author or someone with `settings.manage` can change or delete it. A role
  can carry only permissions its author holds, and is granted under the existing rule that you can only give
  access you hold. Its row in the roles table is named `t{church}_{slug}`, so churches never clash. It is held
  per branch like the built-in roles. Changing it changes its holders' access at once. It cannot be deleted
  while someone holds it, and cannot share a name with another role. Every change is audited.
  `permissions:sync` leaves custom roles alone.
- **Sessions and devices.** Everyone sees where they are signed in (device, IP address, last active) and can
  sign out one device, or all others after confirming their password. An administrator can sign a user out
  on every device (lost phone, someone leaving). All of this is audited.
- **Password policy** per church: minimum length (10 to 64; never below the platform's 10), expiry in days
  (0 = none), and how many earlier passwords cannot be reused (0 = only the current one). An expired
  password holds the user on the security page until they change it, like required two-factor login.
  Earlier password hashes are kept in `password_histories`, trimmed to the policy.

## D-046 Statements and reports: cash flow, notes, comparatives, downloads, letterhead (Phase 4C; plan decisions 4, 6 and 7 confirmed, 19 Oct 2026)

- **Statement of Cash Flows, direct method** (decision 4). Every posted journal that moves a cash or bank
  account is classified once, by the accounts on its other lines:
  - investing if it touches property, plant and equipment, work in progress or fixed deposits;
  - financing if it touches fund balances or loans;
  - operating otherwise.

  Its cash is shared among the lines on the other side in proportion to their amounts. A payment net of WHT
  therefore shows what was actually paid. Operating flows are grouped by the chart's level-2 groups;
  investing and financing flows by account. Transfers between the church's own cash and bank accounts are
  not flows. Opening-balance journals count in the opening cash. Inter-branch accounts are shown together
  when consolidated so they net out.
- **The indirect reconciliation** sits under the statement. It starts from the surplus, reverses income and
  expenditure posted in investing or financing journals (depreciation, donated assets, write-offs, gains and
  losses), and adds the change in each working-capital group. It equals the direct figure by construction;
  the report warns if it ever does not, or if opening cash plus the movement does not equal closing cash.
- **Comparatives.** Income & Expenditure adds a "Prior year" column: the same dates a year earlier, all
  funds together. The Statement of Financial Position shows the last 31 December before the date beside it.
- **Notes to the Statement of Financial Position**: each level-2 group's accounts, with this date and the
  prior year end, then the funds. The totals agree to the statement.
- **Tithes and Offerings by Service** (service name on the giving batch × category × month, months with
  giving only). **Income by Source** splits income by the module that posted it (`journals.source_module`)
  and agrees to income on Income & Expenditure. **Bank Reconciliation Statements** sets out every
  reconciliation in the period in statement form and flags those not signed off.
- **Downloads** (decision 7). Every report downloads as Excel (.xlsx with real numbers, money formatted with
  negatives in brackets, bold totals), PDF or CSV, with the filters on screen. A single `ReportExporter` serves
  both the download buttons and scheduled report emails.
- **Letterhead** (decision 6) on every church PDF (reports, receipts, payment vouchers, reconciliation
  statements, giving statements), on Excel headers and on printouts. It shows the website's church name and
  logo (PNG or JPEG up to 1.5 MB, embedded), and the address and contacts of the document's branch, falling
  back to the church's. Platform subscription invoices keep the platform's own header.

## D-047 Year-end wizard: branch submissions, HQ review, more checks, closing trial balance (Phase 4D; plan decision 5 confirmed, 19 Oct 2026)

- **Branches submit, HQ Finance accepts or returns** (decision 5). Every branch with postings in the year,
  Headquarters included, runs its own checklist and submits (`yearend.submit`: branch admins, treasurers,
  accountants, HQ Finance). Blockers stop a submission, and warnings must be explained.
- **Reviewing a branch.** A user with `periods.manage` who holds an HQ network role accepts the branch's
  year, or returns it with a comment. Nobody accepts their own submission. The church checklist blocks the
  close until every branch is accepted.
- **What a submission keeps:** the branch's checklist, its trial balance at year end, a fingerprint of its
  postings for the year (lines and debits, excluding closing entries) and its history. If the fingerprint
  changes after submitting, the branch must submit again before the year can close.
- A church with **Headquarters only** has nobody to submit to, so it closes as before.
- **New checks**:
  - **Cash counted at year end** (warning): every cash account holding money in a branch needs a
    completed, two-signature count within a week either side of year end.
  - **Depreciation charged for every month** (warning): every active asset has no depreciation due at year
    end. This replaces "a run reached year end".
  - **"No journals waiting for a checker"** is the existing draft-journal blocker, renamed.
- **The closing trial balance** (after the closing journal) is stored with the closed year, together with the
  accepted branches, and downloads as PDF, Excel or CSV. Reopening and closing again stores it afresh.
- **After the close**, the wizard links to the new year's opening position and the asset register brought
  forward. It also copies every approved budget to next year as a draft, with an optional uplift, wherever
  the branch and fund has none yet.
- **Roll-forward needs no carry-forward entries.** Balances are cumulative, so the proof is that the
  position, asset register and pledges on 1 January equal those on 31 December, and depreciation resumes
  in January (`YearEndWizardTest::test_year_end_roll_forward_proof`).

## D-048 Directs area, branch register and branch lifecycle (Phase 5A; plan decisions 1 and 5 confirmed, 19 Oct 2026)

- **Where Directs lives** (decision 1): its own address, `/directs`, set by `TOBAS_DIRECTS_PREFIX`, with
  the same sign-in as the staff app. It is open only to HQ network roles; anyone else gets "not found".
  Its pages use the staff app's layout, with a "Headquarters (Directs)" group in the menu.
  `is_staff_request()` counts Directs as staff, so two-factor login, password expiry and branch status
  apply there too.
- **Branch register**: the organisation tree, each branch's status, pastor, users, last posting and last
  year's year-end submission. A branch page shows its record (editable by `branches.manage`), people,
  recent journals, the branches under it, and its history.
- **Lifecycle** (decision 5), recorded in `branch_events` and the audit trail, always with a reason the
  branch sees:

  | Action | Effect |
  |---|---|
  | Suspend | The branch is read-only for everyone |
  | Lock | Its people are signed out and cannot sign in |
  | Resume | Back to active |
  | Close or merge | Asked for by an HQ Administrator; the branch is read-only ("closing") until HQ Finance (someone else, typing the branch code) confirms it |

  Headquarters itself cannot be suspended, locked, closed or merged.
- **One journal moves every balance** of a closed or merged branch, by account and fund, out of it and
  into the receiving branch (`source_module` `branch_lifecycle`). The branch's own sub-accounts go to their
  parent account on a close. On a merge each gets a twin sub-account in the receiving branch, so detail
  survives and moved bank accounts keep their own ledger account.
- **A merge also moves** members (their pledges follow them), assets in use, active bank accounts, and
  requisitions and bills not yet paid. The ledger amounts behind them move in the journal, so no record
  is double counted.
- **A plain close** first needs those settled: no assets in use, no unpaid requisitions or bills.
- **Both close and merge** need:
  - no drafts of any kind (journals, requisitions, bills, payrolls);
  - no open giving batches or cash counts;
  - no pending top-ups or asset transfers;
  - no active branches under it.

  After the transfer, the branch's sub-accounts, floats and remaining bank accounts are switched off, and
  so are its people.
- **Enforcement in the models.** Every branch-owned model refuses to be saved or deleted in a branch that
  is not active (`BelongsToBranch` → `BranchStatusGuard`). The rule therefore holds in every module and
  for Headquarters too, not only for the branch's own people. The exceptions are messages and public or
  member submissions (prayer requests, contact messages, registrations, portal requests).
- **System work still reaches suspended branches.** Headquarters' own processes (the year-end closing
  journal, depreciation runs) still reach a suspended, locked or closing branch, but never a closed or
  merged one. Nightly automations skip a suspended branch with a note.
- **People in a suspended or closing branch** see a banner and can view, export, sign out and manage their
  own security. Any other change is refused with a plain message.

## D-049 Provisioning wizard, activation links, opening balances, go-live checklist (Phase 5B; plan decisions 2, 3, 7 and 8 confirmed, 19 Oct 2026)

- **Seven saved steps** (Directs › Branch set-ups), recorded in `branch_provisionings` so the wizard can be
  left and resumed. Only `branches.manage` runs it.
  1. Branch: created at once, in the church's plan allowance.
  2. System admin: an existing user, or a new one with no usable password.
  3. Chart review: one universal chart, with the accounts open to branch sub-accounts listed.
  4. Go-live date: the church's fiscal year (decision 2); the year's periods are opened and the date must
     be in an open period.
  5. Opening balances (decision 3).
  6. Licence check (decision 7): the church plan's branch allowance; the plan is recorded. Per-branch
     licences are Phase 8.
  7. Activation.
- **Opening balances** (decision 3) are a trial balance at the go-live date, typed or read from Excel or
  CSV.
  - The file has columns Code, Fund (optional; the default fund otherwise), Debit and Credit, and may have
    title rows above the heading. The branch's own sub-account codes take precedence over the church's.
    Every problem row is reported and nothing is saved until the file is clean.
  - Each fund is balanced to Opening Balance Equity.
  - The journal (type `opening`, module `provisioning`) is a **draft that someone other than the preparer
    posts**. Saving again replaces the draft; changing the go-live date discards a draft on the old date.
    Once posted, corrections are made by journal. "The branch starts with no balances" is recorded
    instead when there are none.
- **Activation** (decision 8) needs the admin, the go-live date, opening balances or "none", and the licence
  check. A new admin is emailed a **one-time link valid for 72 hours** (an `activations` password broker
  over the standard tokens table). It opens `/app/activate/{token}`, where they choose a password under the
  church's password policy and are signed in straight to two-factor set-up. A used or expired link is
  refused; HQ can send a new one. An admin who already signs in needs no link.
- **Go-live checklist** on the branch's Directs page:
  - admin activated with two-factor login;
  - at least two people;
  - a bank account;
  - opening balances posted (or none);
  - first giving batch posted;
  - first bank reconciliation signed off.

  The checklist informs; it does not block.

## D-050 Chart of accounts versions; Headquarters looks after people (Phase 5C; plan decision 4 confirmed, 19 Oct 2026)

- **Draft, then publish** (decision 4). Headquarters' edits to the universal chart (Setup › Chart of
  accounts, `accounts.manage`) wait in one draft version:
  - new accounts and groups;
  - renames and descriptions;
  - opening or closing an account to branch sub-accounts;
  - switching accounts off or on.

  A later edit of the same kind replaces the earlier one, and a change can be taken out or the whole
  draft discarded. **Publish** applies everything in one transaction, for every branch at once, under the
  next version number and a required note. Publishing takes effect immediately, with no future-dated
  versions.
- **Version 1 is the chart as it stood.** It is shown without being stored until the first publish, when
  it is saved, unattributed.
- **What never changes**: codes, types and places (there is no edit for them); posted lines; locked
  accounts' names; the system accounts' active state.
- **Switching off** needs a zero balance across every branch and no active sub-accounts. This is checked
  when queued and again at publishing.
- **Renames keep the old name** in `account_names` with the version it lasted until. The version history
  lists earlier names beside current ones. Reports show today's names.
- **Not versioned**, because they are not Headquarters changing the shared chart:
  - a branch's own sub-accounts (D-031);
  - accounts that modules create for themselves (bank accounts, business units, system accounts);
  - a merge's twin sub-accounts (D-048).
- **Branches are told**: for 14 days after a publish, the dashboard and the chart page show "Chart of
  accounts vN published …: what changed" with the note. The version history is open to everyone who sees
  the chart.
- The plan's "new churches get the latest version as their template" does not apply. Each church owns its
  chart, and new churches start from the platform templates in code (`ChartTemplates`).
- **People** (Directs, `users.manage` with an HQ network role): every user in every branch, filtered by
  branch, role, switched off, active without two-factor login, or never signed in.
  - **Change** (role, branch, active) goes through the existing grant-only-what-you-hold rule
    (`RoleAssignmentService`), so HQ roles are managed here too.
  - **Make branch admin** is a shortcut for that.
  - **Sign out** ends every session.
  - **Reset access** (with a reason) replaces the password with an unusable one, clears two-factor login,
    ends every session and emails a one-time link (the 5B activation broker, 72 hours, "your access was
    reset" wording). It is refused for yourself and for anyone with access you lack.
  - Each change to a person is recorded in their branch's history (`branch_events`) and the audit trail.

## D-051 Auditor mode, pilot pack and the Phase 5 proof (Phase 5D; plan decisions 6 and 9, 19 Oct 2026)

- **Engagements** (decision 6), set in Directs › Auditors (`users.manage`, HQ):
  - each External Auditor gets a branch set (none ticked = every branch; a branch includes those under it);
  - a data window: the dates of the books;
  - an end date (at most two years ahead);
  - a note.

  The latest engagement decides. Setting a new one ends the previous one and signs the auditor out, so the
  limits apply at once. **End now** revokes it.
- **No engagement keeps the old access.** An auditor with no engagement keeps today's whole-church,
  open-ended read access, so existing auditors are not locked out. The Auditors page flags them, and HQ
  narrows them by setting one.
- **Branches.** `BranchContext` narrows a network auditor to the engagement's branches, so every
  branch-scoped list, record and report follows. An auditor limited to some branches cannot open Directs.
- **Dates.** Documents dated outside the window are hidden:
  - journals, giving batches, requisitions, bills, receipts, payrolls and reconciliations (global scopes);
  - report dates are kept inside the window.

  Ledger lines are not filtered, so balances brought forward stay right.
- **An expired or ended engagement** signs the auditor out and refuses sign-in, with a plain message.
- **Watermarks.** Every PDF an auditor opens or downloads (all go through the letterhead partial) carries a
  diagonal "Auditor copy · name · date" and a footer line. Excel and CSV downloads start with an
  "AUDITOR COPY" row. Views and downloads were already logged (D-011). Directs › Auditors shows each
  auditor's 90-day counts, and an activity page lists their last 300 audit-trail entries.
- **Queries** (`audit.query`, auditors only):
  - an auditor asks about a journal, requisition, asset or receipt, found by number, within their
    engagement;
  - the branch answers (people with `journals.view`) with text and up to 5 files, stored as attachments
    like journal documents;
  - the auditor closes the query or asks a follow-up, which reopens it;
  - open queries appear on the branch dashboard;
  - queries are exempt from the branch status guard, so an audit continues while a branch is suspended.
- **Pilot pack** (`docs/pilot/`):
  - the R2 pilot checklist, with server, per-branch and weekly checks and the sign-off sheet that records
    the exit criterion;
  - a short guide for branch admins and treasurers;
  - R2 release notes.

  Decision 9 (the pilot branches, date and host) stays with the owner. The checklist has spaces for it.
- **Proof**: `DirectsProofTest` rehearses the pilot on test data (see `docs/phases/phase-5-exit-evidence.md`).

## D-052 Devices, the offline permit and the sync server (Phase 6A; plan decisions 4, 5, 6, 7 and 8 confirmed, 19 Oct 2026)

- **Devices** (`devices`). A phone or laptop registers through the API with its user's sign-in and a UUID it
  made itself, and gets the next church-wide code (`DEV1`, `DEV2`...). It starts **pending**: its token can
  read its own state but cannot sync. The branch admin approves it under Setup > Devices; a person cannot
  approve their own device. Headquarters sees and revokes every branch's devices under Directs > Devices in
  all branches. A revoke needs a reason, ends its tokens, and the device is told to wipe at its next contact
  (`410 {"wipe": true}`; the time is recorded).
- **Tokens** (decision 8). Sanctum tokens named `device:<uuid>`. Access lasts 12 hours. The refresh token
  lasts the offline window, is stored only as a SHA-256 hash and works once (each refresh rotates both).
  Revoking the device, switching the user off (Users & roles or Directs People), resetting their access, and
  closing their branch all revoke their devices. The `DeviceAuth` middleware also refuses devices of a
  locked, closed or merged branch (`423`) and read-only churches (`402`).
- **Offline permit** (decision 6). Ed25519 (sodium_compat, since XAMPP's PHP lacks the sodium extension),
  with the key pair derived from `APP_KEY`, so rotating the app key re-issues every permit at the next sync.
  It carries church, branch, device, user, plan, issue time, valid-until and last sync time. The window is the
  church setting `offline_window_days` (default 30, kept between 1 and 90). Phase 8 replaces `plan` with the
  licence.
- **Push.** Operations carry an `op_uuid` (idempotency key) and the device's own `seq`. The server applies them
  strictly in `seq` order, each in its own transaction with the device row locked, and stops at a gap with
  `resume_from`. A known `op_uuid` returns its first result as `duplicate`. The brief's `sync_batches` table
  is a `batch_uuid` column on `sync_operations`: a separate table would hold nothing else.
- **Refusals** (decision 7). When the server refuses an operation (validation, a church rule, a closed period,
  a missing record, another branch), it is recorded as `issue` in `sync_conflicts` with the payload and a plain
  reason, and the sequence moves on. The service can resubmit a corrected payload or withdraw it with a note;
  the Sync Issues screen for treasurers comes in 6C.
- **Members** (decision 7). A `version` column goes up on every change, online or synced. A sync that changes
  a member from an older copy still wins, and the details it replaced are logged as `overwritten`. Members
  added offline get their member number from the server and count against the plan's member limit.
  Vendors follow in 6C.
- **Numbers** (decision 5). Batches opened offline keep the device's provisional number beside the server's
  record (`provisional_number`, `client_uuid`, `device_id`). Giving batches still get their gap-free number
  at posting, as before. Documents numbered when recorded (receipts, vouchers) are numbered at sync in 6C.
- **Pull.** `sync_changes` is the change log behind cursors: model observers add a row on every save and
  delete of accounts, funds, income categories, cost centres, vendors, members and giving batches. Cursor
  0 gives a snapshot of reference data, the branch's members and its open batches. Later pulls send only
  changes for the device's branch and church-wide lists, 1,000 at a time.
- **First operations**: open a giving batch, add a gift (to a batch by server id or by the device's
  `client_uuid`, a member likewise), add or change a member. 6C adds petty cash, requisitions, cash counts and
  counting sheets.

## D-053 The offline client: encrypted device store, PIN, Workbox service worker, sync engine (Phase 6B; plan decisions 2, 3, 6 and 9 confirmed, 19 Oct 2026)

- **A small app of its own** at `/app/offline` (`resources/js/offline/`): Vue without Inertia, so it opens with
  no server. It signs in only through the device API (D-052), never with the website's session cookies, so
  the page holds nothing personal and the service worker can keep it. The Inertia staff app is unchanged.
- **Service worker** (decision 2): Workbox through vite-plugin-pwa (`injectManifest`), source
  `resources/js/sw.js`, built to `public/sw.js` (a build artefact, no longer committed). It precaches the
  offline page, its scripts, the shared stylesheet and its Latin fonts (14 files, about 720 KB), chosen from
  Vite's manifest, so it opens offline even if never visited. The staff app's earlier behaviour stays the same:
  - built files are cached as used;
  - pages are network-first, with the last copy kept;
  - nothing from the API, Inertia requests or downloads is cached.

  A page never opened before now points to TOBAS offline. The staff prefix travels in the worker's address
  (`sw.js?staff=/app`).
- **Device store** (Dexie over IndexedDB):
  - `meta` holds only plain identity and counters: the device, the PIN-wrapped key, failed PINs, the next
    sequence number, the pull cursor, the provisional series and the latest time seen;
  - `vault` holds the tokens, the permit and each reference list, encrypted;
  - `outbox` holds each operation encrypted, with only its sequence number, type and status in plain.
- **Encryption and PIN** (decision 3):
  - a random AES-GCM-256 data key, wrapped by a key derived from the PIN (PBKDF2-SHA-256, 310,000
    rounds);
  - the PIN is 6 to 12 digits, and repeated or running digits are refused;
  - the key lives in memory only while unlocked; reloading or closing the app, the Lock button, or
    10 idle minutes locks it;
  - five wrong PINs wipe the device.

  The PIN is set when the device is registered rather than when it is approved, so the tokens are
  encrypted from the first moment. TOBAS cannot recover a forgotten PIN: the device is set up again.
- **Permit on the device** (decision 6): the Ed25519 signature is checked with `@noble/ed25519` (Web Crypto's
  Ed25519 is missing from older Android browsers). Recording pauses when:
  - the permit is missing or its signature fails;
  - the offline window has passed;
  - the clock reads earlier than the device's own latest time (5 minutes' grace) or the server's last sync
    (an hour's grace, for drifting phone clocks).

  Syncing renews the permit.
- **Sync engine** (`sync.js`):
  - push in sequence order, 200 at a time; pull by cursor until `has_more` is false, applying upserts and
    deletes to the encrypted lists;
  - every 5 minutes, when the connection returns, and shortly after each entry; on failure it retries after
    5 s, doubling to at most 5 minutes; Sync now tries at once;
  - a 401 refreshes the tokens once; a refused refresh asks the user to sign in again on the same device;
  - a 410 wipes the device;
  - tokens issued before approval are swapped at the first sync after it (they could not sync);
  - when the server is behind (a `resume_from` earlier than the device sends, e.g. after a restore from
    backup), the device sends its kept copies again from that point. If it cannot, it says so.
- **Device state** now includes `can` (members, giving) so the screens offer only what the user may do; the
  server still checks every operation.
- **Screens in 6B**:
  - set-up (sign in, device name, PIN);
  - unlock;
  - waiting for approval;
  - the status bar, always shown (decision 9);
  - This device: permit, what is kept, entries and their status;
  - removing TOBAS from the device;
  - **Members**, brought forward from 6C as the first offline work: find, add and correct members with
    no connection.

  6C adds giving, petty cash, requisitions and cash counts, and the Sync Issues screen.

## D-054 Offline work, Sync Issues and the year-end device check (Phase 6C; plan decisions 1, 5 and 7 confirmed, 19 Oct 2026)

- **Offline work** (decision 1). The device can send these operations:
  - `giving.batch.open`, `giving.entry.add`;
  - `giving.count.save`, `giving.count.sign`;
  - `petty_cash.voucher`;
  - `requisition.raise`, optionally submitted for review;
  - `cash.count`;
  - `member.upsert`.

  Every operation checks the user's permission on the server. The device's screens show only what the user
  may do, using `can` in the device state. Posting, approving, reviewing, journals, reconciliation, reports,
  the year end and administration stay online.
- **Numbers** (decision 5). Petty cash vouchers, requisitions and cash counts are numbered when recorded:
  - the server gives each the branch's next gap-free number as it applies the operation, so numbers follow
    the order the device recorded them;
  - each keeps the device's provisional number (`PCV-DEV3-0004`, `EXP-…`, `CC-…`, using the server's
    prefixes) with `client_uuid` and `device_id`;
  - a second copy of a document with the same `client_uuid` returns the first, even under a new `op_uuid`.

  Giving batches still get their number at posting. Voucher slips printed on the device show the provisional
  number, or the final one once it is known.
- **Counting sheets offline**: the device user's own signature is sent with the total they saw. If the sheet
  changed since (another counter saved a different count), the signature is refused rather than applied to
  figures they did not see.
- **Floats**: every pull carries the user's active petty cash floats with their ledger balance now. They are
  sent whole, because balances move with each voucher and top-up and not through the change log. The device
  warns before a voucher larger than the float less its unsent vouchers. The server still refuses an
  overspend.
- **Sync Issues** (decision 7): `/app/sync-issues`, under the new permission `sync.resolve` (treasurer,
  branch admin, HQ finance, administrator). It shows:
  - open issues with a plain reason, and an edit form: links between offline records are read-only, and
    record numbers become lists of names;
  - issues dealt with in the last 60 days;
  - member details that last-writer-wins replaced.

  A resubmitted entry is applied **as its author** (the device's user), so maker-checker sees the right
  person; the treasurer is recorded as the one who resolved it. A record missing on the server is explained
  in words ("The account this entry uses (no. 9) is not on the server…") instead of a database error. Pulls
  tell the device what became of its refused entries: sent again (with the number) or withdrawn (with the
  reason).
- **Year end** (brief 7.10): devices report the entries still waiting on them (`?pending=N` on pull and
  `/devices/me`), kept as `devices.pending_count`. The checklist blocks while any approved device of the
  branch has entries waiting, or any Sync Issue is open. The Devices pages show "N waiting on the device".
- **Guide**: "Working without a connection" (set-up, offline work, Sync issues, a lost device), linked from the
  Sync issues and Devices pages.

## D-055 Airplane-mode proof: Playwright on its own database, and the Phase 6 exit (Phase 6D; plan decision 10 confirmed, 19 Oct 2026)

- **Playwright** (`@playwright/test` 1.52, development only) with headless Chromium. `npm run e2e` runs
  `tests/e2e/airplane.spec.cjs`. The network is switched off and on with `context.setOffline`, and a reply is
  lost mid-push by letting a request reach the server and cutting its answer (`route.fetch()` then `abort`).
- **Its own database.** The app runs under `php artisan serve` on port 8765 against `church_acct_e2e`:
  - `APP_CONFIG_CACHE` points at a file that does not exist, so the local site's cached configuration (which
    names the real database) is never used;
  - the cache is in memory, and the queue runs at once.

  `php artisan tobas:e2e` prepares the database (fresh demo church and a float) and does the steps a person
  would do on the website (approve, revoke, close a month), plus `inspect`. It **refuses any database whose name
  does not end in `_e2e`**, and production. This is the lesson of the 18 Oct wipe.
- **Concurrency** is proved with four PHP processes pushing at once into the sync service on MariaDB. `php
  artisan serve` handles one request at a time on Windows, so browser requests could not overlap.
- **CommonJS test files** (`.cjs`): Playwright's ES-module loader hangs at start-up under Node 24 here, even
  with an empty configuration. CommonJS sidesteps it without changing the app's own `"type": "module"`.
- **Server tests added**: two devices interleaving (one gap-free series, a skipping push held back, operations
  applied in sequence order), and paged pulls (1,005 changes over two pages, each once, in order).
- **Phase 6 exit**: the criterion is met. Evidence is in `docs/phases/phase-6-exit-evidence.md`. The
  branch-staff guide is `docs/pilot/OFFLINE-GUIDE.md`.

## D-056 Consolidation worksheets with matched eliminations and drill-down (Phase 7A; plan decisions 1 and 2 confirmed, 19 Oct 2026)

- **Directs › Consolidation** (`reports.view`, Headquarters network roles; for an auditor, their engagement's
  branches). It covers the trial balance, income and expenditure, financial position and cash flows, for the
  whole church or any region, area or parish with every branch under it. Each worksheet has:
  - one column per branch;
  - **Eliminations**;
  - **Consolidated**.
- **Columns** (decision 2):
  - one per branch up to 12;
  - above that, the scope's own books, then one column per branch or region directly under it. A region's
    column opens that region on its own.

  A branch's own sub-accounts show under their Headquarters parent (as D-031), so columns compare like with
  like.
- **Eliminations are matched journal by journal** (decision 1). Inter-branch (due from / due to) and remittance
  accounts (subtypes `interbranch`, `remittance`) are posted on both sides by one journal. Each journal falls
  into one of three cases:
  - its lines on those accounts all lie inside the scope and net to nil: eliminated in full;
  - it has lines with branches outside the scope (for a region, what it owes HQ): kept, and listed;
  - its lines inside the scope do not net to nil: kept, and shown as an **unmatched inter-branch
    difference**, with the journal and branches.

  A one-sided entry and its reversal cancel each other, so they are matched together and drop out.
  Year-end closing is one journal for every branch, so remittances stay matched after a close. The church's own
  statements still leave those accounts out whole; the worksheet is where a mismatch shows.
- **Tie-out** on every worksheet:
  - every line: the columns plus the eliminations equal the consolidated figure;
  - trial balance: debits equal credits;
  - income and expenditure: the surplus equals the church's own statement for the same branches;
  - financial position: assets equal liabilities, funds and the surplus not yet closed, in every column;
  - cash flows: opening, movement and closing cash equal the sum of the columns (cash is never eliminated),
    and each statement agrees with itself.

  This is the **Release R3 exit criterion**, proved by `ConsolidationTest`. That test sets up five branches in
  two levels, with remittances up both levels and a part payment, and checks that each column is that branch's
  own trial balance, figure for figure.
- **Drill-down**: a branch figure opens that branch's ledger for the account (`only_branch=1`: the branch
  alone, not those under it, with its sub-accounts in the parent). Each ledger entry opens the journal with its
  documents and audit trail.
- **Downloads** in Excel, PDF and CSV, with the tie-out and eliminations in the notes, logged as
  `consolidation_exported`. **Scheduled emails** can include the four "Consolidated …" worksheets, for the
  schedule's branch and every branch under it.

## D-057 MIS dashboard, ranking and branch comparison (Phase 7B; plan decision 3 confirmed, 19 Oct 2026)

- **Directs › HQ dashboard** (`reports.view`, Headquarters): the landing page of Directs; people without
  reports still land on the branch register. Its parts:
  - scope: the church or a region;
  - period: this month, last month, this quarter, year to date, last year, or chosen dates;
  - **KPI tiles**: income, expenditure, surplus and cash and bank against the same dates last year, then budget
    used, banks reconciled, giving posted on time and remittances paid;
  - a **12-month chart**;
  - a **heat table** of every branch, ranked by any measure, with a 12-month surplus line per branch.
- **Measures** (`MisService`):
  - **Income and expenditure leave out inter-branch and remittance accounts.** Otherwise a parent would count its
    branches' remittances as its own income, and the church total would count them twice. Remittances are
    measured on their own.
  - **Cash** is the posted balance of cash and bank accounts at the period end.
  - **Budget used**: expenditure against the expense lines of approved budgets, for the months in the period.
  - **Banks reconciled**: active bank accounts with a reconciliation dated in the 45 days before the period end.
    A bank account with no branch counts as Headquarters'.
  - **Giving posted on time**: batches of the period posted within 7 days of the service.
  - **Remittances paid**: paid against due, for remittance periods ending in the period.
  - Church percentages are weighted by what each branch has to measure (banks, batches, remittances due).
- **Colours**: amounts by thirds of the ranking (expenditure is not coloured); percentages 100% green, 50% or more
  amber, below red; budget over 100% red, 90% to 100% amber; negative cash red. **A heat table, not a map**
  (decision 3).
- **Cached for the day** (key: church, scope, branches, dates and today's date), so the page stays quick with many
  branches. The note under the table says so.
- **Compare branches** (`/directs/compare`): two to five branches on the trial balance, income and expenditure or
  financial position. It reuses the consolidation worksheet with each branch's own books and no eliminations,
  shows a total, and offers drill-down and downloads.
- **Downloads and schedules**: the branch table downloads as Excel, PDF or CSV (logged as `mis_exported`) and is a
  schedulable report, "Branch performance".
- **Fix on the way**: `/directs` (and `/app`) were taken by the public site's page address `/{slug}` before their own
  routes were reached, so `/directs` showed "not found". Those two names are now kept out of page addresses.

## D-058 Alerts centre and remittances with due dates, drafts and approval (Phase 7C; plan decisions 5–7 confirmed, 19 Oct 2026)

- **Alerts centre** (Directs › Alerts, `reports.view`; acting on alerts needs the new permission `alerts.manage`,
  given to HQ Finance; admin has everything). An `alerts` table holds one row per condition, keyed by a
  **fingerprint** (kind, branch, record), so checking again updates the row instead of adding another.
  - **Kinds and thresholds** (`AlertService`, thresholds overridable in the church setting `alert_thresholds`):
    - months not closed by the 15th of the next month (one church-wide alert; critical past 60 days);
    - the year end not submitted 60 days after 31 December, while the year is open;
    - a bank account not reconciled for 45 days (one with no branch counts as Headquarters');
    - negative cash or bank balance (critical);
    - a budget line at 100% or more (critical from 120%);
    - unusual transactions in the last 30 days of postings: a line of ₦500,000 or more that is at least 5 times the
      account's 90-day average; a cash payment over ₦200,000; an entry dated more than 60 days before it was posted.
      Opening, closing and reversal journals are left out;
    - a device with entries not synced for 3 days; sync issues waiting 3 days;
    - the subscription renewal approaching or past;
    - a remittance, or a monthly draft, past its due date.
  - **Life of an alert**: open → acknowledged (someone is on it; it can be assigned and noted) → resolved.
    **Alerts close by themselves** when the condition clears (resolved by nobody, note "Cleared by itself").
    **Unusual transactions are sticky**: they stay until a person resolves them with a note, and a resolved one is
    never raised again for the same entry. "Open again" reopens. Every action is in the audit trail.
  - **When it is checked**: `directs:alerts` daily at 06:45, and when someone opens the page if the last check is over
    an hour old (and on "Look again now"). Alerts for branches that are not active are left out.
  - **Delivery** (decision 5): one **daily email digest** of new alerts to Headquarters people with `alerts.manage`
    (each alert is emailed once), and each branch's own open alerts on its dashboard ("Alerts from Headquarters").
    No SMS. The `alerts` table is Headquarters' (tenant-scoped, exempt from the branch scope, like branch events).
- **Remittances**:
  - **Due date** (decision 6): the church setting `remittance_due_day` (default 15; no screen for it yet, like
    `alert_thresholds`) of the month after the period
    end, set when a remittance is posted. **Settled on** is kept when it is fully paid, so lateness is known.
  - **Monthly drafts** (`remittances:draft`, on the 1st at 01:30): a `remittance_drafts` row for each active child
    branch with a rate or levies, for the month just ended, unless that month is already drafted or remitted (one
    per branch and period).
  - **Approval** (decision 7): the branch's own people (`remittances.manage` in that branch) press "Confirm and
    post". The amount is **worked out again** at that moment (late entries count) and posted on both sides as before.
    Headquarters (a network role) may **approve for the branch** with a reason, which is recorded (`on_behalf`).
    **The parent branch's own staff see the draft but may not act on it**: found in the browser check, where
    Headquarters' own treasurer (not a network role) was offered buttons that the server then refused. The page now
    shows "Waiting for … to confirm" to them, and setting a draft aside follows the same rule.
  - **Reminders** (`remittances:remind`, daily at 08:15): to the branch's people with `remittances.manage`, 3 days
    before the due date (once) and every 7 days while overdue, for posted remittances and drafts.
  - **Compliance**: per branch for the year: due, on time, late, outstanding and pending, with a percentage. It is
    shown on the remittances page ("On time this year"), feeds the HQ dashboard as **Remittances on time**, and
    drives the overdue alert.
- **Ledger writes across branches**: a branch treasurer confirming its draft also posts Headquarters' side, so the
  posting runs in `BranchContext::unrestricted()`, which only this trusted service path uses.
- **Branch dashboard**: a "remittance(s) to confirm" task line for people who can confirm them.

## D-059 Interventions: HQ adjusting entries, the interventions log and finding a transaction (Phase 7D; plan decisions 8–10 confirmed, 19 Oct 2026)

- **HQ adjusting entries** (Directs › Adjusting entries; decision 8):
  - a new journal type, `hq_adjustment` ("HQ adjustment"), with the reason in a new `journals.reason` column.
    Branches cannot choose it on their own journal form;
  - **drafted** by a Headquarters network person with `journals.create`, **posted** by another Headquarters network
    person with `journals.post`. Maker-checker applies **whatever the church's `maker_checker_journals` setting**;
  - **only Headquarters posts, discards or reverses** an adjusting entry or its reversal. This is enforced in
    `LedgerService` (`post`, `draftReversal`, and so `reverse`) and on the journal page, not only by hiding
    buttons, so no other route can get round it. A reversal is itself a draft posted by a second person at HQ;
  - Headquarters' own books take an ordinary journal, so an adjusting entry into the root branch is refused;
  - when one posts, the branch's people with `journals.post` or `settings.manage` get an email with the number,
    amount, memo and reason. It is marked "HQ adjustment" in the branch's journals list and on the journal page.
- **Interventions log** (decision 9): the branch history from D-048 (`branch_events`) is the log, now with a link to
  its record (`subject_type`, `subject_id`). `BranchEvent::record()` writes it, and `BranchEvent::WORDS` names each
  action for both sides. It covers:
  - suspend, lock, resume, close and merge (already recorded since 5A);
  - access resets and role changes from People in all branches (already recorded since 5C);
  - **adjusting entries** posted and reversed;
  - **year-end submissions returned** to the branch;
  - **months reopened**: periods belong to the whole church, so one entry is written for **each active branch
    below Headquarters**;
  - **devices revoked** by Headquarters. A device revoked by its own user, or by people in the device's own
    branch, is not an intervention.

  Entries made before 7D are not back-filled beyond what `branch_events` already held.
- **Where it shows**:
  - in Directs, on the branch's page ("History and interventions");
  - for the branch, on **Setup › From Headquarters** (`journals.view`), with the reason, who and when;
  - as a **dashboard notice** of the last 14 days, for the branch's own people (not Headquarters network users,
    who would otherwise see every branch's).
- **Find a transaction** (Directs › Find a transaction, `journals.view`): search every branch, or one, by:
  - journal number, memo, reference or reason;
  - a line's description;
  - the **payee** on the requisition, petty cash voucher or cheque behind the journal;
  - **amount** (the journal total or any line) and dates.

  The journal opens in Directs with its lines, documents and **audit trail**. Opening it is logged (`hq_viewed`),
  and so is each search (`hq_searched`), as auditors' visits are (decision "logged as for auditors").
- **R3 release** (decision 10): `docs/pilot/RELEASE-R3.md` (release notes, upgrade and checklist),
  `docs/phases/phase-7-exit-evidence.md`, and `docs/pilot/HQ-FINANCE-GUIDE.md`. The exit criterion ("consolidated
  statements tie to the sum of branches") is proved by `ConsolidationTest` and by the tie-out line on every
  consolidated statement.
- **Fixes on the way**: four `Journal` relations (`branch`, `poster`, `reversalOf`, `reversedBy`) are now typed,
  which removed their Larastan baseline entries.

## Open: still to confirm

- Logo: the arch-and-path mark is a placeholder until the church supplies its own.
- Seller legal name for invoices (`BILLING_SELLER_NAME`).
- Native-speaker review of the new ha/ig/pcm/yo strings.
- Reporting framework, fiscal year, grace period (brief says 7 days in section 10, 30 days in section 16),
  SMS/email providers, expected branch count.
- How long to keep prayer requests (NDPA storage limitation). Suggestion: delete private requests after
  12 months; nothing is deleted automatically yet.
- Whether branch admins should be able to appoint their own prayer team (today Headquarters does).
- Whether Headquarters needs a screen for the remittance due day and the alert thresholds (D-058). Today they are
  church settings with defaults (15th; ₦500,000 and 5 times the average, and the rest), changed on the server.
