Files
perauset-e2e-mrfzptpr/ai/memory-bank/tasks/perauset-implementation-plan.md
2026-07-11 08:35:17 +02:00

56 KiB

Perauset Village Operations Platform - Implementation Plan

Current State

  • Repo: /Users/yasser/git/perauset
  • Pikku monorepo (Yarn 4, TypeScript, PostgreSQL)
  • Backend on port 6002 (uWebSockets + Pikku)
  • One migration sql/0001-init.sql creating app.user table
  • One function: health-check.function.ts
  • Kysely for DB types, CFWorkerSchemaService for validation
  • DB types generated via kysely-codegen with --default-schema app

Architectural Decisions

Migration Numbering

Continue sequential from existing 0001-init.sql:

  • 0002-roles-permissions.sql
  • 0003-audit-log.sql
  • 0004-stays.sql
  • 0005-retreats.sql
  • 0006-rooms.sql
  • 0007-boats.sql
  • 0008-tasks.sql
  • 0009-kitchen-dietary.sql
  • 0010-inventory.sql
  • 0011-notifications.sql
  • 0012-seed-data.sql

All tables go in the app schema. After each migration batch, run yarn workspace @perauset/functions db:types to regenerate db.types.ts.

Folder Organization within packages/functions/src/

packages/functions/src/
  application-types.d.ts          # UserSession, Config, Services types
  config.ts                       # createConfig
  services.ts                     # createSingletonServices
  middleware.ts                   # CORS etc
  db.types.ts                    # Generated by kysely-codegen

  functions/
    health-check.function.ts     # Existing

    auth/
      get-session.function.ts
      login.function.ts

    users/
      get-user.function.ts
      update-user-profile.function.ts
      list-users.function.ts

    roles/
      assign-role.function.ts
      revoke-role.function.ts
      list-roles.function.ts
      list-user-roles.function.ts

    stays/
      request-stay.function.ts
      review-stay-request.function.ts
      approve-stay-request.function.ts
      reject-stay-request.function.ts
      create-stay-from-request.function.ts
      check-in-stay.function.ts
      check-out-stay.function.ts
      extend-stay.function.ts
      cancel-stay.function.ts
      override-stay-dates.function.ts
      list-stays.function.ts
      list-stay-requests.function.ts

    rooms/
      create-room.function.ts
      block-room.function.ts
      request-room-preference.function.ts
      allocate-room.function.ts
      reallocate-room.function.ts
      release-room.function.ts
      cancel-room-allocation.function.ts
      list-rooms.function.ts
      list-room-allocations.function.ts

    boats/
      create-boat-route.function.ts
      create-boat.function.ts
      create-boat-trip.function.ts
      open-boat-trip.function.ts
      request-boat-seat.function.ts
      review-boat-request.function.ts
      confirm-boat-request.function.ts
      waitlist-boat-request.function.ts
      decline-boat-request.function.ts
      cancel-boat-request.function.ts
      board-manifest-entry.function.ts
      complete-boat-trip.function.ts
      list-boat-trips.function.ts
      list-boat-requests.function.ts

    tasks/
      create-task-template.function.ts
      generate-recurring-tasks.function.ts
      create-task-instance.function.ts
      assign-task.function.ts
      claim-task.function.ts
      accept-task-assignment.function.ts
      decline-task-assignment.function.ts
      start-task.function.ts
      complete-task.function.ts
      mark-task-blocked.function.ts
      reassign-task.function.ts
      list-tasks.function.ts

    retreats/
      create-retreat.function.ts
      update-retreat.function.ts
      add-retreat-person.function.ts
      update-retreat-person-role.function.ts
      link-stay-to-retreat-person.function.ts
      publish-retreat.function.ts
      add-retreat-schedule-item.function.ts
      update-retreat-schedule-item.function.ts
      cancel-retreat.function.ts
      list-retreats.function.ts

    kitchen/
      upsert-dietary-profile.function.ts
      create-meal-service.function.ts
      estimate-meal-attendance.function.ts
      adjust-meal-attendance.function.ts
      list-meal-services.function.ts

    inventory/
      request-inventory.function.ts
      review-inventory-request.function.ts
      fulfill-inventory-request.function.ts
      list-inventory.function.ts

    notifications/
      list-notifications.function.ts
      mark-notification-read.function.ts

    audit/
      list-audit-log.function.ts

  lib/
    audit-context.ts             # Shared helper: sets app.current_user_id on Kysely connection per-request
    permissions.ts               # Tag-based permissions: addPermission('stays.review', [...]) registered per tag
    state-machine.ts             # Shared helper: validateTransition(entity, fromStatus, toStatus)
    errors.ts                    # Domain-specific error classes

Each function file follows the existing Pikku pattern: export a function using pikkuFunc or pikkuSessionlessFunc, define routes with defineHTTPRoutes, and wire them with wireHTTPRoutes.

Function Convention: Zod Schemas + Rich Metadata

All functions must use Zod schemas for input and output with .describe() on every field. Functions must include description and tags metadata. This ensures AI agents (MCP, RPC, etc.) can understand and use the API effectively.

Pattern:

import { z } from 'zod'
import { pikkuFunc, defineHTTPRoutes, wireHTTPRoutes } from '#pikku/pikku-types.gen.js'

export const ApproveStayRequestInput = z.object({
  requestId: z.string().uuid().describe('The stay request ID to approve'),
  reviewNotes: z.string().optional().describe('Optional notes from the reviewer explaining the approval'),
})

export const ApproveStayRequestOutput = z.object({
  requestId: z.string().uuid().describe('The approved stay request ID'),
  status: z.string().describe('New status of the request (approved)'),
})

export const approveStayRequest = pikkuFunc({
  description: 'Approve a submitted or under-review stay request. Transitions status to approved.',
  tags: ['stays.review'],
  auth: true,
  input: ApproveStayRequestInput,
  output: ApproveStayRequestOutput,
  func: async ({ kysely }, { requestId, reviewNotes }, { session }) => {
    // ...implementation
  },
})

export const approveStayRequestRoutes = defineHTTPRoutes({
  auth: true,
  routes: {
    approveStayRequest: {
      route: '/stays/requests/:requestId/approve',
      method: 'post',
      func: approveStayRequest,
    },
  },
})

wireHTTPRoutes({ routes: { approveStayRequest: approveStayRequestRoutes } })

Rules:

  • Every Zod field gets .describe('...') with a human-readable explanation
  • Every function gets description explaining what it does, state transitions, and required permissions
  • Every function gets tags for categorization (domain module + role scope)
  • Input/output schemas are exported as named constants (e.g., ApproveStayRequestInput) for reuse
  • Use z.string().uuid() for IDs, z.string().email() for emails, z.coerce.date() for dates, etc.

E2E Test Approach

Runner: Cucumber.js with tsx for TypeScript. Gherkin .feature files for readable specs, step definitions in TypeScript. API-only tests (no browser/Playwright).

Structure:

