Hamsun. Housekeeping Module · Flow ↗ Workflows editor ↗ Reception inbox

Hamsun Housekeeping

The housekeeping module manages cleaning and deep-cleaning tasks — generated automatically from booking events and the daily schedule, or manually requested by guest or reception. The cleaning team owns this queue end-to-end. Reception only sees a peek (in the command center) so they know what's pending and overdue.

Items requests (towels, linen, amenities, laundry pickup) are not handled here. Those continue to flow through the existing reception request portal.

4 task sources
3 opt-in entry points for auto-daily
5 task subtypes
5 workflow states

What's in the housekeeping module

Hamsun has a recurring confusion between "housekeeping items" (towels, linen) and "housekeeping cleaning" (room cleaning by the cleaning team). This module handles only the latter.

🧹

Housekeeping module · this spec

In scope
  • Daily cleaning
  • Deep cleaning
  • Bathroom-only cleaning
  • Turn-down service
  • Post-checkout cleaning
  • Linen change as part of cleaning visit
🧺

Reception requests · existing portal

Out of scope
  • Extra towels (separately)
  • Toiletries refill
  • Slippers, robe, kettle, hairdryer
  • Amenities top-up
  • Laundry pickup
  • Iron / ironing board

Task sources

Cleaning tasks enter the queue in 4 distinct ways. Two are automatic (system-generated), two are user-driven.

Auto Source 1

On checkout

Trigger bookings.checkin_status changes to CHECKED_OUT
Generates post_checkout cleaning task for that room
Default Always on, per-property override possible
Priority High — blocks the room until done
Auto Source 2

Daily auto-cleaning

Trigger Daily cron at 09:00 PKT ← decide time
Filters Bookings where auto_daily_cleaning = true AND checkin_status = CHECKED_IN
Skips Arrival day · Departure day · DND today · Already cleaned today
Generates daily_clean task per qualifying room
📱 User Source 3

Guest portal

User Guest taps Request cleaning in their stay portal
Choices Daily · Deep · Bathroom only · Refresh · Linen change
Source tag source = 'guest_portal'
Notification WhatsApp ack to guest, plus Slack ping to housekeeping channel
🛎 User Source 4

Reception / PMS

User Reception staff or housekeeping lead manually creates a job
Use cases Walk-in request · Pre-scheduled deep clean · One-off mid-stay
Source tag source = 'staff_manual'
Permission cleaning.create — reception + housekeeping team

Auto-daily cleaning · the booking-level toggle

A booking-level boolean drives whether the daily cron auto-creates a cleaning task each morning. Three entry points let the toggle be flipped — the system stays in sync regardless of how the guest opted in.

The single source of truth

bookings.auto_daily_cleaning
boolean · default false · stored on the booking record

Three ways to flip it

📱
Guest portal
Toggle in /portal/preferences page · saves immediately, WhatsApp confirm
💬
WhatsApp reply
Guest replies DAILY or taps a button on the welcome message · webhook updates the flag
🛎
Reception / PMS
Checkbox on the booking edit form · staff toggles based on guest request at check-in
All three paths write to the same column.
Daily cron reads it once each morning.

Skip rules

Even when auto_daily_cleaning = true, the daily cron still skips a booking when any of these apply:

  • Arrival day — guest is checking in, room was just cleaned post-checkout of the previous guest
  • Departure day — guest is leaving today, the post-checkout source will handle it
  • DND today — booking has cleaning_dnd_today = true (guest told reception "no service today")
  • Already cleaned today — there's already an active or done daily_clean task for this room today
  • Date in the skip list — booking has cleaning_skip_dates array containing today's date (planned skips, e.g. business meeting day)

Task lifecycle · 5 states

Each cleaning task moves through these states. The cleaning team primarily uses the queue; reception sees the live count via the command center peek card.

States

Scheduled Assigned In progress Done | Skipped Cancelled

Actions

Button From To Flags
→ Assignscheduled→ assignedrequires assignee
✓ Startedassigned→ in_progress
✓ Donein_progress→ donenotify guest
⊘ Skip todayany active→ skippedrequires reason
✕ Cancelany active→ cancelleddestructive · note

Subtypes

