56 KiB
Perauset Village Operations Platform - Implementation Plan
Current State
- Repo:
/Users/yasser/git/perauset - Pikku monorepo (Yarn 4, TypeScript, PostgreSQL)
- Backend on port 6002 (uWebSockets + Pikku)
- One migration
sql/0001-init.sqlcreatingapp.usertable - One function:
health-check.function.ts - Kysely for DB types, CFWorkerSchemaService for validation
- DB types generated via
kysely-codegenwith--default-schema app
Architectural Decisions
Migration Numbering
Continue sequential from existing 0001-init.sql:
0002-roles-permissions.sql0003-audit-log.sql0004-stays.sql0005-retreats.sql0006-rooms.sql0007-boats.sql0008-tasks.sql0009-kitchen-dietary.sql0010-inventory.sql0011-notifications.sql0012-seed-data.sql
All tables go in the app schema. After each migration batch, run yarn workspace @perauset/functions db:types to regenerate db.types.ts.
Folder Organization within packages/functions/src/
packages/functions/src/
application-types.d.ts # UserSession, Config, Services types
config.ts # createConfig
services.ts # createSingletonServices
middleware.ts # CORS etc
db.types.ts # Generated by kysely-codegen
functions/
health-check.function.ts # Existing
auth/
get-session.function.ts
login.function.ts
users/
get-user.function.ts
update-user-profile.function.ts
list-users.function.ts
roles/
assign-role.function.ts
revoke-role.function.ts
list-roles.function.ts
list-user-roles.function.ts
stays/
request-stay.function.ts
review-stay-request.function.ts
approve-stay-request.function.ts
reject-stay-request.function.ts
create-stay-from-request.function.ts
check-in-stay.function.ts
check-out-stay.function.ts
extend-stay.function.ts
cancel-stay.function.ts
override-stay-dates.function.ts
list-stays.function.ts
list-stay-requests.function.ts
rooms/
create-room.function.ts
block-room.function.ts
request-room-preference.function.ts
allocate-room.function.ts
reallocate-room.function.ts
release-room.function.ts
cancel-room-allocation.function.ts
list-rooms.function.ts
list-room-allocations.function.ts
boats/
create-boat-route.function.ts
create-boat.function.ts
create-boat-trip.function.ts
open-boat-trip.function.ts
request-boat-seat.function.ts
review-boat-request.function.ts
confirm-boat-request.function.ts
waitlist-boat-request.function.ts
decline-boat-request.function.ts
cancel-boat-request.function.ts
board-manifest-entry.function.ts
complete-boat-trip.function.ts
list-boat-trips.function.ts
list-boat-requests.function.ts
tasks/
create-task-template.function.ts
generate-recurring-tasks.function.ts
create-task-instance.function.ts
assign-task.function.ts
claim-task.function.ts
accept-task-assignment.function.ts
decline-task-assignment.function.ts
start-task.function.ts
complete-task.function.ts
mark-task-blocked.function.ts
reassign-task.function.ts
list-tasks.function.ts
retreats/
create-retreat.function.ts
update-retreat.function.ts
add-retreat-person.function.ts
update-retreat-person-role.function.ts
link-stay-to-retreat-person.function.ts
publish-retreat.function.ts
add-retreat-schedule-item.function.ts
update-retreat-schedule-item.function.ts
cancel-retreat.function.ts
list-retreats.function.ts
kitchen/
upsert-dietary-profile.function.ts
create-meal-service.function.ts
estimate-meal-attendance.function.ts
adjust-meal-attendance.function.ts
list-meal-services.function.ts
inventory/
request-inventory.function.ts
review-inventory-request.function.ts
fulfill-inventory-request.function.ts
list-inventory.function.ts
notifications/
list-notifications.function.ts
mark-notification-read.function.ts
audit/
list-audit-log.function.ts
lib/
audit-context.ts # Shared helper: sets app.current_user_id on Kysely connection per-request
permissions.ts # Tag-based permissions: addPermission('stays.review', [...]) registered per tag
state-machine.ts # Shared helper: validateTransition(entity, fromStatus, toStatus)
errors.ts # Domain-specific error classes
Each function file follows the existing Pikku pattern: export a function using pikkuFunc or pikkuSessionlessFunc, define routes with defineHTTPRoutes, and wire them with wireHTTPRoutes.
Function Convention: Zod Schemas + Rich Metadata
All functions must use Zod schemas for input and output with .describe() on every field. Functions must include description and tags metadata. This ensures AI agents (MCP, RPC, etc.) can understand and use the API effectively.
Pattern:
import { z } from 'zod'
import { pikkuFunc, defineHTTPRoutes, wireHTTPRoutes } from '#pikku/pikku-types.gen.js'
export const ApproveStayRequestInput = z.object({
requestId: z.string().uuid().describe('The stay request ID to approve'),
reviewNotes: z.string().optional().describe('Optional notes from the reviewer explaining the approval'),
})
export const ApproveStayRequestOutput = z.object({
requestId: z.string().uuid().describe('The approved stay request ID'),
status: z.string().describe('New status of the request (approved)'),
})
export const approveStayRequest = pikkuFunc({
description: 'Approve a submitted or under-review stay request. Transitions status to approved.',
tags: ['stays.review'],
auth: true,
input: ApproveStayRequestInput,
output: ApproveStayRequestOutput,
func: async ({ kysely }, { requestId, reviewNotes }, { session }) => {
// ...implementation
},
})
export const approveStayRequestRoutes = defineHTTPRoutes({
auth: true,
routes: {
approveStayRequest: {
route: '/stays/requests/:requestId/approve',
method: 'post',
func: approveStayRequest,
},
},
})
wireHTTPRoutes({ routes: { approveStayRequest: approveStayRequestRoutes } })
Rules:
- Every Zod field gets
.describe('...')with a human-readable explanation - Every function gets
descriptionexplaining what it does, state transitions, and required permissions - Every function gets
tagsfor 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)+wireHTTPRoutesto expose/auth/csrf,/auth/session,/auth/signin,/auth/callback/:provider,/auth/signout - Auth wiring: Separate
packages/functions/src/wirings/auth.wiring.tsfile (following makeanagent pattern) - Dev/test provider: Credentials provider for development/testing. Real providers (Google, etc.) added later.
- Session: JWT-based via Auth.js cookies.
UserSessionpopulated 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/nodee2e/tsconfig.json— target ES2022, module Node18, moduleResolution node16e2e/tests/cucumber.mjs— Cucumber config: requireModule tsx, require support + steps, paths to features, format progress-bar + html reporte2e/tests/support/config.ts— test API URL (http://localhost:6099), test DB name (perauset_test), timeoute2e/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 forwardinge2e/tests/support/hooks.ts— BeforeAll: spawn backend on port 6099 withperauset_testDB 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 withapiGet/apiPost/apiPutmethods,lastResponsestate,lastBodystate, optional session cookiese2e/tests/support/db.ts— direct pg Client connection toperauset_testfor assertions and cleanupe2e/tests/features/phase-0/health-check.feature—GET /health-checkreturns 200 with{ ok: true }e2e/tests/steps/phase-0/health-check.steps.ts— step definitions for health-check feature- Update root
package.jsonto adde2eto workspaces and addtest:e2escript
Verification:
yarn workspace @perauset/e2e teststarts 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,ValidationErrorextending a baseDomainErrorclasspackages/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); throwsConflictErroron invalid transitionpackages/functions/src/lib/permissions.ts-- Uses Pikku'saddPermission(tag, [checkFn])to register tag-based permission checks. For each permission tag (e.g.stays.review,rooms.manage), registers a function that queriesuser_role_assignments+role_permissions+permissionsto 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 tscpasses (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:typesto regeneratedb.types.ts
Verification:
cd backend && yarn dbmigrateruns without errordb.types.tscontainsRole,Permission,RolePermission,UserRoleAssignmentinterfacesyarn workspace @perauset/functions tscpasses with updated types
WP-04: Migration 0003 - Audit Log (Trigger-Based)
Dependencies: WP-03 Estimated Complexity: M
Description: Create a single audit log table with a PostgreSQL trigger function that fires automatically on INSERT/UPDATE/DELETE for any tracked table. No application-level logging code needed — the database handles it. The trigger computes a JSON diff of changed fields (NEW - OLD) for updates, full row for inserts/deletes. A app.current_user_id session variable is set per-request so the trigger can record who made the change.
Deliverables:
sql/0003-audit-log.sql:-- Audit log table CREATE TABLE app.audit_log ( audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), table_name TEXT NOT NULL, record_id UUID NOT NULL, action TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE' user_id UUID, -- from session variable, nullable for system actions changed_fields JSONB, -- for UPDATE: only changed fields {field: {old, new}} -- for INSERT: full new row -- for DELETE: full old row occurred_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX idx_audit_log_table_record ON app.audit_log(table_name, record_id); CREATE INDEX idx_audit_log_user ON app.audit_log(user_id); CREATE INDEX idx_audit_log_occurred ON app.audit_log(occurred_at); -- Generic trigger function CREATE OR REPLACE FUNCTION app.audit_trigger_func() RETURNS TRIGGER AS $$ DECLARE _user_id UUID; _record_id UUID; _changed JSONB; _old JSONB; _new JSONB; _key TEXT; BEGIN -- Get current user from session variable (set per-request by app) BEGIN _user_id := current_setting('app.current_user_id', true)::UUID; EXCEPTION WHEN OTHERS THEN _user_id := NULL; END; IF TG_OP = 'DELETE' THEN _record_id := (row_to_json(OLD) ->> TG_ARGV[0])::UUID; INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields) VALUES (TG_TABLE_NAME, _record_id, 'DELETE', _user_id, row_to_json(OLD)::JSONB); RETURN OLD; ELSIF TG_OP = 'INSERT' THEN _record_id := (row_to_json(NEW) ->> TG_ARGV[0])::UUID; INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields) VALUES (TG_TABLE_NAME, _record_id, 'INSERT', _user_id, row_to_json(NEW)::JSONB); RETURN NEW; ELSIF TG_OP = 'UPDATE' THEN _record_id := (row_to_json(NEW) ->> TG_ARGV[0])::UUID; _old := row_to_json(OLD)::JSONB; _new := row_to_json(NEW)::JSONB; _changed := '{}'::JSONB; FOR _key IN SELECT jsonb_object_keys(_new) LOOP IF _key = 'updated_at' THEN CONTINUE; END IF; IF (_old ->> _key) IS DISTINCT FROM (_new ->> _key) THEN _changed := _changed || jsonb_build_object(_key, jsonb_build_object('old', _old -> _key, 'new', _new -> _key)); END IF; END LOOP; -- Skip if nothing actually changed IF _changed = '{}'::JSONB THEN RETURN NEW; END IF; INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields) VALUES (TG_TABLE_NAME, _record_id, 'UPDATE', _user_id, _changed); RETURN NEW; END IF; RETURN NULL; END; $$ LANGUAGE plpgsql; -- Helper to attach trigger to a table (called in each domain migration) CREATE OR REPLACE FUNCTION app.enable_audit(target_table TEXT, pk_column TEXT DEFAULT 'id') RETURNS VOID AS $$ BEGIN EXECUTE format( 'CREATE TRIGGER audit_%I AFTER INSERT OR UPDATE OR DELETE ON app.%I FOR EACH ROW EXECUTE FUNCTION app.audit_trigger_func(%L)', target_table, target_table, pk_column ); END; $$ LANGUAGE plpgsql; -- Enable audit on existing tables SELECT app.enable_audit('user', 'user_id'); SELECT app.enable_audit('role', 'role_id'); SELECT app.enable_audit('permission', 'permission_id'); SELECT app.enable_audit('role_permission', 'role_id'); -- composite PK, use role_id SELECT app.enable_audit('user_role_assignment', 'assignment_id');- Each subsequent domain migration calls
SELECT app.enable_audit('table_name', 'pk_column')for its new tables - App middleware sets
SET LOCAL app.current_user_id = '<uuid>'per-request via Kysely connection hook packages/functions/src/lib/audit-context.ts— helper that runsSET LOCAL app.current_user_idon the Kysely connection so the trigger knows who made the change- Regenerate
db.types.ts
Verification:
- Migration runs clean
db.types.tscontainsAuditLoginterface- Insert/update/delete a user row → verify
app.audit_loghas corresponding entry - Update a user's
display_name→ verifychanged_fieldscontains{"display_name": {"old": "...", "new": "..."}}(not the full row) tscpasses
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.manageroles:roles.assign,roles.managestays:stays.request,stays.review,stays.manage,stays.view_allrooms:rooms.manage,rooms.request_preference,rooms.view_allboats:boats.manage,boats.request,boats.view_alltasks:tasks.manage,tasks.assign,tasks.claim,tasks.view_allretreats:retreats.manage,retreats.view,retreats.participatekitchen:kitchen.manage,kitchen.view,kitchen.update_dietaryinventory:inventory.manage,inventory.requestnotifications:notifications.viewaudit_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.userwith known emailadmin@perauset.org - Assign admin role to admin user
- Insert roles:
Verification:
- Migration runs clean
- Query
SELECT * FROM app.rolereturns 5 roles - Query
SELECT count(*) FROM app.permissionreturns expected count - Query
SELECT count(*) FROM app.role_permissionreturns 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-jsand@auth/coretopackages/functions/package.jsondependencies packages/functions/src/wirings/auth.wiring.ts— creates Auth.js routes with credentials provider (email/password lookup againstapp.user), wires them viacreateAuthRoutes+wireHTTPRoutesat/authbasePath- Update
packages/functions/src/middleware.ts— addauthJsSession({ 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— addlogin()method that calls/auth/csrfthen/auth/callback/credentialsto 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 infoe2e/tests/steps/phase-0/auth.steps.ts— step definitions
Verification:
- E2E:
GET /auth/sessionwithout cookies returns no session / 401 - E2E: After login via credentials flow,
GET /auth/sessionreturns session with userId e2e/tests/features/phase-0/auth.featurepasses
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, requiresusers.viewpermission or own userpackages/functions/src/functions/users/update-user-profile.function.ts-- PUT /users/:userId/profile, updates first_name, last_name, display_name, phone. Requires own user orusers.manage.packages/functions/src/functions/users/list-users.function.ts-- GET /users, requiresusers.viewpermission, 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.viewtag cannot list users (403) - User without
users.managetag 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? }. Taggedroles.assign.packages/functions/src/functions/roles/revoke-role.function.ts-- POST /roles/revoke, body:{ assignmentId }. Taggedroles.assign.packages/functions/src/functions/roles/list-roles.function.ts-- GET /roles, returns all rolespackages/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.assigntag cannot assign roles (403) - User without
roles.assigntag 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. Taggedaudit_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, andchanged_fieldsshowing the diff - User without
audit_log.viewtag 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.tscontainsStayRequestandStayinterfacestscpasses
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? }. Taggedstays.request. Creates stay_request with statussubmitted.packages/functions/src/functions/stays/review-stay-request.function.ts-- POST /stays/requests/:requestId/review, body:{ reviewNotes? }. Taggedstays.review. Transitions statussubmitted -> under_review.packages/functions/src/functions/stays/approve-stay-request.function.ts-- POST /stays/requests/:requestId/approve, body:{ reviewNotes? }. Taggedstays.review. Transitionssubmitted|under_review -> approved.packages/functions/src/functions/stays/reject-stay-request.function.ts-- POST /stays/requests/:requestId/reject, body:{ reviewNotes }. Taggedstays.review. Transitionssubmitted|under_review -> rejected.packages/functions/src/functions/stays/list-stay-requests.function.ts-- GET /stays/requests, query params:status,userId. Taggedstays.view_allfor 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.requesttag cannot submit a stay request (403) - User without
stays.reviewtag cannot review/approve/reject (403) - User can view own requests but not others' without
stays.view_all(403)
- Submit a stay request, verify status is
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? }. Taggedstays.manage. Request must beapproved. Creates stay with statuspending.packages/functions/src/functions/stays/check-in-stay.function.ts-- POST /stays/:stayId/check-in. Taggedstays.manage. Transitionsconfirmed -> checked_in.packages/functions/src/functions/stays/check-out-stay.function.ts-- POST /stays/:stayId/check-out. Taggedstays.manage. Transitionschecked_in -> checked_out.packages/functions/src/functions/stays/extend-stay.function.ts-- POST /stays/:stayId/extend, body:{ newDepartureDate }. Taggedstays.manage. Only valid whenconfirmedorchecked_in.packages/functions/src/functions/stays/cancel-stay.function.ts-- POST /stays/:stayId/cancel, body:{ reason? }. Taggedstays.manageor 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 }. Taggedstays.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. Taggedstays.view_allfor 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.managetag 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.sqlor createsql/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. Taggedrooms.manage.packages/functions/src/functions/rooms/block-room.function.ts-- POST /rooms/:roomId/block. Taggedrooms.manage. Checks no conflicting allocations.packages/functions/src/functions/rooms/request-room-preference.function.ts-- POST /rooms/requests, body:{ stayId, roomTypePref?, roomIdPref?, notes? }. Taggedrooms.request_preferenceor own stay.packages/functions/src/functions/rooms/allocate-room.function.ts-- POST /rooms/allocations, body:{ roomId, stayId, requestId?, startDate, endDate }. Taggedrooms.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 }. Taggedrooms.manage. Releases old, creates new.packages/functions/src/functions/rooms/release-room.function.ts-- POST /rooms/allocations/:allocationId/release. Taggedrooms.manage. Sets status toreleased.packages/functions/src/functions/rooms/cancel-room-allocation.function.ts-- POST /rooms/allocations/:allocationId/cancel. Taggedrooms.manage.packages/functions/src/functions/rooms/list-rooms.function.ts-- GET /rooms, includes availability info for date range. Taggedrooms.view_all.packages/functions/src/functions/rooms/list-room-allocations.function.ts-- GET /rooms/allocations. Taggedrooms.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.managetag 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_alltag 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. Taggedboats.manage.packages/functions/src/functions/boats/create-boat.function.ts-- POST /boats. Taggedboats.manage.packages/functions/src/functions/boats/create-boat-trip.function.ts-- POST /boats/trips. Taggedboats.manage.packages/functions/src/functions/boats/open-boat-trip.function.ts-- POST /boats/trips/:tripId/open. Taggedboats.manage. Transitionsplanned -> open_for_requests.packages/functions/src/functions/boats/request-boat-seat.function.ts-- POST /boats/trips/:tripId/request-seat. Taggedboats.request. Checks trip isopen_for_requests.packages/functions/src/functions/boats/review-boat-request.function.ts-- POST /boats/requests/:requestId/review. Taggedboats.manage.packages/functions/src/functions/boats/confirm-boat-request.function.ts-- POST /boats/requests/:requestId/confirm. Taggedboats.manage. Checks capacity. Creates manifest entry.packages/functions/src/functions/boats/waitlist-boat-request.function.ts-- POST /boats/requests/:requestId/waitlist. Taggedboats.manage.packages/functions/src/functions/boats/decline-boat-request.function.ts-- POST /boats/requests/:requestId/decline. Taggedboats.manage.packages/functions/src/functions/boats/cancel-boat-request.function.ts-- POST /boats/requests/:requestId/cancel. Own request orboats.manage.packages/functions/src/functions/boats/board-manifest-entry.function.ts-- POST /boats/manifest/:entryId/board. Taggedboats.manage. Setsboarded = true.packages/functions/src/functions/boats/complete-boat-trip.function.ts-- POST /boats/trips/:tripId/complete. Taggedboats.manage.packages/functions/src/functions/boats/list-boat-trips.function.ts-- GET /boats/trips. Taggedboats.view_all.packages/functions/src/functions/boats/list-boat-requests.function.ts-- GET /boats/requests. Own orboats.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.managetag cannot create routes/boats/trips or confirm/decline requests (403) - User without
boats.requesttag 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. Taggedtasks.manage.packages/functions/src/functions/tasks/generate-recurring-tasks.function.ts-- POST /tasks/generate, body:{ templateId, fromDate, toDate }. Taggedtasks.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. Taggedtasks.manage.packages/functions/src/functions/tasks/assign-task.function.ts-- POST /tasks/:taskId/assign, body:{ userId }. Taggedtasks.assign. Creates assignment, transitions taskplanned|open -> assigned.packages/functions/src/functions/tasks/claim-task.function.ts-- POST /tasks/:taskId/claim. Taggedtasks.claim. Self-assignment foropentasks.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 toopenif no other accepted assignments.packages/functions/src/functions/tasks/start-task.function.ts-- POST /tasks/:taskId/start. Assigned user. Transitionsassigned -> in_progress.packages/functions/src/functions/tasks/complete-task.function.ts-- POST /tasks/:taskId/complete, body:{ actualMinutes?, notes? }. Transitionsin_progress -> completed.packages/functions/src/functions/tasks/mark-task-blocked.function.ts-- POST /tasks/:taskId/block, body:{ reason }. Transitionsassigned|in_progress -> blocked.packages/functions/src/functions/tasks/reassign-task.function.ts-- POST /tasks/:taskId/reassign, body:{ userId }. Taggedtasks.assign.packages/functions/src/functions/tasks/list-tasks.function.ts-- GET /tasks. Taggedtasks.view_allfor 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.managetag cannot create templates or generate tasks (403) - User without
tasks.assigntag cannot assign tasks (403) - User without
tasks.claimtag 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? })helperpackages/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-requestnotifies the requesterreject-stay-requestnotifies the requesterassign-tasknotifies the assigneeconfirm-boat-requestnotifies 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. Taggedretreats.manage.packages/functions/src/functions/retreats/update-retreat.function.ts-- PUT /retreats/:retreatId. Taggedretreats.manage. Onlydraftretreats editable.packages/functions/src/functions/retreats/add-retreat-person.function.ts-- POST /retreats/:retreatId/people. Taggedretreats.manage. Body:{ userId, role }.packages/functions/src/functions/retreats/update-retreat-person-role.function.ts-- PUT /retreats/:retreatId/people/:retreatPersonId. Taggedretreats.manage.packages/functions/src/functions/retreats/link-stay-to-retreat-person.function.ts-- POST /retreats/:retreatId/people/:retreatPersonId/link-stay. Taggedretreats.manage. Body:{ stayId }.packages/functions/src/functions/retreats/publish-retreat.function.ts-- POST /retreats/:retreatId/publish. Taggedretreats.manage. Transitionsdraft -> published.packages/functions/src/functions/retreats/add-retreat-schedule-item.function.ts-- POST /retreats/:retreatId/schedule. Taggedretreats.manage.packages/functions/src/functions/retreats/update-retreat-schedule-item.function.ts-- PUT /retreats/:retreatId/schedule/:itemId. Taggedretreats.manage.packages/functions/src/functions/retreats/cancel-retreat.function.ts-- POST /retreats/:retreatId/cancel. Taggedretreats.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 toretreats.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.managetag cannot create/update/publish/cancel retreats (403) - Published retreats visible to authenticated users with
retreats.view, drafts only toretreats.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 orkitchen.manage. Upserts by user_id.packages/functions/src/functions/kitchen/create-meal-service.function.ts-- POST /kitchen/meals. Taggedkitchen.manage.packages/functions/src/functions/kitchen/estimate-meal-attendance.function.ts-- POST /kitchen/meals/:serviceId/estimate. Taggedkitchen.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 orkitchen.manage.packages/functions/src/functions/kitchen/list-meal-services.function.ts-- GET /kitchen/meals. Query params:date,mealType. Taggedkitchen.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.managetag cannot create meal services or estimate attendance (403) - User without
kitchen.viewtag 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. Taggedinventory.request.packages/functions/src/functions/inventory/review-inventory-request.function.ts-- POST /inventory/requests/:requestId/review. Taggedinventory.manage.packages/functions/src/functions/inventory/fulfill-inventory-request.function.ts-- POST /inventory/requests/:requestId/fulfill. Taggedinventory.manage. Updatescurrent_quantityon the item.packages/functions/src/functions/inventory/list-inventory.function.ts-- GET /inventory. Taggedinventory.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.requesttag cannot submit inventory requests (403) - User without
inventory.managetag 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 |