e2e/
  package.json
  tsconfig.json
  tests/
    cucumber.mjs                 # Cucumber config
    support/
      api-client.ts              # HTTP client wrapping fetch, handles auth cookies
      config.ts                  # API URL (http://localhost:6099), test DB name (perauset_test)
      hooks.ts                   # BeforeAll: start server + run migrations + seed; AfterAll: stop server
      world.ts                   # Custom World with api client, auth state, helper methods
      db.ts                      # Direct pg Client for assertions and cleanup
    features/
      phase-0/
        health-check.feature
        users.feature
        roles-permissions.feature
        audit-log.feature
      phase-1/
        stays.feature
        rooms.feature
        boats.feature
        tasks.feature
        notifications.feature
      phase-2/
        retreats.feature
        kitchen.feature
        inventory.feature
    steps/
      phase-0/
        health-check.steps.ts
        users.steps.ts
        roles-permissions.steps.ts
        audit-log.steps.ts
      phase-1/
        stays.steps.ts
        rooms.steps.ts
        boats.steps.ts
        tasks.steps.ts
        notifications.steps.ts
      phase-2/
        retreats.steps.ts
        kitchen.steps.ts
        inventory.steps.ts

Server lifecycle: The hooks.ts BeforeAll spawns the backend server process (tsx backend/bin/start.ts) on port 6099, runs migrations against a perauset_test database, seeds initial data. AfterAll kills the server process.

Auth for tests: Uses @pikku/auth-js. The world.ts creates an auth session by calling the Auth.js CSRF + credentials callback flow (same pattern as makeanagent e2e), then passes session cookies on all API calls.

API calls: fetch() wrapped in api-client.ts with base URL, JSON headers, and session cookie forwarding. Methods: apiGet(path), apiPost(path, body), apiPut(path, body).

Assertions: Node.js built-in assert module in step definitions.

Running: yarn workspace @perauset/e2e test which runs cucumber-js --config tests/cucumber.mjs.

Auth Approach

Uses @pikku/auth-js (same as makeanagent):

  • Middleware: authJsSession({ secretId: 'AUTH_SECRET', mapSession: (claims) => ({ userId: claims.sub }) }) added to HTTP middleware chain
  • Auth routes: createAuthRoutes(config) + wireHTTPRoutes to expose /auth/csrf, /auth/session, /auth/signin, /auth/callback/:provider, /auth/signout
  • Auth wiring: Separate packages/functions/src/wirings/auth.wiring.ts file (following makeanagent pattern)
  • Dev/test provider: Credentials provider for development/testing. Real providers (Google, etc.) added later.
  • Session: JWT-based via Auth.js cookies. UserSession populated by middleware on every request.

Work Packages


WP-01: E2E Test Infrastructure

Dependencies: None Estimated Complexity: M

Description: Set up the e2e test workspace with Cucumber.js, server lifecycle management, API client, and a passing health-check feature.

