Skip to content

GitHub: https://github.com/WAconsult/cruits-housing-ba/blob/main/01-scoping/prd.md

Active Document Registry

Document GitHub Last updated
PRD (this document) prd.md 2026-05-31
Product Backlog backlog.md 2026-05-31
Story Specs — Iteration 1 iteration-1-story-specs.md 2026-05-31
Story Specs — Iteration 2 iteration-2-story-specs.md 2026-06-12
Booking Config & State Machine (draw.io) payment-config-diagram.xml 2026-05-31
Wireframes — Iteration 1 iteration-1-wireframes.html 2026-05-28
GoMeddo Inventory 09-gomeddo-inventory/ 2026-05-22
Legal Research (WGV) wet-goed-verhuurderschap-bed-en-baan-separation.md 2026-05-22

PRD: Cruits Housing — Booking Module

§0. Document Purpose

This PRD defines requirements for the Cruits Housing Booking Module — a housing marketplace connecting housing providers and temporary workers, targeted at temporary workers in the Netherlands. The document is structured as three independent layers:

  1. Housing Core — a provider-agnostic, client-agnostic booking engine with minimal provider and payer management UIs.
  2. Cruits Integration — housing discovery and booking embedded in the Cruits worker app (PWA + mobile).
  3. Workinn Integration — inventory sync and confirmation flow for Workinn Real Estate (via GoMeddo).

This structure ensures the core module can be developed, tested, and extended independently of any specific provider or client application. Requirements in §4 (Housing Core) have zero provider-specific or client-specific dependencies. All provider- and client-specific logic lives exclusively in §5 and §6.

System Context

graph TB
    subgraph Core["§4 Housing Core"]
        HC["Booking Engine\nProvider Admin · Payer Admin\nMessaging · Documents"]
    end

    subgraph CruitsInt["§5 Cruits Integration"]
        CI["Worker UI (PWA + Mobile)\nContext Handshake · Data Return"]
    end

    subgraph WorkinnInt["§6 Workinn Integration"]
        WI["Inventory Sync\nConfirmation Flow · Doc Integration"]
    end

    Worker["Flexworker"] -->|"browse / reserve"| CI
    CI -->|"worker_id · job_location · booking_context"| HC
    HC -->|"address · status · price · payment_form"| CI

    Provider["Housing Provider\n(manual)"] -->|"manage listings"| HC
    Payer["Payer (e.g. Cruits)"] -->|"confirm / approve"| HC

    WI -->|"ListingUnits sync"| HC
    HC -->|"PENDING_PROVIDER request"| WI
    WI -->|"confirmation / rejection"| HC
    WI <-->|"GoMeddo API"| GoMeddo["GoMeddo\n(Workinn)"]

§1. Vision & Problem

The Housing Booking Module is a marketplace application connecting housing providers with temporary workers. It is designed for temporary workers arriving in the Netherlands for short-term employment — workers who need affordable, fast, and legally compliant accommodation near their job site.

Today, housing booking is manual: recruiters of Cruits agency coordinate housing separately from job placement, addresses are entered manually, and delays are frequent. No confirmed housing means no confirmed placement. The Housing Booking Module makes housing a self-serve, digitally confirmed step — integrated where workers already are (the Cruits app), connected to providers who have inventory (Workinn and others), and compliant with Dutch BRP registration requirements.

The module is designed for maximum universality: any housing provider can connect (via direct UI or provider integration), any payer model can be configured (worker-pays, employer-pays, partial subsidy), and any client application can embed the booking experience via API (for MVP it is the Cruits application; post-MVP it can be an independent housing booking application).


§SM. Success Metrics

The primary goal for MVP: a Resident successfully finds and books housing without out-of-app interaction with a Cruits manager.

ID Metric Target Measurement
SM-1 Self-serve booking rate ≥ 80% of bookings completed without out-of-app recruiter coordination Booking records: no manual Cruits-manager intervention logged
SM-2 Booking completion rate ≥ 70% of Reservations reach BOOKED state [ASSUMPTION] Ratio BOOKED : RESERVED across 14-day cohorts

§2. Target Users & Actors

2.1 Flexworker (Resident) A temporary worker, typically Polish or Romanian, newly arrived in the Netherlands for short-term employment. Needs affordable housing near a job site quickly. In MVP, accesses the Housing Module exclusively through the Cruits app (PWA + mobile). The term Resident is used within the Housing Module for the same actor; Flexworker is used in the context of Cruits integration.

2.2 Housing Provider A company or individual offering housing inventory on the platform. At MVP launch: Workinn Real Estate, whose inventory is managed via GoMeddo. May also be smaller operators without external systems, who manage listings directly via Provider Admin.

Note: Provider Admin enables a complete booking flow that is fully independent of any provider integration. The full cycle — listing, reservation, confirmation, signing, booking — can be implemented and tested without an active GoMeddo connection. This eliminates integration-blocking dependencies during development.

2.3 Payer The entity responsible for payment on a booking. May be the Flexworker (direct payment), an agency (Cruits or other), an employer, or a subsidising third party. Payer and Resident are independent entities in every booking — they may or may not be the same person.

Note: When Payer ≠ Resident: direct communication between the Housing Provider and the Payer is out of scope for the Housing Module. Communication between the Resident and their Payer (e.g. Cruits) is handled through the integration layer and is within MVP scope.

2.4 Platform Admin Internal operator managing the platform: onboarding housing providers, platform configuration, monitoring.

Note: Cruits as an organisation is an integration partner, not a UI actor in MVP. Cruits managers receive housing data through the Cruits system (automated data push); they have no dedicated Housing Module UI.


§3. Glossary

