- 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 21 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>
-
Mahmoud Aglan authored
TWO THINGS. 1. Billing sources installed nothing. validate() read information_schema with lower-case keys (column_name, data_type) while this server returns them upper case, so every column looked missing, every source failed validation, and the seed skipped all seven while reporting success. Columns are now aliased explicitly. The defaults moved into BillingSourceService::syncDefaults() so they can be re-installed after a schema change instead of being trapped in a one-shot seed, and anything that still does not fit is named rather than dropped. Found by running the validator against the live database instead of trusting that an empty table meant "nothing to do". 2. The cheque lifecycle now posts. CheckLifecycleService had a correct state machine and zero journal entries, so a cheque moving desk → bank → collected, or bouncing, left no trace in the ledger at all. Each movement now posts through configurable account pointers: deposited Dr شيكات تحت التحصيل / Cr أوراق قبض collected Dr البنك / Cr شيكات تحت التحصيل bounced Dr مدينون (شيكات مرتدة) / Cr شيكات تحت التحصيل endorsed Dr الدائن / Cr أوراق قبض paid Dr أوراق دفع / Cr البنك The bounce charge posts as its own entry so it can be waived without touching the restored debt. Re-presenting a bounced cheque moves it back to under_collection and posts the deposit leg again, so a second and third presentation each leave their own trail. Posting happens AFTER the status commit on purpose: a cheque physically moving to the bank must be recorded even when its accounts are unmapped, otherwise the paperwork and the system disagree. An unpostable move returns a warning. Also corrects a real error along the way: AccountCodes sends a cheque payment straight to the bank. Taking a post-dated cheque is not money in the bank — it is a note receivable until the bank collects it. The counter account is now a configurable pointer per payment method (treasury:method_check → أوراق قبض), so it is fixed from the screen rather than in code, and a header account there is refused with the pointer name to map. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Removes the "this module needs code" category rather than labelling it. Most unbilled money in the ERP has one shape: a module writes a priced row into its own table and never tells accounting. Wiring each module by hand means a developer for every revenue path, forever — which is what I handed over last time instead of solving it. A billing source declares that shape as data: which table holds the money, which column is the amount, which rows are still outstanding, who owes it, and how it posts. One screen then lists every outstanding charge across every source and collects it through PaymentService — the same funnel a member payment uses, so it gets a receipt, treasury custody and a journal entry. Seeded and working immediately: hourly court bookings, sports subscriptions, lockers, facility reservations, private matches, rental invoices, annual member subscriptions. Edge cases handled deliberately: - No free-text SQL anywhere. Filters are structured (column / operator / value) rendered into prepared statements; a settings screen that accepted a WHERE clause would be an injection hole. Identifiers are matched against information_schema and a strict pattern before interpolation. - Every source is re-validated on save AND before every listing, because a migration can drop a column underneath a source that was fine yesterday. An invalid source is shown as broken instead of silently returning nothing. - The amount is re-read from the source row at collection time, never trusted from the form, so a stale list or a tampered field cannot set the charge. - Double-collection is blocked by our own billing_source_collections table rather than the module's paid flag — some sources have no write-back column at all, and a module can overwrite its own flag. The check is repeated at collect time to cover the gap between listing and click. - Partial collection only where the source allows it, never above the row total. - A player is not a member: a member_id that members does not have is dropped rather than tripping the payment foreign key. - Write-back is best-effort and isolated — a missing column must not undo a real payment, so the failure is logged and the receipt stands. - Zero and negative rows are excluded; an empty IN () renders as a false predicate rather than a syntax error. - Payer names resolve in two queries, not two per row. - A source with collections against it deactivates instead of deleting, because those rows are the audit trail for real money. Permission keys were read off role_permissions rather than assumed — payment.create does not exist in this install. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The stream list used LEFT JOIN revenue_posting_rules + GROUP BY s.id and selected r.id / r.stage. MySQL rejects that under only_full_group_by — r.id is not functionally dependent on s.id — so the page threw PDOException on every load. Caught by running the query against the live database rather than trusting that it looked reasonable. The representative rule is now picked in its own aggregate and joined back by id, preferring the collection stage since that is the one people mean by "where does this money go", then the newest effective date and version. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Written to be executed in the room. Each scenario is the sentence the accountant will say, then the exact buttons in order, then the journal entry that appears. Covers the one that prompted it — "the 150,000 splits 30% to this fund, 10% to that one" — plus flat amounts, the percentage-base question, creating a missing fund account mid-meeting, VAT inclusive vs exclusive, deferred subscription revenue, connecting an unconnected path, breaking out an aggregate account, undoing a change, and proving the numbers reconcile. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Answers the question the finance review will actually ask: "we know it is not connected — how do we connect it, from inside the system, now?" Three parts. 1. مركز التوصيل (/accounting/revenue-mapping/connections) Every money path in the ERP in one list, split by WHAT IT NEEDS rather than by severity, because that decides who can close it: - تُوصَّل الآن من الشاشة — the module already fires an event carrying the amount, so mapping the accounts is the whole fix. Has a button. - تحتاج تعديل برمجي — the module writes the money to its own table and fires nothing. Mapping would change nothing, so there is deliberately NO button and the row says exactly what is missing. A button here would be a lie. - موصولة — with the current split shown inline and a rewire button. Each row reads the amount sitting in that module's own table live, so every gap is a number instead of an adjective. 2. Rewire and split anything, including already-connected paths The connected list shows each current split and offers "قسّم على حسابات" when a path still posts to a single account. Any line can be a percentage, a flat amount, or the remainder — so "30% of the 150,000 to this fund, 10% to that one, the rest to membership revenue" is three lines and a save. Saving takes a new version with an effective date; posted entries never move. 3. Create the destination account without leaving the screen A fund that does not exist yet used to mean leaving for the chart of accounts and losing the room. "+ حساب جديد" creates the leaf under a chosen header, takes the next free code, inherits type and nature, and drops straight into the line. Refuses to hang a child off a posting account, which would strand its balance. Also seeds the club fund accounts a distribution rule needs to point at — sports support, member welfare, martyrs stamp (already priced at 5 EGP in the service catalogue with nowhere to post it), federation share, facilities development. They are liabilities, not revenue: money earmarked for a fund is held on that fund's behalf, and posting it to revenue would overstate income. And 27 previously invisible paths are now catalogued with an honest wiring_status, a plain-Arabic note on what is missing, and a pointer to the table holding the money — so the screen shows the whole picture instead of only the working parts. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Terminology, screen-by-screen walkthrough, a demo running order, and — most importantly — an honest status matrix: 267 money paths mapped across 67 modules, 71 of which actually reach the ledger. Section 6 lists what must NOT be demoed or claimed. Being caught overstating in front of accountants is far worse than a known, quantified gap, so the guide leads with the exclusions and gives the exact wording for the hard question. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
cron/runner.php writes a cron_job_log row before every eligible job. That table does not exist, so the runner threw on the first job with shouldRun() === true and none of the 43 scheduled jobs has ever executed: subscription generation, instalment default handling, activity-subscription revocation, academy settlements, coach payroll, monthly depreciation, and every expiry reminder. The container's crontab is present and cron is running — the hourly entry has been firing into an immediate exception the whole time, which is why storage/logs/cron.log does not exist. Creating the table alone would be reckless the night before a finance review: the crontab fires hourly, so all 43 would start on the next tick, and several write off receivables, impose fines, drop memberships and auto-complete waivers (which now post accrual entries). So the runner is additionally gated behind system_config.cron_enabled, seeded to 0. Turn it on from Settings when someone can watch the first run. Until then the runner exits with a clear message rather than pretending to work. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
onPayrollPaid read total_gross and total_net off hr_payroll_runs and grouped hr_payroll_components_log by component_type. None of those columns exist: hr_payroll_runs has gross_earnings / net_salary hr_payroll_components_log has `type`, not component_type hr_payroll_periods has period_code, not period_name Confirmed with SHOW COLUMNS on the live database. The handler threw "Unknown column" on its first query, and the listener only logs, so payroll silently posted NOTHING — no salary expense, no employer insurance share, no withheld tax anywhere in the ledger. It also had the grain wrong. PayrollController dispatches hr.payroll.paid once PER EMPLOYEE; an hr_payroll_runs row is a single payslip, not a whole run, and the period lives in hr_payroll_periods. Every amount needed is on the payslip. Rewritten against the real schema: Dr Salary Expense gross_earnings Dr Employer Insurance Expense insurance_employer Cr Bank net_salary Cr Insurance Payable insurance_employee + insurance_employer Cr Tax Payable tax_amount Cr Employee Loans loan_deduction Cr Other Deductions Payable penalty + absence + other Balances by construction: the payslip satisfies gross - total_deductions = net and the deduction buckets sum to total_deductions. Verified on all three live payslips — e.g. run 1: Dr 15,000.00 + 2,362.50 = Cr 4,402.20 + 3,748.50 + 9,211.80 = 17,362.50. A salary-deducted loan instalment credits the employee-advances receivable rather than being treated as income. Penalties and absence deductions are parked in accrued expenses and registered as a configurable pointer, because whether they belong there or as a reduction of salary expense is a decision for the accountants, not a constant in code. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
onSaleCompleted summed sale_items.total_cost. That column does not exist; the table stores a per-unit cost_price alongside quantity. Confirmed with SHOW COLUMNS on the live database. Every sale therefore threw "Unknown column" inside the sale.completed listener, which is wrapped in a try/catch that only writes to the log. So inventory was relieved in the stock ledger while the general ledger kept carrying it, and no cost of sales was ever recognised — the gross margin on every sale was overstated by its entire cost. Now SUM(cost_price * quantity) over non-refunded lines. This also un-breaks onSaleVoided, which reverses the 'sale_cogs' entry and could never find one. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Assets came to 124,051,895.29 against liabilities + equity of 84,950,136.58. Out by 39,101,758.71 — a balance sheet that does not balance. Cause: the sheet added only the CURRENT fiscal year's net income to equity. No year-end closing entry has ever been posted here (period_closings is empty), so the revenue and expense accounts still carry all-time balances and retained earnings has never absorbed prior years. The earlier years' profit therefore sat in the income accounts and appeared nowhere on the sheet. Verified against the live ledger: liabilities 79,821,436.63 revenue - expenses (all time) 44,230,458.66 (80,820,684.83 - 36,590,226.17) --------------- 124,051,895.29 = total assets, exactly Accumulated profit now runs from the first posted entry rather than the fiscal year start. This stays correct after closing entries begin: a closing entry moves the profit into retained earnings and zeroes the income accounts, so the figure then covers only post-closing activity while the closed profit sits in the equity accounts. The current fiscal year's slice is still returned separately, as current_period_net_income, because that is what the board asks about. Note for the chart: there are no accounts typed 'equity' at all — capital (2101) and retained earnings (210201) are typed 'liability'. That is why total_equity consists solely of the accumulated-profit line. The sheet balances either way, but the classification is worth revisiting with the accountants. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
`reservations` identifies its booker with booker_type plus player_id / member_id. There is no booker_id column — confirmed with SHOW COLUMNS on the live database, not from the migrations. Five call sites queried it anyway, so every one of them threw a SQL error: FacilityDashboards/Controllers/FacilityDashboardController.php (x2) PlaygroundAdmin/Services/ClubDashboardService.php (x2) PlaygroundAdmin/Services/PlaygroundMirrorService.php (x3) FacilityGrids/Services/PoolFinancialService.php (x1) PlayerApi/Services/PlayerBookingService.php (x4) Effect: the facility dashboard, the club-wide playground dashboard, the pool financial panel and the playground mirror hard-500 on every load, and the player app could never create a booking — the INSERT named booker_id too. That matches the data: 7 reservations exist with booker_type set and player_id/member_id both NULL, and zero player bookings. Reads become COALESCE(player_id, member_id); the joins key on the specific column for their booker_type; the INSERT writes player_id. Also PlaygroundMirrorService queried private_match_bookings.match_date, which is booking_date on that table. (live_matches genuinely has match_date, so MatchCenter is untouched.) And sa_bookings / pool_bookings really do have booker_id, so those references are correct and left alone. Every rewritten query was executed against the live database before committing. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The opening figures live in TWO places in this ledger: the chart_of_accounts.opening_balance column AND 24 posted journal entries dated 2024-07-01 with reference_type='opening', totalling 90,601,962.36. Three reports read the column and then also summed the ledger movement that already contained those same entries, counting the opening twice: - Trial balance (LedgerService::getTrialBalance) - General ledger (LedgerService::getAccountLedger) - Balance sheet (FinancialReportService::getBalanceSheet, and the consolidated sheet which delegates to it) Measured on live data, trial balance over FY 2024/2025: 1103 مشروعات تحت التنفيذ reported 85,627,410.75 actual 43,923,543.75 210201 أرباح مرحلة reported -146,645,270 actual -73,322,635 i.e. exactly double on every account carrying an opening balance. The report still footed, because opening balances net to zero across debit and credit — so it looked right and every line was wrong. Only periods containing 2024-07-01 were affected; a 2026 trial balance was already correct. The opening column is now derived as cumulative posted movement BEFORE the period start, which is the standard definition, removes the double count structurally, and works for any period rather than only a year boundary. The trial balance query is also restructured into two independent aggregates so no row multiplication is possible and an account whose only movement predates the period still appears. Income statement was already correct and is unchanged. Also in this commit: - LedgerService::rebuildBalances() + a seed that runs it. The opening import wrote journal rows without going through JournalService, so 24 accounts had a cached current_balance disagreeing with the ledger — retained earnings cached 0.00 against an actual 73,322,635.00. The reports read the ledger and were fine, but the Chart of Accounts screen and the bank-reconciliation opening figure read the cache, which is precisely where an accountant would find a number contradicting the trial balance. - Carnet guest entry never posted. Accounting listened on 'carnet.guest_entry_recorded'; GuestEntryService dispatches 'carnet_guest.entry_recorded' (underscore, not dot). Notifications listens on the correct name, which is why notifications worked and the ledger entry never appeared. Fees were recorded in carnet_guest_entries.amount_paid and posted nowhere. - 'tournament.fee_collected' has no dispatcher anywhere. Documented as dead rather than left looking wired. - Two fiscal years were flagged is_current; the seed leaves exactly the one containing today. FiscalYear::findByDate now resolves overlapping years deterministically (open first, then narrowest range) instead of taking whatever the database returned — this chart has calendar years overlapping a July-June year, so Jul-Dec 2024 matches two. No entry is reassigned; all 795 are already inside their assigned year. - PostingRouter and postViaRule now probe App::db() with try/catch. It is typed `: Database` and throws when unbound rather than returning null, so the previous null guards could never fire. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Generalises the revenue engine from "collection" to every stage a document posts at, and routes all 26 auto-posting paths through it. Two new dimensions on a rule: stage accrual | collection | payment | refund | writeoff | transfer direction inflow → counter account DEBITED, allocation lines CREDITED outflow → allocation lines DEBITED, counter account CREDITED So the same allocation maths now drives revenue, expense, receivable and payable postings. Contra-revenue is always a debit regardless of direction. Where the amounts are computed elsewhere and only the accounts need to be configurable — payroll components, treasury legs, COGS, rental legs — a second mechanism (PostingRouter::accountFor) resolves a configurable account pointer instead of forcing those through the allocator. Both are edited from the same screen. Dead posting paths fixed. Each of these targeted a header account, which JournalService refuses, and the callers only Logger::error — so they have been failing invisibly: - 230601 الموردون is a header → the ENTIRE procurement cycle (vendor invoice, vendor payment, return-to-vendor) could never post. Now 230601002. - 310103 حصة الشركة في التأمينات did not exist at all → payroll dropped the employer insurance line, then a balancing fallback silently increased the bank credit to force the entry to balance, misstating cash. The account is created, and an imbalance now refuses to post and reports instead. - 230804 جاري مصلحة الضرائب is a header → rental VAT could never post. Now 23080404 ضريبة القيمة المضافة. - AccountCodes::INPUT_TAX resolved to 120408 مدينو بيع أوراق مالية, an unrelated account. Input VAT now posts to 12041106. - Member write-off debited MISCELLANEOUS_REVENUE. A bad debt is an expense; it now posts to 3328 ديون معدومة. - $result['entry_id'] is never returned by JournalService (the key is journal_entry_id), so rental invoices, treasury settlements and treasury deposits never linked back to their journal entry. - SUB_TREASURY_CASH points at 12060102 الصندوق بالدولار, the USD box. Left deliberately unmapped and surfaced on the diagnostics page so finance picks the right EGP account rather than having one guessed for them. Accruals now also create the accounts_receivable sub-ledger row alongside the GL entry, which is why that table was empty against 970,592.67 EGP of scheduled instalments. Verified against a full clone of the production schema and chart of accounts in a throwaway database: all six stages post balanced entries, VAT 14% inclusive on 1140 yields 1000 revenue + 140 tax, a five-line split (two fixed + two percentage + remainder) balances to the piastre, and a 12,000 annual subscription produces exactly 12 monthly deferral rows summing to 12,000 with the recognition run posting the current period. 27 allocation unit tests pass. Seeded rules reproduce existing behaviour except where that behaviour was a silent failure. Unconfigured stages still fall through to the legacy path. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Whoever can read the chart of accounts can read where revenue lands; whoever can change it can change the mapping. Without this the محاسب role sees the Accounting menu but gets 403 on the revenue-mapping screen. super_admin holds the '*' wildcard and needs no explicit grant. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Replaces the hardcoded AccountCodes::creditAccountForPaymentType() match statement with a versioned, effective-dated mapping that finance controls from /accounting/revenue-mapping. Every collected amount can now be split across multiple GL accounts by flat amount, percentage, or remainder, with VAT handled as its own layer and deferred revenue amortised over the service period. What the live DB showed, and this addresses: - 4,256,399.96 EGP across 129 transactions posted to a single catch-all account (410515 إيرادات متنوعه) — waiver, separation, death, foreign membership, early settlement and four payment types that had no rule in the code at all and silently fell through to `default`. - 240,582 EGP of divorce fees posted to 410302 «محل 1», a shop rental account. - 120301 العملاء and 230804 جاري مصلحة الضرائب are header accounts, and JournalService rejects posting to headers — so every AR and VAT entry has been failing silently. accounts_receivable holds 0 rows against 970,592.67 EGP of unpaid instalments. Model follows SAP account determination / Dynamics posting profiles, adapted to Egyptian VAT law 67/2016 and EAS 48 revenue recognition: - revenue_streams catalogue of every chargeable thing - revenue_tax_profiles rate + inclusive/exclusive + treatment - revenue_posting_rules versioned, effective-dated, scopeable - revenue_posting_rule_lines the split components - revenue_posting_log which rule version produced which entry - revenue_recognition_schedules deferred revenue amortisation Allocation order is fixed and deterministic: tax extraction, then fixed amounts, then percentages, then a mandatory remainder line that absorbs rounding residue so the entry always balances. Tax is a separate layer rather than a split because inclusive and exclusive pricing are not the same number: 14% of a tax-inclusive 1140 is 140 on revenue of 1000, not 159.60. Deferral is separate for the same reason — it is a split across periods, not accounts. Adds two postable accounts the chart was missing: 120301004 أعضاء النادي (مدينون) and 12041106 ضريبة القيمة المضافة — مدخلات. Seeded rules reproduce current posting behaviour exactly, so this deploy moves no reported number. Streams landing in a catch-all are flagged for review rather than silently re-pointed — repointing them moves real revenue between accounts and is finance's decision. Unconfigured streams fall through to the legacy path unchanged. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 04 Sep, 2026 1 commit
-
-
Mahmoud Aglan authored
The microsite is built for scrolling, so a naive print lost most of it: reveals start invisible, the phone shows one prototype screen at a time, the portal shows one admin screen, and <details> print collapsed. - @media print in styles.css: force reveals visible, drop the fixed nav and prototype tools, start each section on a fresh page, and mark cards, timeline items, tables and price blocks break-inside:avoid so none is split across a page boundary - generate-pdf.mjs (puppeteer): expands both prototypes before printing — the single phone frame becomes a labelled 3x3 grid of nine real app screens, and all seven portal screens are stacked. 1240x1754 pages (A4 proportion at 150dpi), backgrounds on. - Adds a "تحميل العرض PDF" button to the hero; the print stylesheet hides .btn-row so it does not appear inside the PDF itself Output is 14 pages with a real text layer: Arabic extracts correctly and figures stay searchable. Ghostscript compression reaches 2.3MB but its font re-embedding drops Arabic strings from the text layer, so the uncompressed 8.5MB file is kept instead. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 03 Sep, 2026 5 commits
-
-
Mahmoud Aglan authored
Commercial terms: - Price 900,000 -> 500,000 EGP, breakdown rebased to sum exactly (230k app + 160k portal/CMS + 60k gate/invites + 50k launch) - Payments simplified from three milestones to two: 360,000 on signing, 140,000 on delivery - Timeline 4-6 weeks -> 2-4 weeks, timeline recompressed from five milestones to four - Early-signing discount recalculated: 5% = 25,000 (was 45,000) - "غير شاملة ضريبة القيمة المضافة" now stated in the price hero, the totals row, under the payment schedule, in the hero stat and footer, and bolded in the FAQ New scope — club news / blog: - Fourth axis added; news moved out of the deferred list into phase one - Live prototype gains a news feed, article page and a fifth tab, with home showing the two latest items - Portal gains a news management screen: article list with reach stats, editor with category, image drop and publish-notification toggle - Article artwork is a branded crest placeholder, not stock photography — the club supplies real images at launch and inventing them would misrepresent what has been approved Reduced technical detail per request: - Dropped the per-template column-name lists (six of them) and the three import-engine cards, replaced with one plain note - Removed API/OTP/RTL/iOS-14/Android-8 jargon throughout; rewrote the security and extensibility cards in business terms - Simplified the in-scope list wording Also makes .tbl scroll rather than clip: a table too wide for its column silently lost its last cells instead of becoming reachable. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
The prototype in the proposal was eight inline <div>s toggled with display:none — screenshots in a phone bezel. It now embeds a real single-page app from app/, so the screens shown in the proposal are the screens that ship, and the board can actually use it. app/ — hash routed, so any screen is linkable (#/dues): splash, login, otp, home, dues, pay, paying, success, receipts, qr, invites, activities, activity/:id, schedule, notifications, profile State is live, not scripted. Selecting dues recomputes the total before paying; paying clears those dues, creates a receipt and posts a notification; issuing an invite decrements the balance; subscribing to an activity adds the subscription, consumes a place and generates the first invoice into المستحقات. The QR regenerates every 60s against a countdown ring. Dark/light theme persists. Verified end to end by driving the app in headless Chrome, not just by rendering it. Chose an SPA over the multi-page pattern used by the older Proposal/ prototype: no white flash between screens, real forward/back transitions, and shared state across screens, which is the whole point of showing a collection flow. Proposal integration: - Phone hosts <iframe src="app/">; the side list drives it over postMessage and the prototype reports its route back, so the list and the annotation stay in sync when someone navigates inside the phone - Theme toggle, reset, and open-fullscreen controls - Annotation panel rewritten per screen Removed 409 lines of static screen markup and the 93 lines of CSS that served it (.sbar/.app-hd/.mcard/.tabbar/.sport-item/.otp-row/.qr-*), verified dead by checking real class= usage, not substring matches. Adds .dockerignore: the image is built with `COPY . /usr/share/nginx/html`, so any stray working file in this folder would be served publicly. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Emoji render as platform-specific colour cartoons (Apple/Windows/Android each differ) which reads informal in a document going to a club board, and they cannot inherit brand colour. Replaced every one with a 33-symbol inline SVG sprite: 24x24, stroke-based, currentColor, sized in em so each existing icon slot keeps its own scale. Removed: swimmer, bell, receipt, credit card, mobile, bank, football, martial-arts, tennis, cartwheel, page, lock, floppy, plug, envelope, gear, up/down arrows, and the EG regional-indicator flag pair. Also converted the geometric glyphs sitting in icon slots (fisheye, diamonds, house, quadrant-circle, square-fill, clock) plus the list check/x/arrow marks, so the icon layer is uniformly SVG rather than a mix of text glyphs and emoji. A source scan for emoji ranges now returns clean. Sprite is hidden with position/width/height rather than display:none, which can break <use> resolution in some engines. Fixes an unrelated pre-existing contrast bug found while verifying: .card h4 is declared after .dark h4 at equal specificity, so the three cards in the dark flow section rendered navy-on-navy. Added .dark .card h4. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Copy — the previous register read as marketing/AI boilerplate to a board audience. Removed the tells and rewrote in institutional MSA: - Drop staccato fragment headlines and their trailing periods ("ثلاثة محاور. لا أكثر." -> "نطاق المرحلة الأولى: ثلاثة محاور") - Drop rhetorical punchlines ("الاحتكاك يقتل الاشتراك."، "ليست صورًا تخيلية."، "من ينسى، لا يدفع.") for descriptive prose - Drop the «مصنع البيانات» metaphor and the pitch-deck eyebrow "لماذا الآن" - Rewrite all 8 prototype screen annotations from slogans to labels ("البوابة تعرف من يدخل" -> "الدخول بكود QR") - Reduce rhetorical em-dashes; keep structural ones only Schedule — project is 4-6 weeks, not 12. Timeline recompressed from six milestones over 12 weeks to five over 6, with the 4-week case stated as conditional on data and accounts landing in week 1. Updated hero stat, scope lede, price card, plan heading and footer badge to match. Design — elegant crest watermark: - Oversized club crest bleeding off the inline-start edge of the hero, gold-tinted, offset so it does not double with the hero logo - Alternating-side crest ghosts on problem/scope/flow/price sections - Crest in the price card and footer, plus a gold hairline on the footer - Uses background-image + filter, not mask-image: masks give a cleaner silhouette but do not paint in headless Chrome, so this variant is the one that can actually be verified before shipping - Logical properties throughout (inset-inline, margin-inline) so the watermark mirrors correctly in RTL - Added a prefers-reduced-motion guard for the scroll reveal Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Proposal microsite for Nady El-Seid mobile app (sayd-mobile). Arabic copy: - Fix agreement/tamyiz errors: اكتملت العدد -> اكتمل العدد, متأخر يومان -> يومين, "92 يوم" -> "92 يومًا", منها 214 متأخر -> متأخرًا, يومان تدريب -> يوما تدريب (dual mudaf drops nun) - Fix ambiguous/wrong forms: فيتحدث رصيده -> فيُحدَّث رصيده فورًا, الحمام الأولمبي -> حمام السباحة الأولمبي - Remove translationese: comma-lists rewritten with و, passives given back their agents, Egyptian تشتغلون -> تبدأون التشغيل - Unify register to plural address (اضغط -> اضغطوا, شاهد -> شاهدوا) - Apply reviewed headline/lede/ROI rewrites with corrected orthography Commercial terms: - Price 700,000 -> 900,000 EGP; breakdown rebased to sum exactly (400k app + 290k portal + 110k gate/invites + 100k integration) - Remove all post-launch support: annual maintenance contract, 6 free months, 1-year warranty, 99.5% SLA, first-year store fees. Delivery and release only, stated explicitly in a "غير مشمول" block and FAQ - Payments 40/30/20/10 -> 40/25/35: signing, data-entry plan approved + club opens required accounts and services, completed delivery - Timeline week 1-2 renamed to cover data-entry planning and names the account-opening as a parallel club obligation Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 01 Sep, 2026 3 commits
-
-
Mahmoud Aglan authored
-
Mahmoud Aglan authored
Pre-existing uncommitted working-tree change, not part of the member search work. Committed separately so the 478-line reduction stays visible in history. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
Mahmoud Aglan authored
Overhaul /members/search so one bar finds anyone in the club — the member, a spouse, a child or a temporary member — and add an advanced filter panel. MemberSearchService becomes the single source of truth for people search: - UNIONs members + spouses + children + temporary_members into one normalised row per matched PERSON (person_type, relation, parent membership, rank). - Token AND matching on names, so word order no longer matters: "محمود احمد" finds "أحمد سيد محمود". - Arabic orthographic folding (أ إ آ ٱ→ا, ى→ي, ة→ه, ؤ→و, ئ→ي) applied to both the query and the column, so "احمد" matches "أحمد". - Arabic-Indic and Persian digits folded to ASCII before identifier matching. - Relevance ranking: exact membership number / national id, then name prefix, then substring. LIKE wildcards in user input are escaped. Scopes (member/spouse/child/temporary) and fields (name, membership number, national id, phone, form number, passport) are selectable; branch, membership status and membership type filter on the parent membership. A scope whose table lacks the requested field is skipped rather than matching nothing. The legacy search() keeps its exact signature and output shape, so the three existing API consumers are untouched. Also: - Split Member::getStatusOptions() (statuses an employee may ASSIGN) from getAllStatusLabels() (every status, for display/filtering). deceased, transferred and waived exist in live data but were missing from the list, so they could not be filtered on; they are deliberately kept out of the assignable set because the Death, Transfer and Waiver workflows own those transitions. - Dependent deep links honour spouse.view / child.view / temp.view and fall back to the membership file when denied. - Map children.relationship (son/daughter) and temporary_members.category (nanny/parent/unmarried_daughter) to Arabic for display. - The search form submitted to /members, dropping most of what was typed; it now posts back to /members/search. - Sidebar declared member.search while the route requires member.view; aligned. Architecture Map and Dependency Graph updated per project protocol, including the placeholder-ordering constraint in buildScopeQuery() and the three inline member-search SQL blocks that remain unconsolidated. Co-Authored-By:Claude Opus 5 (1M context) <noreply@anthropic.com>
-
- 31 Aug, 2026 2 commits
-
-
Mahmoud Aglan authored
Phase-1 scope only: membership renewals/installments, sports activity invoices, QR gate entry + invitations. Interactive HTML prototypes for 8 mobile screens and 6 portal screens, 6 downloadable CSV import templates, 12-week plan, 700,000 EGP commercial offer. Deployed to CapRover as app 'sayd-mobile'. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com>
-
Mahmoud Aglan authored
Client clarified their earlier request: the member "special discount" dropdown should show board-approved special_discounts AND active عروض مجلس الإدارة (board_offers) cash discounts side by side, not one instead of the other — they'd stopped seeing anything they'd added under Board Offers. - Add members.special_discount_source ('special_discount'|'board_offer') and drop the hard FK on special_discount_id (a single column can no longer FK exactly one table). Integrity is now validated in MemberController::parseDiscountSelection(). - BoardOffer::allActiveWithCashDiscount() surfaces board offers that define a cash discount as selectable options. - SpecialDiscountService::resolveAssignedDiscount()/amountForType() give one place that normalizes "the member's assigned discount" across both source tables — used by BillingService's invoice line item, the show page's applied-discount banner, and the dropdown's own validation. - fill-form/edit/show views render two <optgroup>s ("عروض مجلس الإدارة" / "الخصومات الخاصة") with prefixed option values (bo:<id> / sd:<id>) so a single form field can select from either table; edit.php's live discount-amount preview now handles fixed-amount discounts too, not just percentage. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
-
- 30 Aug, 2026 1 commit
-
-
Mahmoud Aglan authored
fix(members,subscriptions): drop due-date column from family tables, collect annual subscription via الخزنة - Remove the "تاريخ الاستحقاق" column from the spouses/children/temporary members tables on the member show page (display-only, no schema change). - Annual subscription payments no longer post directly from the اشتراك سنوي page. SubscriptionController::payYear() now queues a payment_request instead of calling PaymentService::processPayment() directly; the subscription rows are only marked paid once خزنة العضويات (Membership Treasury / Cashier) actually collects it, via a new payment_request.completed listener (SubscriptionSyncService::completeFamilyYearPayment()). - Closed the same bypass on the legacy generic /payments/process/{id} page, which had its own divergent partial-payment, oldest-year-first logic for annual_subscription that skipped the treasury entirely and violated the all-or-nothing-per-family rule; that path now redirects to the member's subscriptions page instead. Co-Authored-By:Claude Sonnet 5 <noreply@anthropic.com>
-