All notable changes to this app are recorded here. Newest entries on top.
track.mybiz.com swaps in for the platform fallback). A helper line tells the admin every role uses the same sign-in URL; phone+PIN routes them to the right home.POST /api/admin/update-settings now accepts any label_* key (with a 96-char cap) on top of the existing allowlist.dcp_status_label($status) helper in [helpers.php](helpers.php) wraps the platform's get_label() with sensible English defaults. Wired into [login.php](views/public/login.php), [track.php](views/public/track.php), [customer-home.php](views/public/customer-home.php), and [agent-home.php](views/public/agent-home.php) — overrides flow through immediately.label_status_washing = "Cleaning in progress" instantly changes the wording on the public /track progress ladder.available → in_use when payment confirms; flips back to available when the vendor marks the order delivered. Status lost / retired are manual.list (vendor: own; manager: downline; agent: available across downline; admin: all), add, mark-lost, retire, restore. UNIQUE(vendor_user_id, bag_number) returns 409 on duplicates.vendor_user_id (from the agent's bag pick) and only round-robins when null. Bag is set in_use in the same transaction that flips the order to paid.transition-status to delivered flips the order's bag back to available (only if it was in_use — defensive against manual fiddling)./manager-bags; vendor home's bottom-nav "Bags" tab now routes to /vendor-bags./app/drycleanplus/transfers view shows the full log, retry queue, and a "Run cron now" button.paystack_dry_run = 1 so demos work without real Paystack keys — every transfer / refund is recorded with a synthetic DRYRUN_* reference. Flip it off in /app/drycleanplus/settings once you've pasted in your sk_live keys./transfers resets the counter.intake_suspended — new order drafts return HTTP 503 with a friendly message and the admin sees a rose banner on /settings with a "Resume intake" button./customer-refund-new, vendor + agent get 48h to respond, manager (or platform owner) approves/denies at /manager-refunds (or admin /refunds). On approval the refund amount is debited proportionally from every party's wallet, the customer is paid out via Paystack Refund API (or a DRYRUN_REF_* reference in test mode), and the wallet-offset engine withholds debt-recovery from each party's share on future orders. Held money stays in the platform owner's Paystack balance./transferrecipient and stores the paystack_recipient_code against their profile. In dry-run mode a deterministic DRYRUN_RCP_* code is stored instead./app/drycleanplus/orders (every order across the network, filterable by status), /app/drycleanplus/order?id=… (full detail + items + splits + event log), /app/drycleanplus/transfers (cron + retry queue), /app/drycleanplus/refunds (queue with override), /app/drycleanplus/settings (Paystack keys, splits %, markup %, NTFY).cron_drycleanplus_tick registered in [core/cron.php](../../core/cron.php) and [core/cron/tasks.php](../../core/cron/tasks.php) — runs the transfer queue + health check across every install. Admins can also fire it on demand via POST /app/drycleanplus/api/cron/run.dcp_paystack_request, dcp_paystack_create_recipient, dcp_paystack_initiate_transfer to [helpers.php](helpers.php). All three honour paystack_dry_run and return synthetic IDs when set.dcp_enqueue_splits which checks each recipient's wallet at queue time — if negative, holds up to that debt amount from the transfer and emits a recovery_credit ledger row. Platform owner's share also runs through the same offset logic without a transfer row (their money stays in Paystack balance).dcp_run_transfer_queue (called by both the platform cron tick and the admin "Run cron now" button) with the 5-step backoff schedule and auto-suspend check.dcp_wallet_post (atomic balance update + ledger entry) and dcp_refund_debit_split (proportional debits matching the original split percentages).dcp_safe_alter(): transfers.last_attempt_at, transfers.next_attempt_at. The lazy migrator's static-flag pattern means each ALTER fires at most once per process.dcp_process_transfer synthesises a DRYRUN_RCP_USER{id} recipient when none is on file in dry-run mode, so seeded demo data works out of the box.api/orders.php create-draft now refuses with HTTP 503 when intake_suspended = '1'.api/admin.php exposes: update-settings, resume-intake, retry-transfer, reassign-vendor.End-to-end smoke test ran clean: 5 role logins all 200, agent draft → Paystack webhook → 4 transfers success in dry-run, customer refund → manager approval → 5 wallets debited proportionally (-₦720/-₦180/-₦120/-₦36/-₦144 on a ₦1200 refund of a ₦2400 subtotal), follow-up order → wallet holds recover all but vendor's debt (because round-robin picked the other vendor). Admin views all 200 with a real Pancho session. Role-mismatch checks return 403.
order_events, fire NTFY to the customer and agent, and log to audit_log./track?code=… shows the progress ladder.$_PS_CURRENT_USER_ID (set by both /app/{appId} and /p/{uuid}/{appId} dispatchers) instead of $_SESSION['user_id'] — in-app users on /p routes have no Pancho session, so the old check broke the recruitment-chain attribution. New helper dcp_owner_uuid() centralises the lookup.status column in vendor-home's JOIN (orders.status vs users.status) — qualified the WHERE-clause status references with the o. prefix.transition-status action to [api/orders.php](api/orders.php) for vendor + manager + platform_owner workflows. Forward-only chain paid → picked_up → washing → ready → delivered with corresponding *_at column writes.sources/_install.php throwaway tool — re-runnable installer that wires drycleanplus onto the owner UUID and seeds the demo chain (call php apps/drycleanplus/sources/_install.php or pass a UUID as the first arg).charge.success, the webhook verifies the HMAC-SHA512 signature and persists the order as paid. Five-way Transfer API splits land in Milestone B./p/{owner_uuid}/drycleanplus/track?code=DP-7K2X resolves an order without login and shows its current status.dcp_resolve_platform_owner() in [helpers.php](helpers.php) INSERT OR IGNOREs a users row with role='platform_owner' keyed off the Pancho install owner's UUID. Cached in $GLOBALS['_DCP_PLATFORM_OWNER_ID'] for the request.DP-{4-char base32}, generated by dcp_generate_order_code() with retry on collision.dcp_notify() POST to settings.ntfy_server (default https://ntfy.sh). Topic = pancho-drycleanplus-{sha256(phone)[:12]}. Best-effort; failures swallowed.settings table. Default split: vendor 60 / agent 15 / manager 10 / director 3 / platform_owner 12.