# 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**: ```typescript 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.ts` — `apiGet(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.feature` — `GET /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`: ```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 = ''` 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** |