P5.M4d — late check-out workflow
runLateCheckoutAvailabilityCheck in src/workflows/booking-flag.ts.
Fires at departure − 3 days, 15:00Z (9am MT).
Decision tree
- Read pre-checkin submission. Skip if:
- No row OR
addon_late_checkout=0 - No
stripe_payment_intent_id late_checkout_decisionalready set
- No row OR
- Back-to-back check —
fetchBookings(roomId, arrivalFrom:departure, arrivalTo:departure)— any booking arriving on this booking’s departure day? If yes:cancelPaymentIntent(release auth)- Update
late_checkout_decision=decline,payment_status=canceled notifyGuestAddonDeclinedwith reason “back-to-back booking on your departure day”
- No back-to-back: email guest with picker URL
/forms/late-checkout?b={id}. Guest clicks → picks a tier.
Guest picker route
/forms/late-checkout?b={bookingId} (src/routes/forms-late-checkout.ts):
GET: shows 3 tier buttons (12pm $100, 1pm $150, 2pm $200 — pulled
from property_config.addon_pricing.late_checkout).
POST with chosen_time (HH:MM, must match a property tier):
- State guard: skip if
late_checkout_decisionalready set capturePaymentIntent(intent_id, tierPriceInCents)— partial capture at the chosen tier (form authorized at MAX tier price, so any tier ≤ max can be captured)- Update
pre_checkin_submissions:late_checkout_decision=capture,late_checkout_decided_at=now,late_checkout_chosen_time=HH:MM,payment_status=succeeded,payment_captured_at=now notifyGuestAddonConfirmedwithdetail=friendlyTime(time)+ amount- Notify cleaners — for each property cleaner, send email + SMS: “Late checkout confirmed at {Property} on {departure}. Guest departing at {time}. Plan cleaning to start AFTER that time.”
Why MAX-tier authorization at submit
Stripe doesn’t allow capturing MORE than authorized. So we authorize at the highest possible tier price ($200 for 2pm) on form submit. The guest’s chosen tier is captured later (possibly less than $200).
Form copy is honest: “We authorize the max ($200) so any tier you pick later can be charged; you’ll only be charged the actual tier.”
Guest-facing UI
/forms/late-checkout?b=X renders:
Pick your late-checkout time{Property}, departing {date}
[ 12pm — $100 ][ 1pm — $150 ][ 2pm — $200 ]Each button is a separate form POST. After submit, shows confirmed state with the chosen time + charged amount.
State-aware: if already decided, shows “Already confirmed” / “Not available” instead of the picker.
Source
src/workflows/booking-flag.ts—runLateCheckoutAvailabilityChecksrc/routes/forms-late-checkout.ts— GET picker + POST capturesrc/lib/addon-notify.ts— guest notifysrc/twilio/cleaner.ts+src/mailgun/cleaner.ts— cleaner notify- Tests:
test/workflows/addon-workflows.test.ts(4 M4d cases) +test/routes/forms-late-checkout.test.ts(11 cases)