Skip to content

feat(stack2): add Next.js apps/web application and CI verification job - #5

Closed
cryptorevel wants to merge 36 commits into
clawnify:mainfrom
cryptorevel:feat/stack2-verify-web-ci
Closed

feat(stack2): add Next.js apps/web application and CI verification job#5
cryptorevel wants to merge 36 commits into
clawnify:mainfrom
cryptorevel:feat/stack2-verify-web-ci

Conversation

@cryptorevel

Copy link
Copy Markdown

Summary

  • Adds the complete Stack 2 Next.js/React/Drizzle/PostgreSQL application (apps/web) alongside the existing Stack 1 Preact/Hono/D1 app, kept intact for rollback.
  • Adds a verify-web CI job (Node 24, PostgreSQL 17 service container) that installs, typechecks, lints, migrates, unit-tests, integration-tests, builds, and drift-checks apps/web — closing a gap where CI never touched it before.
  • Pins the Node runtime (engines, .nvmrc) and aligns the existing verify job to the same Node 24 major.
  • Adds docs/DEPLOYMENT-RUNBOOK.md and docs/NEXTJS-MIGRATION-INVENTORY.md as the canonical Stack 2 deployment/readiness record.
  • Includes Stack 1 Phase 19D (retention/referral/loyalty/campaigns) work.

Test plan

  • Stack 1: typecheck, lint, architecture guard, build, full suite (1,497/1,497, one known load-sensitive PDF test confirmed clean in isolation) all pass locally.
  • Stack 2: typecheck, lint, unit (108/108), PostgreSQL integration (188/188), production build all pass locally against a disposable database.
  • verify-web hosted-runner confirmation — this PR is what triggers that for the first time.

This PR does not deploy anything, switch DNS, or remove Stack 1. See docs/DEPLOYMENT-RUNBOOK.md for the full remaining cutover checklist.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RjyF6iZJiqvZtxBWiUcfEC

cryptorevel and others added 30 commits August 14, 2026 16:30
…se 9.4

- establish RBAC, workflow engine, rebate and compliance flows
- add financials, technician mobile, advanced scheduler
- add Lead Management through security sweep
- add Notifications through security/data-integrity sweep
- preserve hardened Google Calendar idempotency/concurrency design
- include migrations 0001-0011 and complete regression suite

Verification:
- 733/733 tests
- TypeScript clean
- ESLint clean
- Vite build clean

Known verification gaps:
- Phase 8.6 browser/UI verification pending
- Phase 9.5 browser/UI verification pending
- add Playwright browser verification infrastructure
- verify Notification UI across admin/dispatcher/technician roles
- verify 1440/1024/768/430/390 viewports
- fix SMS consent-source response regression
- fix technician notification-page 403-on-load
- fix mobile input sizing and pagination accessibility
- preserve notification/security/calendar regression guarantees

Verification:
- 733/733 full tests
- 218/218 notification regression
- 42/42 Google Calendar regression
- Playwright real-browser verification passed
- TypeScript clean
- ESLint clean
- Vite build clean

Phase 9 status:
- COMPLETE
- VERIFIED IN REAL BROWSER

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- add DST-aware Google Calendar timezone regression coverage
- verify summer/winter wall-clock preservation
- cover midnight/late-night date boundaries
- document America/Vancouver as the required local business timezone
- preserve deterministic event IDs and Calendar concurrency protections

Verification:
- 742/742 full tests
- 51/51 Google Calendar tests
- TypeScript clean
- ESLint clean
- Vite build clean
- real Google Calendar verified at 11:00-12:30 for JOB-34

Known remaining risk:
- fresh D1 environments still default _meta.timezone to UTC until configured

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- add BUSINESS_TIMEZONE to versioned Global Settings
- seed America/Vancouver for fresh environments
- add shared server-side business timezone resolver
- route Google Calendar and notification reminders through one source
- add admin timezone selector and confirmation flow
- preserve wall-clock scheduling and DST-safe IANA timezone semantics
- eliminate manual fresh-environment _meta timezone correction

Verification:
- 770/770 full tests
- 55/55 Google Calendar regression
- 221/221 Notification regression
- Playwright browser verification passed
- fresh-database timezone test passed
- TypeScript clean
- ESLint clean
- Vite build clean
- real Google Calendar verified

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- fix Lead form mobile CSS specificity
- ensure 16px controls at 430px/390px
- prevent iOS auto-zoom regression
- preserve verified Phase 8 Lead browser flows

Verification:
- 770/770 tests
- Playwright Lead mobile verification passed
- TypeScript clean
- ESLint clean
- Vite build clean

Phase 8:
- COMPLETE
- VERIFIED IN REAL BROWSER

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- add durable job geocoding coordinates and status
- add provider-agnostic geocoding and routing interfaces
- add mock/no-op providers without external network dependencies
- invalidate stale coordinates when a job address changes
- cover geocoding, migration, and cross-domain behavior with tests