daily_clean deep_clean bathroom_only post_checkout turn_down linen_change refresh

Sample timeline · 5-night stay

Guest in Room G06, books Mon–Sat (5 nights). Below shows what tasks the system auto-generates each day under different settings.

A · Booking has auto_daily_cleaning = true

Day
Mon
Tue
Wed
Thu
Fri
Sat
Sun
Stay event
Check-in
In stay
In stay
In stay
In stay
Check-out
Vacant
Auto-tasks
skip · arrival
daily clean
daily clean
daily clean
daily clean
post-checkout

B · Same booking, but guest sets DND on Wed and skips Thu in advance

Day
Mon
Tue
Wed
Thu
Fri
Sat
Sun
Stay event
Check-in
In stay
DND set
In skip-list
In stay
Check-out
Vacant
Auto-tasks
skip · arrival
daily clean
skip · DND
skip · listed
daily clean
post-checkout

C · Booking has auto-daily OFF, but guest manually requests deep clean on Wed

Day
Mon
Tue
Wed
Thu
Fri
Sat
Sun
Stay event
Check-in
In stay
Guest requests
In stay
In stay
Check-out
Vacant
Auto-tasks
deep clean
post-checkout

Data model changes

Two parts: additions to the bookings table for the per-booking preference, and a new cleaning request_type that reuses the existing portal.requests infrastructure.

1. Booking preference columns

-- Per-booking cleaning preferences ALTER TABLE public.bookings ADD COLUMN auto_daily_cleaning boolean DEFAULT false, ADD COLUMN cleaning_pref_time time DEFAULT '10:00', -- when guest prefers cleaning ADD COLUMN cleaning_dnd_today boolean DEFAULT false, -- temporary "no service today" ADD COLUMN cleaning_skip_dates date[] DEFAULT '{}', -- specific dates to skip ADD COLUMN cleaning_special_notes text; -- "guest has a small dog", etc. -- Daily cron resets DND every midnight to false (so it doesn't carry over)

2. Use existing portal.requests with new cleaning type

No new tables — cleaning tasks are portal.requests rows with type_slug = 'cleaning'. This reuses workflow engine, status history, audit log, and the dynamic action bar.

-- New request_type seeded into portal.request_types INSERT INTO portal.request_types (slug, display_name, icon, default_team, is_paid, default_sla_minutes, workflow) VALUES ( 'cleaning', 'Housekeeping (cleaning)', 'broom', 'housekeeping', false, 60, '{ "initial_status": "scheduled", "statuses": [...], "actions": [...], "subtypes": ["daily_clean", "deep_clean", "bathroom_only", "post_checkout", "turn_down", "linen_change", "refresh"] }'::jsonb ); -- New source values to track origin (extends portal.requests.source enum) ALTER TYPE portal.request_source ADD VALUE 'cleaning_cron'; ALTER TYPE portal.request_source ADD VALUE 'checkout_trigger';

3. Optional log table for daily-cleaning bookkeeping

-- One row per booking per day to show "did we run the cron?" CREATE TABLE housekeeping.daily_cron_log ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), ran_at timestamptz NOT NULL DEFAULT now(), booking_id uuid NOT NULL REFERENCES public.bookings(id), ran_date date NOT NULL, outcome text NOT NULL CHECK (outcome IN ( 'task_created', 'skip_arrival', 'skip_departure', 'skip_dnd', 'skip_already_cleaned', 'skip_listed_date', 'skip_pref_off' )), task_id uuid REFERENCES portal.requests(id), UNIQUE (booking_id, ran_date) );

WhatsApp opt-in flow

At check-in (or on first WhatsApp interaction), the system sends a one-tap opt-in for daily cleaning. Guest's reply parses to bookings.auto_daily_cleaning.

Sample conversation

Reply parsing rules

Guest message Effect Bot reply
DAILY, "yes daily", "yes please" Set auto_daily_cleaning = true "Got it — daily cleaning starting tomorrow."
STOP, "no daily", "cancel daily" Set auto_daily_cleaning = false "Daily cleaning paused. Reply DAILY anytime to restart."
SKIP, "skip today", "no clean today" Set cleaning_dnd_today = true "Skipping today's cleaning. Tomorrow as usual."
CLEAN NOW, "send cleaning" Create immediate cleaning task "Cleaning crew on the way — within 30 min."
CHANGE TIME 14:00 Set cleaning_pref_time = '14:00' "Cleaning shifted to 2pm starting tomorrow."

