OViq Hotel ERP — Phase 7 identity/access pending-feature batch Executed verification record: 2026-10-21 Environment: PHP 8.3.6, MySQL 8.0.46, Node.js and headless Chromium. Source targets PHP >=8.2; PHP8.2 itself and non-Chromium browsers were not run. Total: 357 reported assertions/checks across the layers below. These are not 357 unique modules, and passing tests is not a security/production certification. NEW IMPLEMENTED FEATURES - Native TOTP with six published RFC6238 SHA-1 vectors verified. - Local QR enrollment, manual setup key, AES-256-GCM seed encryption. - Password + second-factor sign-in, challenge expiry, replay-counter enforcement. - Ten one-use recovery codes, one-time display, hashed persistence, replacement. - Property-required enrollment, session revocation, sensitive-action reauthentication. - Scoped custom roles, delegation restrictions, user creation/update/deactivation. - Mandatory manual journal/expense request keys bound to original payloads. - No-overwrite app-key provisioning and confirmed/audited CLI account recovery. NEW RACE COVERAGE (independent PHP processes / separate DB connections) - Same TOTP concurrently: one verification accepted, one replay rejected. - Same recovery code concurrently: one consumed, one replay rejected. - Same journal request concurrently: same entry ID returned, one entry persisted. - Same role version concurrently: one edit committed, stale edit rejected. These are targeted correctness races, not sustained load/HA/pentest coverage. BROWSER COVERAGE Enrollment QR renders locally, previous sessions revoked, codes shown once, password-only challenge cannot reach operations, fresh TOTP and recovery sign-in, used recovery rejection, mandatory enrollment enforcement, role-based access, role-change session revocation, retrying journal/expense forms and no external asset requests or uncaught JS errors. Security/challenge pages excluded from caches. OPERATOR / UPGRADE COVERAGE Actual archived Phase6 schema migrates to current schema, rerun is idempotent, backup restore drill passes, keygen preserves an existing key with 0600 permissions, CLI recovery rejects missing confirmation and atomically changes password/MFA state/session version with an audit event. Disposable databases only were used. The database dump does not contain the application encryption key; key backup must be handled separately by the operator. No deployment key is in the ZIP. LIMITATIONS No production infrastructure, provider APIs, email delivery, HR/payroll or unimplemented original modules were certified or connected in this batch. See RELEASE-STATUS.txt for what remains and DEPLOYMENT.txt for mandatory gates. Optional CI workflow is supplied as code, not activated or remotely executed. ==== Existing integration/route regression ==== PASS Authenticated property context resolves assigned property PASS Super-admin permission gate succeeds PASS Housekeeper denied CRM permission PASS Forged CSRF token rejected PASS Issued CSRF token accepted PASS Lead created at version zero PASS Lead stage persists PASS Stale lead update rejected PASS Unknown pipeline stage rejected PASS Cross-property lead update denied PASS Invalid calendar date rejected PASS Blank lead title rejected PASS Work assigned to technician PASS Asset retirement blocked while work outstanding PASS Work lifecycle reaches closed PASS Closed work cannot advance PASS Due maintenance produces one work order PASS Repeated due generation creates no duplicate PASS Retired assets cannot reactivate PASS Event quote equals two hours times hall rate PASS Overlapping event rejected PASS Back-to-back hall booking accepted PASS Hall capacity enforced PASS Completed event cannot reopen PASS Event cancellation persists PASS Cross-property hall booking denied PASS Pricing defaults disabled PASS Disabled pricing engine rejects generation PASS Pricing toggle persists PASS Pricing generates two daily recommendations PASS Regeneration uses unique date/type records PASS No duplicate recommendations PASS Approved recommendation stored as date override PASS Reapplying recommendation rejected PASS Base room-type rate unchanged PASS High-occupancy pricing formula verified PASS Low-occupancy pricing formula verified PASS Audit and mutation transaction rolls back together PASS Workflow audit records written PASS Controller renders crm PASS Controller renders maintenance PASS Controller renders events PASS Controller renders pricing PASS Controller renders reports PASS Date-rate API rejects missing inputs PASS Date-rate API returns approved override PASS Route handler exists /login PASS Route handler exists / PASS Route handler exists /dashboard PASS Route handler exists /pms/calendar PASS Route handler exists /pms/calendar-data PASS Route handler exists /pms/available-rooms PASS Route handler exists /pms/reservations PASS Route handler exists /pms/reservations/create PASS Route handler exists /pms/reservations/{id} PASS Route handler exists /pms/rooms PASS Route handler exists /pms/guests PASS Route handler exists /pms/guests/{id} PASS Route handler exists /pms/night-audit PASS Route handler exists /front-office PASS Route handler exists /pos PASS Route handler exists /pos/orders/{id} PASS Route handler exists /pos/kitchen PASS Route handler exists /housekeeping PASS Route handler exists /inventory PASS Route handler exists /inventory/purchase-orders PASS Route handler exists /inventory/purchase-orders/{id} PASS Route handler exists /accounting PASS Route handler exists /crm PASS Route handler exists /maintenance PASS Route handler exists /events PASS Route handler exists /pricing PASS Route handler exists /reports PASS Route handler exists /reports/export PASS Route handler exists /pms/date-rate PASS Route handler exists /login/verify PASS Route handler exists /security PASS Route handler exists /admin/access PASS Route handler exists /login PASS Route handler exists /logout PASS Route handler exists /pms/reservations PASS Route handler exists /pms/reservations/{id}/checkin PASS Route handler exists /pms/reservations/{id}/checkout PASS Route handler exists /pms/reservations/{id}/transition/{action} PASS Route handler exists /pms/reservations/{id}/charges PASS Route handler exists /pms/reservations/{id}/payments PASS Route handler exists /pms/room-types PASS Route handler exists /pms/rooms PASS Route handler exists /pms/rooms/{id}/status PASS Route handler exists /pms/guests PASS Route handler exists /pms/guests/{id} PASS Route handler exists /pms/night-audit/run PASS Route handler exists /front-office/wakeup PASS Route handler exists /front-office/wakeup/{id}/complete PASS Route handler exists /pos/outlets PASS Route handler exists /pos/tables PASS Route handler exists /pos/categories PASS Route handler exists /pos/items PASS Route handler exists /pos/orders PASS Route handler exists /pos/orders/{id}/items PASS Route handler exists /pos/orders/{id}/items/{item_id}/void PASS Route handler exists /pos/orders/{id}/items/{item_id}/split PASS Route handler exists /pos/orders/{id}/send-kitchen PASS Route handler exists /pos/orders/{id}/pay PASS Route handler exists /pos/orders/{id}/room-charge PASS Route handler exists /pos/kitchen/{item_id}/advance PASS Route handler exists /housekeeping/tasks PASS Route handler exists /housekeeping/tasks/{id}/assign PASS Route handler exists /housekeeping/tasks/{id}/advance PASS Route handler exists /housekeeping/laundry PASS Route handler exists /housekeeping/laundry/{id}/move PASS Route handler exists /housekeeping/lost-found PASS Route handler exists /housekeeping/lost-found/{id}/claim PASS Route handler exists /inventory/items PASS Route handler exists /inventory/stores PASS Route handler exists /inventory/adjust PASS Route handler exists /inventory/purchase-orders PASS Route handler exists /inventory/purchase-orders/{id}/approve PASS Route handler exists /inventory/purchase-orders/{id}/receive PASS Route handler exists /accounting/journal PASS Route handler exists /accounting/expenses PASS Route handler exists /ops/{module}/{action} PASS Route handler exists /login/verify PASS Route handler exists /security/{action} PASS Route handler exists /admin/access/{action} TOTAL 125 assertions passed; 79 registered routes. ==== Prior financial/security concurrency suite ==== PASS Property context established PASS Decimal money addition uses exact minor units PASS Fractional line rounds half up PASS Negative monetary input rejected PASS Excess decimal places rejected PASS Cross-property guest rejected by reservation service PASS Room maximum occupancy enforced PASS Concurrent same-room booking: exactly one writer succeeds PASS Concurrent booking creates only one reservation PASS Unassigned reservations cannot oversell room-type inventory PASS Dirty/cleaning room cannot be checked in PASS Unpaid folio cannot check out PASS Folio includes room tax and extra charges exactly once PASS Identical folio payment replay is idempotent PASS Payment key reuse with changed amount rejected PASS Folio overpayment rejected PASS Paid checkout sends room to cleaning PASS Checkout generates one cleaning task PASS Closed folio rejects charges PASS Occupied restaurant table cannot be reopened PASS Split tax totals use exact minor units PASS POS partial-payment replay creates no duplicate PASS Split payment cannot exceed remaining balance PASS Cannot redistribute partially-paid lines PASS Fully-paid split rejects additional payment PASS All splits settled marks order paid PASS Settled order frees table PASS Settled order cannot be voided PASS Concurrent identical payment requests both resolve successfully PASS Concurrent payment replay persists only one receipt PASS Still-open current business date cannot be closed PASS Concurrent night audit closes date once PASS Night audit posts exactly one room charge per business date PASS Night audit does not double count quoted room amount PASS Opening stock adjustment persists PASS Negative inventory stock rejected PASS Cross-property stock adjustment rejected PASS Partial GRN advances PO state PASS Cumulative GRN over-receipt rejected PASS Weighted-average cost includes stock in both stores PASS Receipt leaves other store quantity unchanged PASS Receiving store gets exact received quantity PASS Fully-received PO closes PASS Double-entry journal balances exactly PASS Journal uses property currency, not hardcoded USD PASS Unbalanced journal rejected PASS Expense cannot post to an asset account PASS Rejected expense leaves no partial record PASS Expense journal preserves expense date PASS Database rejects audit update PASS Database rejects audit deletion PASS Login attempt bucket updated 1 PASS Login attempt bucket updated 2 PASS Login attempt bucket updated 3 PASS Login attempt bucket updated 4 PASS Login attempt bucket updated 5 PASS New session does not bypass database login throttle PASS Native DI resolves typed dependencies PASS Named routes encode path parameters PASS Dynamic SQL identifiers validated TOTAL 60 Phase 6 hardening assertions passed. ==== Identity, access and ledger assertions ==== PASS RFC 6238 published vector 59 PASS RFC 6238 published vector 1111111109 PASS RFC 6238 published vector 1111111111 PASS RFC 6238 published vector 1234567890 PASS RFC 6238 published vector 2000000000 PASS RFC 6238 published vector 20000000000 PASS Generated TOTP secret has 160-bit base32 form PASS Current TOTP matches PASS Used counter cannot match again PASS Non-numeric TOTP rejected PASS Out-of-window TOTP rejected PASS TOTP secret round-trips through authenticated encryption PASS Tampered encrypted secret is rejected PASS Enrollment requires the current password PASS Enrollment issues a local authenticator URI PASS Enrollment issues ten unique recovery codes PASS Enrollment stores encrypted secret and removes plaintext slot PASS Enrollment revokes older sessions and upgrades current session PASS Database stores only hashes of recovery codes PASS Password alone grants no authenticated session for enrolled user PASS Fresh TOTP completes password sign-in PASS Verified sign-in TOTP cannot be replayed PASS Repeated password login returns challenge PASS Unused recovery code completes sign-in PASS Recovery code works once only PASS Enrolled settings administrator can enforce property 2FA policy PASS Required policy prevents disabling 2FA PASS Custom role belongs to current property PASS Staff account is assigned selected custom role PASS Permission update revokes sessions of assigned users PASS Stale role version rejected PASS System role templates are immutable through administration PASS Delegated role administrator cannot grant unheld financial permission PASS Non-super administrator cannot assign super-admin PASS Delegated administrator cannot alter super administrator PASS Administrator cannot edit own assigned role PASS Foreign-property role cannot be assigned PASS Administrator cannot disable own account PASS Cross-property user edits rejected PASS Authorized staff deactivation persists PASS Stale user version rejected PASS Property policy sends unenrolled password-valid user to enrollment PASS Recovery regeneration issues ten replacement codes PASS Regeneration invalidates every old recovery code PASS 2FA disable removes encrypted secret and recovery codes PASS Non-required unenrolled account can use password sign-in PASS Manual journal requires a replay key PASS Identical journal replay returns original entry PASS Changed journal payload cannot reuse a key PASS Identical expense replay returns original expense PASS Expense replay creates only one balanced journal PASS Changed expense payload cannot reuse a key PASS Expense request key cannot be reused for a journal PASS Security audit events written without secrets PASS Audit payloads do not contain authenticator secret TOTAL 55 identity/access/ledger assertions passed. ==== New independent-process identity races ==== PASS Concurrent identical TOTP submissions produce one accepted verification PASS TOTP replay counter is advanced exactly once PASS Concurrent identical recovery codes produce one accepted verification PASS Recovery-code used marker persists once PASS Concurrent identical ledger submissions return the same entry PASS Concurrent ledger replay persists a single balanced entry PASS Concurrent role edits reject the stale version PASS Custom-role version advances once TOTAL 8 Phase 7 multi-process assertions passed. ==== Fresh installer/HTTP regression ==== PASS Uninstalled application routes to public installer PASS Installer rejects missing CSRF PASS Installer database configuration succeeds PASS Installer builds all tables and seeds identity catalog PASS Installer creates administrator and completes lock step PASS Installation lock written PASS Installed wizard cannot be reopened PASS Login rejects missing CSRF PASS Login authenticates and dashboard renders PASS Authenticated route /crm renders PASS Authenticated route /maintenance renders PASS Authenticated route /events renders PASS Authenticated route /pricing renders PASS Authenticated route /reports renders PASS Authenticated route /pms/calendar renders PASS Authenticated route /pms/rooms renders PASS Authenticated route /pos renders PASS Authenticated route /accounting renders PASS Nested-page base URL anchored at application root PASS CRM mutation rejects missing CSRF PASS Lead creation escapes stored HTML when rendered PASS AJAX lead movement persists PASS AJAX stale version returns conflict PASS Invalid report date returns 422 PASS Report CSV export responds PASS Unregistered route returns 404 without fatal error PASS Service worker excludes authenticated page persistence PASS CLI migration runner handles fresh install and rerun PASS Phase 4-style database receives Phase 5 additions PASS Non-CRM role denied CRM page PASS Housekeeper denied pricing page TOTAL 31 HTTP/deployment checks passed. ==== Prior browser workflow/mobile/offline checks ==== PASS Desktop sidebar renders PASS Room type setup submits successfully PASS Room setup creates clean room tile PASS Guest registration reaches profile PASS Walk-in guest checked in through real browser form PASS Folio receipt records through idempotent payment form PASS Paid browser checkout succeeds PASS Tape chart JavaScript renders fourteen-day grid PASS Drag-and-drop CRM movement persists after reload PASS Accessible CRM selector persists stage change PASS Browser route renders /maintenance PASS Browser route renders /events PASS Browser route renders /pricing PASS Browser route renders /reports PASS Browser route renders /accounting PASS Browser route renders /housekeeping PASS Browser route renders /pos PASS Mobile sidebar toggle works at 390px PASS Mobile dashboard avoids document-level horizontal overflow PASS Browser cache contains no authenticated HTML pages PASS Offline navigation displays static fallback, not guest data PASS No uncaught browser JavaScript errors PASS No external asset requests needed for application rendering TOTAL 23 browser checks passed. ==== Property isolation/security HTTP checks ==== PASS Foreign guest detail denied PASS Foreign reservation detail denied PASS Foreign POS order detail denied PASS Foreign outlet detail denied PASS Property-isolated collection /pms/guests PASS Property-isolated collection /pms/reservations PASS Property-isolated collection /pms/reservations/create PASS Property-isolated collection /inventory PASS Property-isolated collection /front-office PASS Property-isolated collection /dashboard PASS Cross-property checkout denied before mutation PASS Foreign inventory input cannot cross resource boundary PASS Unsupported JSON mutation fails closed PASS Disabled account loses existing session access PASS 2FA-marked account cannot bypass the second-factor challenge PASS HTTP login throttle survives six fresh cookie jars TOTAL 16 security HTTP checks passed. ==== Identity/RBAC/ledger browser acceptance ==== PASS Browser creates property-owned role with selected permissions PASS Browser creates staff account assigned to custom role PASS Custom-role staff can open granted CRM module PASS Custom-role staff cannot open access administration PASS Role edit revokes the existing staff session PASS Reauthenticated staff loses revoked CRM permission PASS Enrollment renders local QR payload and manual setup key PASS Confirmed enrollment displays ten one-use recovery codes PASS Enabling two-factor revokes previously active browser session PASS Recovery codes disappear after their one-time display PASS Enrolled password sign-in requires a second-factor challenge PASS Incomplete challenge cannot reach dashboard PASS Current authenticator code completes browser sign-in PASS One-use recovery code completes browser sign-in PASS Used recovery code cannot be replayed in the browser PASS Settings administrator enforces property-wide enrollment policy PASS Required policy restricts unenrolled staff to enrollment page PASS Enrolled administrator retains access administration PASS Repeated journal form submission creates one journal entry PASS Repeated expense form submission creates one expense row PASS Authenticator and recovery pages are never cached by service worker PASS No uncaught JavaScript errors in identity workflows PASS QR and identity screens make no external asset requests TOTAL 23 Phase 7 browser checks passed. ==== Archived Phase6 upgrade and backup restore ==== PASS Archived Phase6 database upgrades through Phase7 PASS Actual upgrade migration rerun is idempotent PASS Unknown legacy inventory ownership is preserved for manual reconciliation PASS Preflight blocks unresolved legacy inventory PASS Preflight reports missing DBA audit guards PASS DBA audit-guard installer succeeds and can be rerun PASS CLI backup produces a checksum-verified SQL dump PASS Backup tool refuses to overwrite an existing file PASS Backup restores matching table and fixture record counts into another database PASS Restore includes both append-only audit triggers TOTAL 10 upgrade/backup checks passed. ==== Key provisioning and CLI recovery ==== PASS Key provisioning creates once and never silently rotates an existing key PASS Provisioned application key is owner-readable only PASS Emergency recovery refuses missing confirmation and replacement password PASS Confirmed operator recovery completes through shipped CLI PASS Recovery resets enrollment, revokes sessions and replaces password atomically PASS Emergency recovery records a DBA audit event TOTAL 6 key/recovery operator checks passed.