Phase 10.0 IMPLEMENTED / VERIFIED.
No real map provider, map UI, technician tracking, or route optimization.
- add server-side Google Maps Platform geocoding adapter
- add provider selection and secret-backed configuration
- add admin/dispatcher geocode endpoint with strict RBAC
- preserve idempotency and address-change invalidation semantics
- normalize provider errors and bound external requests
- verify cost/privacy/cross-domain isolation with tests
- verify one controlled real Google geocode flow

Phase 10.1 IMPLEMENTED / VERIFIED.
No map UI, routes, technician tracking, or route optimization.
- integrate a read-only Google Map into the existing Scheduler
- render stored geocoded Job coordinates without geocoding on map load
- synchronize markers with Scheduler/list selection and active filters/range
- handle non-geocoded Jobs safely
- preserve Admin/Dispatcher RBAC and Technician isolation
- keep server geocoding credentials separate from browser Maps configuration
- preserve Calendar, Notification, scheduling, and timezone behavior
- verify responsive/accessibility behavior in real Chromium

Phase 10.2 IMPLEMENTED / VERIFIED.
Full suite 856/856 PASS before commit.
Real browser verification Part A 20/20 and Part B 22/22.
No route optimization, technician tracking, drag scheduling, push, or deploy.
- add a technician-only daily Route view to the existing Technician Mobile experience
- scope route data server-side to the authenticated technician's own assigned jobs
- derive numbered stop sequence from scheduled_time without persisted route ordering
- render stored geocoded coordinates while keeping non-geocoded stops visible
- synchronize route stop selection with Google Map markers
- reuse existing Phase 6 external navigation and Phase 9.1 On-The-Way flows
- use the existing global BUSINESS_TIMEZONE for route-day semantics
- preserve browser/server Google Maps credential separation and zero geocoding-on-render behavior
- preserve Dispatcher Map, Calendar, Notification, Scheduler, Leads, and Global Settings behavior

Phase 10.3 IMPLEMENTED / VERIFIED.
Full suite 876/876 PASS before commit.
Real browser verification: Technician 33/33 across 1440/1024/768/430/390.
No live tracking, route optimization, travel-time integration, push, or deploy.
Add server-side Google Routes integration for read-only travel-time
information across scheduled technician stops.

- preserve scheduled_time-derived visit order
- use DRIVE with waypoint optimization disabled
- keep Routes credentials server-side
- expose explicit/button-gated route calculation only
- preserve technician ownership and dispatcher/admin RBAC
- handle unmapped stops without fabricated route continuity
- add normalized distance/duration/geometry support
- keep Calendar, Notifications, Scheduler mutations, Leads, ETA,
  traffic, optimization, and live tracking out of scope
- prevent automated tests from making real billed Routes calls when
  local credentials are configured

Real Google Routes verification:
09:00→11:00 = 12,460 m / 1,275 s
11:00→13:00 = 23,743 m / 2,540 s
scheduled order preserved, DRIVE confirmed, optimization disabled,
no browser-side Routes calls or key leakage.

Verification:
923/923 tests passing
TypeScript clean
ESLint clean
Build clean

Responsive 1024/768/430/390 verification remains a disclosed tooling
gap; 1440 verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- document Geocoding API restriction for the server geocoding key
- document Routes API restriction for the server routing key
- preserve browser Maps referrer/API restriction guidance
- record Phase 10.5 security and cost sweep as complete
- no runtime behavior, database, dependency, or API changes

Verification:
923/923 tests passing
TypeScript clean
ESLint clean
Build clean

Phase 10.5 SECURITY / COST SWEEP COMPLETE.
Phase 10.6 NOT STARTED.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Audit-only phase: classify Core Platform vs HVAC/BC/tenant/infrastructure
concerns across the entire schema, API surface, workflow engine, and UI.
No application behavior, schema, or dependency changes.

Key findings:
- Core domain (auth, customers, jobs, scheduling, workflow mechanism,
  leads, compliance, financial, notifications, Calendar, Maps/Geocoding/
  Routing) is already substantially industry-neutral.
- HVAC/BC coupling is narrow and concentrated: a hardcoded 3-value
  JobType union (duplicated in 3 files), 5 rebate-profile columns on the
  generic customers table, and one hardcoded program-dispatch branch in
  the otherwise-generic rebate eligibility engine.
- Only 6 of ~75 API endpoints are HVAC/BC-specific, all cleanly isolated
  as dedicated routes.
- No tenant/organization concept exists anywhere in the schema, auth, or
  settings layer — classified NOT_READY for multi-tenant SaaS, with a
  concrete, evidence-based invasiveness estimate for adding it later.