Deliverables:

  • e2e/package.json — name: @perauset/e2e, deps: @cucumber/cucumber, tsx, typescript, pg, @types/node
  • e2e/tsconfig.json — target ES2022, module Node18, moduleResolution node16
  • e2e/tests/cucumber.mjs — Cucumber config: requireModule tsx, require support + steps, paths to features, format progress-bar + html report
  • e2e/tests/support/config.ts — test API URL (http://localhost:6099), test DB name (perauset_test), timeout
  • e2e/tests/support/api-client.tsapiGet(path, cookies?), apiPost(path, body, cookies?), apiPut(path, body, cookies?) wrapping fetch with base URL, JSON headers, and cookie forwarding
  • e2e/tests/support/hooks.ts — BeforeAll: spawn backend on port 6099 with perauset_test DB env vars, run migrations, wait for server ready. AfterAll: kill server process. Pattern follows makeanagent e2e hooks.
  • e2e/tests/support/world.ts — Custom Cucumber World with apiGet/apiPost/apiPut methods, lastResponse state, lastBody state, optional session cookies
  • e2e/tests/support/db.ts — direct pg Client connection to perauset_test for assertions and cleanup
  • e2e/tests/features/phase-0/health-check.featureGET /health-check returns 200 with { ok: true }
  • e2e/tests/steps/phase-0/health-check.steps.ts — step definitions for health-check feature
  • Update root package.json to add e2e to workspaces and add test:e2e script

Verification:

  • yarn workspace @perauset/e2e test starts the server, runs the health-check feature, passes, shuts down the server
  • Server process does not remain running after tests complete

WP-02: Shared Library - Permissions, State Machine, Errors

Dependencies: None Estimated Complexity: S

Description: Create the shared library modules that all domain functions will depend on. These are pure TypeScript modules with no database dependencies yet (they accept a Kysely instance as a parameter). Note: audit logging is handled entirely by PostgreSQL triggers (see WP-04), so no application-level activity log helper is needed.

Deliverables:

  • packages/functions/src/lib/errors.ts -- NotFoundError, ForbiddenError, ConflictError, ValidationError extending a base DomainError class
  • packages/functions/src/lib/state-machine.ts -- validateTransition(entityType, fromStatus, toStatus) that checks against a map of allowed transitions for each entity type (stay_requests, stays, boat_reservation_requests, task_instances); throws ConflictError on invalid transition
  • packages/functions/src/lib/permissions.ts -- Uses Pikku's addPermission(tag, [checkFn]) to register tag-based permission checks. For each permission tag (e.g. stays.review, rooms.manage), registers a function that queries user_role_assignments + role_permissions + permissions to check if the current user has that permission key. Pikku automatically runs these checks before any function tagged with the matching tag. Supports scoped permissions (e.g. retreat-scoped facilitator roles). Will compile but not run until migration 0002.

Verification:

  • yarn workspace @perauset/functions tsc passes (type-checks cleanly even though DB tables don't exist yet)
  • State machine unit logic can be verified: valid transitions return void, invalid transitions throw

WP-03: Migration 0002 - Roles, Permissions, User Role Assignments

Dependencies: WP-02 Estimated Complexity: M

Description: Create the RBAC schema. Roles are named records. Permissions are granular action keys. role_permissions maps roles to permissions. user_role_assignments maps users to roles with optional scope and time bounds.

Deliverables:

  • sql/0002-roles-permissions.sql:
    app.role (role_id UUID PK, name TEXT UNIQUE NOT NULL, description TEXT, is_system BOOLEAN DEFAULT false, created_at, updated_at)
    app.permission (permission_id UUID PK, key TEXT UNIQUE NOT NULL, description TEXT, module TEXT NOT NULL)
    app.role_permission (role_id FK, permission_id FK, PK(role_id, permission_id))
    app.user_role_assignment (assignment_id UUID PK, user_id FK, role_id FK, scope_type TEXT, scope_id UUID, valid_from TIMESTAMPTZ, valid_until TIMESTAMPTZ, assigned_by UUID FK, created_at)
    
  • Run yarn workspace @perauset/functions db:types to regenerate db.types.ts

Verification:

  • cd backend && yarn dbmigrate runs without error
  • db.types.ts contains Role, Permission, RolePermission, UserRoleAssignment interfaces
  • yarn workspace @perauset/functions tsc passes with updated types

WP-04: Migration 0003 - Audit Log (Trigger-Based)

Dependencies: WP-03 Estimated Complexity: M

Description: Create a single audit log table with a PostgreSQL trigger function that fires automatically on INSERT/UPDATE/DELETE for any tracked table. No application-level logging code needed — the database handles it. The trigger computes a JSON diff of changed fields (NEW - OLD) for updates, full row for inserts/deletes. A app.current_user_id session variable is set per-request so the trigger can record who made the change.

Deliverables:

  • sql/0003-audit-log.sql:
    -- Audit log table
    CREATE TABLE app.audit_log (
      audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
      table_name TEXT NOT NULL,
      record_id UUID NOT NULL,
      action TEXT NOT NULL,  -- 'INSERT', 'UPDATE', 'DELETE'
      user_id UUID,          -- from session variable, nullable for system actions
      changed_fields JSONB,  -- for UPDATE: only changed fields {field: {old, new}}
                             -- for INSERT: full new row
                             -- for DELETE: full old row
      occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
    );
    CREATE INDEX idx_audit_log_table_record ON app.audit_log(table_name, record_id);
    CREATE INDEX idx_audit_log_user ON app.audit_log(user_id);
    CREATE INDEX idx_audit_log_occurred ON app.audit_log(occurred_at);
    
    -- Generic trigger function
    CREATE OR REPLACE FUNCTION app.audit_trigger_func() RETURNS TRIGGER AS $$
    DECLARE
      _user_id UUID;
      _record_id UUID;
      _changed JSONB;
      _old JSONB;
      _new JSONB;
      _key TEXT;
    BEGIN
      -- Get current user from session variable (set per-request by app)
      BEGIN
        _user_id := current_setting('app.current_user_id', true)::UUID;
      EXCEPTION WHEN OTHERS THEN
        _user_id := NULL;
      END;
    
      IF TG_OP = 'DELETE' THEN
        _record_id := (row_to_json(OLD) ->> TG_ARGV[0])::UUID;
        INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
        VALUES (TG_TABLE_NAME, _record_id, 'DELETE', _user_id, row_to_json(OLD)::JSONB);
        RETURN OLD;
      ELSIF TG_OP = 'INSERT' THEN
        _record_id := (row_to_json(NEW) ->> TG_ARGV[0])::UUID;
        INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
        VALUES (TG_TABLE_NAME, _record_id, 'INSERT', _user_id, row_to_json(NEW)::JSONB);
        RETURN NEW;
      ELSIF TG_OP = 'UPDATE' THEN
        _record_id := (row_to_json(NEW) ->> TG_ARGV[0])::UUID;
        _old := row_to_json(OLD)::JSONB;
        _new := row_to_json(NEW)::JSONB;
        _changed := '{}'::JSONB;
        FOR _key IN SELECT jsonb_object_keys(_new)
        LOOP
          IF _key = 'updated_at' THEN CONTINUE; END IF;
          IF (_old ->> _key) IS DISTINCT FROM (_new ->> _key) THEN
            _changed := _changed || jsonb_build_object(_key, jsonb_build_object('old', _old -> _key, 'new', _new -> _key));
          END IF;
        END LOOP;
        -- Skip if nothing actually changed
        IF _changed = '{}'::JSONB THEN RETURN NEW; END IF;
        INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
        VALUES (TG_TABLE_NAME, _record_id, 'UPDATE', _user_id, _changed);
        RETURN NEW;
      END IF;
      RETURN NULL;
    END;
    $$ LANGUAGE plpgsql;
    
    -- Helper to attach trigger to a table (called in each domain migration)
    CREATE OR REPLACE FUNCTION app.enable_audit(target_table TEXT, pk_column TEXT DEFAULT 'id') RETURNS VOID AS $$
    BEGIN
      EXECUTE format(
        'CREATE TRIGGER audit_%I AFTER INSERT OR UPDATE OR DELETE ON app.%I FOR EACH ROW EXECUTE FUNCTION app.audit_trigger_func(%L)',
        target_table, target_table, pk_column
      );
    END;
    $$ LANGUAGE plpgsql;
    
    -- Enable audit on existing tables
    SELECT app.enable_audit('user', 'user_id');
    SELECT app.enable_audit('role', 'role_id');
    SELECT app.enable_audit('permission', 'permission_id');
    SELECT app.enable_audit('role_permission', 'role_id');  -- composite PK, use role_id
    SELECT app.enable_audit('user_role_assignment', 'assignment_id');
    
  • Each subsequent domain migration calls SELECT app.enable_audit('table_name', 'pk_column') for its new tables
  • App middleware sets SET LOCAL app.current_user_id = '<uuid>' per-request via Kysely connection hook
  • packages/functions/src/lib/audit-context.ts — helper that runs SET LOCAL app.current_user_id on the Kysely connection so the trigger knows who made the change
  • Regenerate db.types.ts

Verification:

  • Migration runs clean
  • db.types.ts contains AuditLog interface
  • Insert/update/delete a user row → verify app.audit_log has corresponding entry
  • Update a user's display_name → verify changed_fields contains {"display_name": {"old": "...", "new": "..."}} (not the full row)
  • tsc passes

WP-05: Seed Data Migration

Dependencies: WP-04 Estimated Complexity: M

Description: Insert default roles, permissions, role-permission mappings, and an admin user. This is a SQL migration so it runs automatically.

Deliverables:

  • sql/0004-seed-data.sql:
    • Insert roles: admin, coordinator, community_member, guest, facilitator
    • Insert permissions grouped by module:
      • users: users.view, users.manage
      • roles: roles.assign, roles.manage
      • stays: stays.request, stays.review, stays.manage, stays.view_all
      • rooms: rooms.manage, rooms.request_preference, rooms.view_all
      • boats: boats.manage, boats.request, boats.view_all
      • tasks: tasks.manage, tasks.assign, tasks.claim, tasks.view_all
      • retreats: retreats.manage, retreats.view, retreats.participate
      • kitchen: kitchen.manage, kitchen.view, kitchen.update_dietary
      • inventory: inventory.manage, inventory.request
      • notifications: notifications.view
      • audit_log: audit_log.view
    • Map permissions to roles (admin gets all, coordinator gets manage/review, community_member gets request/claim/view own)
    • Insert admin user into app.user with known email admin@perauset.org
    • Assign admin role to admin user

Verification:

  • Migration runs clean
  • Query SELECT * FROM app.role returns 5 roles
  • Query SELECT count(*) FROM app.permission returns expected count
  • Query SELECT count(*) FROM app.role_permission returns expected mappings
  • Admin user exists and has admin role assignment

WP-06: Auth - @pikku/auth-js Integration

Dependencies: WP-05 Estimated Complexity: M

Description: Integrate @pikku/auth-js for session management. Set up Auth.js routes, session middleware, and a credentials provider for dev/test. Follow the makeanagent pattern.

Deliverables:

  • Add @pikku/auth-js and @auth/core to packages/functions/package.json dependencies
  • packages/functions/src/wirings/auth.wiring.ts — creates Auth.js routes with credentials provider (email/password lookup against app.user), wires them via createAuthRoutes + wireHTTPRoutes at /auth basePath
  • Update packages/functions/src/middleware.ts — add authJsSession({ secretId: 'AUTH_SECRET', mapSession: (claims) => ({ userId: claims.sub as string }) }) to the middleware chain
  • Update backend/bin/start.ts — import the auth wiring file (@perauset/functions/src/wirings/auth.wiring.js)
  • Update e2e/tests/support/world.ts — add login() method that calls /auth/csrf then /auth/callback/credentials to get session cookies (same flow as makeanagent e2e world.ts)
  • e2e/tests/features/phase-0/auth.feature — scenarios: unauthenticated request returns 401, authenticated session returns user info
  • e2e/tests/steps/phase-0/auth.steps.ts — step definitions

Verification:

  • E2E: GET /auth/session without cookies returns no session / 401
  • E2E: After login via credentials flow, GET /auth/session returns session with userId
  • e2e/tests/features/phase-0/auth.feature passes

WP-07: User Management Functions

Dependencies: WP-06 Estimated Complexity: S

Description: Basic user CRUD functions (not generic patch -- specific commands).

Deliverables:

  • packages/functions/src/functions/users/get-user.function.ts -- GET /users/:userId, requires users.view permission or own user
  • packages/functions/src/functions/users/update-user-profile.function.ts -- PUT /users/:userId/profile, updates first_name, last_name, display_name, phone. Requires own user or users.manage.
  • packages/functions/src/functions/users/list-users.function.ts -- GET /users, requires users.view permission, returns paginated list

Verification:

  • E2E: e2e/tests/features/phase-0/users.feature
    • Get admin user by ID returns correct data
    • Update own profile succeeds
    • List users returns at least the admin user
    • Unauthenticated request returns 401
    • User without users.view tag cannot list users (403)
    • User without users.manage tag cannot update another user's profile (403)

WP-08: Role Assignment Functions

Dependencies: WP-07 Estimated Complexity: S

Description: Functions to assign and revoke roles from users.

Deliverables:

  • packages/functions/src/functions/roles/assign-role.function.ts -- POST /roles/assign, body: { userId, roleId, scopeType?, scopeId?, validFrom?, validUntil? }. Tagged roles.assign.
  • packages/functions/src/functions/roles/revoke-role.function.ts -- POST /roles/revoke, body: { assignmentId }. Tagged roles.assign.
  • packages/functions/src/functions/roles/list-roles.function.ts -- GET /roles, returns all roles
  • packages/functions/src/functions/roles/list-user-roles.function.ts -- GET /users/:userId/roles, returns role assignments for a user

Verification:

  • E2E: e2e/tests/features/phase-0/roles-permissions.feature
    • List roles returns seed data roles
    • Admin can assign community_member role to a new user
    • List that user's roles shows the assignment
    • Revoke the assignment, list shows empty
    • User without roles.assign tag cannot assign roles (403)
    • User without roles.assign tag cannot revoke roles (403)

WP-09: Audit Log Query Function

Dependencies: WP-08 Estimated Complexity: S

Description: Read-only endpoint to query the trigger-based audit log. Since all mutations are automatically captured by PostgreSQL triggers, this just needs a query API.

Deliverables:

  • packages/functions/src/functions/audit/list-audit-log.function.ts -- GET /audit-log, query params: tableName, recordId, userId, action, limit, offset. Tagged audit_log.view.

Verification:

  • E2E: e2e/tests/features/phase-0/audit-log.feature
    • After performing a user profile update (from WP-07), query audit log for that table/record
    • Verify the entry exists with action UPDATE, correct user_id, and changed_fields showing the diff
    • User without audit_log.view tag cannot query audit logs (403)

WP-10: Migration 0005 - Stay Requests and Stays

Dependencies: WP-09 (Phase 0 complete) Estimated Complexity: M

Description: Create the presence/stay tables with proper separation of request and confirmed state.

Deliverables:

  • sql/0005-stays.sql:
    app.stay_request (
      request_id UUID PK, user_id UUID FK, requested_arrival DATE NOT NULL, requested_departure DATE NOT NULL,
      purpose TEXT, notes TEXT, stay_type TEXT NOT NULL DEFAULT 'general',
      status TEXT NOT NULL DEFAULT 'submitted',  -- submitted, under_review, approved, rejected, cancelled
      reviewed_by UUID FK, review_notes TEXT, reviewed_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.stay (
      stay_id UUID PK, user_id UUID FK, request_id UUID FK REFERENCES app.stay_request,
      arrival_date DATE NOT NULL, departure_date DATE NOT NULL,
      status TEXT NOT NULL DEFAULT 'pending',  -- pending, confirmed, checked_in, checked_out, cancelled
      stay_type TEXT NOT NULL DEFAULT 'general',
      notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    
  • Regenerate db.types.ts

Verification:

  • Migration runs clean
  • db.types.ts contains StayRequest and Stay interfaces
  • tsc passes

WP-11: Stay Request Functions

Dependencies: WP-10 Estimated Complexity: L

Description: Implement the full stay request lifecycle: submit, review, approve, reject, cancel.

Deliverables:

  • packages/functions/src/functions/stays/request-stay.function.ts -- POST /stays/requests, body: { requestedArrival, requestedDeparture, purpose?, notes?, stayType? }. Tagged stays.request. Creates stay_request with status submitted.
  • packages/functions/src/functions/stays/review-stay-request.function.ts -- POST /stays/requests/:requestId/review, body: { reviewNotes? }. Tagged stays.review. Transitions status submitted -> under_review.
  • packages/functions/src/functions/stays/approve-stay-request.function.ts -- POST /stays/requests/:requestId/approve, body: { reviewNotes? }. Tagged stays.review. Transitions submitted|under_review -> approved.
  • packages/functions/src/functions/stays/reject-stay-request.function.ts -- POST /stays/requests/:requestId/reject, body: { reviewNotes }. Tagged stays.review. Transitions submitted|under_review -> rejected.
  • packages/functions/src/functions/stays/list-stay-requests.function.ts -- GET /stays/requests, query params: status, userId. Tagged stays.view_all for others' requests, own requests always visible.

Verification:

  • E2E: e2e/tests/features/phase-1/stays.feature (request lifecycle)
    • Submit a stay request, verify status is submitted
    • Review it, verify status is under_review
    • Approve it, verify status is approved
    • Submit another, reject it
    • Submit another, cancel it
    • Attempt invalid transition (rejected -> approved), verify error
    • Verify audit log entries exist for each state change (automatic via trigger)
    • User without stays.request tag cannot submit a stay request (403)
    • User without stays.review tag cannot review/approve/reject (403)
    • User can view own requests but not others' without stays.view_all (403)

WP-12: Stay Lifecycle Functions

Dependencies: WP-11 Estimated Complexity: L

Description: Implement confirmed stay lifecycle: create from request, check in, check out, extend, cancel, override dates.

Deliverables:

  • packages/functions/src/functions/stays/create-stay-from-request.function.ts -- POST /stays, body: { requestId, arrivalDate?, departureDate? }. Tagged stays.manage. Request must be approved. Creates stay with status pending.
  • packages/functions/src/functions/stays/check-in-stay.function.ts -- POST /stays/:stayId/check-in. Tagged stays.manage. Transitions confirmed -> checked_in.
  • packages/functions/src/functions/stays/check-out-stay.function.ts -- POST /stays/:stayId/check-out. Tagged stays.manage. Transitions checked_in -> checked_out.
  • packages/functions/src/functions/stays/extend-stay.function.ts -- POST /stays/:stayId/extend, body: { newDepartureDate }. Tagged stays.manage. Only valid when confirmed or checked_in.
  • packages/functions/src/functions/stays/cancel-stay.function.ts -- POST /stays/:stayId/cancel, body: { reason? }. Tagged stays.manage or own stay. Transitions any non-terminal -> cancelled.
  • packages/functions/src/functions/stays/override-stay-dates.function.ts -- POST /stays/:stayId/override-dates, body: { arrivalDate?, departureDate?, reason }. Tagged stays.manage. Admin override (audit trigger captures the diff automatically).
  • packages/functions/src/functions/stays/list-stays.function.ts -- GET /stays, query params: status, userId, from, to. Tagged stays.view_all for all, own stays always visible.

Verification:

  • E2E: e2e/tests/features/phase-1/stays.feature (stay lifecycle)
    • Create stay from approved request
    • Confirm stay (pending -> confirmed)
    • Check in, check out
    • Verify full lifecycle transitions
    • Test extend while checked in
    • Test cancel
    • Test override dates with reason
    • Verify audit log entries exist for each state change (automatic via trigger)
    • User without stays.manage tag cannot create/check-in/check-out/extend/override stays (403)
    • User can cancel own stay but not others' without stays.manage (403)

WP-13: Migration 0006 - Rooms

Dependencies: WP-10 Estimated Complexity: M

Description: Create room-related tables: rooms, room blocks, room requests (preferences), room allocations.

Deliverables:

  • sql/0006-rooms.sql:
    app.room (
      room_id UUID PK, name TEXT NOT NULL UNIQUE, building TEXT, floor INT,
      capacity INT NOT NULL DEFAULT 1, room_type TEXT NOT NULL DEFAULT 'standard',
      amenities JSONB DEFAULT '[]', status TEXT NOT NULL DEFAULT 'available',
      notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.room_block (
      block_id UUID PK, room_id UUID FK, start_date DATE NOT NULL, end_date DATE NOT NULL,
      reason TEXT NOT NULL, blocked_by UUID FK,
      created_at TIMESTAMPTZ DEFAULT now()
    )
    app.room_request (
      request_id UUID PK, stay_id UUID FK, room_type_pref TEXT, room_id_pref UUID FK,
      notes TEXT, status TEXT NOT NULL DEFAULT 'pending',
      created_at TIMESTAMPTZ DEFAULT now()
    )
    app.room_allocation (
      allocation_id UUID PK, room_id UUID FK, stay_id UUID FK, request_id UUID FK,
      start_date DATE NOT NULL, end_date DATE NOT NULL,
      status TEXT NOT NULL DEFAULT 'active',  -- active, released, reallocated, cancelled
      allocated_by UUID FK, notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    
  • Add seed rooms to 0004-seed-data.sql or create sql/0006b-seed-rooms.sql (actually, better to include room seeds in the same migration as a separate statement block)
  • Regenerate db.types.ts

Verification:

  • Migration runs clean
  • Types generated correctly

WP-14: Room Functions

Dependencies: WP-13, WP-12 Estimated Complexity: L

Description: All room management and allocation functions.

Deliverables:

  • packages/functions/src/functions/rooms/create-room.function.ts -- POST /rooms. Tagged rooms.manage.
  • packages/functions/src/functions/rooms/block-room.function.ts -- POST /rooms/:roomId/block. Tagged rooms.manage. Checks no conflicting allocations.
  • packages/functions/src/functions/rooms/request-room-preference.function.ts -- POST /rooms/requests, body: { stayId, roomTypePref?, roomIdPref?, notes? }. Tagged rooms.request_preference or own stay.
  • packages/functions/src/functions/rooms/allocate-room.function.ts -- POST /rooms/allocations, body: { roomId, stayId, requestId?, startDate, endDate }. Tagged rooms.manage. Checks room availability (no overlapping active allocations, no blocks).
  • packages/functions/src/functions/rooms/reallocate-room.function.ts -- POST /rooms/allocations/:allocationId/reallocate, body: { newRoomId, reason }. Tagged rooms.manage. Releases old, creates new.
  • packages/functions/src/functions/rooms/release-room.function.ts -- POST /rooms/allocations/:allocationId/release. Tagged rooms.manage. Sets status to released.
  • packages/functions/src/functions/rooms/cancel-room-allocation.function.ts -- POST /rooms/allocations/:allocationId/cancel. Tagged rooms.manage.
  • packages/functions/src/functions/rooms/list-rooms.function.ts -- GET /rooms, includes availability info for date range. Tagged rooms.view_all.
  • packages/functions/src/functions/rooms/list-room-allocations.function.ts -- GET /rooms/allocations. Tagged rooms.view_all.

Verification:

  • E2E: e2e/tests/features/phase-1/rooms.feature
    • Create a room
    • Create a stay, allocate room to stay
    • Attempt double-allocation for overlapping dates, verify conflict error
    • Block a room, attempt allocation during block, verify error
    • Reallocate to different room
    • Release room
    • User without rooms.manage tag cannot create/allocate/block/release rooms (403)
    • User can request room preference for own stay but not without rooms.request_preference (403)
    • User without rooms.view_all tag cannot list all allocations (403)

WP-15: Migration 0007 - Boats

Dependencies: WP-10 Estimated Complexity: M

Description: Create boat transport tables.

Deliverables:

  • sql/0007-boats.sql:
    app.boat_route (
      route_id UUID PK, name TEXT NOT NULL, origin TEXT NOT NULL, destination TEXT NOT NULL,
      typical_duration_minutes INT, notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now()
    )
    app.boat (
      boat_id UUID PK, name TEXT NOT NULL, capacity INT NOT NULL,
      status TEXT NOT NULL DEFAULT 'active', notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now()
    )
    app.boat_trip (
      trip_id UUID PK, route_id UUID FK, boat_id UUID FK,
      departure_time TIMESTAMPTZ NOT NULL, arrival_time TIMESTAMPTZ,
      status TEXT NOT NULL DEFAULT 'planned',  -- planned, open_for_requests, boarding, in_transit, completed, cancelled
      available_seats INT NOT NULL,
      notes TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.boat_reservation_request (
      request_id UUID PK, trip_id UUID FK, user_id UUID FK,
      passenger_count INT NOT NULL DEFAULT 1, luggage_notes TEXT,
      status TEXT NOT NULL DEFAULT 'submitted',  -- submitted, under_review, confirmed, waitlisted, declined, cancelled
      reviewed_by UUID FK, review_notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.boat_manifest_entry (
      entry_id UUID PK, trip_id UUID FK, user_id UUID FK, request_id UUID FK,
      seat_number INT, boarded BOOLEAN DEFAULT false, boarded_at TIMESTAMPTZ,
      notes TEXT, created_at TIMESTAMPTZ DEFAULT now()
    )
    
  • Regenerate db.types.ts

Verification:

  • Migration runs clean, types regenerated

WP-16: Boat Functions

Dependencies: WP-15 Estimated Complexity: L

Description: Full boat transport request and manifest lifecycle.

Deliverables:

  • packages/functions/src/functions/boats/create-boat-route.function.ts -- POST /boats/routes. Tagged boats.manage.
  • packages/functions/src/functions/boats/create-boat.function.ts -- POST /boats. Tagged boats.manage.
  • packages/functions/src/functions/boats/create-boat-trip.function.ts -- POST /boats/trips. Tagged boats.manage.
  • packages/functions/src/functions/boats/open-boat-trip.function.ts -- POST /boats/trips/:tripId/open. Tagged boats.manage. Transitions planned -> open_for_requests.
  • packages/functions/src/functions/boats/request-boat-seat.function.ts -- POST /boats/trips/:tripId/request-seat. Tagged boats.request. Checks trip is open_for_requests.
  • packages/functions/src/functions/boats/review-boat-request.function.ts -- POST /boats/requests/:requestId/review. Tagged boats.manage.
  • packages/functions/src/functions/boats/confirm-boat-request.function.ts -- POST /boats/requests/:requestId/confirm. Tagged boats.manage. Checks capacity. Creates manifest entry.
  • packages/functions/src/functions/boats/waitlist-boat-request.function.ts -- POST /boats/requests/:requestId/waitlist. Tagged boats.manage.
  • packages/functions/src/functions/boats/decline-boat-request.function.ts -- POST /boats/requests/:requestId/decline. Tagged boats.manage.
  • packages/functions/src/functions/boats/cancel-boat-request.function.ts -- POST /boats/requests/:requestId/cancel. Own request or boats.manage.
  • packages/functions/src/functions/boats/board-manifest-entry.function.ts -- POST /boats/manifest/:entryId/board. Tagged boats.manage. Sets boarded = true.
  • packages/functions/src/functions/boats/complete-boat-trip.function.ts -- POST /boats/trips/:tripId/complete. Tagged boats.manage.
  • packages/functions/src/functions/boats/list-boat-trips.function.ts -- GET /boats/trips. Tagged boats.view_all.
  • packages/functions/src/functions/boats/list-boat-requests.function.ts -- GET /boats/requests. Own or boats.view_all.

Verification:

  • E2E: e2e/tests/features/phase-1/boats.feature
    • Create route, boat, trip
    • Open trip for requests
    • Submit seat request
    • Confirm request, verify manifest entry created
    • Attempt over-capacity booking, verify error
    • Board passenger, complete trip
    • Test waitlist and decline flows
    • User without boats.manage tag cannot create routes/boats/trips or confirm/decline requests (403)
    • User without boats.request tag cannot request a seat (403)
    • User can cancel own request but not others' without boats.manage (403)

WP-17: Migration 0008 - Tasks

Dependencies: WP-10 Estimated Complexity: M

Description: Create task management tables.

Deliverables:

  • sql/0008-tasks.sql:
    app.task_template (
      template_id UUID PK, title TEXT NOT NULL, description TEXT,
      category TEXT NOT NULL, estimated_minutes INT,
      recurrence_rule TEXT,  -- e.g. 'daily', 'weekly:mon,wed,fri', 'monthly:1,15'
      default_assignee_role TEXT,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.task_instance (
      task_id UUID PK, template_id UUID FK,
      title TEXT NOT NULL, description TEXT, category TEXT NOT NULL,
      due_date DATE, due_time TIME,
      status TEXT NOT NULL DEFAULT 'planned',  -- planned, open, assigned, in_progress, completed, blocked, cancelled
      priority TEXT NOT NULL DEFAULT 'normal',  -- low, normal, high, urgent
      estimated_minutes INT, actual_minutes INT,
      notes TEXT, completed_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.task_assignment (
      assignment_id UUID PK, task_id UUID FK, user_id UUID FK,
      status TEXT NOT NULL DEFAULT 'pending',  -- pending, accepted, declined
      assigned_by UUID FK, assigned_at TIMESTAMPTZ DEFAULT now(),
      responded_at TIMESTAMPTZ
    )
    
  • Regenerate db.types.ts

Verification:

  • Migration runs clean, types regenerated

WP-18: Task Functions

Dependencies: WP-17 Estimated Complexity: L

Description: Full task lifecycle functions.

Deliverables:

  • packages/functions/src/functions/tasks/create-task-template.function.ts -- POST /tasks/templates. Tagged tasks.manage.
  • packages/functions/src/functions/tasks/generate-recurring-tasks.function.ts -- POST /tasks/generate, body: { templateId, fromDate, toDate }. Tagged tasks.manage. Creates individual task_instance rows for each occurrence (not a magical repeating row).
  • packages/functions/src/functions/tasks/create-task-instance.function.ts -- POST /tasks. Tagged tasks.manage.
  • packages/functions/src/functions/tasks/assign-task.function.ts -- POST /tasks/:taskId/assign, body: { userId }. Tagged tasks.assign. Creates assignment, transitions task planned|open -> assigned.
  • packages/functions/src/functions/tasks/claim-task.function.ts -- POST /tasks/:taskId/claim. Tagged tasks.claim. Self-assignment for open tasks.
  • packages/functions/src/functions/tasks/accept-task-assignment.function.ts -- POST /tasks/assignments/:assignmentId/accept. Own assignment only.
  • packages/functions/src/functions/tasks/decline-task-assignment.function.ts -- POST /tasks/assignments/:assignmentId/decline. Own assignment only. Task goes back to open if no other accepted assignments.
  • packages/functions/src/functions/tasks/start-task.function.ts -- POST /tasks/:taskId/start. Assigned user. Transitions assigned -> in_progress.
  • packages/functions/src/functions/tasks/complete-task.function.ts -- POST /tasks/:taskId/complete, body: { actualMinutes?, notes? }. Transitions in_progress -> completed.
  • packages/functions/src/functions/tasks/mark-task-blocked.function.ts -- POST /tasks/:taskId/block, body: { reason }. Transitions assigned|in_progress -> blocked.
  • packages/functions/src/functions/tasks/reassign-task.function.ts -- POST /tasks/:taskId/reassign, body: { userId }. Tagged tasks.assign.
  • packages/functions/src/functions/tasks/list-tasks.function.ts -- GET /tasks. Tagged tasks.view_all for all, own assigned tasks always visible.

Verification:

  • E2E: e2e/tests/features/phase-1/tasks.feature
    • Create template, generate recurring tasks for a week
    • Verify correct number of task instances created
    • Assign task, accept assignment, start, complete
    • Test claim flow
    • Test block and unblock (blocked -> in_progress)
    • Test decline and reassign
    • Invalid transitions rejected
    • User without tasks.manage tag cannot create templates or generate tasks (403)
    • User without tasks.assign tag cannot assign tasks (403)
    • User without tasks.claim tag cannot claim open tasks (403)
    • User can only accept/decline own assignments

WP-19: Migration 0009 - Notifications

Dependencies: WP-10 Estimated Complexity: S

Description: Create notifications table.

Deliverables:

  • sql/0009-notifications.sql:
    app.notification (
      notification_id UUID PK, user_id UUID FK,
      type TEXT NOT NULL,  -- e.g. 'stay_approved', 'task_assigned', 'boat_confirmed'
      title TEXT NOT NULL, body TEXT,
      entity_type TEXT, entity_id UUID,
      read BOOLEAN DEFAULT false, read_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ DEFAULT now()
    )
    CREATE INDEX idx_notification_user ON app.notification(user_id, read, created_at DESC);
    
  • Regenerate db.types.ts

Verification:

  • Migration runs clean, types regenerated

WP-20: Notification Functions + Integration

Dependencies: WP-19 Estimated Complexity: M

Description: Notification query endpoints plus a shared helper that domain functions can call to create notifications.

Deliverables:

  • packages/functions/src/lib/notifications.ts -- createNotification(kysely, { userId, type, title, body?, entityType?, entityId? }) helper
  • packages/functions/src/functions/notifications/list-notifications.function.ts -- GET /notifications. Own notifications. Query params: read, limit, offset.
  • packages/functions/src/functions/notifications/mark-notification-read.function.ts -- POST /notifications/:notificationId/read. Own notification only.
  • Update key domain functions to call createNotification:
    • approve-stay-request notifies the requester
    • reject-stay-request notifies the requester
    • assign-task notifies the assignee
    • confirm-boat-request notifies the requester

Verification:

  • E2E: e2e/tests/features/phase-1/notifications.feature
    • Approve a stay request, verify notification created for requester
    • List notifications, verify it appears
    • Mark as read, verify it's marked
    • User can only see and mark own notifications
    • Cannot mark another user's notification as read (403)

WP-21: Migration 0010 - Retreats

Dependencies: WP-10 Estimated Complexity: M

Description: Create retreat-related tables.

Deliverables:

  • sql/0010-retreats.sql:
    app.retreat (
      retreat_id UUID PK, title TEXT NOT NULL, description TEXT,
      start_date DATE NOT NULL, end_date DATE NOT NULL,
      status TEXT NOT NULL DEFAULT 'draft',  -- draft, published, in_progress, completed, cancelled
      max_participants INT, location TEXT,
      created_by UUID FK, published_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.retreat_person (
      retreat_person_id UUID PK, retreat_id UUID FK, user_id UUID FK,
      role TEXT NOT NULL DEFAULT 'participant',  -- participant, facilitator, organizer, support
      stay_id UUID FK,  -- optional link to a stay
      status TEXT NOT NULL DEFAULT 'registered',  -- registered, confirmed, cancelled, no_show
      notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(),
      UNIQUE(retreat_id, user_id)
    )
    app.retreat_schedule_item (
      item_id UUID PK, retreat_id UUID FK,
      title TEXT NOT NULL, description TEXT,
      start_time TIMESTAMPTZ NOT NULL, end_time TIMESTAMPTZ NOT NULL,
      location TEXT, facilitator_id UUID FK,
      item_type TEXT NOT NULL DEFAULT 'session',  -- session, meal, free_time, ceremony, other
      sort_order INT DEFAULT 0,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    
  • Regenerate db.types.ts

Verification:

  • Migration runs clean, types regenerated

WP-22: Retreat Functions

Dependencies: WP-21, WP-12 Estimated Complexity: L

Description: Full retreat management functions.

Deliverables:

  • packages/functions/src/functions/retreats/create-retreat.function.ts -- POST /retreats. Tagged retreats.manage.
  • packages/functions/src/functions/retreats/update-retreat.function.ts -- PUT /retreats/:retreatId. Tagged retreats.manage. Only draft retreats editable.
  • packages/functions/src/functions/retreats/add-retreat-person.function.ts -- POST /retreats/:retreatId/people. Tagged retreats.manage. Body: { userId, role }.
  • packages/functions/src/functions/retreats/update-retreat-person-role.function.ts -- PUT /retreats/:retreatId/people/:retreatPersonId. Tagged retreats.manage.
  • packages/functions/src/functions/retreats/link-stay-to-retreat-person.function.ts -- POST /retreats/:retreatId/people/:retreatPersonId/link-stay. Tagged retreats.manage. Body: { stayId }.
  • packages/functions/src/functions/retreats/publish-retreat.function.ts -- POST /retreats/:retreatId/publish. Tagged retreats.manage. Transitions draft -> published.
  • packages/functions/src/functions/retreats/add-retreat-schedule-item.function.ts -- POST /retreats/:retreatId/schedule. Tagged retreats.manage.
  • packages/functions/src/functions/retreats/update-retreat-schedule-item.function.ts -- PUT /retreats/:retreatId/schedule/:itemId. Tagged retreats.manage.
  • packages/functions/src/functions/retreats/cancel-retreat.function.ts -- POST /retreats/:retreatId/cancel. Tagged retreats.manage. Any non-terminal -> cancelled.
  • packages/functions/src/functions/retreats/list-retreats.function.ts -- GET /retreats. Published retreats visible to all authenticated users. Draft visible to retreats.manage.

Verification:

  • E2E: e2e/tests/features/phase-2/retreats.feature
    • Create retreat, add people with different roles
    • Add schedule items
    • Publish retreat
    • Link stay to retreat person
    • Cancel retreat
    • Verify retreat people are NOT a subtype of guest (separate table, separate lifecycle)
    • User without retreats.manage tag cannot create/update/publish/cancel retreats (403)
    • Published retreats visible to authenticated users with retreats.view, drafts only to retreats.manage

WP-23: Migration 0011 - Kitchen and Dietary

Dependencies: WP-10 Estimated Complexity: M

Description: Create kitchen/dietary tables.

Deliverables:

  • sql/0011-kitchen-dietary.sql:
    app.dietary_profile (
      profile_id UUID PK, user_id UUID FK UNIQUE,
      dietary_type TEXT NOT NULL DEFAULT 'omnivore',  -- omnivore, vegetarian, vegan, pescatarian, other
      allergies TEXT[], intolerances TEXT[],
      preferences TEXT, notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.meal_service (
      service_id UUID PK, date DATE NOT NULL, meal_type TEXT NOT NULL,  -- breakfast, lunch, dinner, snack
      expected_count INT, actual_count INT,
      menu_description TEXT, notes TEXT,
      status TEXT NOT NULL DEFAULT 'planned',  -- planned, prepared, served, cancelled
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(),
      UNIQUE(date, meal_type)
    )
    app.meal_attendance (
      attendance_id UUID PK, service_id UUID FK, user_id UUID FK,
      status TEXT NOT NULL DEFAULT 'expected',  -- expected, confirmed, declined, attended
      dietary_notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now(),
      UNIQUE(service_id, user_id)
    )
    
  • Regenerate db.types.ts

Verification:

  • Migration runs clean, types regenerated

WP-24: Kitchen Functions

Dependencies: WP-23 Estimated Complexity: M

Description: Kitchen and dietary profile functions.

Deliverables:

  • packages/functions/src/functions/kitchen/upsert-dietary-profile.function.ts -- PUT /kitchen/dietary-profile. Own profile or kitchen.manage. Upserts by user_id.
  • packages/functions/src/functions/kitchen/create-meal-service.function.ts -- POST /kitchen/meals. Tagged kitchen.manage.
  • packages/functions/src/functions/kitchen/estimate-meal-attendance.function.ts -- POST /kitchen/meals/:serviceId/estimate. Tagged kitchen.manage. Auto-populates from checked-in stays for that date.
  • packages/functions/src/functions/kitchen/adjust-meal-attendance.function.ts -- PUT /kitchen/meals/:serviceId/attendance/:userId. Own attendance or kitchen.manage.
  • packages/functions/src/functions/kitchen/list-meal-services.function.ts -- GET /kitchen/meals. Query params: date, mealType. Tagged kitchen.view.

Verification:

  • E2E: e2e/tests/features/phase-2/kitchen.feature
    • Create dietary profile for user
    • Create meal service
    • Estimate attendance (should pick up checked-in stays)
    • Adjust individual attendance
    • List meals for a date
    • User can update own dietary profile but not others' without kitchen.manage (403)
    • User without kitchen.manage tag cannot create meal services or estimate attendance (403)
    • User without kitchen.view tag cannot list meal services (403)

WP-25: Migration 0012 - Inventory

Dependencies: WP-10 Estimated Complexity: S

Description: Create inventory tables.

Deliverables:

  • sql/0012-inventory.sql:
    app.inventory_item (
      item_id UUID PK, name TEXT NOT NULL, category TEXT NOT NULL,
      unit TEXT NOT NULL DEFAULT 'unit',  -- unit, kg, liter, etc.
      current_quantity NUMERIC DEFAULT 0, min_quantity NUMERIC DEFAULT 0,
      location TEXT, notes TEXT,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    app.inventory_request (
      request_id UUID PK, item_id UUID FK, requested_by UUID FK,
      quantity NUMERIC NOT NULL, reason TEXT,
      status TEXT NOT NULL DEFAULT 'submitted',  -- submitted, approved, fulfilled, rejected, cancelled
      reviewed_by UUID FK, review_notes TEXT,
      fulfilled_at TIMESTAMPTZ,
      created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now()
    )
    
  • Regenerate db.types.ts

Verification:

  • Migration runs clean, types regenerated

WP-26: Inventory Functions

Dependencies: WP-25 Estimated Complexity: M

Description: Inventory request lifecycle.

Deliverables:

  • packages/functions/src/functions/inventory/request-inventory.function.ts -- POST /inventory/requests. Tagged inventory.request.
  • packages/functions/src/functions/inventory/review-inventory-request.function.ts -- POST /inventory/requests/:requestId/review. Tagged inventory.manage.
  • packages/functions/src/functions/inventory/fulfill-inventory-request.function.ts -- POST /inventory/requests/:requestId/fulfill. Tagged inventory.manage. Updates current_quantity on the item.
  • packages/functions/src/functions/inventory/list-inventory.function.ts -- GET /inventory. Tagged inventory.manage. Includes low-stock flag.

Verification:

  • E2E: e2e/tests/features/phase-2/inventory.feature
    • Create inventory items (via direct DB or admin endpoint)
    • Request inventory
    • Review and fulfill
    • Verify quantity updated
    • Verify low-stock detection
    • User without inventory.request tag cannot submit inventory requests (403)
    • User without inventory.manage tag cannot review/fulfill requests or list inventory (403)

Dependency Graph (Execution Order)

WP-01 (e2e infra)          WP-02 (shared libs)
      |                         |
      v                         v
WP-03 (roles migration) <------+
      |
      v
WP-04 (audit log + triggers)
      |
      v
WP-05 (seed data)
      |
      v
WP-06 (auth/session)
      |
      v
WP-07 (user functions)
      |
      v
WP-08 (role functions)
      |
      v
WP-09 (audit log query) -----> Phase 0 Complete
      |
      v
WP-10 (stays migration) -----+------+------+------+
      |                       |      |      |      |
      v                       v      v      v      v
WP-11 (stay requests)   WP-13   WP-15  WP-17  WP-19
      |                 (rooms) (boats) (tasks)(notifs)
      v                   |      |      |      |
WP-12 (stay lifecycle)   v      v      v      v
      |              WP-14  WP-16  WP-18  WP-20
      |              (room   (boat  (task  (notif
      |               funcs)  funcs) funcs) funcs)
      |                                    |
      +------------------------------------+----> Phase 1 Complete
      |
      v
WP-21 (retreats migration) ----+------+
      |                        |      |
      v                        v      v
WP-22 (retreat funcs)     WP-23   WP-25
                          (kitchen)(inventory)
                            |      |
                            v      v
                          WP-24  WP-26
                          (kitchen(inventory
                           funcs)  funcs)
                            |      |
                            +------+----> Phase 2 Complete

Parallelization Notes

Within Phase 1, the following can be developed in parallel after WP-10:

  • WP-11/WP-12 (stays)
  • WP-13/WP-14 (rooms)
  • WP-15/WP-16 (boats)
  • WP-17/WP-18 (tasks)
  • WP-19/WP-20 (notifications)

Within Phase 2, after WP-21:

  • WP-22 (retreats), WP-23/WP-24 (kitchen), WP-25/WP-26 (inventory) can proceed in parallel.

WP-01 and WP-02 have no dependencies and can be done in parallel from day one.


Summary

Phase Work Packages Est. Total Complexity
Phase 0 WP-01 through WP-09 3S + 5M + 1L = ~9 units
Phase 1 WP-10 through WP-20 2S + 4M + 5L = ~11 units
Phase 2 WP-21 through WP-26 1S + 4M + 1L = ~6 units
Total 26 work packages ~26 units