Guest portal toggle

A "Cleaning preferences" section in the guest's stay portal — same toggle, surfaced visibly so guests don't need WhatsApp.

Cleaning preferences
🧹
Daily cleaning
Cleaning crew visits every day around 10:00 AM
🌙
Skip today
No cleaning visit today only · resumes tomorrow
Preferred time
10:00 AM · tap to change

Cron & triggers

Three pieces of automation glue the module together.

1

Postgres trigger · post-checkout cleaning

runs on update
On UPDATE OF checkin_status ON public.bookings when new value is CHECKED_OUT and old value wasn't, insert a portal.requests row with type_slug = 'cleaning', subtype = 'post_checkout', source = 'checkout_trigger', scheduled_for = now().
2

Daily cron · auto-daily-cleaning

09:00 PKT daily
New edge function daily-cleaning-cron. Loops every booking where auto_daily_cleaning = true AND checkin_status = CHECKED_IN. Applies skip rules. For each qualifying booking, inserts a cleaning request with subtype = 'daily_clean', source = 'cleaning_cron'. Logs every booking checked into housekeeping.daily_cron_log with the outcome.
3

WhatsApp inbound · reply parser

on incoming message
Extends the existing WhatsApp inbound webhook edge function. Parses guest replies for DAILY, STOP, SKIP, CLEAN NOW, CHANGE TIME hh:mm. Updates bookings.auto_daily_cleaning / cleaning_dnd_today / cleaning_pref_time via the booking inferred from sender's WhatsApp number. Sends confirmation reply.
4

Daily DND reset

23:59 PKT daily
Tiny SQL job: UPDATE bookings SET cleaning_dnd_today = false WHERE cleaning_dnd_today = true. Runs at end-of-day so the flag doesn't accidentally carry into the next day.

Open decisions · 8 questions for you

Before I write the schema migration and edge functions, please confirm these:

Q1
What time should the daily cron run?
Options: 09:00 · 10:00 · 11:00 · configurable per property. Affects when cleaning crew sees their day's queue. Currently I assume 09:00 PKT.
Q2
Default for new bookings — auto-daily on or off?
If on by default (opt-out), maximises cleaning revenue and consistency but risks unwanted visits. If off by default (opt-in), respects guest privacy but kills daily-cleaning adoption rate. My read is off, with WhatsApp opt-in offered at check-in — but you may prefer otherwise per property type (CLF apartments vs FSL hotel rooms might differ).
Q3
Skip arrival day & departure day automatically?
Arrival: room is fresh from previous post-checkout, so daily cleaning would be redundant. Departure: post-checkout cleaning will fire anyway. Recommend: skip both automatically. Confirm?
Q4
Deep cleaning schedule for long stays?
For stays > 7 nights, do you want an automatic weekly deep clean? Or always manual? Currently no auto-deep. Would surface in the timeline if added.
Q5
Cleaning team — separate "cleaning" team or part of housekeeping team?
In the data model, default_team = 'housekeeping' right now. If you have a separate cleaning crew, change to 'cleaning' and create that team. Affects who gets Slack notifications and assignment dropdowns.
Q6
WhatsApp opt-in — when does the bot send the message?
Options: at booking confirmation (days/weeks early) · day before check-in · at check-in · only if guest hasn't opted in 12h after check-in. My read: at check-in as part of the welcome message. Confirm.
Q7
Naming the request type — cleaning or housekeeping?
The seeded housekeeping request_type currently handles items (towels, linen). If you want this new module also called housekeeping, we need to rename the old one to housekeeping_items first. Otherwise this becomes cleaning as a separate type. Recommend: keep them distinct in the DB (cleaning vs housekeeping_items) but UI can label both as "Housekeeping".
Q8
Does post-checkout cleaning ever bill the guest?
Currently is_paid = false for all cleaning. But damaged-rooms or extra-mess clean might warrant a damage or extra clean fee. Out of scope for v1, but worth noting. Confirm v1 = always free?