A cancelled booking affects two parties: the customer loses their appointment and the host loses revenue. Notifying only the customer leaves the host scrambling — they may have already prepared, turned away other clients, or allocated staff. Notifying only the host violates the customer's reasonable expectation of a cancellation record. CWE-391 and ISO 25010 reliability.fault-tolerance apply to both sends: a retry-less single attempt means a provider outage at cancellation time silently strands one or both parties. Businesses handling volume bookings compound this into systematic host blindness whenever email infrastructure degrades.
High because failing to notify the host of a cancellation causes real operational harm — wasted preparation, no-show assumptions, and revenue disputes — that a simple retry-equipped dual send prevents.
Queue a single job that carries both recipient addresses, then fan out inside the worker. This ensures both sends share the same retry budget and the same transactional atomicity as the status change:
// src/lib/bookings.ts
async function cancelBooking(bookingId: string, cancelledBy: 'customer' | 'admin') {
const booking = await db.bookings.update(bookingId, {
status: 'cancelled',
cancelled_by: cancelledBy,
cancelled_at: new Date(),
})
await emailQueue.add(
'send-cancellation',
{
bookingId,
customerEmail: booking.customer_email,
hostEmail: booking.host_email,
cancelledBy,
},
{ attempts: 5, backoff: { type: 'exponential', delay: 2000 } }
)
}
Verify the worker sends to both customerEmail and hostEmail before marking the job complete.
ID: booking-notifications-reminders.confirmation-email.cancellation-email-60s
Severity: high
What to look for: Find the booking cancellation trigger (status = "cancelled"). Trace the code path from cancellation to email dispatch. Count the number of email recipients: there must be exactly 2 (customer AND host). Verify both emails use the same job queue with retry logic as confirmation emails. Check that both sends happen in the same code path as the cancellation status change.
Pass criteria: Count all email recipients on cancellation. ALL of the following: (1) On booking cancellation, at least 2 recipients (customer AND host) receive emails — quote the code that queues both. Report the count: "2 recipients notified" even on pass. A cancellation that only notifies 1 party does NOT count as pass. (2) Emails use a job queue with at least 3 retry attempts. (3) Both are dispatched in the same function as the status change to "cancelled".
Fail criteria: Cancellation emails not sent at all. Sent only to customer (host not notified) or only to host. Sent without retry logic. Dispatched from a separate polling job. Fewer than 2 recipients.
Skip (N/A) when: No email service dependency found (same criteria as confirmation-sent-60s skip).
Detail on fail: State how many recipients receive cancellation emails and name them. Quote the function where cancellation is handled. Example: "cancelBooking() at src/lib/bookings.ts:89 sends email to customer only via sendCancellationEmail(booking.customer_email). Host (booking.host_email) is never notified.".
Remediation: Use the same job queue pattern as confirmation emails, queuing for both parties:
// src/lib/bookings.ts
async function cancelBooking(bookingId: string, cancelledBy: 'customer' | 'admin') {
const booking = await db.bookings.update(bookingId, {
status: 'cancelled',
cancelled_by: cancelledBy,
cancelled_at: new Date()
})
// Queue emails to BOTH customer and host
await emailQueue.add('send-cancellation', {
bookingId,
customerEmail: booking.customer_email,
hostEmail: booking.host_email,
cancelledBy,
}, {
attempts: 5,
backoff: { type: 'exponential', delay: 2000 }
})
}
Cross-reference: Booking Flow & Lifecycle audit checks the cancellation state transition. Email/SMS Compliance audit checks the cancellation email content. Database Audit checks that the cancellation status change and email queue are transactionally consistent.