Holding Pool: No-Show Chips + hold_since Backfill (2026-07-31)

Context: Dustin asked where hold tracking lives (answer: Holding Pool, sidebar Monitoring, entirely.it.com/holding-pool, up_owner/up_admin), then asked for two things: backfill the 12 legacy holds missing hold_since, and surface missed-appointment association per hold — a no-show hold aged past 30 days is capturable revenue (trip-charge it, close it out).

Backfill (data, via supabase_write_service.update_project in-container): 12 on-hold rows had NULL hold_since. Stamped from last real activity: 8 from last site visit (2025-01-31 … 2026-07-24), 2 from date-invoice-sent (blkhse.44725→2024-07-10, blkhse.45193→2024-12-27), 3 from the PMS migration floor 2026-03-15 (blkhse.45083/45031/45087 — no real activity date exists anywhere; “held since at least when it entered Mostly”). MAN-9132 was NOT legacy — a current project held without a stamp. Verified: 0 NULLs remain among 24 on-hold rows.

No-show feature (entirely-api 9c77c6f, portal 4347716):

  • GET /portal/holding-pool adds per-project no_show_count + last_no_show (batch site_visits visit_status=‘No-Show’ lookup) and trip_charge_rate (ADM-MISS unit_price, $400 — never hardcoded in UI).
  • HoldingPool page: gold outline “No-Show” chip under 30 days on hold; solid red “Trip-Charge Ready” at 30+ (BILLABLE_HOLD_DAYS=30). Summary bar adds “No-Show Holds” and “Billable Now” (count + ~$ estimate). Toolbar gains “No-Shows Only” filter. Old generic 30-day “Invoice” chip remains for holds without a no-show.
  • Live at ship: MAN-9007 (125d) + MAN-9030 (113d) trip-charge ready (~$800); MAN-9147/9153 fresh no-show holds that ripen at 30 days.

One-click closeout (entirely-api 0f30a8c, portal 4e775d3): POST /portal/holding-pool/{id}/trip-charge-closeout (owner only) — guards (on hold + No-Show visit + not already invoiced), ensures the ADM-MISS SOW line (dedup, NO rescheduling RFI), persists invoice overrides stripping every non-ADM-MISS line ("" kills auto PRD-DAY/ ADD-BKFL; context built enrich=True first so WTR/ENC parents land in the remove-list), drafts with override_validation=True, verifies drafted subtotal == trip charge (mismatch aborts + clears overrides), sends via the normal pipeline with new send_invoice(suppress_cert_notice=True), releases the hold, stamps portal_state.trip_charge_closeout {invoice_number,total,at,by} as the re-intake breadcrumb. UI: red “Bill Trip Charge & Close Out” button with inline confirm on ripe cards. CAUTION for re-intake: the persisted remove-all-but-ADM-MISS override MUST be swapped (not left) if the job is ever reactivated for a real inspection, or the final invoice will bill only the trip charge.

Reactivation flow (entirely-api 47fe494+f5d4890, portal b3125a5): Branch resubmits a trip-charged job → intake dup-check annotates matches with trip_charge_closeout AND exempts them from the terminal-status filter (paid trip charge auto-closes the project, which previously dropped it from dup matches — a resubmission would have spawned a real duplicate). Wizard shows a “Trip-Charge Closed Out” banner; the add-visit action calls POST /portal/holding-pool/{id}/trip-charge- reactivate (roles = HOLD_ROLES) first: (1) inspection invoice number = {base}-B (-C on re-reactivation) via new invoice_numbering.register_suffixed_number; _lookup_existing now returns the LATEST row per record so idempotent numbering reuses the suffix; trip-charge invoice keeps {base} + its ledger row; (2) override swap — closeout’s “bill only ADM-MISS” replaced with its inverse (exclude billed ADM-MISS + memo citing the trip-charge invoice); (3) status → Intake via allow_backward; scope re-confirmation happens naturally (reparse edits clear the ABSoW stamp). Idempotent on retries. KNOWN EDGE: after reactivation, projects.invoice_number = {base}-B, so a remittance paying {base} may park in pending_matches for manual confirm (existing recon flow) rather than auto-matching. tests/test_trip_charge_reactivate.py (5). scheduler-intake.mmd updated.

Latent bug found + fixed (entirely-api 7cb67f2): ss_read._get_client() does not exist (correct: supabase_service.get_client). The PRE-EXISTING cancel-future-visits block in place_on_hold has been silently failing on it (caught as a warning) — portal holds were not cancelling future scheduled visits. Fixed all 3 call sites. Damage check: 0 on-hold projects currently have future non-cancelled visits, so no cleanup was needed.