Files
perauset-e2e-mrfz8msi/ai/memory-bank/tasks/perauset-implementation-plan.md
2026-07-11 08:21:55 +02:00

1250 lines
56 KiB
Markdown

# 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 = '<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** |