Term Definition
ListingUnit The atomic bookable unit of inventory: a bed in a shared room, or a private room (MVP types).
Unit Mode A ListingUnit operates in one of two modes: concrete (specific unit with an ID, e.g. Bed #3) or pool (a category with available count, e.g. "Shared Bed — 5 available"). Mode is set by the Provider at creation.
ListingUnitSlot A child record of a concrete-mode ListingUnit representing one individually identified physical position: floor + room + bed (unique together). Each slot can be independently reserved. Pool-mode units have no slots — capacity is expressed as total_slots count only.
total_slots For pool-mode ListingUnits: the Provider-configured static count of bookable positions in the pool (e.g., "10 shared beds of this type"). Stored on the model. The available_count exposed in API responses is derived from total_slots minus active reservations at query time and is never stored as a persistent field.
capacity The maximum number of occupants that can be accommodated in a ListingUnit. Set by the Provider per listing; defaults to the number of ListingUnitSlot records (concrete mode) or total_slots (pool mode) if not specified. Distinct from available_count: capacity describes physical occupancy limit; available_count describes how many slots are currently open for booking.
Listing A ListingUnit as presented in worker-facing discovery. A pool-mode unit surfaces as one card showing available count.
Reservation A temporary hold on a ListingUnit, marking the start of the booking flow. Unit is held; booking is not yet confirmed.
Booking A fully confirmed Reservation — provider has confirmed availability and payer has confirmed payment intent.
Booking State Machine RESERVED → PENDING_PROVIDER → [PENDING_PAYER →] [SIGNING →] BOOKED → CHECKED_IN → ENDED. If payer = resident, PENDING_PAYER is skipped. If signing is not configured for this booking, SIGNING is skipped. SIGNING is always the final step before BOOKED, so that all documents (accommodation agreement and any payer documents) can be collected in a single signing envelope. Terminal failure states: RESERVATION_FAILED, CANCELLED. BOOKED → CHECKED_IN is triggered by an explicit Provider check-in action via Provider Admin (FR-91). CHECKED_IN → ENDED is initiated exclusively by an explicit checkout action (Resident self-checkout or Provider checkout confirmation); the system never triggers ENDED automatically on period.end. Cancellation (→ CANCELLED) is only available from BOOKED state; once a booking reaches CHECKED_IN, it can only proceed to ENDED.
booking_context The operational context passed by the integration client to the Housing Module per worker session. Specifies which actions are available to the worker: browse only; direct payment reservation; indirect payment reservation (with a designated third-party payer). If indirect payment is enabled, the context also includes the permitted reservation period (start and end dates) and the maximum daily cost. The integration client is solely responsible for ensuring that permitted_reservation_period values comply with SNF norm 5.2 and applicable WGV/CAO obligations; the Housing Module enforces these values as received and performs no independent compliance check on the period dates.
confirmed_address The legally valid NL postal address of the booked unit: street name, house number, postcode, city. Stored in the Booking record and pushed to the integration client on BOOKED.
period A date range (start date, end date). Check-in and check-out times are configured by the Provider per listing. Used in Booking records, pricing calculations, and registration.
Payer Entity responsible for payment. Independent of Resident. Determined by the worker's payment choice at reservation time, within the constraints of booking_context.
Resident Person physically occupying the booked housing unit.
Provider Housing company or individual listing inventory on the platform.
Provider Admin Desktop-first management UI for Providers: inventory, availability calendar, pricing, document templates, residents, messaging.
Payer Admin Desktop-first management UI for Payers: resident list, settlements report, notifications.
Ghost Alarm A manual flag raised by a Provider or Payer when a Resident's physical presence at the booked address is in question. Triggers notification to all parties. No mandatory system consequence.
Provider-Agnostic API Contract The abstract interface all provider integrations must implement, covering: inventory synchronisation, availability checking, reservation request, reservation cancellation, retrieval of the signing document package, booking confirmation, and booking modification.

§4. Feature Requirements — Housing Core

All requirements in this section are provider-agnostic and client-agnostic. No FR in §4 may reference Workinn, GoMeddo, Cruits, or any specific integration.

§4.0 Identity & Access

FR-65. The Housing Module MUST maintain its own identity store for all actor types (Flexworker, Provider, Payer, Platform Admin). In MVP, actor accounts are created by Platform Admin only; no self-registration is available.

FR-66. All Housing Module operations (read or modify booking, listing, or personal data) MUST require an authenticated session. Unauthenticated requests MUST be rejected with an appropriate error.

FR-67. Access MUST be scoped by actor role. A Flexworker MAY NOT perform Provider Admin or Payer Admin actions. A Provider MAY NOT access another Provider's data. A Payer MAY NOT access another Payer's data.

FR-68. Integration clients authenticate to the Housing Module via dedicated integration credentials (client ID + secret or equivalent). Integration credentials are issued per client and scoped to the permitted API surface.

FR-69. When an integration client passes a worker_id in the booking context, the Housing Module MUST ensure a corresponding Flexworker (Resident) identity record exists before permitting any booking action on their behalf. The integration client MUST pass alongside worker_id: source_client, full_name, email (nullable), and phone (nullable). The Housing Module applies an upsert on (worker_id, source_client): creates the Resident on first encounter; updates mutable contact fields (full_name, email, phone) on subsequent encounters. Every change to email or phone MUST be recorded in a contact history log with valid_from / valid_until timestamps to support signing audit and dispute resolution. If an updated email or phone value already exists on a different Resident record, the conflicting field MUST NOT be applied and a conflict record MUST be created for Platform Admin review; the Resident and any in-progress booking are not otherwise blocked. A booking MUST NOT transition to SIGNING state if the Resident has no email and the Provider has signing configured; the booking remains in PENDING_PROVIDER until the conflict is resolved and an email is assigned. Platform Admin provisioning is not required. [OQ-012 resolved — see A-09]

FR-70. GDPR / AVG Compliance. The Housing Module MUST: - Obtain explicit consent from a Flexworker before storing personally identifiable information (name, address, identity data). Consent is recorded with a timestamp and consent version. - Provide Flexworkers the ability to submit a data anonymisation request. Requests are acknowledged with a reference number and queued for processing. - Maintain a Platform Admin view of pending anonymisation requests; Platform Admin processes these manually in MVP. Automated processing is post-MVP.

FR-73. confirmed_address and identity data stored in Booking records MUST be access-restricted to: the Resident, the Provider of the booking, the Payer of the booking, and Platform Admin. Integration clients receive this data only via explicitly authorised data push (FR-55).


§4.1 Listings & Inventory

FR-1. A Provider MUST be able to create a ListingUnit of type bed (shared room) or private_room (MVP types).

FR-2. Housing Core implements a two-level inventory hierarchy: Building → ListingUnit. Floor and Room intermediate levels are not supported in MVP. [OQ-010 resolved]

A Building entity MUST hold: name, address (street, city, postcode, country), coordinates (lat/lng), optional neighbourhood (free-text district or area name entered by Provider, e.g. "Noord", "Jordaan"), and optional media. Address and coordinates are stored once per Building and shared across all its ListingUnits; they are not duplicated per unit.

A ListingUnit MUST belong to exactly one Building and MUST carry: type (bed / private_room), title, description, capacity (max occupants), amenities list, media (photos and/or videos; Building media serves as fallback if unit has none), price, availability, unit mode (concrete or pool), and optional room area (room_area_m2 in m²; for bed-type units this is the total shared room area).

Media rules: - Supported media types: photos (JPEG, PNG) and videos (MP4). Both may coexist in a listing's media collection. - Each listing has exactly one hero image — the primary photo shown on listing cards in discovery. The hero must be a photo (not a video). The Provider explicitly designates the hero image; if the hero photo is deleted, the system automatically promotes the next photo in the collection to hero. - Videos are shown only in the listing detail gallery (with a Play button). A video is never used as the hero image on a listing card. - Provider Admin address entry uses Google Maps address autocomplete for fast input and geocoding. After autocomplete, a map preview is shown; the Provider can drag the map pin to manually adjust the coordinates before saving.

Currency: - MVP supports Euro (€) only. Multi-currency is out of scope. - All financial values are stored and displayed in Euro. Currency indicator (€) MUST appear on all forms and views where financial values are entered or displayed.

FR-3. A Provider MUST be able to set unit mode at creation: - concrete — Provider manages specific numbered units; each has its own ID; worker selects a specific unit at reservation. - pool — Provider declares a type and available count; specific assignment is communicated at check-in.

Design note — mixed bed/room booking (temporary MVP limitation): The two-level Building → ListingUnit hierarchy means a Provider cannot simultaneously offer individual beds and the full room as a unit from the same physical space without risk of double-booking. A physical room with 3 beds must be represented as either a bed-type ListingUnit (individual beds, Concrete or Pool mode) or a private_room-type ListingUnit (the whole room) — not both at the same time. If a Provider wants to temporarily switch, they must manually deactivate one listing before activating the other.

This is an accepted limitation for MVP. Workinn's current production system (GoMeddo / Charlie Works) does not use mixed bed/room booking — individual beds are the operational unit. GoMeddo natively supports a "virtual resource" (pool) concept that would enable booking a room as a unit while consuming individual bed slots, but surfacing this through Cruits Housing would require a third inventory level or a linked-ListingUnit mechanism. This capability may be added post-MVP, alongside or after the GoMeddo provider integration (Epic 8).

FR-4. A Provider MUST be able to update any ListingUnit attribute. A Provider MUST be able to deactivate a ListingUnit; deactivated units are removed from discovery but do not affect active bookings.

FR-5. A Provider MAY synchronize inventory via a provider integration module (§6). Synchronised units are represented as ListingUnits in Housing Core and are governed by all §4 rules.

FR-90. A Provider MAY set a Housing Quality Score (PKS) on a ListingUnit — a non-negative decimal number with no upper bound. PKS is optional; a listing without a PKS value remains fully functional and discoverable. PKS is displayed on the listing card and listing detail view when set. When the session context includes a PKS cost table (FR-45), the Housing Module uses the listing's PKS value to determine the effective maximum daily cost threshold for indirect payment eligibility; listings without a PKS value use the session's fallback max_daily_cost. PKS may be updated or cleared by the Provider at any time without affecting active bookings.

Design decision — Provider inventory management mode (mutual exclusion): A Provider operates in one of two inventory management modes, set at account setup via Django Admin: - Manual mode: The Provider manages all listings directly via Provider Admin (create, edit, deactivate, delete). No external integration is active for this Provider. - Integration mode: Inventory is synchronised from an external system (e.g. GoMeddo via Epic 8). Listings are read-only in Provider Admin and are managed exclusively through the integration.

Mixing both modes within a single Provider account is not supported — it creates unresolvable conflicts between manual and integration-managed listing state. A Provider wishing to switch modes must contact Platform Admin. The mode is not configurable by the Provider themselves.


§4.2 Availability & Pricing

FR-6. A Provider MUST be able to set a price per ListingUnit per rental period. Default Period unit (day, week, or month) is configurable per Provider.

FR-84. The system MUST maintain an immutable price change log per ListingUnit. Each entry records: price, price_period_unit, timestamp of the change, and the actor (Provider user) who made it. An entry is created on listing creation (initial price) and on every subsequent update to price or price_period_unit. The log is accessible to the Provider from the listing detail view in Provider Admin.

FR-7. A Provider MUST be able to block capacity for a ListingUnit for a specified date range via the availability calendar. Blocks are represented as bookings with booking_type = provider_block and status PROVIDER_BLOCKED. For pool-mode units, the Provider specifies how many slots to block (1 up to total_slots); partial blocking is supported. For concrete-mode units, the Provider blocks a specific slot or all slots. A provider block immediately reduces available_count for the affected period and is counted alongside resident bookings in all availability checks.

FR-8. A Provider MUST be able to configure payment terms per payer-provider agreement: prepay (N days before arrival), postpay (N days after arrival), or custom schedule. Terms are presented to the matching Payer at booking confirmation. All Payers are of a single type; no Payer type enumeration is required. [OQ-011 resolved]

Provider contract-only mode (part of FR-8 scope). A Provider MUST be able to enable an "Only payers with a special contract are allowed" flag via the Default Payment Terms section in Provider Admin. When this flag is on: (a) the Provider signals they work exclusively with third-party Payers who have an active PayerProviderAgreement set to indirect_payment = enabled; (b) direct worker payment (worker_direct) is not accepted for any of their listings; © the Default Payment Terms fields (payment schedule, billing period, invoice generation) are inactive while this flag is on, as those terms apply only to direct-payment bookings. Delivered in Story 1.9a (Iteration 2) as a checkbox in the Default Payment Terms block; payment terms fields remain placeholders until Story 1.9 (Iteration 4).

FR-9. A Provider MUST be able to set a minimum stay period per ListingUnit.

FR-85 [Nice to Have — post-MVP]. House Rules & Restrictions. A Provider MUST be able to configure house rules and restrictions per ListingUnit (e.g., no smoking, no pets, quiet hours after a specified time). Rules are displayed as a distinct "House Rules" section on the listing detail screen, separate from the amenities list.

FR-10. A Listing card in discovery MUST display: title, type, city, neighbourhood (if set on the Building), distance to reference point (if provided), price per period, available count (pool mode), PKS score (if set on the ListingUnit — FR-90), and at least one media item. Type displays as: bed → "Shared Room"; private_room → "Single Room". Location displays as "[City], [Neighbourhood]" when neighbourhood is set, or "[City]" when it is not.


§4.3 Booking Engine

Visual reference: payment-config-diagram.xml — full booking state machine with all payment configuration branches, pre-BOOKED cancellation (FR-75), and BOOKED-state events (Overstay · Ghost Alarm · Non-payment · Cancellation). Open in draw.io.

FR-11. A Flexworker MUST be able to create a Reservation for an available ListingUnit. On success: unit (or pool slot) is held; booking transitions to RESERVED.

FR-12. On Reservation creation, the system MUST immediately initiate provider confirmation (transition to PENDING_PROVIDER). For listings not backed by an external integration, this step is auto-confirmed instantly. [ASSUMPTION]

FR-13. The reservation hold window (maximum time before automatic release to RESERVATION_FAILED) MUST be configurable per Provider via Provider Admin Settings. System defaults apply when a Provider has not configured a value.

Hold window parameters and system defaults: standard hold window (default: 7 days); short-notice hold window (default: 24 hours); short-notice threshold — check-in within this many days triggers the short-notice window (default: 3 days); minimum-notice cap — the hold window MUST end no later than N days before period start (default: 2 days).

The PENDING_PAYER approval window is configured the same way per Provider, with a system default of 7 days. There is no short-notice variant for the payer approval window.

FR-14. On provider confirmation: - If payer ≠ resident → transition to PENDING_PAYER; notify Payer via configured channel. - If payer = resident → transition to SIGNING (if signing enabled) or directly to BOOKED (if no signing).

FR-15. A Payer MUST be able to confirm or reject a PENDING_PAYER reservation via Payer Admin UI or via direct API call. On confirmation: transition to SIGNING (if signing enabled) or to BOOKED (if no signing). When a Payer has auto_approve = Yes configured, the Housing Module automatically confirms all PENDING_PAYER reservations for that Payer without manual action. For Payers with auto_approve = No, no further automatic confirmation logic exists within the Housing Module; conditional approval logic (budget checks, worker eligibility rules) is the Payer system's responsibility and is exercised via the direct API call path.

FR-16. On payer rejection or hold window expiry → RESERVATION_FAILED; hold released; Flexworker and Payer notified.

FR-17. A BOOKED record MUST store: resident_id, payer_id, unit_id, period (start–end), price, payment_form, contract reference (if applicable), full state transition history with timestamps.

FR-18. Flexworker and Payer MUST each receive a notification at every state transition relevant to them.

Booking State Machine

stateDiagram-v2
    [*] --> RESERVED : Worker creates Reservation
    RESERVED --> PENDING_PROVIDER : Auto — on creation

    PENDING_PROVIDER --> PENDING_PAYER : Provider confirms\n[payer ≠ resident]
    PENDING_PROVIDER --> SIGNING : Provider confirms\n[payer = resident · signing enabled]
    PENDING_PROVIDER --> BOOKED : Provider confirms\n[payer = resident · no signing]
    PENDING_PROVIDER --> RESERVATION_FAILED : Provider rejects / timeout

    PENDING_PAYER --> SIGNING : Payer confirms\n[signing enabled]
    PENDING_PAYER --> BOOKED : Payer confirms\n[no signing]
    PENDING_PAYER --> RESERVATION_FAILED : Payer rejects / timeout

    SIGNING --> BOOKED : Signing completed
    SIGNING --> RESERVATION_FAILED : Signing expired

    BOOKED --> CHECKED_IN : Provider check-in
    BOOKED --> CANCELLED : Cancellation (before check-in)
    CHECKED_IN --> ENDED : Resident or Provider checkout
    RESERVATION_FAILED --> [*]
    ENDED --> [*]
    CANCELLED --> [*]

    [*] --> PROVIDER_BLOCKED : Provider creates block (FR-7)
    PROVIDER_BLOCKED --> CANCELLED : Provider removes block

FR-80. ENDED trigger. CHECKED_IN → ENDED MUST be initiated by an explicit actor action: (a) Resident self-checkout via the worker-facing UI, or (b) Provider checkout confirmation via Provider Admin. The system MUST NOT automatically transition a booking to ENDED solely because the period.end date has been reached. A booking can only reach ENDED from CHECKED_IN state — see FR-91.

FR-91. Provider Check-in. A Provider MUST be able to mark a BOOKED booking as checked in via Provider Admin. On check-in: the booking transitions to CHECKED_IN state; a BookingStateTransition entry is recorded with actor = provider; the Resident receives a push notification. CHECKED_IN is a mandatory intermediate state between BOOKED and ENDED: checkout (FR-80) is only available from CHECKED_IN; cancellation (FR-76) is only available from BOOKED.

FR-81. Overstay handling. When a CHECKED_IN booking's period.end date is reached and no checkout has been initiated: - The system MUST send an overstay notification to both the Provider and the Resident. - The booking MUST remain in BOOKED state. - The Provider MAY flag the booking as Overstay (a manual flag analogous to Ghost Alarm: no mandatory system consequence; event logged in booking history; notification sent to Payer).

Note: Non-payment by the Resident after the payer-covered period ends is a legal matter governed by art. 7:231 BW; resolution is outside the scope of the Housing Module.


§4.4 Payment Model

Visual reference: see planning-artifacts/payment-config-diagram.xml — Section A shows payment_form selection tree, cost check (FR-46), FR-82 notice, and payer=resident vs payer≠resident booking paths.

FR-19. Every Booking MUST carry independent resident_id and payer_id fields. These may reference the same entity.

FR-20. The system MUST support the following payment_form values: worker_direct, payer_prepay_deduct, payer_postpay_deduct, payer_subsidy_full, payer_subsidy_partial. The applicable form for a booking is determined by the worker's payment choice at reservation time (within the constraints of booking_context) or by payer-provider agreement.

FR-21. Payment terms (timing, schedule, deduction rules) MUST be configurable per payer-provider agreement and applied automatically on booking confirmation.

FR-79. A Provider MUST be able to enable automatic invoice generation per payer-provider agreement. When enabled: - A Provider MUST be able to upload an invoice template (PDF format) used to generate invoices for that agreement. - Invoices are generated automatically as PDF documents according to the payment schedule defined in the applicable payment terms (FR-8); invoice generation frequency matches the billing period unit (e.g., weekly terms → weekly invoices). - Generated invoices MUST be accessible to the Payer from the Settlements view (FR-36) and to the Provider from Provider Admin. - Payment tracking, outstanding balance calculation, and reconciliation are out of scope for MVP.


§4.5 Document Signing

FR-23. Document signing is OPTIONAL per Provider. If not configured, no signing step occurs in the booking flow.

FR-24. If document signing is enabled for a Provider, the signing step is inserted as the final step before BOOKED — after payer confirmation (when payer ≠ resident) or after provider confirmation (when payer = resident). This ensures all documents for a booking — including accommodation agreement and any payer-related documents — can be collected in a single signing envelope.

FR-25. A Provider MUST be able to configure the signing mode for their listings: - Per-booking: A new signing envelope is generated for each booking. The Resident signs for each booking. - Framework: A single contract is signed once per Resident-Provider pair. Subsequent bookings under the same pair proceed without re-signing. Any changes to period or price after the framework contract is signed are communicated via notification only; no new signature is required.

Both modes MUST be supported in MVP. Contract template format: [TBD]. The system generates a signing envelope from the provider-uploaded template for each booking where per-booking mode is active, or upon first booking in framework mode.

FR-26. The system MUST support integration-generated contracts: an integration module MAY provide a pre-generated contract document for a booking. This replaces the provider-uploaded template for that booking. If neither an integration-generated document nor a provider-uploaded template exists when a booking reaches the signing step, and signing is required, the booking MUST transition to RESERVATION_FAILED and the event MUST be logged for Platform Admin review.

FR-27. DocuSign is the signing platform for MVP. The Resident MUST sign. Payer signing is out of scope for MVP. Contract language, template localisation, and signing sequence are the Provider's responsibility; the Housing Module acts as a passthrough for envelope creation and completion events.

FR-28. On DocuSign envelope completion, the Housing Module MUST download and store the completed signed PDF to internal storage, independent of the DocuSign envelope lifecycle. The completed contract MUST be accessible by Resident, Provider, and Payer from the booking detail view, including after the booking reaches ENDED or CANCELLED. The completed contract MAY also be transferred to the Provider's system via API.

FR-74. If signing is enabled and the DocuSign signing envelope expires before all required signatures are collected, the booking MUST transition to RESERVATION_FAILED. The Flexworker MUST be notified. Envelope resend is not supported in MVP; the worker must create a new Reservation from scratch.


§4.6 Provider Admin

FR-29. A Provider MUST have access to a desktop-first management UI. The following actions MUST be available:

Section Minimum Actions
Listings Create, edit, deactivate; view list
Availability Calendar Set available / unavailable by date range per unit or pool
Document Templates Upload contract template; view active template; replace; configure signing mode (per-booking or framework)
Active Residents View list with per-booking status; mark Resident as checked in; raise Ghost Alarm
Archive View past bookings (ENDED / CANCELLED) with same fields as Active view
Pricing & Payment Terms Set price per period; configure payment terms per payer-provider agreement
Settings Configure account-level defaults: default rental period unit and default minimum stay period for new listings
Messaging Send and receive messages per active booking
Invoice Settings Enable automatic invoice generation per payer-provider agreement; upload invoice template (PDF)

FR-30. The Active Residents view MUST show per booking: Resident name, unit, period, booking status, payment status.

FR-31. A Provider MUST be able to raise a Ghost Alarm for any active Resident. Effect: system sends notification to the Payer and the Resident; event is logged in Booking history. No mandatory system consequence follows.

FR-32. A Provider MUST receive a notification when a Ghost Alarm is raised by a Payer for one of their Residents.

FR-33. The Archive view MUST display past bookings (state: ENDED or CANCELLED) with the same fields as the Active view.


§4.7 Payer Admin

FR-34. A Payer MUST have access to a desktop-first management UI. The following actions MUST be available:

Section Minimum Actions
Residents View active and historical bookings for this Payer's residents; view booking detail; confirm or reject pending reservations
Settlements View financial report (amount, period, status per booking; aggregate totals); export
Notifications View booking state transition notifications; raise Ghost Alarm

FR-35. The Residents view MUST show per booking: Resident name, unit, Provider, period, booking status, payment status.

FR-36. The Settlements view MUST provide a financial report: amount per booking, period, status (paid / pending / overdue), and aggregate totals.

FR-38. A Payer MUST be able to raise a Ghost Alarm for any of their active Residents (same mechanics as FR-31; notifications go to Provider + Resident).

FR-39. A Payer MUST receive a notification when a Ghost Alarm is raised by the Provider for one of their Residents.


§4.8 Messaging

FR-40. The system MUST provide in-app messaging between a Resident (via client integration) and the Provider for each active Booking.

FR-41. Message history MUST be preserved for the duration of the Booking and for the same retention period as the booking record itself (i.e., until the booking record is purged or anonymised). After ENDED or CANCELLED, message history remains accessible to Provider (from the Archive view) and to Resident (from their booking history). Access rights mirror those of the archived booking record. [OQ-008 resolved]

FR-42. MVP messaging is in-app only. WhatsApp and other external channel integrations are post-MVP.

FR-43. Booking state transition notifications are delivered as follows: Flexworker receives in-app push notification (via the Cruits mobile app); Provider receives in-app desktop notification; Payer receives in-app desktop notification. Email fallback for all actors is post-MVP.


§4.9 Cancellation

Visual reference: see planning-artifacts/payment-config-diagram.xml — Section B (Cancellation branch): window check, reason input, disclaimer + acknowledgement, notify-all-3, CANCELLED terminal; pre-BOOKED path (FR-75) shown as sidebar note.

FR-75. A Flexworker MAY cancel a booking in any pre-BOOKED state (RESERVED, PENDING_PROVIDER, SIGNING, PENDING_PAYER). On cancellation: the unit hold is released; all parties are notified; booking state transitions to CANCELLED.

FR-76. Cancellation of BOOKED bookings. [OQ-009 resolved] - Permitted actors: Flexworker (Resident), Provider, and Payer may each initiate cancellation of a BOOKED booking. - Reason required: The cancelling actor MUST select or enter a cancellation reason before submitting. The reason is stored in the booking history. - Disclaimer: The UI MUST display a confirmation screen before the cancellation is submitted, informing the cancelling actor that by proceeding they assume responsibility as defined by the accommodation contract and applicable legislation (WGV, BW art. 7:231). The actor MUST explicitly acknowledge this disclaimer before the cancellation proceeds. - Cancellation window: Each Provider and each Payer MAY configure a minimum-notice cancellation window (e.g., 48 hours before period.start). If configured, the system MUST reject an out-of-window cancellation attempt with an appropriate error. Flexworker-initiated cancellations are subject to the Provider's configured window. - Notifications: On cancellation, all three parties (Flexworker, Provider, Payer) MUST receive a notification stating the cancelling actor's identity and the stated reason. - State transition: BOOKED → CANCELLED; unit hold released. Cancellation is not available once a booking has reached CHECKED_IN state (FR-91). - Financial consequences: The system records the cancellation event, actor, and reason in booking history. Penalty enforcement, refund processing, and financial settlement are outside the Housing Module scope for MVP and are governed by the accommodation contract.


Dutch Wet goed verhuurderschap (BWBR0048028, in force 1 July 2023, Art. 2 lid 3) requires that rental agreements for labour migrants be recorded separately from employment contracts and that the rental lifecycle be independent of the employment lifecycle — on three layers: document, lifecycle, and legal contingency. Violations carry administrative fines up to €90,000 per offence and public naming-and-shaming publication. See 10-legal-research/wet-goed-verhuurderschap-bed-en-baan-separation.md for full analysis.

The key operational constraint for this system:

A booking system that auto-ends a rental when the payer's covered period ends — and the covered period equals the placement period — creates a de-facto lifecycle coupling that is non-compliant, even if the rental contract is a physically separate document.

Compliance decisions

Risk Decision Implemented by
System auto-ENDED at period.end = placement end ENDED is never auto-triggered; explicit checkout action required FR-80
Worker unaware of right to extend independently Mandatory confirmation notice at reservation + 28-day advance notification FR-82, FR-83
Worker cannot stay past period.end Booking stays BOOKED past period.end; Provider flags Overstay if needed FR-81
SNF 4-week post-employment housing right Cruits is contractually responsible for encoding this in permitted_reservation_period FR-45 note
permitted_reservation_period creates de-facto coupling It constrains what the payer covers, not what the worker may occupy; the worker retains the right to continue via direct payment (FR-82(b)) FR-82, FR-45 note

Note on direct payment availability

A Provider or integration client may restrict worker_direct as a payment option for business reasons — this is not a per-se WGV violation. However, when worker_direct is not available in a worker's booking_context, the integration client (Cruits) must ensure that either: (a) the worker can access a direct-payment session via an alternative path, or (b) the provider–client agreement includes a post-placement housing continuity arrangement that satisfies SNF norm 5.2. The Housing Module does not enforce this; it is a contractual obligation between Cruits and its provider partners.

FR-83. Booking period end notification. The system MUST notify the Resident in advance of their booked period ending: - For bookings with period ≥ 28 days: a "booking period ending soon" notification MUST be sent to the Resident 28 days before period.end. The notification MUST inform the Resident of their right to make a new reservation for continued housing. - For bookings with period < 28 days: this notification content MUST be included in the BOOKED confirmation notification sent at the time of booking confirmation.


§5. Feature Requirements — Cruits Integration

All worker-facing housing UI in MVP lives within this integration layer. Housing Core exposes APIs; Cruits renders native UI pages in its own PWA and mobile apps.

§5.1 Context Handshake

Visual reference: see planning-artifacts/payment-config-diagram.xml — booking_context entry point, reservation-permitted check, and direct/indirect payment option branches (FR-44–46).

FR-44. The Cruits Integration MUST pass the following to the Housing Module on each worker session launch: worker_id, job_location (coordinates and display name, when entry point has job context), and a booking context.

FR-45. The booking context MUST specify which payment options are available to the worker for this session: - Whether reservation is permitted at all (browse-only mode). - Whether the worker may pay directly (direct payment option). - Whether the worker may reserve with a designated third-party payer (indirect payment option). If enabled, the booking context MUST also specify: the permitted reservation period (a start date and an end date, typically tied to the worker's employment period), the maximum daily cost, and the billing period unit.

Any combination of payment options is valid: both may be available simultaneously, only one may be available, or neither (browse-only).

Note — SNF compliance responsibility: The integration client (Cruits) is solely responsible for ensuring that the permitted_reservation_period start/end dates it passes comply with SNF norm 5.2 (minimum 4-week post-employment housing right), the applicable ABU/NBBU uitzend-CAO, and WGV Art. 2 lid 3. The Housing Module enforces these values as received.

FR-46. The Housing Module MUST enforce all booking context constraints as server-side API invariants. A browse-only session MUST be rejected at the API level if a reservation attempt is made. An indirect-payment reservation MUST be rejected server-side if the unit's cost exceeds the maximum daily cost at the moment of Reservation creation.

Payment Choice at Reservation

flowchart TD
    Cruits["Cruits App"] -->|"worker_id · job_location · booking_context"| HM["Housing Module"]
    HM --> ResEnabled{"Reservation\npermitted?"}
    ResEnabled -->|"no"| Browse["Browse only\n(server-enforced)"]
    ResEnabled -->|"yes"| PayOpts["Payment options\navailable to worker"]
    PayOpts --> Direct["Direct payment\n(if enabled in context)"]
    PayOpts --> Indirect["Indirect payment\n(if enabled in context,\nand price ≤ max daily cost)"]
    Direct --> SM1["State Machine\npayer = resident path\n→ BOOKED"]
    Indirect --> SM2["State Machine\npayer ≠ resident path\n→ PENDING_PAYER"]
    SM1 --> Push["Push data to Cruits"]
    SM2 --> Push

§5.2 Worker-Facing UI

FR-47. A Flexworker MUST be able to browse available Listings sorted by proximity to the selected job location. When no job location is selected, listings are sorted by weekly price ascending. [ASSUMPTION]

FR-48. A Flexworker MUST be able to filter listings by: job location (dropdown of all active platform job locations in "Employer, City" format; pre-selected from session context when available), room type, maximum distance from selected location (inactive when no location is selected), maximum price per period, amenities (multi-select from the fixed Amenity Reference list; AND logic — listing must have all selected amenities), and stay period (check-in and check-out dates). When stay dates are specified, listings that are fully unavailable for the entire requested period are excluded from discovery results. For indirect-payment sessions, the stay dates filter is pre-populated with permitted_period_start/end from the booking context and constrained to that range.

FR-49. A Flexworker MUST be able to view a Listing detail: media, title, description, room type, price per period, distance to job site, available count (pool) or unit identifiers (concrete).

FR-50. A Flexworker MUST be able to create a Reservation from the Listing detail view when the booking context permits.

FR-82. Indirect-payment reservation confirmation notice. When a Flexworker creates a Reservation under indirect payment (payer ≠ resident), the booking flow MUST display a confirmation notice before the Reservation is submitted. The notice MUST inform the worker: - (a) The period covered by the payer and the payer's name. - (b) That the worker may independently book housing for any period outside the payer-covered window, including via direct payment if that option is available to them. - © That the worker will receive a reminder notification 28 days before the booked period ends (or, for stays shorter than 28 days, this information is included in the booking confirmation message).

The worker MUST explicitly acknowledge this notice before the Reservation is submitted.

FR-51. For concrete-mode listings: the Reservation step MUST present available specific units (identified by floor / room / bed) for the worker to select before confirming.

FR-52. For pool-mode listings: the Reservation step confirms a pool slot; specific unit assignment is communicated by the Provider at check-in.

FR-53. A Flexworker MUST be able to view the current status and details of their active Reservation or Booking.

FR-54. A Flexworker MUST be able to access in-app messaging with their Provider from the booking status view. Messaging is initiated from an active booking and "Message" button on the listing detail screen (pre-booking, no active booking yet)

FR-86 [Nice to Have — post-MVP]. Saved/Bookmarked Listings. A Flexworker MUST be able to save a listing to a personal bookmark list from the listing card or detail screen, and view saved listings in a dedicated section. Bookmarks are tied to the worker's identity and persist across sessions.

FR-87 [post-MVP]. Listing Ratings & Reviews. A listing detail screen MUST display an aggregate star rating and individual text reviews submitted by past Residents after checkout. Ratings are averaged across all reviews for the listing.

FR-88 [Nice to Have — post-MVP]. Interactive Map on Listing Detail. The listing detail screen MUST include an interactive map widget showing the listing's pin location, using the coordinates stored on the Building. Map rendering uses the Maps SDK already integrated in the Cruits app; no SDK selection is required.


§5.3 Data Return to Cruits

FR-55. On transition to BOOKED, the Housing Module MUST push to the Cruits Integration: confirmed_address, booking_status, period (start–end), price_per_period, payment_form. The push payload MUST include a unique event_id; delivery is at-least-once; Cruits-side deduplication on event_id is expected. [ASSUMPTION]

FR-56. On Booking cancellation or RESERVATION_FAILED, the Housing Module MUST push a status update to Cruits, including a unique event_id. [ASSUMPTION]

FR-77. For pool-mode bookings, when the Provider assigns a specific unit to a Resident post check-in, the Housing Module MUST update the confirmed_address in the Booking record and re-trigger the data push to the Cruits Integration with the updated address and a new event_id. [ASSUMPTION]


§6. Feature Requirements — Workinn Integration

This section defines requirements for Workinn Real Estate as the first provider. Workinn manages inventory in GoMeddo. §4 and §5 have zero Workinn-specific dependencies.

§6.1 Inventory Sync

FR-57. The Workinn integration module MUST sync Workinn inventory from GoMeddo into Housing Core as ListingUnits, operating in concrete unit mode.

FR-58. Each sync cycle MUST capture per unit: type (Bed / Private Room), GoMeddo resource hierarchy position (Building → Floor → Apartment → Bed), address, price, media, and availability status. The GoMeddo Building maps to a Housing Building entity (address and coordinates stored at this level); the GoMeddo Bed maps to a Housing ListingUnit. Intermediate GoMeddo levels (Floor, Apartment) are consumed during sync for media fallback resolution (FR-59) but are not persisted as separate entities in Housing Core.

FR-59. The integration module MUST apply GoMeddo media fallback during sync: if a Bed has no media, traverse up the GoMeddo hierarchy — Apartment → Floor → Building — until media is found; if none is found at any level, assign a placeholder image. The resolved media is stored on the Housing ListingUnit. GoMeddo Building-level media is also stored on the Housing Building entity for use as a general property-level fallback.

FR-60. GoMeddo inventory sync is event-driven: the integration module MUST subscribe to GoMeddo webhook events for unit availability and price changes. A Platform Admin MUST be able to trigger a full resync manually. Delta-sync is the default operating mode; full resync is used for recovery and initial onboarding.

FR-78. Sync operations MUST NOT alter the availability, price, or status of any ListingUnit that has a booking in a non-terminal state (RESERVED, PENDING_PROVIDER, SIGNING, PENDING_PAYER, or BOOKED). If a sync event conflicts with an active booking, the system MUST create a conflict record and notify Platform Admin. The booking state is preserved until the conflict is manually resolved.


§6.2 Provider Confirmation Flow

FR-61. When a Reservation for a Workinn listing reaches PENDING_PROVIDER, the integration module MUST call the GoMeddo API to confirm availability and place a hold.

FR-62. A GoMeddo confirmation response MUST trigger the state transition per FR-14.

FR-63. A GoMeddo rejection or timeout MUST trigger RESERVATION_FAILED per FR-16. Timeout threshold and retry policy: see NFR-8.


§6.3 Document Integration

FR-64. If GoMeddo generates an accommodation agreement PDF for a booking, the integration module MUST provide this document to Housing Core as an integration-generated contract (FR-26), replacing the provider-uploaded template for that booking. If GoMeddo does not provide a document, Housing Core falls back to the provider-uploaded template (FR-25); if no template exists and signing is required, the booking transitions per the FR-26 error path.


§7. Non-Functional Requirements

NFR-1. Provider-agnostic architecture. Adding Provider #2 MUST require zero changes to Housing Core (§4) or Cruits Integration (§5) code.

NFR-2. Client-agnostic architecture. Adding a new client (standalone housing app, Charlie Works) MUST require zero changes to Housing Core (§4).

NFR-3. Platform scope. Cruits Integration UI (worker-facing) is rendered natively by the Cruits app and MUST be fully functional on iOS, Android, and PWA. Provider Admin and Payer Admin are desktop-first; mobile compatibility is not required for MVP.

NFR-4. Geography. MVP geography is the Netherlands. Address format, BRP compliance, and legal references are NL-specific.

NFR-5. Language. All MVP UIs are English-only. Polish and Romanian support are post-MVP.

NFR-5a. Currency. MVP supports Euro (€) only. Multi-currency is out of scope. All financial values are stored and displayed in Euro; a currency indicator (€) MUST appear on all financial input fields and display values across all UIs.

NFR-6. Performance. The discovery listings list MUST display the first results within 3 seconds measured as LCP on a 4G connection. The list uses infinite scroll (lazy load) — results load progressively as the user scrolls; there is no numbered page navigation. [ASSUMPTION]

NFR-7. Availability. Target uptime during NL business hours (Monday–Friday, 08:00–18:00 CET/CEST): 99.5%. Availability SLA outside business hours is TBD.

NFR-8. Integration resilience. Each external dependency MUST have a defined failure behaviour: - GoMeddo sync failure: retain last known ListingUnit data; log error; notify Platform Admin. - GoMeddo confirmation timeout: retry up to 2 times with 5-second backoff; if all retries exhausted, trigger RESERVATION_FAILED per FR-63. - DocuSign envelope creation failure: retry once after 60 seconds; if retry fails, log event and notify Platform Admin; booking remains in PENDING_PROVIDER until manually resolved. - Housing-to-Cruits webhook delivery failure: retry with exponential backoff (3 attempts); if all retries exhausted, alert Platform Admin; Booking state in Housing is not affected.


§8. User Journeys

UJ-1: Worker books housing — worker pays directly Actor: Flexworker (§2.1). Booking context: direct payment option enabled.

  1. Worker opens Housing tab in Cruits. Sees Listings near job location.
  2. Browses, filters, opens a Listing detail.
  3. Taps Reserve. Selects "I pay" option. For concrete units: selects specific unit. Confirms.
  4. System: RESERVEDPENDING_PROVIDER.
  5. Provider confirms (payer = resident, no PENDING_PAYER).
  6. If signing required: DocuSign envelope sent; Worker signs → BOOKED.
  7. If no signing: → BOOKED directly.
  8. Housing pushes confirmed address + booking data to Cruits.
  9. Worker sees booking confirmation with address and period.

UJ-2: Worker books housing — third-party payer (e.g. Cruits) Actor: Flexworker (§2.1). Booking context: indirect payment option enabled.

  1. Worker opens Housing tab in Cruits. Sees available options at reservation: direct payment and indirect payment.
  2. Listings where price exceeds the context maximum show indirect payment option as disabled with explanation.
  3. Worker selects eligible listing, chooses indirect payment at reservation. Confirms.
  4. System: RESERVEDPENDING_PROVIDER.
  5. Provider confirms → PENDING_PAYER. Payer notified.
  6. Payer confirms manually via Payer Admin, or Payer system confirms via API (see UJ-5).
  7. If signing required: signing envelope sent → Worker signs → BOOKED.
  8. Housing pushes data to Cruits. Worker notified.

UJ-3: Provider onboards and lists inventory (manual) Actor: Housing Provider (§2.2).

  1. Provider signs in to Provider Admin. Creates ListingUnits with type, location, pricing, media.
  2. Configures availability calendar.
  3. Configures payment terms per payer-provider agreement (FR-8).
  4. Optionally configures document signing: uploads contract template and selects signing mode — per-booking or framework (FR-25).
  5. Listings appear in discovery for eligible workers.

UJ-4: Ghost Alarm raised by Provider Actor: Housing Provider (§2.2).

  1. Provider notices a Resident has not been seen for several days.
  2. Provider opens Resident record → triggers Ghost Alarm.
  3. System sends notification to Payer and Resident. Logs event in Booking history.
  4. Payer sees Ghost Alarm flag in Payer Admin. Takes action as appropriate.

UJ-5: Payer confirms a pending reservation Actor: Payer (§2.3).

  1. Payer Admin receives notification: new reservation pending confirmation.
  2. Payer opens Residents view; sees booking in PENDING_PAYER with Resident name, unit, Provider, period, price.
  3. Payer reviews details and confirms manually — or Payer system confirms via API call.
  4. Booking transitions to SIGNING (if signing enabled) or BOOKED (if no signing). Flexworker and Provider notified.

Rejection path: Payer rejects → RESERVATION_FAILED; Flexworker notified.


UJ-6: Platform Admin onboards a new Provider Actor: Platform Admin (§2.4).

  1. Platform Admin creates a Provider account in Housing Module (name, contact, configuration).
  2. Configures Provider-level settings: signing requirement. (Provider configures their own hold window via Provider Admin Settings.)
  3. Optionally links Provider to a provider integration module (e.g., Workinn).
  4. Provider receives access credentials and signs in to Provider Admin.
  5. Provider adds listings per UJ-3; inventory appears in discovery.

§9. Post-MVP / Out of Scope

Item Notes
Whole-unit apartment booking Post-MVP
Standalone housing app (worker-facing, independent of Cruits) Post-MVP; Housing Core designed to support it with zero core changes
WhatsApp / external messaging integration Post-MVP
Provider #2 integration Post-MVP; architecture supports addition without core changes
Polish and Romanian UI Post-MVP
Web fallback housing search (outside Cruits app) Post-MVP
Workinn Services App integration Post-MVP (app does not yet exist)
Charlie Works integration Post-MVP
Automated GDPR anonymisation Post-MVP; manual process via Platform Admin in MVP
Email fallback for booking notifications Post-MVP
Payer signing of booking contracts Post-MVP
House rules & restrictions per listing Post-MVP / Nice to Have — FR-85
Saved/bookmarked listings Post-MVP / Nice to Have — FR-86
Listing ratings and reviews Post-MVP — FR-87
Interactive map on listing detail Post-MVP / Nice to Have — FR-88; Maps SDK already in Cruits app
Pre-booking inquiry (messaging from listing detail) Post-MVP — FR-89; MVP messaging scoped to active bookings only

§10. Open Items

All questions resolved prior to Iteration 1 kickoff. See RAID Log — Closed Open Questions for full history (OQ-001–OQ-012).


§11. Assumptions

See RAID Log — Assumptions (A-01, A-02, A-04–A-09).

Comments