Files
clienthub/.planning/phases/05-offer-system/05-VERIFICATION.md
T

15 KiB

phase, verified, status, score, overrides_applied, human_verification
phase verified status score overrides_applied human_verification
05-offer-system 2026-05-30T00:00:00Z human_needed 7/7 must-haves verified 0
test expected why_human
Visit /admin/offers and create a macro-offer, then a micro-offer under it, then create an offer service and assign it via the checkbox list Macro and micro appear in the list; checkbox toggles assignment; cumulative price updates on refresh RSC form actions with router.refresh() — cannot verify round-trip DB mutation without running the app
test expected why_human
Visit /admin/projects/[id], open the Offerte tab, assign a micro-offer, set accepted_total, then remove it Assignment persists on refresh; accepted_total saves on blur; removal disappears on refresh Inline onBlur save and useTransition interactions require live browser session
test expected why_human
Visit /client/[token] for a project that has active offers with accepted_total set Sidebar shows 'Offerte Attive' section with public_name, cumulative service price, and final accepted price; NO internal names visible Security critical — confirming internal_name never surfaces in the rendered HTML requires browser inspection of a live session with real data
test expected why_human
Visit /admin/forecast after assigning at least one offer with accepted_total 12-month table shows non-zero totals in the correct months based on start_date + duration_months spread Forecast correctness depends on real start_date values and duration_months; verifying bucket computation requires live data

Phase 5: Offer System Verification Report

Phase Goal: Implement a complete hierarchical Offer System — macro-offers -> micro-offers -> services — with admin catalog CRUD, project assignment, client dashboard visibility (public_name only), and 12-month revenue forecast. Verified: 2026-05-30T00:00:00Z Status: human_needed Re-verification: No — initial verification

Goal Achievement

Observable Truths

# Truth Status Evidence
1 Five tables exist in schema: offer_macros, offer_micros, offer_services, offer_micro_services, project_offers VERIFIED All five pgTable exports confirmed in src/db/schema.ts lines 199-268
2 No existing table (clients, projects, payments, phases) modified or dropped VERIFIED All four original tables intact at schema.ts lines 14, 39, 53, 109; no DROP or ALTER COLUMN found
3 /admin/offers page with CRUD for macros, micros, and services VERIFIED src/app/admin/offers/page.tsx exists; calls getCatalogWithMicros + getAllOfferServices; forms wired to createMacro, createMicro, createOfferService, deleteMacro, deleteMicro, toggleOfferServiceActive
4 OffersTab in admin project workspace for offer assignment VERIFIED src/components/admin/tabs/OffersTab.tsx exists; TabsTrigger value="offers" at project page line 77; TabsContent value="offers" at line 141; OffersTab imported and receiving projectOffers + availableMicros props
5 Client dashboard shows offer public_name ONLY — never internal_name VERIFIED getProjectView() selects only offer_micros.public_name (client-view.ts line 282); OffersSection.tsx has zero internal_name occurrences; ClientView.activeOffers type exposes only public_name, cumulative_price, accepted_total
6 /admin/forecast shows 12-month revenue projection VERIFIED src/app/admin/forecast/page.tsx exists; calls getRevenueForecast12Months(); renders 12-row table with month label, projected total, proportional bar, and totals footer
7 Forecast computed without DB persistence VERIFIED src/lib/forecast-queries.ts contains zero db.insert / db.update calls; algorithm builds 12 in-memory buckets and distributes accepted_total / duration_months per month via JS loop

Score: 7/7 truths verified

Required Artifacts

Artifact Expected Status Details
src/db/schema.ts Five new pgTable definitions appended VERIFIED All five tables present with correct FK constraints (offer_micros.macro_id cascade, project_offers.micro_id restrict, composite PK on offer_micro_services)
src/lib/offer-queries.ts Read queries: getCatalogWithMicros, getAllOfferServices, getMicroAssignedServiceIds VERIFIED All three functions exported; real DB queries with joins, no hardcoded data
src/app/admin/offers/actions.ts Server actions for all offer catalog mutations VERIFIED 7 exported actions: createMacro, deleteMacro, createMicro, deleteMicro, createOfferService, toggleOfferServiceActive, updateMicroOfferServices
src/app/admin/offers/page.tsx RSC page listing macros, micros, offer services VERIFIED RSC page with revalidate = 0; three sections fully implemented
src/components/admin/offers/ServiceCheckboxList.tsx Client component for many-to-many service assignment VERIFIED "use client"; useState Set; useTransition + router.refresh(); wired to updateMicroOfferServices
src/components/admin/tabs/OffersTab.tsx Project workspace tab for offer assignment VERIFIED "use client"; assignOfferToProject, removeProjectOffer, updateProjectOfferTotal all called
src/app/admin/projects/project-actions.ts Three new offer server actions VERIFIED assignOfferToProject, removeProjectOffer, updateProjectOfferTotal — all call requireAdmin(); all revalidate /admin/forecast
src/lib/admin-queries.ts Extended with ProjectOfferWithMicro type, getProjectFullDetail extension, getClientActiveOffers VERIFIED Lines 189-202 (type); lines 601-602 (return); lines 646-687 (getClientActiveOffers with public_name only)
src/lib/client-view.ts Extended ProjectView and ClientView with activeOffers VERIFIED activeOffers field in both interfaces; query at lines 279-309; returned at line 354
src/lib/forecast-queries.ts getRevenueForecast12Months() with 12-bucket JS algorithm VERIFIED File exists; DB query + JS bucket fill; rounds to 2 decimal places; no DB writes
src/components/client/OffersSection.tsx Client-facing offer card (public_name only) VERIFIED Zero internal_name occurrences; renders public_name, cumulative_price, accepted_total
src/app/admin/forecast/page.tsx Admin /admin/forecast RSC page VERIFIED RSC page; calls getRevenueForecast12Months(); table with month/total/bar/footer
src/components/admin/NavBar.tsx NavBar with /admin/offers and /admin/forecast links VERIFIED Both links confirmed at lines 21 and 27
From To Via Status Details
src/app/admin/offers/page.tsx src/lib/offer-queries.ts getCatalogWithMicros() import WIRED Direct import; called in Promise.all
src/app/admin/offers/actions.ts src/db/schema.ts offer_macros, offer_micros, offer_services, offer_micro_services imports WIRED All four tables imported and used in mutations
src/components/admin/offers/ServiceCheckboxList.tsx src/app/admin/offers/actions.ts updateMicroOfferServices import WIRED Imported and called in toggle handler
src/components/admin/tabs/OffersTab.tsx src/app/admin/projects/project-actions.ts assignOfferToProject import WIRED All three offer actions imported and called
src/app/admin/projects/[id]/page.tsx src/lib/admin-queries.ts getProjectFullDetail() — includes projectOffers WIRED projectOffers + availableMicros destructured from detail; passed to OffersTab
src/app/admin/clients/[id]/page.tsx src/lib/admin-queries.ts getClientActiveOffers() import WIRED Parallel fetch at line 14-17; activeOffers rendered at lines 112-130
src/lib/client-view.ts src/db/schema.ts project_offers + offer_micros join in getProjectView() WIRED Explicit select on public_name only; result mapped to activeOffers
src/app/client/[token]/page.tsx src/lib/client-view.ts projectViewToClientView adapter WIRED activeOffers: view.activeOffers at line 69
src/components/client-dashboard.tsx src/components/client/OffersSection.tsx OffersSection import WIRED Imported at line 10; rendered conditionally at lines 58-63
src/app/admin/forecast/page.tsx src/lib/forecast-queries.ts getRevenueForecast12Months() import WIRED Direct import; called in RSC function body

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
src/app/admin/offers/page.tsx catalog, allServices getCatalogWithMicros(), getAllOfferServices() — DB queries on offer_macros, offer_micros, offer_services Yes — live DB SELECT with joins FLOWING
src/components/admin/tabs/OffersTab.tsx projectOffers, availableMicros getProjectFullDetail() extended queries on project_offers JOIN offer_micros Yes — live DB SELECT FLOWING
src/components/client/OffersSection.tsx offers (activeOffers) getProjectView() -> projectOfferRows (project_offers JOIN offer_micros) + cumulativePriceRows (SQL SUM) Yes — live DB queries; returns undefined when empty (not []) FLOWING
src/app/admin/forecast/page.tsx forecast getRevenueForecast12Months() -> DB SELECT project_offers JOIN offer_micros + JS bucket algorithm Yes — reads from DB; pure JS computation for monthly split FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
TypeScript compiles without errors npx tsc --noEmit Exit 0, no output PASS
Five offer tables defined in schema grep "^export const offer_macros|offer_micros|offer_services|offer_micro_services|project_offers" schema.ts 5 matches PASS
requireAdmin in all offer actions grep -c "requireAdmin" src/app/admin/offers/actions.ts 8 (definition + 7 calls) PASS
revalidatePath in all offer actions grep -c "revalidatePath" src/app/admin/offers/actions.ts 8 PASS
No internal_name in OffersSection grep "internal_name" src/components/client/OffersSection.tsx 0 matches PASS
No internal_name in forecast page grep "internal_name" src/app/admin/forecast/page.tsx 0 matches PASS
activeOffers adapter wired grep "activeOffers" src/app/client/[token]/page.tsx 1 match: activeOffers: view.activeOffers PASS
No DB writes in forecast-queries.ts grep "db.insert|db.update" src/lib/forecast-queries.ts 0 matches PASS
NavBar links present grep "/admin/offers|/admin/forecast" NavBar.tsx 2 matches PASS

Requirements Coverage

Requirement Source Plan Description Status Evidence
OFFER-01 05-02 Admin crea macro-offerte con nome interno, nome pubblico, promessa trasformazione SATISFIED createMacro action + form in /admin/offers page; Zod schema validates all three fields
OFFER-02 05-02 Macro-offerte hanno micro-offerte figlie con nome, promessa, durata in mesi SATISFIED createMicro action + nested form per macro in /admin/offers page; duration_months field included
OFFER-03 05-02 Admin crea servizi con nome, prezzo, descrizione; assegnabili a micro-offerte via multi-select SATISFIED createOfferService action; ServiceCheckboxList with updateMicroOfferServices (delete+re-insert); offer_micro_services junction table
OFFER-04 05-03 Admin assegna micro-offerte a progetto; vede offerte attive per progetto e cliente SATISFIED OffersTab in project workspace; assignOfferToProject/removeProjectOffer actions; getClientActiveOffers on /admin/clients/[id]
OFFER-05 05-04 Dashboard cliente mostra offerte con nome pubblico, prezzo cumulativo, prezzo finale; mai internal_name SATISFIED OffersSection renders public_name + cumulative_price + accepted_total; getProjectView() selects public_name only; 0 internal_name occurrences in client component
OFFER-06 05-04 Admin ha vista forecast 12 mesi da offerte attive, durata, accepted_total; breakdown mensile SATISFIED /admin/forecast page with getRevenueForecast12Months(); JS bucket algorithm; 12 rows with totals footer

All 6 requirements (OFFER-01 through OFFER-06) are SATISFIED.

Anti-Patterns Found

File Pattern Severity Impact
None found

No TODO/FIXME/PLACEHOLDER comments in any Phase 5 files. No hardcoded empty arrays returned as final data. No stub implementations. No console.log-only handlers.

Note: OffersTab.tsx renders offer.micro_internal_name (line 67) — this is intentional and correct. The tab is admin-only under /admin/projects/*, which is Auth.js session-guarded. The plan's threat model (T-05-08) explicitly accepts this as correct admin-visible data.

Human Verification Required

1. Admin offer catalog round-trip

Test: Create a macro-offer at /admin/offers, add a micro-offer under it with duration_months=3, create an offer service with a price, then toggle the service checkbox to assign it to the micro. Refresh the page. Expected: Macro appears in the list; micro is nested under it showing cumulative price equal to the service price; checkbox remains checked after refresh. Why human: Server action + router.refresh() round-trips require a live browser session against the production DB.

2. Project offer assignment and accepted_total save

Test: At /admin/projects/[id], open the Offerte tab. Assign the micro-offer created above. Enter an accepted_total value and click away (onBlur). Refresh the page. Expected: The micro-offer appears in the active assignments list with duration and start date. The accepted_total value persists after refresh. Why human: onBlur inline save behavior and server action response cannot be verified without a running browser.

3. Client dashboard security — no internal_name visible

Test: Navigate to /client/[token] for a client whose project has at least one micro-offer assigned with an accepted_total set. Inspect the "Offerte Attive" sidebar block. Also inspect the page HTML source. Expected: Only public_name (e.g. "Starter Branding") appears — never the internal_name (e.g. "Entry A"). Individual service prices do not appear. Only cumulative price and final accepted price show. Why human: Security verification requires confirming the rendered HTML and network responses contain no internal data. Code confirms this structurally, but a live check is required for the security constraint.

4. Forecast accuracy

Test: With at least one project_offer having accepted_total=1200 and duration_months=3 starting from the current month, visit /admin/forecast. Expected: The current month, next month, and the month after each show €400.00 (1200/3). Other months show €0 / dash. Why human: Bucket algorithm correctness depends on real start_date values stored in production DB relative to current date.

Gaps Summary

No gaps found. All 7 must-haves are verified against the actual codebase. All 6 OFFER requirements are accounted for with satisfying implementation evidence. The phase goal is structurally achieved — human verification is required only to confirm live runtime behavior and the security property (no internal_name visible to clients).


Verified: 2026-05-30T00:00:00Z Verifier: Claude (gsd-verifier)