# No-show follow-up sent only after no-show status is explicitly set on the booking record

- **Pattern:** `ab-000578` (`booking-notifications-reminders.reminders.no-show-follow-up`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000578
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000578)

## Why it matters

A cron job that sends follow-up emails to all bookings where `start_time < now AND status = 'confirmed'` will send follow-ups to every completed booking that was never explicitly marked as completed — which in many booking systems is the majority. ISO 25010 functional-suitability.functional-correctness requires that no-show follow-ups fire only on verified no-shows, not on a time-based proxy. Without an explicit `NO_SHOW` status in the booking state machine, operators have no way to distinguish a completed appointment from a no-show, and any automation built on that distinction will misfire. Automated no-show detection without host confirmation also creates legal exposure if follow-up communications are construed as accusations.

## Severity rationale

Medium because a time-triggered no-show follow-up sends accusatory or unwanted messages to customers who attended their appointment but whose booking was never marked as completed.

## Remediation

Add `NO_SHOW` to the booking status enum and expose an explicit host-action endpoint that sets it. Trigger the follow-up email from the status-change handler, not from a time-based cron:

```ts
// prisma/schema.prisma
enum BookingStatus {
  PENDING
  CONFIRMED
  COMPLETED
  NO_SHOW
  CANCELLED
}

// src/app/api/bookings/[id]/no-show/route.ts
export async function POST(req: Request, { params }: { params: { id: string } }) {
  const booking = await db.bookings.update({
    where: { id: params.id },
    data: { status: 'NO_SHOW', marked_no_show_at: new Date() },
  })
  await emailQueue.add('send-no-show-followup', {
    bookingId: booking.id,
    customerEmail: booking.customer_email,
    hostEmail: booking.host_email,
  })
  return Response.json({ ok: true })
}
```

Remove any cron job that infers no-show status from appointment time alone.

## Detection

- **ID:** `no-show-follow-up`
- **Severity:** `medium`
- **What to look for:** Find no-show handling in the codebase. Search for: (1) A `no_show` or `no-show` status value in the booking status enum or status field. (2) The mechanism that sets this status — it should be a manual action by the host/admin (API endpoint, admin UI button) or a webhook from a check-in system, NOT an automatic timeout. (3) A no-show follow-up email that is ONLY triggered when `status = 'no_show'` is set, not when the appointment time passes. Trace the email trigger to confirm it is event-driven (status change), not time-driven (cron that checks past appointments).
- **Pass criteria:** Enumerate all booking statuses (at least 4 distinct values expected). ALL of the following: (1) `no_show` is an explicit, distinct status in the booking model — list all status values and confirm no_show is among them. (2) No-show status is set by a manual action (admin endpoint, host UI) — quote the handler. An automatic timeout that sets no-show does NOT count as pass. (3) Follow-up email is triggered by the status change, not by a time-based check — quote the trigger.
- **Fail criteria:** No-show follow-up sent automatically after appointment time passes (even completed bookings would receive it). No distinct `no_show` status (system only has confirmed/cancelled). No-show follow-up does not exist. Follow-up triggered by a cron job that checks "appointment time < now AND status = confirmed" — this catches completed-but-not-updated bookings.
- **Skip (N/A) when:** No no-show handling found in the codebase — searched for "no_show", "no-show", "noshow" in status fields, route handlers, and email templates.
- **Detail on fail:** Describe the no-show mechanism found (or "none found"). State how the follow-up is triggered. Example: `"No explicit no_show status — status enum at prisma/schema.prisma:15 has only 'pending', 'confirmed', 'cancelled', 'completed'. A cron job at src/jobs/no-show-check.ts:5 runs hourly and sends follow-up emails to all bookings where start_time < now AND status = 'confirmed' — this incorrectly sends follow-ups for appointments that completed but were not marked as 'completed'."`.
- **Remediation:** Add an explicit no-show status and trigger follow-up only on status change:

  ```ts
  // prisma/schema.prisma
  enum BookingStatus {
    PENDING
    CONFIRMED
    COMPLETED
    NO_SHOW     // Explicit no-show status
    CANCELLED
  }

  // src/app/api/bookings/[id]/no-show/route.ts — host marks no-show
  export async function POST(req: Request, { params }: { params: { id: string } }) {
    const booking = await db.bookings.update({
      where: { id: params.id },
      data: { status: 'NO_SHOW', marked_no_show_at: new Date() }
    })

    // Follow-up triggered by status change, not by time
    await emailQueue.add('send-no-show-followup', {
      bookingId: booking.id,
      customerEmail: booking.customer_email,
      hostEmail: booking.host_email,
    })

    return Response.json({ ok: true })
  }
  ```
- **Cross-reference:** Booking Flow & Lifecycle audit checks the booking status state machine. Database Audit checks the status enum integrity. Email/SMS Compliance audit checks follow-up email compliance.

---

## External references

- iso-25010 functional-suitability.functional-correctness

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-000578
