Skip to content

P5.M4d — late check-out workflow

runLateCheckoutAvailabilityCheck in src/workflows/booking-flag.ts. Fires at departure − 3 days, 15:00Z (9am MT).

Decision tree

  1. Read pre-checkin submission. Skip if:
    • No row OR addon_late_checkout=0
    • No stripe_payment_intent_id
    • late_checkout_decision already set
  2. Back-to-back checkfetchBookings(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
    • notifyGuestAddonDeclined with reason “back-to-back booking on your departure day”
  3. 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):

  1. State guard: skip if late_checkout_decision already set
  2. capturePaymentIntent(intent_id, tierPriceInCents)partial capture at the chosen tier (form authorized at MAX tier price, so any tier ≤ max can be captured)
  3. 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
  4. notifyGuestAddonConfirmed with detail=friendlyTime(time) + amount
  5. 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.tsrunLateCheckoutAvailabilityCheck
  • src/routes/forms-late-checkout.ts — GET picker + POST capture
  • src/lib/addon-notify.ts — guest notify
  • src/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)