- 11 Sep, 2026 10 commits
-
-
DevPilot authored
The cheque fields stay in the voucher form for every payment method, so check_date posts as '' when paying cash or by transfer. That went straight into a DATE column and MySQL rejected the row with "Incorrect date value: ''", so no non-cheque voucher could be saved at all. Send NULL instead; VoucherService already accepted null. Found by driving the voucher screen end-to-end while building the step-by-step accounting tutorial.
-
DevPilot authored
167-page Arabic guide covering every money-related screen in the system (139 screens across 20 modules): journal entries, chart of accounts, all financial statements, checks/bounced checks, bank reconciliation, treasury, procurement + tenders, purchase orders, fixed assets, auctions, rentals, sales/POS, payroll/tax/insurance, membership and sports-activity billing, pricing/discounts, and the reports catalog. Every screenshot is real, captured logged in as super admin against the live instance — not mockups. Building it surfaced and fixed three live bugs along the way (see previous commit): PO balance/purchase-volume reports referencing non-existent poi.quantity/qty_received columns, a missing price-deviation view, and HR insurance form2's wrong join.
-
DevPilot authored
PO balance and purchase-volume reports referenced poi.quantity / poi.qty_received, which don't exist — the real columns are quantity_ordered / quantity_received. Price-deviation report pointed at a view that was never created. HR insurance form2 joined hr_salary_adjustments through employees.id, but that table links straight to hr_employee_profiles via employee_profile_id (confirmed against the model and every other query on that table) — the extra employees join used a column, employee_id, that was never there. Found live via BOOT FAILURE pages while capturing screenshots for the accountant's tutorial PDF.
-
DevPilot authored
Covers GL sync preview, Cash Flow Statement, Statement of Changes in Equity, asset maintenance/sale, the government procurement tender cycle, and the auctions module — step-by-step with exact menu paths and button labels for the club's accountant.
-
DevPilot authored
Controller selects employees as full_name_ar; both custody_index and custody_transfer views were reading name_ar, which doesn't exist on that result set. Live crash: Undefined array key "name_ar" at custody_index.php:14. Pre-existing bug, unrelated to recent changes — just hadn't been hit until now. Confirmed via repo-wide grep this was the only place with the mismatch.
-
DevPilot authored
New Auctions module covering the full committee-driven auction cycle: one auction carries one booklet and many lots, each lot bundles one or more assets/facilities and snapshots their cost/depreciation/book value at lot-creation time, technical committee marks lots fit or not fit for disposal, every bid (including losing ones) stays on the lot, and the financial committee's award is per-lot so different lots can go to different winners. An asset only flips to disposed once its settlement is fully paid — not at award time — and disposal reuses the existing GL gain/loss posting path. A rental-type award creates a lease contract and marks the facility under lease instead of sold.
-
DevPilot authored
feat(procurement): government tender cycle — committees, technical/financial gate, multi-vendor award Adds the documented, auditable purchase cycle on top of the existing PR/quote infrastructure: a tender booklet (كراسة شروط) per PR, vendor invitations that open a quote record per vendor (rejected quotes stay on file with their reason, never deleted), technical committee evaluation with the 3-accepted-offer minimum before financial review (otherwise the tender is flagged for re-tender with a reason), a financial committee comparison scoped to technically-accepted quotes only, and item-level award that can split one PR across several vendors and therefore several purchase orders — replacing the old strict PR:PO 1:1 assumption. Every PO produced carries its tender and evaluation id back to the original PR.
-
DevPilot authored
Closes the remaining gap in the fixed-asset lifecycle (acquisition, depreciation, disposal/sale already existed) by adding a maintenance log per asset with cost, vendor, and next-due tracking. Each category now carries its own maintenance expense account, mirroring how depreciation and disposal are already mapped, and maintenance cost posts Dr expense / Cr cash-bank-payable through the same operational posting pipeline.
-
DevPilot authored
Rounds out the القوائم المالية group alongside the existing income statement, balance sheet, and consolidated balance sheet. Cash flow uses the indirect method with a reconciling line so it always foots to the real cash-account movement; the equity statement ties exactly to the balance sheet's total_equity at both ends of the period.
-
DevPilot authored
Lets a super admin review every unposted payment/fine/installment/sale/ payroll/refund/rental-deposit row before committing the sync, instead of posting blind from the dashboard warning banner.
-
- 10 Sep, 2026 2 commits
-
-
DevPilot authored
Filters sidebar items/submenus live as the user types, normalizing Arabic orthographic variants (hamza forms, ta marbuta, alef maksura, Arabic-Indic digits) and doing typo-tolerant approximate matching against label_ar, label_en, and the route path. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
-
DevPilot authored
Controllers select suppliers.name_ar and members.full_name_ar, but the views referenced a nonexistent 'name' key, fataling the page.
-
- 08 Sep, 2026 2 commits
-
-
DevPilot authored
Four-pillar decomposition (membership/money, activities, events, gate/invitations) with data model, API spec, UX/motion system, and delivery plan.
-
DevPilot authored
I broke this. The page-actions block reads $gracePeriod to build the bulk-generate confirmation, but $gracePeriod was computed inside the content block — and page_actions renders first, so the variable did not exist yet. Production turns that notice into a fatal, so the contract page died outright. It had always been wrong; it was simply unreachable, because the guard above it requires status approved or active and until yesterday no contract could reach those states (the approve button was hidden on drafts). Making approval possible made the broken path reachable, and I shipped it without exercising the page in the state I had just unlocked. The shared variables now sit above both sections, which is where anything either section needs has to be. Why the test missed it: I was piping render output through `grep -v Warning:`, which filtered out the exact notice that is fatal in production. The check now installs an error handler that promotes every notice to an exception — the same thing ExceptionHandler does — and renders the contract page in all eight statuses and the invoice page in all three. All pass. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
- 07 Sep, 2026 13 commits
-
-
DevPilot authored
Asked how to make a contract live, and the honest answer was that you could not find out from the screen — so the screen now says it. The approve card was shown only when status was exactly 'pending_approval'. A contract created from the form lands on 'draft' (the controller even defaults to 'pending', which is not in the status list at all), so the one button that makes a contract live was invisible on precisely the contracts that needed it. It now appears for draft, pending and pending_approval. Added a four-step tracker across the top — العقد اتسجّل ← اعتماد العقد ← توليد الفواتير ← تحصيل الفاتورة — with the ticks reflecting real state and a line naming the single next action and where its button is. Underneath it, in words: which day of the month this contract's invoices fall due, read from the contract's own payment_due_day rather than assumed to be the 5th, that a short month pulls it back to the last day, and that the late fee is computed from the due date automatically. Invoice rows now carry a تحصيل button straight to the collection form when they are unpaid, instead of a عرض link that gave no hint the money is taken there. Verified rendered in draft, approved and active: the next-step line changes with state, the due day reads from the contract, and 11 unpaid rows show تحصيل while the paid one shows عرض. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
The rent could be invoiced but not collected. `payInvoice` required a `payment_id` that already existed, and the form asked the accountant to type "رقم الدفعة من النظام" — a number they had no way to obtain, because nothing on this screen created a payment. So a generated invoice could never be settled. `collectInvoice` takes the money properly: pick the treasury, the method and the date, and it writes the payment against that treasury, then settles the invoice through the same RentalInvoiceService::markPaid the old path used — so the late fee is still calculated from the due date and the journal entry is unchanged. payInvoice is left in place for the case it was built for: a cashier who already raised the receipt and has its number. Verified end to end on a production clone: contract approved, 12 monthly invoices generated on the contract's own payment_due_day, one collected into «الخزنة الرئيسية», and the entry posted Dr الصندوق / Cr إيجار محلات + ض.ق.م with the trial balance still at 0.00. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Swept every link and form action in the financial modules against the real route patterns. The asset problem was not a one-off. **Screens you could reach but not use.** Three edit screens existed, worked, and had nothing linking to them: vendor invoices, return-to-vendor, and the legacy pricing configs. Buttons added on both the list and the detail page, gated on the same status the controller already enforces — draft only, since neither document should change once verified or submitted. **Links pointing at routes that do not exist.** - Asset custody was entirely unusable: both the transfer button and the transfer form posted to /inventory/assets/{id}/transfer, but the route is /inventory/assets/custody/{id}/transfer. Nobody could ever hand over an asset. - Pricing configs was broken at both ends — the list linked to /pricing/{id}/edit and the form posted to /pricing/{id}, while the routes are under /pricing/configs. The edit screen was unreachable and unsavable. - Quote comparison posted to /select-winner, which was never built; the endpoint is /procurement/quotes/evaluate. - The quotes list "عرض" pointed at a detail page that does not exist. It now opens the response screen, which is what a quote is actually for. - "تظلم" on a fine was an <a>, so it issued a GET against a POST-only route. It is a form now, with a confirmation, since it records something. **Buttons for things that were never built.** BOM edit had no route and no controller method — a guaranteed 404 in two places. Removed rather than faked; creating and viewing a BOM both work, and editing one is a feature nobody has written yet. Settlements had exactly one action and it opened a detail page that does not exist either; replaced with اعتماد, the action that does exist and the only thing a draft settlement is waiting on. Not changed, having checked: vouchers and budgets flagged in the sweep were artifacts of my own parser — PHP unescapes '\\d+' in single quotes, and the budgets links emit a query string from inside the PHP tag. Both resolve fine. Verified: 1503 routes resolve, 255 controllers instantiate, 243 services load, zero orphaned edit screens remain, and every screen touched renders. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Two things I got wrong when I opened up direct asset registration. **There was nowhere to type what the asset IS.** The register carried a tag, a category and a serial — the name was borrowed from `inventory_items.name_ar`, which works only while every asset comes from stock. A building, a court or a transformer has no stock row, so the detail screen showed an empty cell and the form never asked. The register is meant to be read by someone doing a physical count: "أثاث ومعدات" tells them nothing, "مكيف سبليت ٣ حصان — قاعة الاجتماعات" tells them exactly what to look for. `asset_name` is now a required field, backfilled from the linked item where there is one and from the category otherwise, so no existing row loses the name it was already showing. It is also searchable. **The edit route had no button.** `/inventory/assets/{id}/edit` existed and worked; nothing linked to it, so once an asset was saved there was no way back in from the UI. Buttons on both the detail page and the register rows, hidden for disposed assets since those are history. Also corrected the labels that direct registration made wrong: the column header and search hint still said "الصنف", and the empty state still claimed assets are only created automatically when stock of type "أصل" is received. Verified on a production clone: name saves, displays on both screens, is findable by search, both edit buttons render, 1503 routes resolve. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
A bounce was one status change and one entry. It is actually four separate facts, and collapsing them is where the books go wrong. 1. The debt comes back. A cheque was never money, it was a promise; when it fails the drawer owes again. This part already worked. 2. The BANK charges the club. That charge leaves the club's account whoever ends up bearing it, so it posts Dr مصروفات بنكية / Cr البنك the moment it happens. Nothing recorded it before — which means the bank reconciliation could never have tied out on any month with a bounce in it. 3. Somebody bears that charge. Billing the drawer is a separate claim posted separately, so waiving it later does not touch the original debt. Saying the club bears it while also billing the drawer is now refused: it is a contradiction that quietly inflates income. 4. It has to end. Collected, replaced, re-presented and cleared, sent to legal, or written off. A bounce with no ending is a receivable nobody is chasing. Only the write-off posts here (Dr ديون معدومة / Cr شيكات مرتدة, for the cheque plus any fees billed on top, because both are being given up); the others are closed by events that already post on their own, and posting again would double them. The register now remembers what a bounce actually needs: the bank's reason code, how many times the cheque has been presented, how many times it came back, what the bank took, what was billed, who bore it, the protest number, and how it ended. Reasons that carry criminal liability in Egypt — insufficient funds, a closed account, a stop-payment on a valid cheque — are flagged, because the club's response differs even though the entry does not. The other half is delayed collection: a cheque past its due date that has NOT bounced. The bank has said nothing, so there is no accounting event and the screen posts nothing — but it is money the club is counting on and has not got, split by whether it never went to the bank or went and never came back. Also fixed: presentation_count only counted retries, so a cheque presented once and returned read as never presented. It now increments on every trip to the bank, in the transition itself rather than in the retry path. Verified on a production clone through the real EventBus: a 50,000 cheque deposited, bounced with a 75 bank charge and 100 billed to the drawer, produced four correct entries; re-presented and bounced again, totals accumulated to 150 and 200; written off for 50,200; and every guard fired — bouncing something not under collection, an unknown reason code, a negative charge, club-bears-plus-bill, resolving twice, and re-presenting after resolution. Trial balance diff 0.00. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
The trial balance showed a number with no way to ask where it came from. Every account row is now a link into that account's ledger, carrying the same date range and the same cost-centre/branch filters — so the movements you land on are exactly the ones that produced the figure you clicked. The ledger itself now reads newest first. The running balance is still accumulated in date order, because "the balance after this entry" only means anything with the earlier entries already in; the list is reversed after that accumulation, so the order flips and every balance still says what it says. The opening-balance row moved to the bottom accordingly — it is the oldest line on a newest-first list. Each entry number links to the full journal entry, so a line in one account opens the whole قيد with its other side. The reference type is shown under the reference number, which is usually what tells you which operation raised it. Verified against 779 movements on the cash account: order is newest first, the running balance on the top row equals the closing balance, and both screens render with 165 clickable rows and 779 entry links. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
There were nine accounting documents, each covering part of the picture and pointing at the others for the rest — so no one file answered a question end to end. They are now one: docs/الدليل-المحاسبي-الكامل.md, 1,607 lines, self-contained, with no reference to any sibling guide. Merged in full, nothing dropped: - معالج توزيع المبالغ — the five scopes, stage skipping, member-category precedence, the running remainder, the mandatory remainder line, the resulting-entry panel, versioning on save, and all 17 rejection cases - أنواع البنود for both collection and disbursement, and why a stamp fee booked as revenue overstates profit and misstates the tax base - الضرائب — inclusive vs added vs exempt vs zero-rated, and why exempt blocks input-tax recovery while zero-rated does not - مسار الفلوس — the four chains, the FIFO ageing behind «فين الفلوس دلوقتي», and the treasury reclassification with its three guarantees - الاستحقاقات — including the point the whole thing turns on: collection posts revenue directly, so an accrual left standing books the income twice - سد الفجوات, the three-way purchase match, the cheque lifecycle through bounce and re-presentation, vouchers, chart-of-accounts management - the fixed-asset cycle, reclassification, payroll, inventory, closing, reports, the 39-tool index, troubleshooting, and the open decisions Verified: 26 diagrams parse against the real mermaid parser, all 59 routes it cites resolve against the router, all 37 table-of-contents anchors resolve, and a 29-topic coverage check against the replaced files comes back clean. The nine originals remain in git history. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Every item I had left as "the accountant must decide" now has a screen to decide it on. Nothing accounting-related is left needing code. **فئات الأصول — /inventory/asset-categories.** The screen I referenced in the manual and in a seed comment, and had not written. Depreciation posts through these three accounts per category; a category missing either depreciation account is silently skipped, so the asset ages in the register and the balance sheet never moves. The screen leads with exactly that failure — categories that hold assets but cannot post — offers only postable accounts, and refuses a half-mapped category, because one depreciation account without the other cannot balance an entry. **إعادة تبويب الحسابات — /accounting/reclassification.** A balance in the wrong account cannot be edited: the ledger is the record, and a balance that disagrees with the entries behind it is worse than a wrong one. So it moves by a dated, balanced, reversible entry. It lists what is genuinely stuck — 8 accounts here, including 43.9m on «مشروعات تحت التنفيذ» and 4.8m on a header — derives the DIRECTION from the balance rather than asking (getting that backwards doubles a balance instead of moving it), and refuses a header or cross-type destination. To empty a header at all, JournalService needed to allow one posted there. That is the only way out in double-entry, so the flag exists, is set by this service alone, and the service independently refuses to let a header be the destination. **رسملة مشروع تحت التنفيذ.** A finished project is not a purchase — the money was spent over months and sits in CIP, which deliberately does not depreciate. Capitalising moves the accumulated cost to the asset account so depreciation can start. Booking it as a purchase would credit cash for money already spent. Now a third option on the asset form, next to purchase and opening balance. **أرشفة سنة مالية — /accounting/fiscal-years.** Overlapping years were flagged but unfixable from the UI. Archiving retires one without deleting it, and is refused outright when the year carries entries or is the current one. The manual is rewritten around this: every wizard now has numbered UI steps, and a new index lists all 39 accountant-facing tools with what each does. The decisions section now names the tool for each item instead of describing the problem. Verified on a production clone with all 409 foreign keys: reclassification moves 4,799,436 off a header and leaves it at 0.00 with all guards firing, capitalisation posts Dr Asset / Cr CIP and posts nothing without a CIP account, categories screen offers 593 postable accounts and no headers, 1,499 routes resolve, 254 controllers instantiate, 242 services load, trial balance diff 0.00, and all 56 diagrams across docs/ parse against the real mermaid parser. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
One file the accountant can work from: every step names the screen, the route, the button, and the journal entry it produces. Covers setup, the fixed-asset cycle, daily, monthly and year-end routines, payroll, inventory, tax, closing, and how to check the books are sound. The last section is the part that matters for the meeting — seven things the system cannot decide on its own, each with the screen to decide it on: the overlapping fiscal years, the empty asset register against 4.7m of machinery and 41.7m of construction in progress, the opening balances sitting on header accounts, the placeholder bank account numbers, three payroll runs calculated but never paid, nine broken player/member links, and the fact that no period has ever been closed. Every figure is read from the live books, every one of the 23 routes it cites resolves against the router, and all 15 diagrams parse against the real mermaid parser — as do the other 42 across docs/. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Auditing the basics against the live books turned up four things that could never have worked, plus a fifth that worked too well. CLOSING A FISCAL YEAR WAS IMPOSSIBLE — three separate faults stacked: 1. `Database::execute()` did not exist. Six call sites used it — closing a period, setting the current fiscal year, setting the default bank account, rebuilding an account balance, deleting a facility blackout — and every one was a fatal the moment it ran. Added to Database rather than patched at the call sites. This alone is why period_closings was empty. 2. Closing a year requires every month closed first, then dates its entry the last day of the year — inside a month it just required to be closed. The two rules deadlocked. The closing entry, and only it, may now post there. 3. The same entry has to zero accounts retired during the year, which the inactive-account guard refused. Closing an account is exactly when you must be able to clear it. 4. And `period_closings.period` is varchar(7) while the year-end key is `YYYY-MM-YE` — ten characters — so the entry posted and the record failed, leaving the year half-closed. It now runs end to end: 19 lines, revenue and expenses zeroed to retained earnings, year marked closed, balance sheet still balanced. PAYROLL POSTED TWICE. Salaries are the largest single expense the club books and nothing guarded the reference, so a retried event or a second click doubled the expense, the withheld tax and the insurance liability. Every other posting in that service guards; this one did not. FIXED ASSETS COULD NOT BE CREATED AT ALL. The club carries 4,705,686 of machinery and 41,703,867 of construction in progress, and `asset_register` is empty — so nothing has ever depreciated. Not because depreciation is unwired, it posts correctly, but because there was no create route, no create method, and nothing anywhere that inserted an asset. `item_id` was NOT NULL against inventory_items, so registering a building meant inventing a stock row for it. Now: item_id and warehouse_id are nullable, the eight asset categories the chart already defines are seeded with their three GL accounts each, and there is a form. It distinguishes a purchase (posts Dr Asset / Cr Cash-or-Payable) from an opening-balance asset (posts nothing — the cost is already in the ledger, and posting it again would double the balance sheet). Asset disposal never worked either: `AccountCodes` was not imported, so the handler threw a fatal that the bootstrap's catch swallowed. Assets came off the register and stayed on the balance sheet. The register's own queries inner-joined inventory_items and warehouses, which would have hidden every real fixed asset behind an empty list and a "not found" page. LEFT joins, and the category name where there is no stock item. The depreciation month picker posted `month` while the controller read `period_month`, so whichever month the accountant chose was dropped and the current one ran instead. Overlapping fiscal years are now shown on the fiscal-years screen. The club carries calendar years alongside a July–June year, so entries in the overlap belong to both and closing one leaves the other open over the same transactions. Only the club can say which convention is real, so it is surfaced, not guessed. Verified on a clone of production with all 409 foreign keys: full asset cycle posts (purchase, depreciation by category, disposal with its 5,000 loss), all 1,489 routes resolve, 252 controllers instantiate, 241 services load, trial balance diff 0.00, balance sheet balanced. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
The accrual failure had a data cause: nine sa_players rows hold a membership NUMBER in member_id (897000, 1015, 101) where a row id belongs. The code no longer breaks on them, but the underlying problem is still real and invisible — the obligation posts to the ledger while the debt never reaches the member's account, so nobody chases it. That is not something an accountant can decide alone; only whoever knows the player can say which member they are. So it goes on the gaps screen, which is where the consequence shows up, with the claim count and amount riding on each one and a link straight to the player's edit form. Where the number matches an actual membership_number the likely member is offered — two of the nine resolve that way. The comparison is numeric on purpose. membership_number is a varchar in utf8mb4_unicode_ci and the cast of an integer carries the connection collation, so string-comparing them raises an illegal-mix-of-collations error. It did, and the catch swallowed it and returned an empty list — a broken query and a clean bill of health looked identical on screen. That catch now logs. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Booking the accruals against the live books exposed a real defect, and it cost one duplicate entry before it was caught. Nine rows in sa_players carry a membership NUMBER in member_id (101, 1015, 897000) instead of a member row id. The accrual run reached the first of them, the foreign key on accounts_receivable.member_id refused the insert, and the exception escaped the loop that records claims in posting_accruals. That loop runs AFTER the journal entry is posted, so the entry stood at the full batch total with only the claims written before the failure. The next run read the remaining documents as never accrued and posted a SECOND entry for them — and would have posted one more every night, because the failure is deterministic. AccrualService::batch now treats a claim it cannot write as something to report, not something to throw: the entry is already posted, so abandoning the rest of the batch is the one response guaranteed to corrupt the ledger. A member id that does not resolve costs the receivable, never the claim — posting_accruals is the system of record and already tracks obligations for non-members. Member existence is resolved once per batch, not once per claim. Phase_112_003 repairs what the two runs left: the duplicate is reversed rather than deleted so the correction stays visible, its claims move to the entry that actually posted their money, and the 110 claims plus 32 receivables that were never written are backfilled against it. It verifies the entry and its claims agree before finishing, and does nothing at all unless production matches the exact broken shape. Why the clone missed it: the verification harness built tables with CREATE TABLE ... LIKE, which silently drops foreign keys — so the constraint that breaks production did not exist in the clone, and the run came back green. The harness now copies DDL via SHOW CREATE TABLE and asserts the foreign key count matches the source (409/409). Verified against a clone of the actual broken production state: net movement for sa_subscription_accrual is 104,769.00 not 184,938.00, all 135 claims equal their entry to the cent, 1,316 claims total 835,167.93, trial balance diff 0.00, and both the repair and the fixed runner post nothing on a second pass. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Phase_107_002 has never run. Its first INSERT wrote to chart_of_accounts.notes, a column that does not exist, so the migration threw and MigrationRunner swallowed it — leaving 3,032,258 EGP of member advances sitting in revenue this whole time. Writing to description_ar instead lets it complete. That exposed a second problem the original migration would have caused. It deactivates 410503/504/505, but payment:down_payment carries an active posting rule crediting 410503. Deactivating the account without moving the rule means every future down payment records a receipt and posts no journal entry, because JournalService refuses an inactive account — quietly worse than the overstatement being fixed. The rule now moves to the matching contract-liability account, which is where a down payment belonged anyway: it is an advance from the moment it is collected, and only becomes revenue as the service is delivered. Otherwise the account being emptied would just refill. Also seeds the four bank accounts the chart already names as the club's current accounts. bank_accounts was empty, which is why the cash chain's deposit step had nothing to resolve and reported red. Account numbers are deliberate placeholders reading "رقم الحساب غير محدد" — a seed has no business inventing an IBAN that could reach a printed deposit slip — and the seed never overwrites a row someone has already filled in. And drops POST /waivers/{id}/pay: WaiverController::pay() does not exist, so the route was a guaranteed 500. Leftover from a direct-payment design that send-to-cashier replaced; nothing posts to it. Verified on a full 369-table production clone: balances move (410503/504/505 -> 0, deactivated; 23081119/20/21 created), entry balances Dr=Cr=3,032,258.00, re-runs post nothing, down payments still post clean, no active rule anywhere resolves to a dead or header account, all four chains green, trial balance diff 0.00. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
- 06 Sep, 2026 7 commits
-
-
DevPilot authored
`GapController::currentEmployee()` was declared private where App\Core\Controller declares it protected. PHP rejects that at class-load time, not at call time, so every request to /accounting/gaps returned a 500 — the screen was unreachable since it shipped in caca1389. The override was redundant anyway: the parent's currentEmployee() does exactly the same thing. Removed it, and the now-unused App import with it. Missed because the earlier verification exercised the services and rendered the views directly, but never instantiated the controller — which is precisely where this class of error surfaces. Added that check (reflect + instantiate every new controller, resolve every route handler class and method) and ran it across all 1,486 routes in the system. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Finished what last night shipped as configured-but-manual. Mapped the Installments module properly this time: cheques for a membership installment plan live in `installment_cheques`, a table with no status lifecycle and no connection to the accounting cheque system at all (negotiable_instruments sits at zero rows) — two insert sites (ChequeController::storeBatch / store) plus a historical-backfill path in RetroactiveMembershipService. The fee cannot be folded into the plan's own total: `total_with_interest` is recalculated from principal and interest alone (InstallmentController::recalculate) and both intake paths hard-validate cheques against it — anything added would be silently erased or reported as a shortfall. So it bills as its own claim, the same shape as every other accrual in this system: Dr member receivable / Cr the clearing-fee revenue account already sitting in the chart from last night (410540), collected alongside the member's next ordinary payment. The billing unit is the CHEQUE, not the batch, via a `clearing_fee_charged` flag added to installment_cheques — which is also the idempotency key. A plan finished across three cashier visits bills three times for exactly the cheques each visit added; a retried request bills nothing twice. The 102 live cheques already on file are marked charged in the same migration — they were handed over before this feature existed, and backfilling a charge onto them would invent something no member agreed to. Verified against a scratch copy of production: a fresh 3-cheque batch bills exactly 75.00 across 3 receivables, a replay bills nothing, a second visit adding 2 more cheques to the same plan bills exactly 50.00, a branch with no fee configured bills nothing and leaves those cheques correctly unbilled (not stuck), and the historical 102 stay untouched. Trial balance nets to 0.00. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
feat(accounting): implement accountant's spec sheet — split fees, branch policy, government withholding Sheraton's accountant handed over a 274-row spec for tomorrow's meeting. Most of it was already correct (payroll, rentals, academies); this closes the concrete gaps and builds the tools for the parts that are policy decisions, not code. ADDITION-FEE REVENUE SPLIT (spec rows 72-88) The spec asks for the fee split "according to the ratio of the revenue" without ever giving a ratio — but the ratio already existed: ChildFeeCalculator/SpouseFeeCalculator compute a membership-value component, a form-fee component and an annual-subscription component on every addition. It just wasn't persisted past the display breakdown, so by payment time nothing to split by. Now stored (fee_component_* columns) and posted as three separate credits instead of one lump "dependant addition" line, with a safety check that skips the split (falls through unchanged) if the collected amount doesn't match the stored components exactly. MEMBERSHIP-FORM STAMP FEE (row 12) BillingService's own price breakdown already reads "form fee: 500 — stamp: 5" — the 505 EGP was never wrong, accounting just credited all of it to form revenue. Now splits the 5 EGP to the government stamp liability that already existed in the chart, unused (23082103 طابع الشهداء). Only fires where a branch has it configured — every other branch posts exactly as before. BRANCH FEE SETTINGS — a real tool, not a guess The spec argues with itself about several fees: "add this at Sheraton" against "cancel it everywhere" in consecutive lines, "each branch has its own card-commission rate" with a rate given for exactly one branch. /accounting/branch-fees is where that gets decided per branch or once for all of them, with a one-click "generalize to every branch" action — covers the stamp fee, cheque clearing fee, bounced-cheque fee, and a tiered card commission (threshold + rate + whole-vs-excess basis, with a live preview). Seeded active for Sheraton with the exact figures the spec gives; every other branch starts with nothing configured. CARD COMMISSION — posted as an expense, not left in the bank figure Implemented as a small adjusting entry after the main collection posts (Dr commission expense / Cr the same card account, reclassifying part of what was already debited) rather than rewritten into ~30 payment types' posting logic. Verified: 50,000 EGP visa payment above a 10,000 threshold at 2% posts exactly 800.00, non-visa payments are untouched, replay does not double-post. GOVERNMENT WITHHOLDING ON EXPENSES (rows 223-264) Ordinary stamp duty, additional stamp duty, commercial-profits withholding — deducted at source from every vendor invoice, reusing liability accounts the chart already had unused (23081202-23081204). Rates are not guessed: Egyptian withholding schedules are progressive law, not a percentage this migration could safely invent, so it ships at 0% and inactive until finance sets real rates on the same settings screen. Accounts payable now records the NET amount owed after withholding, which is what the club actually pays. CASH DISBURSEMENT BANNED FOR EXPENSES (rows 190-191) "As a government institution, cash disbursement is forbidden — every expense by cheque." Enforced once, at VendorPaymentService::createPayment, with a system_config kill switch for the day this genuinely needs an exception (logged as a deliberate override, not a silent bypass). NOTES PAYABLE — cheques now a liability until the bank clears them onVendorPaymentCompleted was crediting Bank directly the moment a cheque was issued, before the bank had paid anything. Now credits أوراق دفع (notes payable) instead, and /accounting/notes-payable is the monthly reconciliation the spec asks for at rows 267-269: pick the cheques the bank statement confirms cleared, post one closing entry. Zero live vendor payments existed, so this changes no historical data. Verified end-to-end against a scratch copy of production: every scenario checked exactly against hand-computed expected values (not just "it posted something") — split amounts, commission arithmetic, withholding math, net payable — trial balance nets to 0.00, zero unbalanced entries, zero postings to header accounts, every path idempotent under replay. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
One walkthrough covering everything the three features do, aimed at the accountant who has to run them and the developer who has to maintain them. The two existing files stay as references; this is the guided path through them. 21 Mermaid diagrams, all verified to parse: - what was broken, in three pictures - split vs move — why the allocation wizard could not express a chain - the settlement bug as a sequence, including why a balanced entry hid it - the accrue / collect / release cycle and where the double-count would have been - why a scanner beats event listeners, with the coach payroll listener as the worked example - the three-way match for procurement - first-run order, daily routine, monthly close - a troubleshooting decision tree and a table of the actual error messages - an ER diagram of the five new tables Also states plainly the four things that still need a finance decision, including the two that are deliberately left matching a mapping I believe is wrong, because accrual and collection must agree. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Six streams were left unbooked because the source records no amount: a pool zone with no ticket price and no attendance, a player card with no fee column, a booking the code writes as zero. The scanner refuses to invent a figure, and should — a wrong number in the books is harder to find than a missing one, and it looks settled. But "the system cannot tell you" is not "nobody knows". Finance knows what a lane costs. This adds the screen where they say so. /accounting/gaps — one card per gap: see the blocker what exactly is missing, and why it blocks propose a value a flat rate per unit, or the recorded amount see the consequence "احسبلي هينزل كام" computes without writing commit reason and effective date are mandatory The reason is required because it is what turns an invented number into a management estimate — a legitimate basis to account on, provided it is stated, approved and attributable. The effective date is required because backdating a rate onto years of history rewrites results for periods already reported. Nothing posts from this screen. It records a decision; the accrual scanner acts on it next pass. Four new runners stay silent until a decision exists, so the rules ship configured but inert. Academy settlements get a different tool, because it is a different problem: the settlement engine reads `academy_contracts` (empty) while the 13 real contracts live in `sa_academy_contracts`. The tables are not copies — the settlement table carries settlement_day, grace_period_days and penalty_rate_pct, which the engine calculates with and the other table lacks. So the tool COPIES rather than moves, with the missing terms supplied by the accountant rather than defaulted silently, and the source contracts keep working untouched. Verified: gaps stay silent with no decision; a rate of 25/booking books 8,800 over 352 bookings and re-running books nothing; switching off stops future accrual without reversing what was booked; all four validation rules reject bad input; 13 contracts import and re-import is a no-op. Trial balance still nets to 0.00. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
DevPilot authored
Two gaps closed, both of which let real money go unrecorded. CHAINS — money moving between accounts across several entries The allocation wizard splits one amount inside one entry. It had no answer for the same money moving through a sequence of entries as separate actions happen: cash into a safe, settled to main, banked. Each hop must clear the account the previous one filled, and nothing enforced that. Concretely broken: every cash collection debited one global account regardless of which safe took it, then the settlement credited an unmapped `treasury:sub_cash` pointer that fell back to الصندوق بالدولار — an account no collection had ever touched. The sub-safes were also pointed at the USD/EUR cash boxes, so 972,791 EGP of pound takings sat in foreign-currency accounts. A chain step now declares where it leaves money and which earlier step it clears; the counter side is derived by re-resolving that step's rule against the same document, so a chain cannot be authored that fails to net to zero. Each safe owns a GL account, resolved through one service both the engine and the chain call. Guards refuse a hop whose two sides resolve to the same account, or whose type puts both on the same side. The historic misposting is corrected by a reviewable journal entry — not by rewriting posted history — and cannot be posted twice. ACCRUALS — obligations the ledger was never told about 24 revenue streams were marked `needs_code`. 18 are now booked, finding 1,316 claims worth 835,167 EGP that were nowhere in the accounts. Built as a scanner over the source tables rather than event dispatches in twelve modules: an event can be missed or misnamed — the coach payroll listener was bound to a name nothing dispatched — while a scanner is self-healing, retroactive and idempotent. Critically, collection here posts revenue directly, so accruing without releasing would double-count on every future payment. Each accrual is released when its document is paid, by mirroring the same rule. Outflows that had no entry at all are wired too: staff loans (an asset, not an expense), end of service, coach fees, goods receipt (against a new clearing account, so the invoice does not book inventory twice), depreciation per asset category, stock variances, asset disposal, and fine waivers. 6 streams are deliberately left unbooked — no amount or no counterparty recorded, so any figure would be a guess. They are listed on screen with what each one needs. Verified against a scratch copy of production: trial balance nets to 0.00, no unbalanced entries, no postings to header accounts, every pass idempotent. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
-
- 05 Sep, 2026 6 commits
-
-
Mahmoud Aglan authored
One document covering the whole revenue posting system end to end: how a payment becomes a journal entry, the 97 streams across 14 categories, the six stages and which apply to which category, direction, the five scopes, the wizard step by step, the allocation waterfall, line types, VAT, deferred revenue, rule resolution by specificity, versioning, every validation the system enforces, and worked scenarios. Includes the three accounting defects found in the live data — revenue recognised on collection rather than accrual (36 of 42 rules), nothing deferred at all, and 3,032,258 EGP of customer advances sitting in revenue accounts — with the correct treatment for each. 18 mermaid diagrams, all verified to render with mermaid-cli 11. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The wizard could only ever rewire one stream at a time, so making a policy ("this is how we split membership money") meant repeating the same work per stream — 97 streams across 14 categories. The split is now written once and pushed onto whatever it should govern. Scopes: this stream, a whole category, a hand-picked set, everything not yet mapped, or all of it. The screen shows the resolved target list and the count before anything is written, and each target still gets its own versioned rule — nothing is shared and nothing is retroactive. AllocationPlanService is the guard rail. Every save runs resolveTargets -> validate -> apply, apply() re-validates on its own (there is no FK on account_id, so an unvalidated write would point rules at accounts that do not exist), and the whole batch is one transaction — a single bad target rolls back all of it rather than leaving the chart half-rewired. Refused before a row is written, all verified against a clone of production: header accounts, missing/archived accounts, no remainder line, two remainder lines, percentages over 100, zero and negative values, a line type that does not match its account's type, an expense line on a collection, mixing inflow and outflow streams in one scope, an effective date inside a closed period, a deferred line with no recognition account, and a stage the category cannot produce (skipped with a reason rather than written). Also: - Direction now drives which line types exist at all, and is editable per stream from the wizard. Phase_106_001 corrects 11 payroll/procurement streams seeded as 'inflow' — a salary expense and a supplier payment are debits, and the screen was offering revenue accounts for them. - parseLines() accepted 5 of the 12 line types the schema allows, which made outflow streams impossible to configure at all. It now accepts all of them. - Deferred revenue is configurable from the wizard (months + recognition account, filtered to revenue accounts). - The entry preview inverts for outflow: split lines debit, cash credits. Arithmetic verified against RevenueAllocator at 0.03, 1.00, 150,000.00 and 999,999.99 EGP, and with 14% inclusive VAT — balanced in every case. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Splitting a payment across GL accounts previously meant the advanced rule editor — a table of lines with no feedback until save. The finance team's actual question is "20% here, 30% there, where does the rest go?", which needs the remainder visible while you allocate. - New wizard screen: base amount (pre-filled from the stream's actual collection average), member-category scope, progressive allocation with the unallocated balance as a running figure plus a proportional bar, a mandatory remainder destination, and a live journal-entry preview that balances before you can save. - The wizard's arithmetic mirrors RevenueAllocator exactly — tax off the top, fixed lines from the pool, percentages of net-after-fixed — so the preview is what actually posts. Verified against the live allocator: 150,000 → 20%/30%/rest = 30,000 / 45,000 / 75,000, and with 14% inclusive VAT = 26,315.79 / 39,473.69 / 65,789.47 on net 131,578.95. - Wire member_category through resolveRule() with specificity scoring (category > branch > payment method), so a working-member split wins over the general rule with no extra configuration. - update() resolves and supersedes only the same-scope rule, and no longer reads $memberCategory before assigning it. - Wizard is now the default action from the mapping list and the connection centre; the advanced editor moves behind a
⚙ link. - Arabic tutorial in docs/معالج-توزيع-المبالغ.md. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Ten scenarios in click order, covering what the review will actually ask: splitting a fee across funds, connecting an unconnected charge (and declaring a new source live), the rent cycle end to end, the cheque lifecycle including bounce and re-presentation, expenses, restructuring the chart, VAT, deferred revenue, breaking out an aggregate, and proving the numbers reconcile. The closing line is the point: when asked about something not listed, the answer is no longer "that needs development" — it is "we define it as a billing source or a voucher type, let's do it now". Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The chart could only be created and edited. Restructuring it — the thing that actually gets asked for — meant a developer. Adds move / delete / promote-demote with the guards a ledger needs, each refusing with the specific reason rather than failing later at month end: MOVE - Refuses a move under the account's own descendant, which would detach the subtree from the root and make the tree query loop. - Refuses a parent of a different account_type — that would file an asset under liabilities and quietly corrupt the balance sheet. - Refuses a non-header parent. - Carries the whole subtree and recomputes every level beneath. DELETE - An account with posted history is ARCHIVED, never deleted: removing it would leave old journal lines pointing at a name that no longer exists. The button relabels itself to "أرشفة" and says why. - Refuses while children exist, or while anything still points at it — posting rules, tax profiles, voucher types, vouchers, bank accounts, treasuries — and lists what, so the blocker is actionable. - System accounts can be deactivated, not removed. PROMOTE / DEMOTE - Refuses to promote an account that already carries movement; a header takes no entries, so its balance would be stranded. - Refuses to demote one with children; entries would post at a summary level and double-count up the tree. The management panel loads the usage check before offering anything, so a button that is going to be refused is disabled with the reason instead of being offered and failing. Reference scanning tolerates a missing table or column, so a trimmed or older install does not break the screen. Descendant walking is iterative and guarded against a pre-existing cycle rather than recursing into a hang. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
VOUCHERS (سندات الصرف والقبض) Pay an expense without thinking in debits and credits. The clerk says "صرف ٥٬٠٠٠ دعاية نقدي" — picks a type, the cash account, and what it was for — and the double entry is derived and previewed live before saving. outflow (صرف) Dr each expense line Cr cash / bank inflow (قبض) Dr cash / bank Cr each revenue line Voucher TYPES are rows, not code: a club adds "سند صرف كهرباء" with its account pre-selected from the screen. Seeded with general, advertising, maintenance, utilities, bank, and two receipt types. Handled deliberately: - Input VAT splits out per line, so a supplier invoice with 14% recoverable tax records the expense net and the tax in its own asset account without the clerk doing the arithmetic. Inclusive and exclusive are both correct. - Every account is checked postable before saving; a header or inactive account is named in the error rather than failing at post time. - A line pointing at the cash account itself is refused — the entry would cancel to nothing. - Posting is idempotent: a voucher that already carries a journal entry is refused. - Cancelling a POSTED voucher reverses its entry rather than deleting it; a posted entry is answered with an opposite entry, never erased. - Voucher numbers retry on collision so two clerks saving at once cannot take the same number. - A type in use deactivates instead of deleting, so its vouchers keep their type. - Approval is optional per type and blocks posting until granted. INSTRUMENT SEED FIX Phase_104_003 died on a duplicate key and never recorded: its existence check filtered on is_header = 0, so it missed 230602 أوراق الدفع قصيرة الأجل — which exists as a header — and tried to insert it. Existence is now checked by code alone, and notes payable hangs at 23060201 underneath it. Account codes for the seeded voucher types were read off the live chart rather than guessed: 3303/3304/3305 are all headers, and 3305 is stationery, not advertising. They now point at 330702 دعاية و إعلان, 33061 صيانة مباني, and 330401 كهرباء. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-