- Zero P0 findings. Proposes 6 small, additive, independently-shippable
  sub-phases (11.1-11.6), none a rewrite, none changing current HVAC/
  CleanBC/BC Hydro behavior.

Verification:
923/923 tests passing
TypeScript clean
ESLint clean
Build clean

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…udit

Re-verified §14's provider integration classifications against direct
interface inspection:

- Google Calendar: corrected from PROVIDER_ADAPTER to DOMAIN_COUPLED —
  no generic CalendarProvider interface exists; Google-specific types
  flow directly into calendar-sync.ts's business logic.
- Cloudflare R2 Storage: added to the integration table (omitted from
  the original pass despite being in this audit's named scope) —
  DOMAIN_COUPLED, storage.ts types directly against R2Bucket/R2ObjectBody
  with no generic ObjectStorage interface.
- Resend/Twilio: confirmed and cited more precisely as
  GENERIC_CAPABILITY + PROVIDER_ADAPTER (clean interface + isolated
  adapter, matching the Geocoding/Routing template).

Added risk-register entries G13/G14 (both P2) for the two corrected
findings. No other section changed. No application, schema, or
dependency changes.

Verification:
923/923 tests passing
TypeScript clean
ESLint clean
Build clean

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduce the first structural boundary for Platform Generalization
without changing runtime behavior.

- organize industry/regional code under per-bundle modules/
- move BC rebate server logic and eligibility tracker UI into
  modules/programs/bc
- preserve the existing server/client security boundary
- add architecture-boundary guard tooling
- document Core/HVAC/BC module dependency direction
- leave JobType, workflows, rebates, tenancy, assets, APIs, and DB
  behavior unchanged

Verification:
923/923 tests passing
architecture guard passing
TypeScript clean
ESLint clean
Build clean
browser smoke previously verified with zero paid-provider calls

Phase 11.1 IMPLEMENTED / VERIFIED.
Phase 11.2 NOT STARTED.
Replace duplicated server-side JobType/WORKFLOWS declarations with a
single typed registry while preserving all persisted IDs and runtime
behavior.

- derive JobType from JOB_TYPE_REGISTRY
- keep STANDARD, CLEANBC, and BC_HYDRO IDs unchanged
- compose BC workflow definitions from the regional program module
- preserve existing workflow sequences and transitions byte-for-byte
- retain strict runtime validation and explicit unknown-type rejection
- keep rebate business logic unchanged for Phase 11.3
- preserve server/client bundle boundaries with consistency coverage
- keep database, API contracts, RBAC, dependencies, and UI behavior unchanged

Verification:
936/936 full suite passing
623/623 targeted domain regression passing
workflow 22/22
rebate 16/16
architecture guard passing
TypeScript clean
ESLint clean
Build clean
real Playwright workflow/job-type smoke verified

Phase 11.2 IMPLEMENTED / VERIFIED.
Phase 11.3 NOT STARTED.
Extract BC rebate/program ownership from generic Core while preserving
all CleanBC and BC Hydro behavior.

- move rebate dispatch behind the BC program registry/capability
- introduce a BC-module-owned customer rebate profile table
- backfill existing rebate-profile values with an additive migration
- preserve existing customer/API compatibility
- retain legacy customer columns during the transition
- keep eligibility thresholds, workflow semantics, audit snapshots,
  RBAC, and public program IDs unchanged
- preserve Phase 11.2 job-type/workflow registry behavior
- keep tenancy, assets/equipment, and future business modules out of scope

Verification:
950/950 tests passing
architecture guard passing
TypeScript clean
ESLint clean
Build clean
independent code review: no blocking findings
security review: no blocking findings

Phase 11.3 IMPLEMENTED / VERIFIED.
Phase 11.4 remains deferred.
Phase 11.5 NOT STARTED.
Introduces the minimum durable tenant/organization boundary across the
data model and application architecture without changing current
single-company behavior (Phase 11.5).

- New `organizations` table (migration 0015); default organization
  backfills every pre-existing row via `NOT NULL DEFAULT 1`, safe for
  fresh DBs, upgraded DBs, and the test harness's reseed replay alike.
  No inline REFERENCES (D1 rejects ALTER TABLE ADD COLUMN combining
  REFERENCES with a non-NULL DEFAULT) — application-enforced only,
  disclosed trade-off.
- `organization_id` added to the 9 direct tenant-owned root tables
  (users, customers, technicians, service_types, materials, jobs,
  invoices, leads, global_settings); everything else stays tenant-safe
  via parent-JOIN at the route layer, no duplicated column. 10 new
  composite indexes.
- Server-derived tenant context only: getSessionUser() ->
  actorOrganizationId(c) -> scoped query. No route ever trusts a
  client-supplied organization/tenant id.
- ~80 route handlers scoped across Users/Customers/Leads/Jobs/
  Technicians/Scheduler/Maps-Routing/Google Calendar/Notifications/
  Financial/BC rebate/Global Settings, composed with existing RBAC
  (role AND organization ownership, not either alone). Two pre-existing
  ownership gaps unrelated to tenancy (addJobMaterial/addJobNote/
  addChecklistItem, invoiceFromJob/updateInvoice) fixed as part of the
  same pass.
- P0 fix: syncAllJobsForUser() ("Sync Now") previously synced every
  organization's jobs into the calling user's personal Google Calendar
  — found independently by two adversarial reviews, now scoped to the
  caller's own organization, covered by a new regression test. Two
  lower-severity findings (403-vs-404 on GET /api/invoices/{id}/payments;
  updateCustomer's no-op-on-unknown-id convention) also fixed.
- New test/tenant-isolation.test.ts (22 tests): full Org A/Org B
  isolation matrix including mass-assignment resistance and proof Lead
  conversion never merges across organizations.
- users.email and the 3 identifier-column unique constraints, plus
  _meta's global counters, deliberately KEPT GLOBAL this phase —
  disclosed, deferred to Phase 11.6+ (tracked as new risk G15).
- Zero API contract or client UX changes; no new dependencies; no
  tenant switcher/self-signup/billing/white-labeling built.

Verified: full suite 973/973, tsc/eslint/check:architecture/vite build
clean, real wrangler dev + real local D1 browser verification (fixtures
cleaned, DB confirmed back to baseline). Independent Architecture, Code
Review, and Security review completed; all findings fixed and
re-verified. SaaS readiness reassessed NOT_READY -> PARTIALLY_READY.
Phase 11.6 not started.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Close the final Phase 11 tenant-isolation blocker discovered during the
production-readiness security review.

- scope automatic Calendar fan-out sync/delete to the job organization
- preserve the existing tenant-safe manual Sync Now path
- add regression coverage for automatic Calendar fan-out isolation
- add final tenant-isolation coverage for user account-management routes
- cover Notifications, BUSINESS_TIMEZONE, Calendar retry sync, and paginated totals
- preserve existing APIs, single-company UX, migrations, and dependencies
- keep Phase 11.4 Assets/Equipment deferred

Verification:
979/979 tests passing
architecture guard passing
TypeScript clean
ESLint clean
production build clean
independent architecture/security/testing reviews completed
real two-organization browser verification completed

Phase 11.6 COMPLETE / VERIFIED.
Phase 11 FUNCTIONALLY COMPLETE / VERIFIED.
SaaS readiness remains PARTIALLY_READY.
Phase 11.4 — generic, tenant-safe Asset/Equipment foundation. Closes the
platform-generalization audit's §12 finding (no Property/Location/Asset
entity anywhere). Core term "Asset"; UI labels it "Equipment" for HVAC
users.

- Organization -> Customer -> Asset ownership (DIRECT_TENANT_COLUMN,
  matching jobs/customers/leads); Job <-> Asset many-to-many via
  job_assets, since real HVAC visits commonly service multiple units.
- HVAC asset-type registry (src/server/modules/hvac/asset-types.ts, 7
  types) composed in index.ts, mirroring Phase 11.2's JOB_TYPE_REGISTRY
  pattern for avoiding an HVAC-only Core union -- needs no
  architecture-guard exception since asset types carry no per-type
  engine behavior.
- Full RBAC: admin/dispatcher manage Assets and job links; technician
  view-only for linked equipment on their own assigned job (reuses the
  existing canActorAccessJobCompliance ownership predicate).
- Cross-customer and cross-org linking are default-DENY; retire-via-
  status is the soft lifecycle, hard delete blocked while referenced by
  a job.
- Additive migration 0016 (assets, job_assets) -- no existing table
  touched, fresh-DB and upgrade safe.

Four independent reviews (architecture, code, security, testing) found
and fixed two real issues before commit: an asset could be silently
reassigned to a different customer while still linked to a job's history
(now blocked with 409), and a wildcard-dense manufacturer/search value
could crash listAssets with an uncaught D1 "LIKE pattern too complex"
500 (fixed with LIKE-escaping plus a wildcard-count cap, since escaping
alone doesn't prevent D1's complexity limit). Two low-severity findings
are disclosed and deliberately deferred, matching existing app-wide
precedent: Asset API responses include organization_id in the raw row
(same as GET /api/customers/{id} already does), and Customer deletion
cascades through Assets bypassing the delete guard (same as
jobs/invoices already cascade).

979 baseline -> 1009/1009 tests (30 new in test/assets.test.ts).
TypeScript, ESLint, architecture guard, and production build all clean.
Real-browser-verified (Chromium): admin Customer -> Equipment -> Add/Edit
Asset -> Job -> Link Equipment -> Scheduler -> Job Detail; technician
read-only linked-equipment view with no edit/link/unlink controls; Org B
tenant isolation confirmed with zero cross-org data visible.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Introduces a durable, tenant-safe Quote/Estimate domain: Quote (identity)
-> immutable, versioned QuoteVersion commercial snapshots -> QuoteLineItem,
with a full lifecycle FSM (draft/sent/accepted/rejected/expired/cancelled).
All money is integer cents, server-computed and stored at write time only
- totals are never client-supplied and never recomputed from later mutable
state. Tenant isolation and RBAC (admin/dispatcher manage, technician
blanket-blocked) are enforced identically to the existing Lead/Asset/
Invoice domains, verified with two-org adversarial tests.

Hardening applied before this checkpoint: quotes.accepted_version_id gives
Phase 13 (Contracts/E-Sign) an exact, permanent snapshot of which version
was accepted, independent of current_version_id. quote_versions.row_version
adds optimistic-concurrency (compare-and-swap + bounded retry) around
totals recomputation, closing a disclosed lost-update race on concurrent
line-item mutations; retry exhaustion maps to 409, and row_version itself
is never exposed through any API response.

Reviewed against several future ServiceTitan-residential-style
capabilities without building them: Good/Better/Best (Quote Options) is
classified additive-with-small-migration and deliberately deferred (no
quote_options table); Pricebook integration is classified
snapshot-ready-with-small-extension; Technician Present Mode is confirmed
architecturally supported via the existing response-shaping layer. None
of these are implemented by this commit.

Verification: 1048/1048 tests (base Quote suite plus concurrency, tenant
isolation, RBAC, and mass-assignment coverage), tsc/eslint/architecture
guard/production build all clean, real-browser admin/dispatcher/technician
and cross-org verification performed with all synthetic fixtures cleaned
up afterward. Four independent reviews (architecture, code, security,
testing) across the implementation and hardening passes, all findings
fixed and re-verified.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Implements the first complete Contracts/E-Sign module on top of Phase 12's
Quotes foundation: Accepted Quote -> Contract -> immutable Contract Version
-> Signature Request (per signer, token-gated) -> public signing ceremony
-> append-only Signature Event ledger -> hashed Signed Document -> Evidence
Package.

- Exact Quote binding: contracts.accepted_quote_version_id is captured once
  at creation from quotes.accepted_version_id and never re-read; Quotes'
  own FSM makes "accepted" fully terminal, so the source cannot drift.
- Contract Versions are immutable once sent; draft edits are row_version
  CAS-guarded. Two-layer status: a bare user-triggered FSM plus a derived
  layer (mirrors financial.ts's recalculateStatus) for signed/declined/
  partially_signed/expired.
- ESignProvider abstraction (mirrors notification-providers.ts) with
  LocalEsignProvider as the only implementation this phase - no paid
  vendor added.
- Public signing tokens: crypto-random, SHA-256 hashed at rest, 14-day
  lazy expiry, replay-safe, idempotent submission, generic error responses
  (no enumeration). A narrow /api/public/* prefix auth exemption is the
  sole authority boundary - the token, never a client-supplied org id.
- Signed documents are hashed (SHA-256) and stored server-side in R2 under
  a tenant-scoped, never-overwritten key; evidence package exposes the
  full signer/consent/event/hash record.
- RBAC (admin/dispatcher only, technician blocked server-side) and tenant
  isolation verified across every route, including the public token flow.

Independent code, security, and testing reviews (via Agent tool) found and
closed three real defects before commit: a cross-tenant IDOR in
deleteContractSigner (signer-activity check wasn't contract-scoped), a
missing CAS guard in resendSignatureRequest, and a dead/unreachable
contract-level "expired" status derivation. Also closed: addContractSigner
lacked a server-side draft-only guard, and no guard existed against a
second live Contract per accepted Quote. 6 regression tests added directly
from these findings.

Tests: 1089/1089 (1048 baseline + 41 in test/contracts.test.ts).
tsc/eslint/architecture-guard/build all clean. Verified via a real local D1
migration upgrade and a full Chromium browser pass (admin sign lifecycle,
technician blocked client+server side, signed R2 artifact independently
fetched and verified).

Not committed: real paid E-sign provider, webhooks, signed-document
download route, multi-signer signing-order UI, Customer Portal, deposit/
payment linkage. This module is a technical record of electronic-signature
evidence, not a substitute for legal advice on enforceability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Company Profile with tenant logo, a professional signed Contract PDF
(Type + Draw e-sign, Certificate of Completion with actual IP/User-Agent
evidence, immutable R2-stored artifact with SHA-256 verification on
read), automatic customer signed-copy email with delivery status/retry/
idempotency, and a professional Invoice PDF (View/Download/Print,
intentionally live-rendered to reflect the mutable payment/rebate
state, unlike the immutable signed Contract).

1173/1173 tests, tsc/eslint/architecture-guard/build all clean. Real-
browser accepted (Chromium) for every primary flow, including a real
mouse-drawn signature, artifact hash equality, email failure/retry,
Invoice mutable-state behavior, and the branding old/new snapshot test.
Tenant/RBAC re-confirmed with a live Technician session (403 at both
UI and API layers).

Known limitations, disclosed rather than silently skipped: true
responsive-breakpoint (1440/430/390) verification was blocked by a
browser-tooling limitation in this environment, not an identified app
defect. Keyboard accessibility was spot-checked, not exhaustively
swept. One pre-existing, out-of-scope issue was found and left open:
unauthorized direct navigation to /invoices/:id (and likely /jobs/:id,
/customers/:id) logs an uncaught console exception before the
already-correct 403/redirect takes effect — no security or functional
impact, predates this phase, tracked as a backlog item.

Does not include online/manual payments, Phone Operations, Reporting,
Accounting expansion, a Customer Portal, or Pricebook/Good-Better-Best.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Invoice delivery is now explicit and independent of payment recording:
issuing/paying an invoice never auto-sends email, and sending an invoice
never records a payment. New POST /api/invoices/{id}/send with delivery
status/retry/resend; the prior automatic issue/payment emails (a Phase
5-era violation of this rule) were removed from issueInvoiceRoute and
recordPaymentRoute, with the old functions/templates left intact for
historical-data compatibility.

Manual/on-site payments (Cash, Check, Credit/Debit Card, E-Transfer, Bank
Transfer, Financing, Other) work without any prior invoice email, capture
amount/method/date/reference/notes/received-by/source, and never require
delivery. Partial, multiple, and full payments accumulate correctly with
server-authoritative balance math; overpayment is rejected via a single
atomic guarded INSERT (no read-then-write race); payments are void-only
(soft-void, excluded from balance, never hard-deleted, retained in
history with the voided state visible).

New PaymentProvider abstraction (src/server/payment-provider.ts) backed
only by a MockPaymentProvider this phase - no real provider credentials
or SDK. Online payment flow: opaque public payment-session token
(256-bit, only its SHA-256 hash persisted, shown once), 30-minute expiry,
server-authoritative amount, HMAC-verified webhook with atomic
claim-based replay protection, graceful "Pay Online unavailable" when no
provider secret is configured (every other financial workflow keeps
working). No `receipts` table - a Receipt is a live-rendered view of one
immutable payment row, reusing the existing PDF kernel and Company
Profile branding; View/Download/Print share one server route, and
receipt email is optional for manual payments (auto-sent only for
online-provider payments) and never invalidates the underlying payment
on failure.

Migration 0021 is additive only: new payment_sessions table plus three
nullable/defaulted columns on payments (source, received_by,
payment_session_id). Three independent reviews (security, architecture,
testing) found and fixed two real races (an overpayment race and a
webhook-vs-manual-payment race) and one inaccurate migration comment.

Verified: 53/53 test files, 1217/1217 tests, TypeScript/ESLint/
architecture-guard/build all clean. Real-browser acceptance pass
confirmed the critical never-sent-invoice -> on-site cash payment -> zero
customer email scenario, partial/multiple/full payment accumulation
across three methods, Receipt view/email, the mock online-payment flow
end-to-end through the public /pay/:token page, void/reversal, and
payment-link anti-enumeration behavior.

Known limitations, not fixed here: real mobile/tablet breakpoint
verification is blocked by a browser-automation tooling limitation
(resize_window does not change the actual viewport in this environment);
accessibility was smoke-tested, not exhaustively swept (no WCAG
compliance claim); no production payment provider is configured or
recommended. Two pre-existing, out-of-scope findings were surfaced and
documented but not fixed: Job Compliance's Submit Report dialog doesn't
save its draft first and gives no visible error on the resulting 400,
and the Record Payment modal's form labels lack programmatic
association.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Restrict Global Settings (GET /api/settings) and Company Profile
management to admin-only, closing a pre-existing gap where any
authenticated role could read the full settings list. Enforcement is
server-side (RBAC checks in the route handlers, 403 for authenticated
non-admins, 401 reserved for unauthenticated) as well as UI (sidebar/
router already fail closed for non-admins, confirmed unchanged and
correct).

A narrow `reference_data` category carve-out keeps dropdown option
lists (referral source, heating source, lead-lost reason) readable by
all roles, since dispatcher/technician-facing forms depend on them and
they carry no financial or threshold content. A new, equally narrow
GET /api/config/business-timezone route replaces Technician Route
View's prior dependency on the full (now locked-down) settings list.

Company Profile management routes were already admin-only; internal
PDF rendering (Contract/Invoice/Receipt) calls getCompanyProfile()
directly server-side, never through the gated HTTP route, so this
change does not affect PDF generation.

Google Calendar integration is intentionally left unrestricted to all
roles: it is a per-user personal OAuth connection scoped by user_id,
not an organization-level integrations surface, and there is no
org-level Integrations Management console to hardenit does not exist
in this codebase and was not invented here.

Tenant isolation is unchanged (organization always session-derived,
never client-supplied). Added RBAC regression coverage in
test/settings.test.ts and test/business-timezone.test.ts, including a
fix for a stale pre-existing test that assumed the old open-read
behavior.

Verification: tsc, eslint, and check:architecture clean; production
build clean; focused Phase 13C tests 81/81 pass. Full suite showed
transient timeout failures across several unrelated, untouched test
files on repeated runs (a pre-existing, load-related flakiness
pattern, not tied to this change) — every affected file passed 100%
when re-run in isolation except test/contracts.test.ts, which timed
out on a different unrelated test each run despite never exercising
any route this diff touches; treated as pre-existing infrastructure
flakiness, not a regression.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- add versioned effective-dated organization tax profiles
- support component-based taxes and editable jurisdiction presets
- support tax enabled/disabled and inclusive/exclusive pricing
- support default and line-item taxability
- persist tax snapshots for financial documents
- preserve Quote/Contract/Invoice historical tax integrity
- preserve Phase 13B payment/receipt behavior
- enforce Admin-only Tax Settings with tenant isolation
- add regression coverage and documentation
- note responsive verification limitation and legacy fallback

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- document existing Answermachine voice runtime and provider architecture
- define OFS as the single user-facing control plane
- recommend hybrid Voice Engine integration boundary
- map Twilio/OpenAI Realtime/Media Streams/STT/TTS/transfer capabilities
- define per-organization OFS service credential and server-resolved tenant scope
- identify CRM/scheduler integration boundaries
- document privacy/security/failure-mode requirements
- classify migration actions and file-level map
- define Phase 15 and Phase 16 scopes
- preserve Voice Copilot as a separate later track

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds the OFS-side data model, API, RBAC, and admin/dispatcher UI
foundation for a future integration with a separate Voice Engine
runtime (an AI phone receptionist bridging Twilio to the OpenAI
Realtime API, evolved from the Answermachine reference project per
docs/PHASE14-ANSWERMACHINE-INTEGRATION-AUDIT.md's approved hybrid
architecture). The Voice Engine runtime itself is not in this repo
and was not built this phase.

- Tenant-scoped, purely additive schema (migrations/0023, 12 tables):
  versioned Phone Operations settings/modes, versioned voice agents,
  phone numbers with per-number agent binding, a call FSM with
  idempotent event/transcript recording, snapshot-once agent config
  per call, outcomes/transfers, and an audit trail.
- Per-organization Voice Engine credentials: Twilio account token
  encrypted at rest (reuses the existing crypto.ts mechanism), and a
  hashed bearer service credential the separate runtime presents to
  OFS's new runtime API. organization_id is always resolved
  server-side (session, dialed number, or the credential itself),
  never trusted from a request body.
- Twilio adapter (hand-rolled signature verification + REST calls,
  no vendor SDK) with webhook signature validation and idempotent
  replay handling for both inbound calls and status callbacks.
- Admin/Dispatcher/Technician RBAC, verified server-side and in a
  real browser for all three roles.
- New OFS-native Phone Operations UI (Calls/Settings/Voice
  Agents/Phone Numbers/Credentials).
- Phase 16 (Customer/Lead/Job writes from call data) is explicitly
  out of scope and not implemented.

Independent Security, Architecture, and Testing reviews were run and
every finding fixed: a confirmed P0 (a replayed Twilio webhook with
the same CallSid crashed with an unhandled 500 - now idempotent), two
P1s (a phone number's bound voice agent was never actually used at
call time; a failed outbound Twilio placement left a call stuck
queued forever, permanently consuming a concurrency-cap slot - both
fixed), plus several P2/P3 hardening fixes (defense-in-depth tenant
checks on transcript/outcome/transfer routes, closed a Twilio-webhook
number-enumeration oracle, locked the agent-rename UI trap, sanitized
provider error text, completed TwiML XML escaping).

Two items are deliberately documented, not fixed, for this foundation
phase: a narrow check-then-insert race on the concurrency/daily-cap
counts, and global (not per-organization) phone-number uniqueness
with no real Twilio-ownership verification (see the in-code
`ponytail:` comments and docs/PHASE15-PHONE-OPERATIONS-FOUNDATION.md
for the full reasoning and upgrade path).

Verified: typecheck/lint/architecture guard clean, production build
clean, full suite 55/55 files and 1307/1307 tests passing (sequential
run), real-browser acceptance for Admin/Dispatcher/Technician.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- integrate Phone Operations with Customers, Leads, Scheduler, Jobs, and follow-ups
- add safe caller matching and manual correction workflows
- add server-authorized tool registry and tenant-safe runtime actions
- preserve canonical OFS domain/state-machine rules
- add call-to-business linkage and auditability
- harden Phone Operations CRM-context UI
- complete Technician mobile/tablet hardening
- add mobile drawer and accessibility fixes
- verify Admin, Dispatcher, and Technician browser flows
- verify 1440/1024/768/430/390 breakpoints with no whole-page overflow
- preserve Phase 15 Phone Operations security and RBAC
- document deferred systemic accessibility findings and Phase 15 known risks
- verify 1328/1328 tests passing

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- add organization-scoped Pricebook for equipment, parts, materials, services, labor, and other items
- add SKU, manufacturer/model, categories, cost, sell price, taxability, and active state
- add equipment and warranty metadata
- add Pricebook search and pagination
- integrate Pricebook selection with Quote line snapshots and historical price preservation
- add optional Asset provenance linkage with tenant validation
- enforce Admin management, Dispatcher read-only access, Technician restrictions, and cost secrecy
- preserve manual Quote lines and existing Contract/Invoice snapshot semantics
- fix browser-discovered money-input and metadata-key corruption bugs
- preserve documented last-write-wins concurrency limitation
- verify the full automated quality gate

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cryptorevel and others added 6 commits August 27, 2026 13:48
- add Quote-version estimate options with GOOD/BETTER/BEST/CUSTOM tiers
- add recommended-option support and independent option totals
- add Pricebook/manual option snapshots with historical price integrity
- add secure public Estimate sharing and atomic selection
- record acceptance evidence and share audit events
- convert only selected option into Contract
- preserve tax snapshots, Quote versioning, and existing Contract/Invoice behavior
- add Estimate PDF and public comparison UI
- enforce RBAC, cost secrecy, tenant isolation, and IDOR protection
- preserve Phase 17 CAS limitation and documented Phase 18 deferred items
- verify full automated/browser gates

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- verify all current OFS routes across desktop, tablet, and mobile breakpoints
- close deferred responsive gaps for Pricebook and Good/Better/Best Estimates
- re-verify Phone Operations and Technician mobile behavior
- eliminate whole-page horizontal overflow across the acceptance matrix
- harden shared ConfirmDialog focus, Escape, and dialog semantics
- fix in-scope form-label accessibility gaps
- add real-browser ConfirmDialog regression coverage
- preserve Admin/Dispatcher/Technician/Public RBAC
- document remaining legacy label and bespoke-modal accessibility debt
- verify 1414/1414 tests passing

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- add organization-scoped maintenance plans and membership entitlements
- add versioned maintenance agreements with covered-equipment snapshots
- add immutable versioned Legal Terms and exact signed Terms binding
- add maintenance e-sign flow and signed agreement document generation
- capture explicit auto-renew consent without enabling automatic renewal
- add versioned maintenance checklist templates and digital service reports
- track maintenance visit entitlements with idempotent consumption
- reuse existing tax, asset, PDF, R2, company-profile, and job-compliance infrastructure
- enforce tenant isolation, RBAC, public-token security, and cross-org IDOR protection
- expose production Admin, Dispatcher, Technician, and public signing workflows
- document historical-integrity guarantees and Phase 19C/19D boundaries
- verify 1454/1454 tests passing

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xWcVcWiXJTPQwvbQeKWy4
- add recurring maintenance schedules and durable occurrence tracking
- generate maintenance Jobs idempotently through existing OFS Job workflows
- add safe renewal automation using Phase 19B Agreement and Membership state
- require fresh customer acceptance for material renewal changes
- carry renewal term expiry forward for subsequent renewal cycles
- add 60/30/14-day renewal reminders through the existing notification pipeline
- add automation-run history with strict tenant isolation
- add pause/resume/manual-run operational controls
- prevent duplicate Jobs, reminders, and concurrent renewal claims
- preserve existing tax, e-sign, asset, RBAC, and notification infrastructure
- document known Agreement expires_at UI and narrow concurrency limitations
- verify 1472/1472 tests passing

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015xWcVcWiXJTPQwvbQeKWy4
Adds the complete Stack 2 Next.js/React/Drizzle/PostgreSQL application
(apps/web) alongside the existing Stack 1 Preact/Hono/D1 app, plus a
verify-web CI job, Node 24 runtime pin, deployment runbook and migration
inventory docs, and Phase 19D retention/referral/campaigns work on Stack 1.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjyF6iZJiqvZtxBWiUcfEC
Resolves conflicts in src/server/index.ts and src/server/schema.sql:
kept HEAD's auth middleware/actorOrganizationId and origin's
app-side seed-data plumbing (ensureSeeded), moved the timezone
meta row into META_DEFAULTS alongside it, and dropped origin's
now-superseded nextInvoiceIdentifier duplicate (already lives in
financial.ts on this branch).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjyF6iZJiqvZtxBWiUcfEC
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant