Skip to main content

EP-008 — Banquet & Events Management RTM

EPIC: EP-008 Banquet & Events Management Reconciled: 2026-05-25 (Wave 1 fan-out — BF-010 Banquet Event flow precursor) Pattern: Follows the BF-001 pilot at EP-002-rtm.md; see PLAYBOOK.md. Source of truth for "% passing": hmp-docs/docs/platform-registry/2026-05-16-13-flow-demo-readiness.md (the 13-flow doc doesn't enumerate banquet-lifecycle.flow.spec.ts, so flow pass-rates below are tagged TBD).

Status legend​

StatusMeaning
BuiltCode exists + tests exist + tests pass
Built-UntestedCode exists, no automated test coverage (or test exists but is a stub)
PartialCode exists for some acceptance criteria, gaps for others
StubCode is a placeholder / TODO / returns mock data
Not StartedNo code

Summary​

MetricCount
Total stories13
Catalog claims "Built"8
Verified-Built2 (US-008-004 venues, US-008-009 floor-plan)
Built-Untested1 (US-008-008 — calendar wired to API but no Jest/Playwright coverage on the page)
Partial7 (US-008-001, -002, -003, -005, -006, -011, -013)
Stub2 (US-008-007 BEO/Proposal — endpoints exist on contracts.service.ts only; US-008-010 multi-venue — schema only)
Not Started1 (US-008-012 inquiry-to-quotation as a dedicated workflow — flow-test only)
Drift findings11 (D-001..D-011; 4 P0 / 5 P1 / 2 P2)
[SME-PROPOSED] refinements19

Numbering convention note — the EPIC body (epics/EP-008-banquet-events.md) uses 2-digit story IDs (US-008-01..US-008-13) with one set of titles; the catalog (01-epic-catalog.md) uses 3-digit IDs (US-008-001..US-008-013) with a DIFFERENT title-to-ID mapping (e.g. catalog US-008-007 = "BEO / Proposal PDF Generation"; EPIC body US-008-07 = "Function Sheet Generation"). The reviews/EP-008/US-008-NNN-review.md files follow the catalog numbering. This RTM follows the catalog per playbook Task 8 convention. The mismatch is the most acute P0 drift in this EPIC — see D-001.


US-008-001 — Event Creation (7-Status Lifecycle)​

Catalog status: Built (P1, 5 points) Verified status: Partial — CRUD + status machine + venue-conflict + EVT- numbering all shipped; cancellation-charge calc, contract-signed gate on TENTATIVE→CONFIRMED, capacity validation, and audit timeline are all gaps per the review

Code locations:

  • hmp-banquet-service/src/events/events.controller.ts:31-166 — @Controller('events') with @Roles('SUPER_ADMIN','ADMIN','MANAGER') + @RequiresFeature('banquet'); routes GET/POST /, GET/PATCH /:id/status, GET/POST/PATCH/DELETE /:id/items, GET/PATCH /:id/group + /group/room-block
  • hmp-banquet-service/src/events/events.service.ts:22-30 — STATUS_TRANSITIONS 7-state machine (INQUIRY→QUOTATION_SENT→TENTATIVE→CONFIRMED→IN_PROGRESS→COMPLETED, plus →CANCELLED from any pre-COMPLETED)
  • hmp-banquet-service/src/events/events.service.ts:46-52 — generateEventNumber() returns EVT-YYYYMM-{6-char base36} (note: review file says EVT-XXXXXX sequential — code has since been hardened to date-prefix + random suffix; review is stale)
  • hmp-banquet-service/src/events/events.service.ts:58-92 — checkVenueConflict() with setup/teardown buffer overlap check
  • hmp-banquet-service/src/events/event-group.service.ts — createGroupForEvent() auto-creates Group with roomEstimate = ceil(expectedGuests/2) on CONFIRMED
  • hmp-banquet-service/src/events/dto/event.dto.ts — CreateEventDto, UpdateEventStatusDto, CreateEventItemDto, EventFilterDto, toEventResponseDto
  • hmp-banquet-service/src/events/consumers/ — domain consumers (per CLAUDE.md inventory: GuestErasedConsumer lives here)
  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/page.tsx — event list page
  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/new/page.tsx — new-event placeholder (review file flagged it as text-only; not re-verified)
  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/[id]/page.tsx — event detail (overview, items, payments, client info, room block)

Unit + integration tests:

  • hmp-banquet-service/src/events/events.service.spec.ts — event-booking lifecycle coverage (create, conflict, status transitions; review confirms PASS on the 4 enumerated cases)
  • hmp-banquet-service/src/events/event-group.service.spec.ts — room-block group creation/cancellation
  • hmp-banquet-service/src/events/__tests__/event-cross-tenant-denial.spec.ts — 8 tests, Gold-Standard Pick D phase-3 port from hmp-pos-service (shipped 2026-05-24). Asserts findOne ForbiddenException on cross-tenant, findAll tenantId-pinned scoping, create rejects cross-tenant venueId + cross-property-same-tenant venueId + leaks no tenant info via NotFoundException. Orphaned-test today: no story acceptance criterion references it (see D-005)
  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/__tests__/events-page.test.tsx — event list page test (existence verified; coverage TBD)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/banquet-lifecycle.flow.spec.ts :: "Scenario 2 — create event in INQUIRY" + "Scenario 4 — walk status machine INQUIRY→TENTATIVE→CONFIRMED" — TBD (not in 13-flow doc; tagged featureSkip if banquet flag off for canary tenant) (2026-05-16-13-flow-demo-readiness.md)

Real-backend smokes: none specific found

E2E (Playwright): none specific found for banquet under hmp-web-dashboard/e2e/

Use cases referencing this story: none located

SME notes:

  • The 7-state machine is encoded as a static STATUS_TRANSITIONS map. Precondition checks are NOT enforced: INQUIRY→QUOTATION_SENT does not require items.length > 0; TENTATIVE→CONFIRMED does not require a signed contract or 25%-of-total payment; CONFIRMED→IN_PROGRESS does not require Date.now() >= event.startDateTime. Per the review, this is the highest-severity gap in this story.
  • EVT- numbering: code uses date-prefix + 6-char random suffix (EVT-202604-AB12CD). EPIC body Scenario "Capture multi-day wedding inquiry" implies EVT-2026-0042 (date-year + sequential daily counter). The review's spec citation EVT-YYYYMMDD-NNN adds a day-component and daily-reset sequence which is again different. 3 different number formats across catalog, EPIC body, and review → pick one in spec refinement.
  • No Reservation.outboxEvent-equivalent for banquet — event.status.changed is published fire-and-forget after the Prisma write. A status PATCH that succeeds in DB but fails to publish leaves the analytics MV silently behind. (Common pattern across banquet — see EP-008 D-008.)
  • The Indian-wedding multi-day reality (mehndi/sangeet/wedding/reception) is captured in the EPIC body's Scenario 3 (parent + sub-events) but the schema today has no parentEventId FK column to support it — see US-008-010 D-007.

Drift findings:

D-001 — [P0] [Catalog-vs-EPIC-body] EPIC body uses 2-digit story IDs (US-008-01..US-008-13) with one title-to-ID mapping; catalog uses 3-digit IDs (US-008-001..US-008-013) with a DIFFERENT mapping. Examples: catalog US-008-001 = "Event Creation (7-Status Lifecycle)" vs EPIC body US-008-01 = "Event Inquiry & Creation"; catalog US-008-007 = "BEO / Proposal PDF Generation" vs EPIC body US-008-07 = "Function Sheet Generation". Reviews follow catalog. A new engineer reading the EPIC body for "US-008-07 = function sheets" will look up reviews/EP-008/US-008-007-review.md and find a BEO/Proposal review — confusion guaranteed.

  • [SME-PROPOSED] Adopt catalog numbering as canonical, rewrite epics/EP-008-banquet-events.md §10 detailed-stories to match (3-digit IDs + catalog titles), and add an alias line > Previously titled "X" in EPIC body draft prior to 2026-05-25 reconciliation to each section. Mirror the EP-002 D-007 fix pattern.

D-002 — [P0] [Tests-assert-behavior-story-doesn't-describe] The event-cross-tenant-denial.spec.ts suite (8 tests, shipped 2026-05-24) asserts ForbiddenException on cross-tenant findOne, tenantId-pinned findAll, and venue-tenant + venue-property guards on create(). NONE of these defenses appear in the US-008-001 acceptance criteria, nor in the BNQ-010 venue-tenant guard story (which doesn't exist as a separate story today).

  • [SME-PROPOSED] Add AC to US-008-001: "EventsService SHALL reject cross-tenant reads with ForbiddenException and cross-tenant venueId on create with ForbiddenException BEFORE any write. List endpoints SHALL pin where.tenantId to the JWT-derived tenantId; client-supplied propertyId filter is a narrowing filter only, never a tenant-scope replacement."
  • [SME-PROPOSED] Author a new story US-008-NEW-VENUE-TENANT-GUARD (BNQ-010) documenting the venue-tenant + venue-property cross-check explicitly, with the 8-test suite as its evidence file. Closes the orphaned-test gap.

US-008-002 — Venue Management (6 Setup Types)​

Catalog status: Built (P1, 3 points) Verified status: Partial — CRUD + availability shipped; duplicate-name check, deactivation, day-by-day calendar, capacity-vs-event validation, DELETE endpoint, and isActive/images/floorPlanUrl DTO fields all missing per review

Code locations:

  • hmp-banquet-service/src/venues/venues.controller.ts — GET /venues?propertyId=, POST /venues, GET /venues/:id, PATCH /venues/:id, GET /venues/:id/availability?startDate=&endDate=
  • hmp-banquet-service/src/venues/venues.service.ts — Venue CRUD; checkAvailability() returns {isAvailable, conflicts} (multi-day overlap correctly handled)
  • hmp-banquet-service/src/venues/dto/ — CreateVenueDto (no receptionCapacity / images / floorPlanUrl / isActive per review)
  • hmp-banquet-service/src/spaces/ — hourly meeting-room overlay that re-uses the Venue model (filters by hourlyRate)
  • hmp-web-dashboard/src/app/(dashboard)/banquet/venues/page.tsx — venue list (review flagged MOCK_VENUES; not re-verified)
  • hmp-web-dashboard/src/app/(dashboard)/banquet/venues/[id]/ — venue detail page

Unit + integration tests:

  • hmp-banquet-service/src/venues/venues.service.spec.ts — venue CRUD + availability
  • hmp-web-dashboard/src/app/(dashboard)/banquet/venues/__tests__/ — [CITED-BUT-NOT-FOUND] (directory exists, contents not enumerated)

Flow tests:

  • hmp-infrastructure/testing/flow-tests/src/flows/banquet-lifecycle.flow.spec.ts :: "Scenario 1 — a venue exists" — TBD (not in 13-flow doc) (2026-05-16-13-flow-demo-readiness.md)

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • Six setup-type capacity columns (theaterCapacity, classroomCapacity, banquetCapacity, uShapeCapacity, boardroomCapacity, plus maxCapacity) are on the Venue model per CLAUDE.md. Spec body Scenario "Create venue with multiple setup types" mentions Reception as a 7th capacity — code does NOT have receptionCapacity column. Indian receptions (especially wedding receptions for 600–1000 pax) are the highest-pax setup; missing this column will force operators to abuse banquetCapacity for reception pax.
  • No DELETE endpoint — review flags. Real-world: a property closing a venue cannot remove it. Workaround is isActive=false which is also not in the DTO. Today a closed venue stays in dropdown lists.
  • Spaces module (hourly) and Venues module (full-day) both write to the same venues table but with different rate columns (hourlyRate vs baseRate/halfDayRate). Per CLAUDE.md "Spaces vs Venues" — hourly bookings create EventBooking records with type MEETING. This dual-write semantics is undocumented in the story.

Drift findings:

D-003 — [P1] [Story-incomplete + India-hospitality-relevant] Venue model lacks receptionCapacity column despite EPIC body Scenario referencing a 600-pax Reception layout. Indian weddings are reception-heavy (600–2000 pax). Operators today abuse banquetCapacity for reception pax, breaking per-setup-type rate cards.

  • [SME-PROPOSED] Add AC: "Venue model SHALL carry 7 capacity columns: maxCapacity + per-setup {theatre, classroom, uShape, boardroom, banquet, reception, cocktail} — all Int?. Frontend setup-type dropdown SHALL show only the configured layouts (capacity > 0)."
  • [SME-PROPOSED] Add AC: "Per-setup-type rental rate SHALL be configurable via a VenueRate child table {venueId, setupType, hourlyRate?, halfDayRate?, fullDayRate?, currency='INR', validFrom, validTo}. The flat baseRate/halfDayRate/hourlyRate columns on Venue are the default fallback when no VenueRate row matches."

US-008-003 — Function Sheet Management​

Catalog status: Built (P1, 5 points) Verified status: Partial — CRUD + version increment + approval shipped; GET-returns-404 (vs template-from-event-data per spec), no edit-lock after approve, no version history, no status gate, no department-routing Kafka, no PDF — all gaps per review

Code locations:

  • hmp-banquet-service/src/function-sheets/function-sheets.controller.ts — GET/PUT /events/:eventId/function-sheet, POST /events/:eventId/function-sheet/approve
  • hmp-banquet-service/src/function-sheets/function-sheets.service.ts — CRUD; updateFunctionSheet() increments version + clears approvedBy/approvedAt
  • hmp-banquet-service/prisma/schema.prisma (FunctionSheet) — eventId (1:1), setupInstructions, seatingArrangement, menuDetails JSON, beverageDetails JSON, avRequirements JSON, timeline JSON, staffAssignments JSON, version, approvedBy/At
  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/[id]/function-sheet/page.tsx — function-sheet page (existence verified; coverage TBD)

Unit + integration tests: [CITED-BUT-NOT-FOUND] — review enumerates no function-sheets.service.spec.ts; CLAUDE.md test inventory also omits one

Flow tests: none enumerated

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • Function sheets are CORE to Indian banquet operations — chef, AV, housekeeping, security, valet all coordinate from this single document. Missing PDF generation + missing department-routing Kafka means ops teams still print from JSON view or screenshot — every property reports this as the #1 friction.
  • Once approved, sheet is supposed to lock for edits (spec criterion 3). Today updateFunctionSheet() accepts edits regardless of approvedAt. Risk: silent overwrite of approved sheet on the day of event → chef receives a stale version.
  • All 6 JSON-blob columns (menuDetails, beverageDetails, avRequirements, timeline, staffAssignments, seatingArrangement) are unstructured. There's no validator on the JSON shape — a malformed timeline JSON is accepted and renders empty. Indian-wedding muhurat ("auspicious time" — wedding ceremony slot) needs a structured timeline with IST timezone + eventType enum to be useful.

Drift findings:

D-004 — [P1] [Story-incomplete + ops-critical] No edit-lock after approval + no PDF export + no department-routing Kafka. Each is independently a P2 but compounded they make the function sheet feature unusable in real banquet ops — chefs work from PDFs, not dashboard URLs.

  • [SME-PROPOSED] Add AC: "Once approvedAt is set, subsequent PUT to the function sheet SHALL return 409 code=FUNCTION_SHEET_LOCKED unless the caller passes ?force=true&reason=<text> (manager-only role gate); forced unlocks SHALL increment version, clear approvedAt/By, and write a FunctionSheetAuditLog row."
  • [SME-PROPOSED] Add AC: "Function sheet PDF SHALL be generated via the existing Puppeteer pipeline in contracts.service.ts (the same per-property branding pipeline). PDF is downloadable via GET /events/:eventId/function-sheet/pdf?department=kitchen|av|housekeeping|all — department parameter filters which JSON blocks are rendered."
  • [SME-PROPOSED] Add AC: "On approveFunctionSheet, publish Kafka banquet.function_sheet.approved with {eventId, version, approvedBy, approvedAt, departments[]} — consumed by notification-service to route per-department notifications (kitchen email, AV WhatsApp, housekeeping app push)."

US-008-004 — Event Items & Costing (Venue/Food/Bev/AV/Decor)​

Catalog status: Built (P1, 5 points) Verified status: Partial — item CRUD + total recalc shipped; GST auto-calc, status-gate for COMPLETED/CANCELLED, costPrice population, bulk add, item-level Kafka — all NOT_IMPL per review

Code locations:

  • hmp-banquet-service/src/events/events.controller.ts:93-141 — POST/GET/PATCH/DELETE /events/:id/items
  • hmp-banquet-service/src/events/events.service.ts (addItem, updateItem, removeItem, recalculateTotal) — item CRUD; total = SUM(lineTotal) across items
  • hmp-banquet-service/src/events/dto/event.dto.ts (CreateEventItemDto) — @IsEnum on 6 categories (VENUE/FOOD/BEVERAGE/AUDIO_VISUAL/DECORATION/OTHER per code; CLAUDE.md also lists AV_EQUIPMENT/DECOR/MISCELLANEOUS as the EPIC body taxonomy — drift)
  • hmp-banquet-service/prisma/schema.prisma (EventItem) — quantity, unitPrice, total, menuItemId (P3-2 soft FK to pos-service MenuItem for ingredient-cost breakdown), costPrice (column EXISTS per review but NEVER populated; not in DTO)
  • hmp-banquet-service/src/costing/ (/events/:id/costing, /menu-items/:id/cost) — ingredient-cost breakdown via PosServiceClient when menuItemId set

Unit + integration tests:

  • hmp-banquet-service/src/costing/costing.service.spec.ts — food cost calculation tests (per CLAUDE.md inventory)
  • hmp-banquet-service/src/events/events.service.spec.ts — item CRUD (assumed; CLAUDE.md cites the file)

Flow tests:

  • banquet-lifecycle.flow.spec.ts :: "Scenario 3 — attach food + AV line items" + "Scenario 5 — list items" — TBD

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • No GST calculation — review's #1 gap. In Indian banquet ops, GST is split: 5% (food at in-house restaurant), 18% (venue rental + AV + decor + beverages — except alcohol which is state-VAT, not GST). The DTO accepts unitPrice and total as flat numbers — there's no gstRate / gstAmount / totalExclGst / totalInclGst split. Folio postings downstream cannot reconstruct the correct GST breakdown for the GSTR-1 return.
  • costPrice column exists but is never written — making P&L (US-008-006) impossible to compute from real cost data. The frontend instead uses hardcoded 35%/20% ratios (see US-008-006 D-006).
  • 6-category enum drift: code uses VENUE/FOOD/BEVERAGE/AUDIO_VISUAL/DECORATION/OTHER; EPIC body Scenario uses VENUE/F&B/Bar/Decor/AV; review uses F&B / AV Equipment / Decor / Venue Rental / Staffing / Miscellaneous. Three different category taxonomies. The STAFFING category is in the review but NOT in code — banquet staff costs (waiters, captains, security per pax) have no item-type today.

Drift findings:

D-005 — [P0] [Story-incomplete + India-hospitality-critical + GST-blocker] No GST split on event items. India GST-CGST/SGST/IGST split for banquet events: food = 5%, venue rental + AV + decor + beverages (non-alcoholic) = 18%, alcohol = state-VAT (state-dependent, not GST). Without per-item GST split, folio cannot produce a compliant GSTR-1 entry for the event. Revenue-leakage risk: if GST is computed at folio level using a default 18%, the 5% food slice over-pays GST by 13%.

  • [SME-PROPOSED] Add AC: "EventItem SHALL carry gstRate (Decimal), gstAmount (Decimal), totalExclGst (Decimal), totalInclGst (Decimal). Default gstRate SHALL be resolved from category: FOOD=5, BEVERAGE_NON_ALCOHOLIC=18, BEVERAGE_ALCOHOLIC=null (operator enters state-VAT manually with vatRate override), VENUE=18, AV=18, DECOR=18, STAFFING=18, OTHER=18. Manual override allowed."
  • [SME-PROPOSED] Add AC: "Item category enum SHALL be unified to: VENUE | FOOD | BEVERAGE_NON_ALCOHOLIC | BEVERAGE_ALCOHOLIC | AV_EQUIPMENT | DECOR | STAFFING | MISCELLANEOUS. Migration from current 6 values: AUDIO_VISUAL→AV_EQUIPMENT, DECORATION→DECOR, OTHER→MISCELLANEOUS, BEVERAGE splits into the two new enum values (default existing rows to NON_ALCOHOLIC; manual review for alcohol-containing menus)."
  • [SME-PROPOSED] Add AC: "costPrice field SHALL be exposed in CreateEventItemDto and UpdateEventItemDto (Decimal, nullable, role-gated to MANAGER+ — sales-coordinator role CANNOT see/set costPrice). Populated value flows to US-008-006 P&L."

US-008-005 — Payment Milestones (Advance/Milestone/Final)​

Catalog status: Built (P1, 5 points) Verified status: Partial — flat payment recording shipped; NO milestone schedule, NO PENDING/PAID/OVERDUE status, NO receipt numbering, NO CRON overdue detection, NO refund logic, NO partial-payment-against-milestone — every milestone-flavoured criterion is NOT_IMPL per review

Code locations:

  • hmp-banquet-service/src/event-payments/event-payments.controller.ts — GET/POST /events/:eventId/payments
  • hmp-banquet-service/src/event-payments/event-payments.service.ts — create payment (flat record) + recalc event.paidAmount
  • hmp-banquet-service/prisma/schema.prisma (EventPayment) — eventId, paymentType (ADVANCE|MILESTONE|FINAL), amount, paymentMode, reference, receiptNumber, dueDate, paidDate
  • (no PaymentMilestone model)

Unit + integration tests: [CITED-BUT-NOT-FOUND] — no event-payments.service.spec.ts enumerated in CLAUDE.md test inventory

Flow tests: none enumerated

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • The schema has paymentType and dueDate / paidDate columns, so a single-payment-per-milestone model is half-built. The missing pieces are: (a) schedule generation on event CONFIRMED, (b) milestone-status tracking (PENDING/PAID/OVERDUE), (c) overdue-detection CRON, (d) refund. Without a PaymentMilestone model, partial-payments-against-a-milestone are impossible to model.
  • Indian-wedding payment realities: 30%/40%/30% (advance/pre-event/final) is one common split but cancellation-policy schedules (50% non-refundable advance, 100% within 14 days) are extremely common. Spec gives no flexibility — it hardcodes 25/25/40/10 per the review.
  • Razorpay subscription/payment-link integration (per platform CLAUDE.md hmp-payment-service) is the obvious downstream — banquet payments today have no Razorpay handoff path. Wedding clients want to pay via UPI link, not bank-NEFT-with-reference manually entered by sales-coordinator.
  • TRAI DND: payment-due reminder SMS need a transactional template (not promotional) AND a registered sender ID — today's "no SMS reminder" gap is also a downstream telephony-compliance problem when implemented.

Drift findings:

D-006 — [P0] [Story-incomplete + revenue-protection] No milestone schedule generation on event CONFIRMED. Real-world flow: client confirms wedding → sales coordinator MUST manually create payments with due dates → invariably forgets the pre-event milestone → wedding day arrives with unpaid balance → painful collection on the day.

  • [SME-PROPOSED] Add AC: "On EventBooking transition to CONFIRMED, generate PaymentMilestone rows from a per-property PaymentMilestoneTemplate (default template: Advance 25% on D-confirm, Pre-event 50% on D-event minus 30, Final 25% on D-event plus 7). Template editable per property under PATCH /properties/:id/banquet-payment-template. Each milestone gets PENDING status, calculated dueDate, calculated amount from event total."
  • [SME-PROPOSED] Add AC: "Payment record now FKs to PaymentMilestone (nullable for non-milestone direct payments). When a payment lands, update the milestone to PAID if amount >= milestone.amount; PARTIALLY_PAID otherwise. A daily CRON at 02:00 IST scans all PENDING milestones with dueDate < today and flips to OVERDUE; publish banquet.payment.overdue for notification-service."
  • [SME-PROPOSED] Add AC: "Receipt number SHALL be generated on every paid milestone in format BNQ-RCP-YYYYMMDD-NNN (daily sequence reset, atomic via pg_advisory_xact_lock). Receipt PDF downloadable via GET /payments/:id/receipt.pdf — reuses Puppeteer pipeline."

US-008-006 — Event P&L Report (Estimated vs Actual)​

Catalog status: Partial (P2, 3 points) Verified status: Partial — front-end P&L page exists with HARDCODED 35%/20% cost ratios (no real cost data); backend has NO P&L endpoint; EventItem.costPrice column never populated; no estimated-vs-actual delta view

Code locations:

  • hmp-banquet-service/src/costing/ — /events/:id/costing returns ingredient-cost breakdown via PosServiceClient (per CLAUDE.md) — NOT a P&L endpoint, but the closest existing surface
  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/[id]/pnl/page.tsx — P&L page (review confirms hardcoded ratios)
  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/[id]/pnl/floor-plan/ — sub-route (floor-plan within P&L?? — directory exists; appears to be misplaced; verify)

Unit + integration tests:

  • hmp-banquet-service/src/costing/costing.service.spec.ts — food cost calc (per CLAUDE.md) — covers /events/:id/costing, NOT P&L
  • (no pnl.service.spec.ts)

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • The hardcoded ratios (F&B 35% of revenue as cost, AV 20%, Staff ₹200/pax) in the dashboard are dangerously plausible — a Banquet Manager seeing a clean P&L page won't realize the numbers are not derived from real costs. This is a "looks right, is wrong" surface — higher severity than a blank page.
  • The /events/:id/costing endpoint already does ingredient-cost lookups via PosServiceClient. That's the right backend foundation — P&L should aggregate costing + event-payments + event-items.costPrice into a single response.
  • "Estimated vs Actual" comparison requires capturing the estimate at quotation-sent time (a snapshot) and comparing to final actuals. Today there's no snapshot mechanism — modifying an item rewrites the estimate, losing the original.
  • Indian-banquet margin reality: weddings target 30–40% gross margin; corporate conferences 50%+; social events 25–35%. Benchmark comparison (spec criterion 6) is the highest-value future addition but blocked on the snapshot mechanism.

Drift findings:

D-007 — [P1] [Stub-shipped-as-Partial + dangerous-UX] Dashboard P&L page uses hardcoded ratios but presents as real data. A new banquet manager will trust the numbers. This is the only "Partial" in the catalog that should arguably be Stub because the visible output is plausible-but-fake.

  • [SME-PROPOSED] Bump status from Partial → Stub until backend P&L endpoint ships.
  • [SME-PROPOSED] Add AC: "Backend SHALL expose GET /events/:id/pnl returning {estimated: {revenue, cost, margin, marginPct}, actual: {revenue, cost, margin, marginPct}, variance: {revenuePct, costPct, marginPct}, breakdown: {byCategory: [...], byMilestone: [...]}} — estimated from item.unitPrice * quantity + costPrice * quantity; actual from event-payments aggregated + post-event cost entries; breakdown joins EventItem.category + PaymentMilestone.name."
  • [SME-PROPOSED] Add AC: "Until backend P&L ships, the dashboard P&L page SHALL display a banner: 'Cost estimates use industry averages, not your property's actual costs. Enter cost prices on event items to enable real P&L.' — protects banquet managers from acting on fake margins."

US-008-007 — BEO / Proposal PDF Generation​

Catalog status: Not Started (P2, 5 points) Verified status: Stub — endpoints declared on contracts.service.ts per CLAUDE.md (POST /events/:id/contract/generate, GET /events/:id/contract, POST /events/:id/contract/accept) — but these are LEGAL CONTRACTS, NOT BEOs or Proposals; review verdict NOT_IMPL stands for BEO/Proposal specifically

Code locations:

  • hmp-banquet-service/src/contracts/contracts.controller.ts — POST /events/:id/contract/generate, GET /events/:id/contract, POST /events/:id/contract/accept (LEGAL contract — different purpose)
  • hmp-banquet-service/src/contracts/contracts.service.ts — Puppeteer-based PDF generation, fallback to HTML when Puppeteer unavailable, accept/sign workflow (per CLAUDE.md Gotcha #1)
  • hmp-banquet-service/src/contracts/contract-template.ts — inline HTML template (no property branding, no GST breakdown per review)
  • (no beo.service.ts / beo.controller.ts / proposal.service.ts)

Unit + integration tests:

  • hmp-banquet-service/src/contracts/contracts.service.spec.ts — contract gen/accept tests (per CLAUDE.md)
  • (no BEO or Proposal tests)

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • The infrastructure is HALF-built: Puppeteer + Chromium are already in the Docker image (per CLAUDE.md Gotcha #1, PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser). Building BEO and Proposal templates is a template-authoring + content-aggregation task, not a tooling task.
  • BEO is operational (every department reads it); Proposal is sales-facing (client reads it). They share the event-data spine but diverge sharply on layout. Each should be its own template, not one shared "event PDF".
  • Per CLAUDE.md Gotcha #1: when user-editable contract templates land (BNQ-027 referenced), move puppeteer to a dedicated rootless sidecar — this is the same architectural decision point for BEO/Proposal.
  • The contract template runs WITHOUT property branding. Indian-hotel proposals are signature pieces — branded letterhead, GSTIN, signature blocks for both parties, terms in English + (sometimes) Hindi. Spec criterion 3 (property branding) is foundational; without it, sales coordinators will keep using Word/Excel as the review notes.

Drift findings:

D-008 — [P0] [Status-mislabel + revenue-blocker] Catalog shows Not Started and review verdict is NOT_IMPL, but the infrastructure (Puppeteer + Chromium image, contract template pattern) is HALF-built — the missing piece is template authoring + content aggregation, not new tooling. Sales teams are blocked on this; "Not Started" understates the readiness.

  • [SME-PROPOSED] Reclassify as Stub (endpoints declared via contracts.* adjacent module; template authoring + branding system needed).
  • [SME-PROPOSED] Add AC: "Service SHALL expose POST /events/:id/proposal/generate?version=N and POST /events/:id/beo/generate?revision=R. Both reuse the contracts.service.ts Puppeteer pipeline. Proposals are version-tracked (ProposalVersion table: {eventId, version, generatedAt, generatedBy, pdfS3Key, changesSinceLastVersion[]}); BEOs are revision-tracked (BeoRevision table: same shape with revision numbering)."
  • [SME-PROPOSED] Add AC: "Both PDFs SHALL include property branding sourced from PropertyTheme (per BF-001 D-005 / AP-T02-NEW-BRANDING) — logo (S3 presigned), Midnight Sovereign palette, GSTIN, registered address. Proposals additionally include GST breakdown table per the per-item GST rates from D-005, terms & conditions block (configurable per property), and a 2-party signature block."

US-008-008 — Event Calendar View​

Catalog status: Built (P1, 3 points) Verified status: Built-Untested — calendar shipped per EPIC body §4.2 (basic monthly view, color-coded by status); drag-reschedule + week/day views + time-slot granularity all flagged Partial in EPIC body; no dedicated Jest/Playwright tests located

Code locations:

  • hmp-web-dashboard/src/app/(dashboard)/banquet/page.tsx — banquet landing (per directory listing); calendar may be a component within this — [CITED-BUT-NOT-FOUND] for the exact calendar component file path
  • (no dedicated calendar backend endpoint — calendar likely fetches GET /events with date filter)

Unit + integration tests: [CITED-BUT-NOT-FOUND] — no calendar.test.tsx enumerated

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • EPIC body §4.2 explicitly flags calendar as "Partial" but catalog says "Built". The "Built" call seems to weigh frontend-page-exists more heavily than feature-completeness. The drag-to-reschedule + week/day views + time-slot granularity gaps are real.
  • The same-day conflict-detection scenario (Spec Scenario 2 for US-008-09 in EPIC body) requires time-slot overlap math at the dashboard layer — today's checkVenueConflict() on the backend uses startDateTime/endDateTime with setup/teardown buffers (good), but the dashboard calendar's same-day rendering is purely visual.
  • Indian wedding-season real reality: a single Saturday in Nov-Feb might have 4-6 simultaneous functions across 3+ venues — the calendar's color-coded monthly view is INADEQUATE; sales coordinators need a "Saturday tab" that flattens 4-6 events into a single time-track view.

Drift findings: (none unique — feeds the EPIC body's existing Partial flag; covered in summary as Built-Untested)

  • [SME-PROPOSED] Add AC: "Calendar SHALL support 3 view modes: month (current), week, day. Week + day views SHALL render events with hour-level granularity and visualize setup/teardown buffer separately from the event slot. Backend SHALL accept GET /events?from=X&to=Y&granularity=hour returning the same payload — no new endpoint."
  • [SME-PROPOSED] Add AC: "Calendar page SHALL have a Vitest test covering: 1) renders events grouped by venue, 2) colour codes match status, 3) hover shows event tooltip, 4) clicking opens event detail. AND a Playwright test covering: 1) filter by venue, 2) navigate month-prev/next, 3) drag-to-reschedule (when shipped per criterion above)."

US-008-009 — Venue Floor Plan Editor (Konva)​

Catalog status: Built (P2, 5 points) Verified status: Built — per EPIC body §4.2 Konva.js-based drag-drop editor shipped; supports tables/chairs/stage/dance floor/buffet/AV; floor plans saved as JSON + PNG export

Code locations:

  • hmp-web-dashboard/src/app/(dashboard)/banquet/events/[id]/pnl/floor-plan/ — floor-plan sub-route under pnl (path looks unusual — likely should be under [id]/ directly, not under pnl/; verify)
  • (Konva.js component file path not enumerated — [CITED-BUT-NOT-FOUND])
  • hmp-banquet-service/prisma/schema.prisma (Venue.floorPlan per CLAUDE.md) — column exists for floor-plan storage

Unit + integration tests: [CITED-BUT-NOT-FOUND] — no floor-plan.test.tsx enumerated

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • Path [id]/pnl/floor-plan/ is suspicious — floor plans logically belong to the event detail, not under the P&L sub-tree. Likely a routing-tree oversight from the original page split. Worth verifying and moving in a future refactor.
  • Real-world floor plan editing is a SECONDARY workflow for most Indian properties (the operations team draws plans on paper or PowerPoint). The Konva editor is over-engineered for the typical 5-star property and under-tested for the chains that would actually use it.
  • Floor plans saved as JSON: useful for template re-use across events (spec criterion 2 "reuse floor plan template"). Today's storage as JSON-blob on Venue is right; what's missing is the explicit FloorPlanTemplate entity that's not tied to a specific event (mentioned in EPIC body Scenario 2).

Drift findings: (none net new — UI works; reusability template is the SME ask)

  • [SME-PROPOSED] Add AC: "Floor plans SHALL be persistable as templates under FloorPlanTemplate {id, venueId, name, setupType, layoutJson, thumbnailPngS3Key, createdBy, isDefault} — separate from per-event saved floor plans. Editor toolbar SHALL have 'Save as Template' (CTA-secondary) alongside 'Save for this Event' (CTA-primary)."

US-008-010 — Multi-Venue Event Support​

Catalog status: Not Started (P3, 5 points) Verified status: Stub — schema supports it (per EPIC body §4.1 "Multi-Venue Events: Partial — Schema supports it, API logic for conflict check incomplete"); no API enforcement; no parent-child UI

Code locations:

  • hmp-banquet-service/prisma/schema.prisma (EventBooking) — supports parent-child relationship per EPIC body §4.1; column likely parentEventId String? — [CITED-BUT-NOT-FOUND] (CLAUDE.md model row doesn't enumerate)
  • (no multi-venue.service.ts)
  • (no multi-venue UI in banquet/events/)

Unit + integration tests: none

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • This is the CORE Indian-wedding feature — 3-day wedding (mehndi/sangeet/wedding/reception) needs to be ONE booking with sub-events at different venues. Treating it as 4 independent events loses the parent-aggregate view (total revenue, total payments, total guest count).
  • The conflict-check gap (per EPIC body §5 gap #4 "Multi-venue conflict check incomplete — High, S, P1") is the highest-risk gap for the highest-revenue event class. Booking Grand Ballroom for Wedding 10 AM-4 PM AND Reception 7 PM-12 AM on same day → today no automatic gap-validation, can produce no-buffer schedule.
  • Catalog priority P3 is WRONG — Indian-wedding revenue contribution (30-40% of total per EPIC §1) makes this P0/P1.

Drift findings:

D-009 — [P0] [Catalog-priority-mismatch + India-revenue-critical] Catalog marks this P3 ("low priority"). For an Indian banquet platform that targets weddings (30-40% of property revenue per EPIC §1), multi-venue/multi-day support is P0 — without it, the wedding-focused properties cannot use the platform for the majority of their revenue.

  • [SME-PROPOSED] Bump priority P3 → P0 in catalog.
  • [SME-PROPOSED] Add AC: "EventBooking SHALL carry parentEventId String? (self-FK, nullable). Master event = parentEventId IS NULL with expectedGuests and totalAmount aggregated from children. Sub-event = parentEventId IS NOT NULL with own venue/dates/menu. GET /events/:id?includeChildren=true returns master + children. Payment milestones SHALL be created on the master only — children inherit financial roll-up."
  • [SME-PROPOSED] Add AC: "Multi-day events SHALL respect a configurable per-venue setupTime and teardownTime (default 60 min each). checkVenueConflict() SHALL enforce a minimum back-to-back gap of previousEvent.teardownTime + nextEvent.setupTime between two same-venue events. Insufficient gap returns 409 code=VENUE_GAP_INSUFFICIENT with the offending pair."

US-008-011 — Event Staffing Assignment​

Catalog status: Partial (P2, 3 points) Verified status: Partial — schema has staffAssignments JSON on FunctionSheet per CLAUDE.md; no UI for assignment; no link to HR/staff roster per EPIC body §4.1

Code locations:

  • hmp-banquet-service/prisma/schema.prisma (FunctionSheet.staffAssignments JSON) — free-form JSON blob
  • (no staffing.service.ts)
  • (no staff-roster cross-service client — hmp-auth-service owns users but no StaffServiceClient exists)

Unit + integration tests: none

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • "Staffing assignment" with no HR/staff roster integration is just text-editing today. Real value would be assignedTo: userId with auto-conflict-check (is this captain double-booked across two events at 8 PM?).
  • Indian banquet operations: typical staffing for a 500-pax wedding = 1 captain + 2 supervisors + 25 service staff + 4 chefs + 2 AV operators + 2 housekeeping + 4 security = 40+ people. Hand-typing this into a JSON blob is impossible to maintain.
  • No labour-compliance gate today: shift duration (8 hr max per Karnataka Shops & Establishments Act), minimum-wage compliance per state, overtime tracking — none enforced.

Drift findings:

D-010 — [P1] [Story-incomplete + cross-service-integration] Staffing has no link to user identity (just free-form JSON), no conflict detection across events, no role/skill matching, no labour-compliance gate. A "Partial" rating overstates the readiness.

  • [SME-PROPOSED] Demote to Stub until cross-service integration shipped.
  • [SME-PROPOSED] Add AC: "Create EventStaffAssignment {id, eventId, userId, role (CAPTAIN|SUPERVISOR|WAITER|CHEF|AV|HK|SECURITY), shiftStart, shiftEnd, isLead, hourlyRate?, totalHours} model. Conflict-check: refuse insert if the user has another EventStaffAssignment overlapping [shiftStart, shiftEnd] for a different event in same property. POST /events/:id/staff and GET /events/:id/staff endpoints."
  • [SME-PROPOSED] Add AC: "Per-shift duration MUST be ≤ 8h (configurable per Property.maxShiftHours) — soft warning on insert if exceeded; hard reject if Property.enforceShiftLimits=true. Saves Indian-state Shops & Establishments Act compliance."

US-008-012 — Event Inquiry to Quotation Flow​

Catalog status: Built (P1, 3 points) Verified status: Not Started as a DEDICATED workflow — the underlying status-machine transition INQUIRY→QUOTATION_SENT works (covered by US-008-001 events.service tests) but there is NO inquiry-capture form, NO quotation-builder UI, NO send-quotation-via-email path, NO public inquiry-receive endpoint for booking-engine integration

Code locations:

  • hmp-banquet-service/src/events/events.service.ts (updateStatus) — handles the INQUIRY→QUOTATION_SENT transition (with NO precondition check that items exist — flagged in US-008-001 review)
  • (no inquiry-intake form on hmp-web-dashboard/src/app/(dashboard)/banquet/events/new/)
  • (no public @Public() inquiry-receive endpoint for booking-engine handoff)

Unit + integration tests:

  • events.service.spec.ts — covers the status transition mechanically

Flow tests: none enumerated for the inquiry→quotation workflow specifically

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • "Built" in the catalog feels wrong — the status transition exists, but the WORKFLOW (capture inquiry → build quotation → send PDF → track response) is missing every step except the status change itself. Catalog conflates "transition exists" with "workflow exists".
  • Real-world Indian-wedding inquiry funnel: 90% inquiries via WhatsApp Business / phone / walk-in, 8% via web form, 2% via OTA cross-sell. The booking engine + WhatsApp Business API integration is the obvious missing channel.
  • The TENTATIVE state (between QUOTATION_SENT and CONFIRMED) is supposed to model "client has verbally committed but not paid advance". Today's flow has no "hold venue tentatively for 7 days" semantics — first-come-first-served at CONFIRMED level only.

Drift findings:

D-011 — [P1] [Status-mislabel + booking-engine-integration-gap] Catalog Built overstates readiness. The status transition mechanic exists; the workflow does not. New banquet PMs reading the catalog will conclude the inquiry funnel is solved.

  • [SME-PROPOSED] Demote to Stub.
  • [SME-PROPOSED] Add AC: "TENTATIVE state SHALL hold the venue for a configurable tentativeHoldHours (default 168 = 7 days). A daily CRON at 03:00 IST scans TENTATIVE events with (now - statusChangedAt) > tentativeHoldHours and auto-transitions to QUOTATION_SENT (releasing the venue), publishing banquet.tentative.expired."
  • [SME-PROPOSED] Add AC: "Booking-engine SHALL expose a public POST /api/booking-engine/banquet/inquiry endpoint (API-key gated @Public(), similar to reservation booking-engine endpoints) that creates an INQUIRY-status event for the property. Captcha required. Auto-acknowledges via email; emits banquet.inquiry.received for sales-team notification."

US-008-013 — Event Cancellation with Charges​

Catalog status: Partial (P1, 3 points) Verified status: Partial — status transition →CANCELLED works from any pre-COMPLETED state per code; NO charge calculation per US-008-001 review; NO refund flow; NO cancellation-policy model on banquet (banquet does NOT re-use the property-service CancellationPolicy per US-002-006)

Code locations:

  • hmp-banquet-service/src/events/events.service.ts (updateStatus) — handles → CANCELLED transitions
  • hmp-banquet-service/src/events/event-group.service.ts (cancelGroupForEvent) — releases the room block
  • (no cancellation-policy.service.ts in banquet-service — property-service's policies are NOT consumed here)
  • (no refund handling — would need a hmp-payment-service handoff per CLAUDE.md modular-payments-platform pattern)

Unit + integration tests:

  • event-group.service.spec.ts — covers room-block cancellation

Flow tests: none

Real-backend smokes: none

Use cases referencing this story: none

SME notes:

  • Banquet cancellation policies are MORE complex than room cancellations: typically a sliding scale based on days-to-event (e.g. >90d=10% non-refundable, 60-90d=25%, 30-60d=50%, <30d=100%). Today there's no policy model, so cancellation is a free-text decision.
  • DPDPA refund-SLA gap (per EP-002 D-010) applies here too: a cancellation must commit to a refund SLA. Today no SLA contract.
  • Russian-doll problem: a multi-day wedding cancellation (D-009) should cascade to all sub-events with their own charge calculations — not one charge for the whole booking.

Drift findings: (feeds D-009 multi-venue + D-006 milestone-cancel-refund cascades; no fully new finding)

  • [SME-PROPOSED] Add AC: "Banquet cancellation SHALL evaluate a BanquetCancellationPolicy per event (configurable per property, default sliding scale 90d/60d/30d/event-day). Charge amount = event.totalAmount * policy.matchRule(daysUntilEvent).chargePct. Charge adjusts against advance paid via PaymentMilestone updates; refund delta posted via PaymentServiceClient (Razorpay refund) with refundSlaHours=168 (D-010 parallel)."
  • [SME-PROPOSED] Add AC: "Cancellation of a master event (with parentEventId IS NULL) SHALL cascade to all sub-events with the same policy; refund total aggregates across children. Cancellation of a sub-event SHALL only release that venue slot and trigger its proportional charge — master event remains active with reduced total."