Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.encoreos.io/llms.txt

Use this file to discover all available pages before exploring further.

Version: 2.1.4
Last Updated: 2026-05-18
Constitution Reference: Section 1.3 (Integration Patterns - Pattern 3)
API Contracts provide synchronous request-response interactions between cores. These are REST APIs for direct data queries between cores when event-driven patterns are not suitable.

API Contract Standards

All API contracts MUST follow:
  • Versioning: /api/v1/{core}/{resource}
  • Authentication: JWT auth with RLS enforcement
  • Rate Limiting: Per-organization limits
  • Error Handling: Standard HTTP status codes
  • Response Format: JSON with consistent structure

Machine / organization API access (PF-97)

Interactive users authenticate with Supabase Auth JWTs; callers validate organization_id against session membership (pf_user_role_assignments / pf_has_org_access) as documented per endpoint. Programmatic access (organization API keys, service accounts, future OAuth client credentials) is specified in PF-97: Multi-Tenant Organization API Access. Machine clients use a separate credential lifecycle (issue, rotate, revoke, scope) and MUST NOT rely on interactive user RBAC alone. Integration contracts and error shapes for machine clients will live in PF-97 integration once implementation is available. Related: PF-30-EN-01 machine principals & scopes, PF-89-EN-01 machine client versioning.

Status Legend

  • Implemented - Fully implemented and tested
  • 📝 Planned - Specified but not yet implemented
  • 🟡 In Progress - Partially implemented

Planned API Contracts

CL-68: Residential Episode Context (planned)

Provider: CL (CL-68) via @/platform/clinical platform layer (synchronous; SECURITY DEFINER RPC) Consumers: PM-74 (authorization context), GR-27 (compliance counters) Status: 📝 Planned Spec Reference: CL-68 BHRF Clinical Residential Program; full contract CL-GR-PM-BHRF-EPISODE-LIFECYCLE Purpose: Lets PM/GR read BHRF clinical-residential episode context without querying CL tables (no cross-core import). Returns coded context only — no clinical narrative. 42 CFR Part 2: locValue is redacted to null (not omitted) when CL-11 SUD consent is absent/revoked, and each access is audit-logged per §2.31; the function returns null only when the episode is inaccessible/not found. Consumers MUST treat locValue === null as “LOC not disclosable” (no fallback guessing). CL is architecturally downstream — consumers depend on this platform layer, not on CL-68.

Auth & Error Model

  • Authentication: caller must present a valid bearer token (401 if unauthenticated).
  • Tenant isolation: caller must be org-authorized for organizationId (pf_has_org_access or equivalent) validated before any lookup. Parameter scoping alone is not a boundary. Required permission scope: cl.bhrf.view (residential episode context read).
  • Error → return mapping (the two cross-tenant cases are distinct and unambiguous):
    • 401 — unauthenticated request.
    • 403 — authenticated but not org-authorized for the supplied organizationId (cross-tenant/mismatched token, or missing cl.bhrf.view). Decided before any lookup.
    • top-level null (404 semantics) — caller is org-authorized for organizationId, but the episodeId does not exist within that authorized tenant scope (existence is not revealed). Never used for an authorization failure.
    • locValue === null — record exists but LOC redacted by 42 CFR Part 2 (CL-11 consent absent/revoked); the context object is still returned.
    • Operational failures return standardized platform errors — do not overload top-level null.
  • Audit: every access attempt is logged per §2.31, including whether locValue was redacted and any denied-access (401/403) outcome.

Synchronous API Contract

TypeScript Interface:
// @/platform/clinical
export async function getResidentialEpisodeContext(params: {
  organizationId: string;
  episodeId: string;
}): Promise<{
  status: 'pending' | 'clinically_active' | 'discharged' | 'cancelled';
  locValue: string | null;        // null when CL-11 SUD consent absent/revoked (redacted, not omitted)
  assessmentCompleted: boolean;
  assessmentDueDate: string;
  initialPlanDueDate: string;
} | null>;                        // null only if the episode is inaccessible/not found

GR-20: External LMS Bridge (planned)

Provider: GR-20 (External LMS Bridge) Consumers: GR-02 (training assignments), HR-22 / HR-27 (completion evidence) when implemented Status: 📝 Planned Spec Reference: GR-20 External LMS Bridge Purpose: Outbound REST/sync and optional inbound webhooks to external LMS vendors (SCORM/xAPI adapters). Contract payloads and auth live in the spec and future GR-20-*-INTEGRATION.md; this row satisfies integration-contract coverage for GR-20 until the integration doc ships.

Synchronous API Contract

Endpoints:
  • Pull Completions: GET /lms/completions?org_id={org_id}&since={date}
  • Push Enrollment: POST /lms/enrollments
TypeScript Interface:
interface LmsAdapter {
  vendor: 'relias' | 'healthstream' | 'cornerstone' | 'adp_learning' | 'workday_learning';
  pullCompletions(orgId: string, since: Date): Promise<ExternalCompletion[]>;
  pushEnrollment(orgId: string, e: EnrollmentInput): Promise<{ external_id: string }>;
}

interface ExternalCompletion {
  external_completion_id: string;
  external_user_id: string;       // resolved to hr_employees.id via integration mapping
  external_course_id: string;     // resolved to gr_training_courses.id via integration mapping
  completed_at: string;
  ceu_credits?: number;
  raw: unknown;                    // vendor payload, audit copy
}
Error Handling:
  • pushEnrollment failures: Event not acknowledged on failure; retried on next cron run
  • After 5 consecutive failures for the same enrollment, gr_lms_sync_runs.status marked 'failed' with requires_attention flag
  • Unmapped vendor users/courses: logged to gr_lms_sync_runs.error_summary, records_failed incremented
Supported Vendors:
  • Relias
  • HealthStream
  • Cornerstone
  • ADP Learning
  • Workday Learning
Rate Limiting:
  • Pull operations: ≤10k completions per run, chunked per gr_module_settings.lms.max_pull_batch_size
  • Target: <5 minutes per pull run (p95)
Required Schemas:
  • gr_lms_integrations: vendor configuration + vault secret reference
  • gr_lms_user_mappings: vendor user ID ↔ hr_employees.id
  • gr_lms_course_mappings: vendor course ID ↔ gr_training_courses.id
  • gr_lms_sync_runs: audit trail of sync operations
Error Codes:
  • UNMAPPED_USER: No mapping exists for external_user_id
  • UNMAPPED_COURSE: No mapping exists for external_course_id
  • VENDOR_AUTH_FAILED: Invalid credentials
  • VENDOR_RATE_LIMIT: Vendor rate limit exceeded
  • SYNC_RETRY_EXHAUSTED: 5+ consecutive failures

PF-43: Resource Quota Check (Database)

Provider: PF-43 (Tenant Resource Quotas)
Consumers: Edge functions, server-side callers, client via useResourceQuota hook
Status: 📝 Planned
Integration Doc: PF-43-tenant-resource-quotas-INTEGRATION.md
Purpose: Server-side quota check before operations; client hook useResourceQuota calls backend/edge that uses this function. Database function: pf_check_resource_quota(organization_id, resource_type, requested_amount)
Returns: (allowed, remaining, reset_at, quota_id, limit_type)reset_at is timestamptz (quota window reset); callers use it for countdown/display.
Request (logical):
  • organization_id (UUID): Tenant context
  • resource_type (TEXT): One of storage, api_calls, users, custom_objects, workflow_executions
  • requested_amount (NUMERIC, optional): Amount to check; default 1
Response: Allowed boolean, remaining quota, reset_at (quota window reset), quota_id, limit_type (soft/hard). Writes to pf_resource_usage and pf_quota_violations via SECURITY DEFINER. Client hook: useResourceQuota({ organizationId, resourceType, requestedAmount? }) — see spec API Design and integration doc.

PF-91: Compliance Edge Functions (internal)

Provider: PF-91 (Compliance Automation & Regulatory Dashboard)
Consumers: Supabase cron, authenticated org admins (evidence only)
Status: 📝 Planned
Integration Doc: PF-91-compliance-automation-regulatory-dashboard-INTEGRATION.md
Purpose: Internal Edge Function invocations (not public REST versioning). Contract shapes belong in the integration doc and supabase/functions/*/README.md; this row tracks ownership.
FunctionAuth patternRequest (logical)Response (logical)
compliance-run-checksService role / cron (verify_jwt documented false if cron-only)Optional organization_id for single-tenant run{ runs_completed, checks_inserted } — no PHI payloads
compliance-phi-scanService role / cronOptional organization_id{ columns_scanned, rows_upserted, capped: boolean }
generate-compliance-evidenceUser JWT + pf.compliance.evidence.generateorganization_id, framework, date_range_start, date_range_end{ evidence_id, status } — client polls row + signed URL
Storage: Evidence ZIPs and sidecar files use private bucket compliance-evidence with org-first path segment; signed URL download (see PF-91 integration doc). Note: Final JSON Schemas — see PENDING_CONTRACTS.md until PF-91 implementation freezes payloads.

PF-96: Jurisdiction Profile Resolution RPC

Provider: PF-96 (Medicaid State Compliance Configuration) Consumers: All CL/PM features, PF-91 compliance dashboard, edge functions Status: 📝 Planned Integration Doc: PF-96-medicaid-state-compliance-configuration-INTEGRATION.md Spec Reference: PF-96 spec Purpose: Server-side jurisdiction profile resolution with four-tier merge (Federal Baseline → State Profile → Org Overrides → Site Overrides). Used by frontend hook useJurisdictionProfile() and edge function helper getJurisdictionProfile(). Database RPC: pf_resolve_jurisdiction_profile(p_org_id UUID, p_site_id UUID DEFAULT NULL) Type: PostgreSQL RPC (SECURITY DEFINER) Resolution Order: Federal baseline → State profile (from pf_org_jurisdiction_config) → Org overrides → Site overrides (if p_site_id provided) Parameters:
  • p_org_id (UUID, required): Organization ID
  • p_site_id (UUID, optional): Site ID for site-level profile override
Returns: JSONB with the merged profile:
interface JurisdictionProfile {
  profile_id: string;           // UUID of effective profile
  state_code: string;           // e.g., "AZ", "CA", "US"
  program_code: string;         // e.g., "az-ahcccs", "ca-medi-cal", "us-federal-baseline"
  display_name: string;         // e.g., "Arizona AHCCCS"
  clinical: ClinicalRules;      // Merged clinical rule pack
  billing: BillingRules;        // Merged billing rule pack
  compliance: ComplianceRules;  // Merged compliance rule pack
}
Frontend Hook:
import { useJurisdictionProfile } from '@/platform/jurisdiction';

const { data: profile, isLoading, error } = useJurisdictionProfile(siteId?);
// Accesses: profile?.clinical, profile?.billing, profile?.compliance
Edge Function Helper:
import { getJurisdictionProfile } from '../_shared/jurisdiction.ts';

const profile = await getJurisdictionProfile(supabaseClient, orgId, siteId?);
Merge Behavior:
  • Per-rule-pack JSONB merge (clinical, billing, compliance packs merged separately)
  • Array fields replaced entirely (not appended) on override
  • Federal baseline fields cannot be weakened (validated via pf_validate_jurisdiction_override() trigger)
Performance: Profile resolution < 50ms p95; results cached client-side (5min staleTime, 10min gcTime) Tenant Authorization: Caller must be a member of p_org_id (verified via pf_has_org_access()); if p_site_id is provided, the site must belong to the organization. Unauthorized calls return 403. See also:

PF-101: Google Workspace Integration Edge Functions

Provider: PF-101 (Google Workspace Integration) Consumers: HR-01, CE-07, CE-21, PF-10, PF-11, PF-35, PF-86 Status: 🚧 Work in progress / partial delivery Integration Doc: PF-101-google-workspace-integration-INTEGRATION.md Spec Reference: PF-101 spec Purpose: Edge function contracts for Google Workspace Admin SDK Directory, Gmail, Calendar/Meet, Drive, Chat, Licensing, and Reports APIs. All functions use PF-76 credential retrieval, PF-42 rate limiting where applicable, sanitized errors, and correlation IDs.

google-workspace-test-connection

Type: Edge Function (HTTP) Auth: JWT + pf.google_workspace.test permission Purpose: Validate capability scopes and health Request:
{
  organization_id: uuid;
  connection_id: uuid;
  capabilities?: ('directory' | 'gmail' | 'calendar' | 'drive' | 'chat' | 'licensing' | 'reports')[];
}
Response:
{
  success: boolean;
  connection_id: uuid;
  tested_capabilities: {
    capability: string;
    status: 'ok' | 'failed';
    error_code?: string;
  }[];
  correlation_id: string;
}
Error Codes:
  • MISSING_CONNECTION: Connection not found
  • INVALID_CREDENTIALS: Credentials invalid or expired
  • INSUFFICIENT_SCOPES: Required scopes not granted
  • BAA_NOT_ATTESTED: BAA attestation required for PHI-capable operations

google-workspace-directory-sync

Type: Edge Function (Service Role / Scheduled) Auth: Service role (scheduled worker) Purpose: Batch Directory user/group/org unit sync Tenant-Scoping: REQUIRED - All queries MUST include .eq('organization_id', organizationId) and .eq('connection_id', connectionId) where applicable Request:
{
  organization_id: uuid;
  connection_id: uuid;
  sync_mode: 'full' | 'incremental';
}
Response:
{
  sync_id: uuid;
  users_synced: number;
  groups_synced: number;
  org_units_synced: number;
  errors: number;
  correlation_id: string;
}
Validation: Function must assert presence of tenant-scoping filters in all multi-tenant table queries.

google-workspace-provision-user

Type: Edge Function (Event Consumer + Service Role) Auth: Service role (event consumer) Purpose: Provision/link one Workspace user Tenant-Scoping: REQUIRED - All queries MUST include .eq('organization_id', organizationId) and .eq('connection_id', connectionId) where applicable Request:
{
  organization_id: uuid;
  employee_id: uuid;
  profile_id?: uuid;
  department_id?: uuid;
  position_id?: uuid;
  correlation_id: string;
}
Response:
{
  success: boolean;
  google_user_id?: string;
  link_id?: uuid;
  error?: string;
  correlation_id: string;
}
Validation: Function must assert presence of tenant-scoping filters in all multi-tenant table queries.

google-workspace-offboard-user

Type: Edge Function (Event Consumer + Service Role) Auth: Service role (event consumer) Purpose: Suspend user, revoke licenses, remove groups, queue Drive review Tenant-Scoping: REQUIRED - All queries MUST include .eq('organization_id', organizationId) and .eq('connection_id', connectionId) where applicable Request:
{
  organization_id: uuid;
  employee_id: uuid;
  termination_date: timestamp;
  correlation_id: string;
}
Response:
{
  success: boolean;
  user_suspended: boolean;
  licenses_revoked: number;
  groups_removed: number;
  drive_transfer_queued: boolean;
  correlation_id: string;
}
Validation: Function must assert presence of tenant-scoping filters in all multi-tenant table queries.

google-workspace-gmail-send

Type: Edge Function (HTTP) Auth: JWT + sender policy Purpose: Shared Gmail sender path with sender allowlist, BAA gate, PF-86 signature, and redacted audit Request:
{
  organization_id: uuid;
  from: string;
  to: string[];
  cc?: string[];
  bcc?: string[];
  subject: string;
  body_html: string;
  body_text?: string;
  attachments?: { filename: string; content: string; mime_type: string }[];
  correlation_id?: string;
}
Response:
{
  success: boolean;
  message_id?: string;
  error?: string;
  correlation_id: string;
}
Error Codes:
  • SENDER_NOT_ALLOWED: Sender not in allowlist
  • BAA_NOT_ATTESTED: BAA required for Gmail operations
  • QUOTA_EXCEEDED: Gmail send quota exceeded

google-workspace-calendar-sync

Type: Edge Function (JWT/Service Role) Auth: JWT or service role Purpose: Calendar/Meet event sync and CE wrapper support Tenant-Scoping: REQUIRED - All queries MUST include .eq('organization_id', organizationId) and .eq('connection_id', connectionId) where applicable Request:
{
  organization_id: uuid;
  action: 'create' | 'update' | 'delete' | 'query_freebusy';
  event_data?: {
    summary: string;
    start: timestamp;
    end: timestamp;
    attendees?: string[];
    description?: string;
    create_meet?: boolean;
  };
  freebusy_query?: {
    calendar_ids: string[];
    time_min: timestamp;
    time_max: timestamp;
  };
  correlation_id?: string;
}
Response:
{
  success: boolean;
  event_id?: string;
  meet_link?: string;
  freebusy?: Record<string, { busy: { start: timestamp; end: timestamp }[] }>;
  error?: string;
  correlation_id: string;
}
Validation: Function must assert presence of tenant-scoping filters in all multi-tenant table queries.

google-workspace-drive-sync

Type: Edge Function (JWT/Service Role) Auth: JWT or service role Purpose: Drive metadata and mapping sync Tenant-Scoping: REQUIRED - All queries MUST include .eq('organization_id', organizationId) and .eq('connection_id', connectionId) where applicable Request:
{
  organization_id: uuid;
  connection_id: uuid;
  action: 'sync_metadata' | 'create_folder' | 'share_resource';
  resource_data?: {
    name?: string;
    parent_id?: string;
    permissions?: { email: string; role: string }[];
  };
  correlation_id?: string;
}
Response:
{
  success: boolean;
  resource_id?: string;
  resources_synced?: number;
  error?: string;
  correlation_id: string;
}
Validation: Function must assert presence of tenant-scoping filters in all multi-tenant table queries.

google-workspace-reports-ingest

Type: Edge Function (Service Role / Scheduled) Auth: Service role (scheduled worker) Purpose: Reports API audit ingestion Tenant-Scoping: REQUIRED - All queries MUST include .eq('organization_id', organizationId) and .eq('connection_id', connectionId) where applicable Request:
{
  organization_id: uuid;
  connection_id: uuid;
  report_types?: ('admin' | 'login' | 'drive' | 'calendar')[];
  start_time?: timestamp;
  end_time?: timestamp;
}
Response:
{
  success: boolean;
  reports_ingested: number;
  events_processed: number;
  correlation_id: string;
}
Validation: Function must assert presence of tenant-scoping filters in all multi-tenant table queries.

google-workspace-chat-notify

Type: Edge Function (Service Role / PF-10) Auth: Service role (PF-10 consumer) Purpose: Send approved notifications to Chat spaces Tenant-Scoping: REQUIRED - All queries MUST include .eq('organization_id', organizationId) and .eq('connection_id', connectionId) where applicable Request:
{
  organization_id: uuid;
  connection_id: uuid;
  space_id: string;
  message: {
    text: string;
    cards?: unknown[];
  };
  correlation_id?: string;
}
Response:
{
  success: boolean;
  message_id?: string;
  error?: string;
  correlation_id: string;
}
Validation: Function must assert presence of tenant-scoping filters in all multi-tenant table queries.
Security & Compliance:
  • No PHI-capable operations unless pf_google_workspace_connections.baa_attested_at is set
  • All credentials stored in PF-76 only
  • Domain-wide delegation scopes versioned and reviewed
  • No PHI/PII in logs, cache keys, or telemetry
See also:

PM-47: Managed Care Auth Tracking

Provider: PM-47 (Managed Care Authorization Tracking) Consumers: PM-08 (claims submission), PM dashboard, edge functions Status: 📝 Planned Integration Doc: PM-47-managed-care-authorization-tracking-INTEGRATION.md Spec Reference: PM-47 spec Purpose: Edge functions for auth expiration monitoring and pre-submission auth-to-claim validation.

Edge Function: pm-auth-expiration-alerts

Type: Scheduled (Cron) Method: Invoked by pg_cron daily at 6:00 AM Auth: Service role (cron invocation) Schedule: 0 6 * * * Purpose: Check for authorizations reaching configured alert thresholds (14/7/3/1 days before expiration) and send PF-10 notifications to assigned UR coordinators. Request: None (cron-triggered, no request body) Response:
{
  organizations_processed: number;
  alerts_sent: number;
  errors: number;
}
Processing:
  1. Query pm_managed_care_authorizations where expiration_date - CURRENT_DATE matches alert intervals from pm_module_settings.auth_alert_intervals_days
  2. For each matching authorization, send notification via PF-10 with patient, payer, service type, expiration date, and remaining units
  3. Track sent alerts to avoid duplicates (idempotent)
Performance: Process up to 10,000 active authorizations per organization in < 30s (NFR-1)

Edge Function: pm-auth-claim-validation

Type: HTTP POST Method: POST Auth: JWT (user context) Permission Required: pm.managed_care_auth.validate Purpose: Validate claim lines against active authorizations before claim submission. Called by PM-08 claim submission workflow. Request:
{
  claim_id: string;              // UUID — pm_claims.id
  claim_lines: Array<{
    line_number: number;
    date_of_service: string;     // ISO 8601 date
    units: number;
    service_type: string;        // e.g., 'residential', 'php', 'iop'
  }>;
  organization_id: string;       // UUID — tenant context
}
Response:
{
  valid: boolean;                // Overall validation result
  results: Array<{
    line_number: number;
    status: 'pass' | 'fail';
    reason?: string;             // e.g., 'no_active_auth', 'units_exceeded', 'date_outside_auth'
    authorization_id?: string;   // UUID of matching auth (if found)
  }>;
}
Validation Logic:
  1. For each claim line, query pm_managed_care_authorizations where:
    • organization_id matches
    • status IN ('active', 'expiring')
    • service_type matches claim line service type
    • effective_date <= date_of_service <= expiration_date
  2. Check that used_units + claim_line_units <= authorized_units
  3. Return structured validation result per line
Performance: Complete validation in < 500ms for batch of up to 50 claim lines (NFR-1) Idempotency: Safe to re-run; no state changes during validation Error Response Schema:
{
  error: {
    code: string;           // Canonical error code
    message: string;        // User-friendly error message
    details?: any;          // Optional additional context
  }
}
Status Code Mappings:
  • 400 Bad RequestINVALID_REQUEST - Invalid input (missing fields, malformed data)
  • 403 ForbiddenACCESS_DENIED - Auth/permission failure (missing pm.managed_care_auth.validate permission or organization_id mismatch)
  • 404 Not FoundCLAIM_NOT_FOUND - Missing claim_id in request
  • 500 Internal Server ErrorSERVER_ERROR - Server-side processing error
Tenant Validation Requirements:
  • Organization ID Validation: The incoming organization_id in the request body MUST be validated against the authenticated JWT tenant context. Reject requests where organization_id does not match the JWT org claim.
  • Permission Check: Caller MUST have pm.managed_care_auth.validate permission for the organization. Return 403 ACCESS_DENIED if permission check fails.
  • Application-Layer Filtering: Implementations MUST apply organization_id filtering on all mutations (UPDATE/DELETE operations) to prevent cross-tenant access.
  • Database Layer: RLS policies and tenant-isolation MUST be enabled at the DB layer as defense-in-depth to prevent cross-tenant data access even if application-layer filtering fails.

CL-02-EN-60: Intake Element Completeness Checker RPC

Provider: CL (Clinical Core) Consumers: Internal use (triggers, application-layer completeness checks) Status: ✅ Complete Integration Doc: CL-02-EN-60-jurisdiction-aware-assessment-requirements-INTEGRATION.md Spec Reference: CL-02-EN-60 spec Purpose: Generic intake assessment element completeness checker that accepts a dynamic list of required elements from jurisdiction profiles, replacing Arizona-specific logic. Database RPC: cl_check_intake_elements(p_assessment_id UUID, p_required_elements JSONB) Type: PostgreSQL RPC (SECURITY DEFINER) Call Pattern: Synchronous Parameters:
  • p_assessment_id (UUID, required): Intake assessment ID
  • p_required_elements (JSONB, required): JSON array of required element codes (e.g., '["presenting_problem","medical_history","sdoh_screening_completed"]')
Returns: JSONB with per-element boolean completion status:
{
  "presenting_problem": true,
  "history_of_present_illness": true,
  "medical_history": false,
  "mental_health_history": true,
  "substance_use_history": true,
  "social_history": false,
  "preliminary_diagnoses": true,
  "sdoh_screening_completed": true
}
Element Code Mapping: Each element code maps to a specific column or condition in cl_intake_assessments:
  • chief_complaint, presenting_problemchief_complaint IS NOT NULL AND <> ''
  • history_present_illness, history_of_present_illnesshistory_of_present_illness IS NOT NULL AND <> ''
  • medical_historymedical_history IS NOT NULL AND <> ''
  • mental_health_history, psychiatric_historymental_health_history IS NOT NULL AND <> ''
  • substance_use_historysubstance_use_history IS NOT NULL AND <> ''
  • social_historysocial_history IS NOT NULL AND <> ''
  • preliminary_diagnoses, diagnostic_formulationpreliminary_diagnoses IS NOT NULL AND jsonb_array_length(preliminary_diagnoses) > 0
  • sdoh_screening_completedsdoh_screening_id IS NOT NULL
Error Behavior:
  • Returns NULL if assessment not found
  • Returns empty object {} if p_required_elements is empty array
  • Unknown element codes return false in result object
Versioning: Internal RPC; no external API versioning. Schema changes require migration coordination with consuming features. Related RPCs:
  • cl_check_ahcccs_18_elements(p_assessment_id UUID) — Legacy wrapper (DEPRECATED); calls cl_check_intake_elements with Arizona 18-element defaults
Trigger Usage:
  • trg_cl_intake_element_complete on cl_intake_assessments calls this function to maintain intake_element_complete column
See also:

FA-01: Episode Balance Query

Endpoint: /api/v1/fa/episode-balance Method: GET Provider: FA (Finance & Accounting) Consumer: RH (Recovery Housing) — episode detail page and bed board payment status Status: 🟡 Partial — Edge function fa-episode-balance + hook useFaEpisodeBalance; FA AR ledger join 📋 Planned Spec Reference: Purpose: Query payment status for an episode to display in the RH episode detail view and (optionally) the bed board. Enables RH to surface billing information without direct database access to FA tables. Request:
GET /api/v1/fa/episode-balance?episode_id={episode_id}&as_of_date={date}
Authorization: Bearer {jwt_token}
Query Parameters:
  • episode_id (required): UUID of episode
  • as_of_date (optional): Date to calculate balance as of (ISO 8601 format, defaults to current date)
Response Schema (200 OK):
{
  episode_id: uuid;
  organization_id: uuid;
  site_id?: uuid;
  current_balance: number;              // Balance as of as_of_date
  past_due_balance: number;              // Amount past due (>30 days)
  last_payment_date?: date;             // Most recent payment date
  last_payment_amount?: number;         // Most recent payment amount
  payment_status: 'current' | 'past_due' | 'paid_in_full' | 'delinquent';
  total_charged: number;                 // Total charges for episode
  total_paid: number;                    // Total payments received
  account_status: 'active' | 'closed' | 'on_hold';
  payment_history: [{
    payment_id: uuid;
    payment_date: date;
    amount: number;
    method: 'check' | 'ach' | 'card' | 'cash' | 'other';
    reference_number?: string;
  }];
  next_payment_due_date?: date;          // Next scheduled payment
  next_payment_due_amount?: number;      // Next scheduled payment amount
}
Error Response Schema:
{
  error: {
    code: 'EPISODE_NOT_FOUND' | 'ACCESS_DENIED' | 'INVALID_DATE' | 'SERVER_ERROR';
    message: string;                      // Human-readable error message
    details?: {
      episode_id?: uuid;                 // Episode ID that was queried
      organization_id?: uuid;             // Organization context
    };
  }
}
Error Codes:
  • 400 Bad Request: Invalid query parameters (e.g., malformed UUID, invalid date format)
    • Error code: INVALID_DATE or INVALID_EPISODE_ID
  • 401 Unauthorized: Missing or invalid JWT token
    • Error code: UNAUTHORIZED
  • 403 Forbidden: User does not have access to episode (RLS policy violation)
    • Error code: ACCESS_DENIED
  • 404 Not Found: Episode not found in FA system or episode belongs to different organization
    • Error code: EPISODE_NOT_FOUND
  • 429 Too Many Requests: Rate limit exceeded
    • Error code: RATE_LIMIT_EXCEEDED
    • Response includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset headers
  • 500 Internal Server Error: Internal FA error
    • Error code: SERVER_ERROR
Authentication:
  • JWT required in Authorization: Bearer {token} header
  • Token validated against Supabase Auth
  • RLS policies enforce organization isolation
  • User must have access to the episode’s organization
Rate Limiting:
  • 100 requests/minute per organization
  • Rate limit headers included in all responses:
    • X-RateLimit-Limit: 100
    • X-RateLimit-Remaining: Remaining requests in current window
    • X-RateLimit-Reset: Unix timestamp when limit resets
Implementation Notes:
  • Balance calculation includes all transactions up to as_of_date
  • Past due balance calculated as transactions >30 days overdue
  • Payment status determined by balance and payment history
  • All amounts in organization’s base currency
  • Response cached for 5 minutes (same episode_id + as_of_date)
Version: v1.0.1
Last Updated: 2026-03-20

FA-10: Tax Reporting APIs

Status: 📝 Planned
Spec Reference: FA-10 (Tax Reporting & Compliance)
Last Verified: N/A – planned

API 1: Generate 1099 Forms

Endpoint: /api/v1/fa/tax/1099/generate
Method: POST
Provider: FA (Finance & Accounting)
Consumer: Internal (FA module)
Status: 📝 Planned
Tenant Context Enforcement:
  • The organization_id from the request body MUST be validated against the authenticated session’s organization membership using pf_has_org_access(organization_id, auth.uid())
  • If the request organization_id does not match any organization the caller has access to, return 403 ACCESS_DENIED
  • Do NOT trust organization_id from the request body alone; always derive from authenticated session or validate via helper function
PII/PHI Logging Guidelines:
  • NEVER log: Vendor names, vendor emails, tax identification numbers (EIN/SSN), bank account fragments, payment amounts in error logs
  • ALLOWED in logs: UUIDs (form_id, vendor_id, tax_year_id), error codes, operation timestamps, organization_id
  • Restrict error.details to non-sensitive identifiers only (UUIDs, codes, timestamps)
Request Schema:
{
  organization_id: uuid;
  tax_year_id: uuid;
  form_type?: '1099-MISC' | '1099-NEC' | 'both';
}
Response Schema (200 OK):
{
  generated_count: number;
  forms: [{
    form_id: uuid;
    vendor_id: uuid;
    vendor_name: string;
    total_payments: number;
  }];
}
Usage Example (Write Endpoint):
// Generate 1099 forms
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');

const response = await fetch('/api/v1/fa/tax/1099/generate', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session.access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    organization_id: session.user.user_metadata.organization_id,
    tax_year_id: taxYearId,
    form_type: 'both',
  }),
});

if (!response.ok) {
  const error = await response.json();
  if (error.error?.code === 'ACCESS_DENIED') {
    throw new Error('Access denied: organization not accessible');
  }
  throw new Error(error.error?.message || 'Failed to generate 1099 forms');
}

const result = await response.json();

FA-11: Fixed Assets APIs

Status: 📝 Planned
Spec Reference: FA-11 (Fixed Assets & Depreciation)
Last Verified: N/A – planned

API 1: Asset Depreciation

Endpoint: /api/v1/fa/assets/{asset_id}/depreciation
Method: GET
Provider: FA (Finance & Accounting)
Consumer: FA-07 (Financial Reporting)
Status: 📝 Planned (Implementation: client-side — Supabase queries from FA module)
Tenant Context Enforcement:
  • The asset’s organization_id MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the asset belongs to an organization the caller cannot access
PII/PHI Logging Guidelines:
  • NEVER log: Asset serial numbers, purchase prices, vendor details, location addresses in error logs
  • ALLOWED in logs: UUIDs (asset_id, journal_entry_id), error codes, timestamps, organization_id
Response Schema (200 OK):
{
  asset_id: uuid;
  asset_name: string;
  total_depreciation: number;
  depreciation_history: [{
    depreciation_month: date;
    depreciation_amount: number;
    journal_entry_id: uuid;
  }];
}

API 2: Process Monthly Depreciation

Endpoint: /api/v1/fa/assets/depreciation/process
Method: POST
Provider: FA (Finance & Accounting)
Consumer: Internal (FA module)
Status: 📝 Planned (Implementation: client-side — Supabase mutations from FA module)
Tenant Context Enforcement:
  • The organization_id from the request body MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the organization is not accessible
PII/PHI Logging Guidelines:
  • NEVER log: Asset serial numbers, purchase prices, vendor details, location addresses in error logs
  • ALLOWED in logs: UUIDs (organization_id, asset_id), error codes, timestamps, counts
Request Schema:
{
  organization_id: uuid;
  depreciation_month: date; // YYYY-MM-01 format
  asset_ids?: uuid[]; // Optional: specific assets, otherwise all active
}
Response Schema (200 OK):
{
  processed_count: number;
  total_depreciation: number;
  journal_entries_created: number;
  assets: [{
    asset_id: uuid;
    depreciation_amount: number;
    journal_entry_id: uuid;
  }];
}
Usage Example (Write Endpoint):
// Process monthly depreciation
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');

const response = await fetch('/api/v1/fa/assets/depreciation/process', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session.access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    organization_id: session.user.user_metadata.organization_id,
    depreciation_month: '2026-01-01',
  }),
});

if (!response.ok) {
  const error = await response.json();
  if (error.error?.code === 'ACCESS_DENIED') {
    throw new Error('Access denied: organization not accessible');
  }
  throw new Error(error.error?.message || 'Failed to process depreciation');
}

const result = await response.json();

FA-12: Expense Management APIs

Status: 📝 Planned
Spec Reference: FA-12 (Expense Management & Reimbursements)
Last Verified: N/A – planned

API 1: Approve Expense Report

Endpoint: /api/v1/fa/expenses/reports/{report_id}/approve
Method: POST
Provider: FA (Finance & Accounting)
Consumer: FW-03 (Approval Workflow)
Status: 📝 Planned (Implementation: client-side — Supabase + workflow from FA/FW modules)
Tenant Context Enforcement:
  • The expense report’s organization_id MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the report belongs to an organization the caller cannot access
PII/PHI Logging Guidelines:
  • NEVER log: Employee names, receipt images, credit card numbers, bank account details, expense descriptions containing PHI
  • ALLOWED in logs: UUIDs (report_id, employee_id), error codes, timestamps, organization_id, status codes
Request Schema:
{
  approved: boolean;
  comments?: string;
  approved_amount?: number;
}
Response Schema (200 OK):
{
  expense_report_id: uuid;
  status: 'approved' | 'rejected';
  approved_at: timestamp;
}
Usage Example (Write Endpoint):
// Approve expense report
const response = await fetch(`/api/v1/fa/expenses/reports/${reportId}/approve`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session.access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    approved: true,
    comments: 'Approved per policy',
    approved_amount: 150.00,
  }),
});

if (!response.ok) {
  const error = await response.json();
  throw new Error(error.error?.message || 'Failed to approve expense report');
}

const result = await response.json();

FA-13: Project Accounting APIs

Status: 📝 Planned
Spec Reference: FA-13 (Project Accounting & Grant Tracking)
Last Verified: N/A – planned

API 1: Project Budget vs Actual

Endpoint: /api/v1/fa/projects/{project_id}/budget-vs-actual
Method: GET
Provider: FA (Finance & Accounting)
Consumer: FA-07 (Financial Reporting), FA-08 (Budgeting)
Status: 📝 Planned (Implementation: client-side — Supabase/RPC from FA module)
Tenant Context Enforcement:
  • The project’s organization_id MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the project belongs to an organization the caller cannot access
PII/PHI Logging Guidelines:
  • NEVER log: Grant recipient names, client identifiers, project descriptions containing PHI
  • ALLOWED in logs: UUIDs (project_id, account_id), error codes, timestamps, organization_id
Query Parameters:
  • period_start (optional): Start date for period
  • period_end (optional): End date for period
Response Schema (200 OK):
{
  project_id: uuid;
  project_name: string;
  period_start: date;
  period_end: date;
  budget_total: number;
  actual_total: number;
  variance: number;
  variance_percentage: number;
  by_account: [{
    account_id: uuid;
    account_name: string;
    budget_amount: number;
    actual_amount: number;
    variance: number;
  }];
}

FA-14: Cash Management APIs

Status: 📝 Planned
Spec Reference: FA-14 (Cash Management & Treasury)
Last Verified: N/A – planned

API 1: Cash Position

Endpoint: /api/v1/fa/cash/position
Method: GET
Provider: FA (Finance & Accounting)
Consumer: FA-07 (Financial Reporting), FA-16 (Analytics)
Status: 📝 Planned (Implementation: client-side — Supabase/hooks from FA module)
Tenant Context Enforcement:
  • The organization_id MUST be derived from the authenticated session (not from query parameters)
  • If a request includes organization_id in query params, validate it matches the caller’s accessible organizations via pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED for mismatches
PII/PHI Logging Guidelines:
  • NEVER log: Bank account numbers, routing numbers, account holder names, transaction details
  • ALLOWED in logs: UUIDs (organization_id), error codes, timestamps, aggregated amounts (without account-level detail)
Query Parameters:
  • as_of_date (optional): Date for cash position (defaults to current date)
Response Schema (200 OK):
{
  organization_id: uuid;
  position_date: date;
  operating_cash: number;
  payroll_cash: number;
  reserve_cash: number;
  grant_cash: number;
  other_cash: number;
  total_cash: number;
  cash_by_fund: Record<string, number>;
}
Usage Example (Read Endpoint):
// FA-14: Cash Position query
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';

export function useCashPosition(asOfDate?: Date) {
  return useQuery({
    queryKey: ['cash-position', asOfDate],
    queryFn: async () => {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) throw new Error('Not authenticated');

      const params = new URLSearchParams();
      if (asOfDate) {
        params.append('as_of_date', asOfDate.toISOString().split('T')[0]);
      }

      const response = await fetch(
        `/api/v1/fa/cash/position?${params.toString()}`,
        {
          headers: {
            Authorization: `Bearer ${session.access_token}`,
            'Content-Type': 'application/json',
          },
        }
      );

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error?.message || 'Failed to fetch cash position');
      }

      return response.json();
    },
  });
}

API 2: Update Cash Position

Endpoint: /api/v1/fa/cash/position
Method: PUT
Provider: FA (Finance & Accounting)
Consumer: Internal (FA module), FA-06 (Bank Reconciliation)
Status: 📝 Planned (Implementation: client-side — Supabase mutations from FA module)
Tenant Context Enforcement:
  • The organization_id from the request body MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the organization is not accessible
PII/PHI Logging Guidelines:
  • NEVER log: Bank account numbers, routing numbers, account holder names, transaction details
  • ALLOWED in logs: UUIDs (organization_id, position_id), error codes, timestamps, aggregated amounts
Request Schema:
{
  organization_id: uuid;
  position_date: date;
  operating_cash?: number;
  payroll_cash?: number;
  reserve_cash?: number;
  grant_cash?: number;
  other_cash?: number;
  cash_by_fund?: Record<string, number>;
}
Response Schema (200 OK):
{
  position_id: uuid;
  organization_id: uuid;
  position_date: date;
  total_cash: number;
  updated_at: timestamp;
}
Usage Example (Write Endpoint):
// Update cash position
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');

const response = await fetch('/api/v1/fa/cash/position', {
  method: 'PUT',
  headers: {
    'Authorization': `Bearer ${session.access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    organization_id: session.user.user_metadata.organization_id,
    position_date: '2026-01-15',
    operating_cash: 50000.00,
    payroll_cash: 25000.00,
  }),
});

if (!response.ok) {
  const error = await response.json();
  if (error.error?.code === 'ACCESS_DENIED') {
    throw new Error('Access denied: organization not accessible');
  }
  throw new Error(error.error?.message || 'Failed to update cash position');
}

const result = await response.json();

API 3: Create Investment

Endpoint: /api/v1/fa/investments
Method: POST
Provider: FA (Finance & Accounting)
Consumer: Internal (FA module)
Status: 📝 Planned
Tenant Context Enforcement:
  • The organization_id from the request body MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the organization is not accessible
PII/PHI Logging Guidelines:
  • NEVER log: Investment account numbers, bank details, account holder names
  • ALLOWED in logs: UUIDs (organization_id, investment_id), error codes, timestamps
Request Schema:
{
  organization_id: uuid;
  investment_type: string;
  amount: number;
  maturity_date: date;
  interest_rate: number;
  notes?: string;
}
Response Schema (201 Created):
{
  investment_id: uuid;
  investment_number: string;
  organization_id: uuid;
  status: 'active';
  created_at: timestamp;
}
Usage Example (Write Endpoint):
// Create investment
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');

const response = await fetch('/api/v1/fa/investments', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session.access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    organization_id: session.user.user_metadata.organization_id,
    investment_type: 'Certificate of Deposit',
    amount: 100000.00,
    maturity_date: '2026-12-31',
    interest_rate: 4.5,
  }),
});

if (!response.ok) {
  const error = await response.json();
  if (error.error?.code === 'ACCESS_DENIED') {
    throw new Error('Access denied: organization not accessible');
  }
  throw new Error(error.error?.message || 'Failed to create investment');
}

const result = await response.json();

FA-15: Cost Allocation APIs

Status: 📝 Planned
Spec Reference: FA-15 (Cost Allocation & Indirect Cost Rates)
Last Verified: N/A – planned

API 1: Allocate Indirect Costs

Endpoint: /api/v1/fa/idc/rates/{rate_id}/allocate
Method: POST
Provider: FA (Finance & Accounting)
Consumer: FA-13 (Project Accounting)
Status: 📝 Planned
Tenant Context Enforcement:
  • The rate’s organization_id and all project organization_id values MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if any referenced entity belongs to an inaccessible organization
PII/PHI Logging Guidelines:
  • NEVER log: Project names containing client identifiers, grant recipient details
  • ALLOWED in logs: UUIDs (rate_id, project_id, journal_entry_id), error codes, timestamps, organization_id
Request Schema:
{
  fiscal_period_id: uuid;
  project_ids?: uuid[]; // Optional: specific projects, otherwise all active
}
Response Schema (200 OK):
{
  allocation_count: number;
  total_allocated_idc: number;
  allocations: [{
    project_id: uuid;
    project_name: string;
    allocated_idc: number;
    journal_entry_id: uuid;
  }];
}

FA-16: Financial Analytics APIs

Status: 📝 Planned
Spec Reference: FA-16 (Financial Analytics & Dashboards)
Last Verified: N/A – planned

API 1: Financial Dashboard

Endpoint: /api/v1/fa/analytics/dashboard
Method: GET
Provider: FA (Finance & Accounting)
Consumer: Internal (FA module)
Status: 📝 Planned
Tenant Context Enforcement:
  • The organization_id MUST be derived from the authenticated session (not from query parameters)
  • If a request includes organization_id in query params, validate it matches the caller’s accessible organizations via pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED for mismatches
PII/PHI Logging Guidelines:
  • NEVER log: Individual transaction details, vendor names, employee names, account-level breakdowns containing identifiers
  • ALLOWED in logs: UUIDs (organization_id, kpi_id), error codes, timestamps, aggregated metrics only
Query Parameters:
  • period_start (optional): Start date for analytics
  • period_end (optional): End date for analytics
Response Schema (200 OK):
{
  organization_id: uuid;
  period_start: date;
  period_end: date;
  metrics: {
    total_revenue: number;
    total_expenses: number;
    net_income: number;
    total_cash: number;
  };
  kpis: [{
    kpi_id: uuid;
    kpi_name: string;
    kpi_value: number;
    target_value?: number;
    status: 'on_target' | 'warning' | 'critical';
  }];
  trends: {
    revenue_trend: number[]; // Array of values over time
    expense_trend: number[];
    cash_trend: number[];
  };
}

FA-17: Intercompany APIs

Status: 📝 Planned
Spec Reference: FA-17 (Intercompany Transactions)
Last Verified: N/A – planned

API 1: Generate Intercompany Eliminations

Endpoint: /api/v1/fa/intercompany/eliminations/generate
Method: POST
Provider: FA (Finance & Accounting)
Consumer: FA-09 (Consolidation)
Status: 📝 Planned
Tenant Context Enforcement:
  • The consolidation’s organization_id and all related organization IDs MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • For intercompany transactions, validate access to both source_org_id and target_org_id
  • Return 403 ACCESS_DENIED if the caller lacks access to any involved organization
PII/PHI Logging Guidelines:
  • NEVER log: Organization names, transaction descriptions containing identifiers
  • ALLOWED in logs: UUIDs (consolidation_id, elimination_id, journal_entry_id), error codes, timestamps
Request Schema:
{
  consolidation_id: uuid;
  fiscal_period_id: uuid;
}
Response Schema (200 OK):
{
  elimination_count: number;
  total_elimination_amount: number;
  eliminations: [{
    elimination_id: uuid;
    intercompany_account_id: uuid;
    elimination_amount: number;
    journal_entry_id: uuid;
  }];
}

FA-18: Revenue Recognition APIs

Status: 📝 Planned
Spec Reference: FA-18 (Revenue Recognition Advanced)
Integration: FA-18-revenue-recognition-advanced-INTEGRATION.md
Last Verified: 2026-04-26 — contract frozen (implementation pending migrations)

API 1: Recognize Revenue

Endpoint: /api/v1/fa/revenue/recognize
Method: POST
Provider: FA (Finance & Accounting)
Consumer: Internal (FA module)
Status: 📝 Planned
Implementation note: Primary server entry point is RPC fa_process_revenue_recognition(organization_id, recognition_date, fiscal_period_id, user_id) per integration doc. This HTTP route, when implemented, is a thin wrapper that enforces PF-30 fa.revenue_recognitions.process and maps the body to RPC arguments. Tenant Context Enforcement:
  • All revenue schedules’ organization_id values MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid()) (or equivalent FA tenant helper)
  • Return 403 ACCESS_DENIED if any schedule belongs to an inaccessible organization or the caller lacks fa.revenue_recognitions.process
PII/PHI Logging Guidelines:
  • NEVER log: Contract details, client names, revenue source identifiers
  • ALLOWED in logs: UUIDs (schedule_id, recognition_id, journal_entry_id), error codes, timestamps, organization_id
Request Schema:
{
  recognition_date: date;
  fiscal_period_id: uuid;
  schedule_ids?: uuid[]; // Optional: specific schedules, otherwise all due for the period
}
Response Schema (200 OK):
{
  recognition_count: number;
  total_recognized: number;
  recognitions: [{
    recognition_id: uuid;
    revenue_schedule_id: uuid;
    recognition_amount: number;
    journal_entry_id?: uuid;
    journal_entry_line_id?: uuid;
  }];
}

FA-19: Financial Close APIs

Status: 📝 Planned
Spec Reference: FA-19 (Financial Close Management)
Last Verified: N/A – planned

API 1: Approve Close Period

Endpoint: /api/v1/fa/close/periods/{period_id}/approve
Method: POST
Provider: FA (Finance & Accounting)
Consumer: Internal (FA module)
Status: 📝 Planned
Tenant Context Enforcement:
  • The close period’s organization_id MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the period belongs to an inaccessible organization
PII/PHI Logging Guidelines:
  • NEVER log: Approval notes containing sensitive details, financial summaries with identifiers
  • ALLOWED in logs: UUIDs (period_id, approved_by), error codes, timestamps, organization_id, status codes
Request Schema:
{
  approval_notes?: string;
}
Response Schema (200 OK):
{
  close_period_id: uuid;
  status: 'locked';
  approved_at: timestamp;
  approved_by: uuid;
}
Usage Example:
// RH-01: Query payment status in episode detail view
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';

export function useEpisodeBalance(episodeId: string, asOfDate?: Date) {
  return useQuery({
    queryKey: ['episode-balance', episodeId, asOfDate],
    queryFn: async () => {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) throw new Error('Not authenticated');

      const params = new URLSearchParams({
        episode_id: episodeId,
      });
      if (asOfDate) {
        params.append('as_of_date', asOfDate.toISOString().split('T')[0]);
      }

      const response = await fetch(
        `/api/v1/fa/episode-balance?${params.toString()}`,
        {
          headers: {
            Authorization: `Bearer ${session.access_token}`,
            'Content-Type': 'application/json',
          },
        }
      );

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error?.message || 'Failed to fetch balance');
      }

      return response.json();
    },
    enabled: !!episodeId,
    staleTime: 5 * 60 * 1000, // 5 minutes
  });
}
Testing Requirements:
  • Unit tests for balance calculation logic
  • Integration tests for RLS enforcement
  • E2E tests for full request/response cycle
  • Rate limiting tests
  • Error handling tests for all error codes

RH-01: Current Residence/Bed Query (CL Chart — Optional)

Endpoint: /api/v1/rh/current-residence Method: GET Provider: RH (Recovery Housing) Consumer: CL (Clinical & EHR) — patient chart, read-only Status: 📝 Planned (CL-01 chart enhancements — optional) Spec Reference: Purpose: Allows CL to display the patient’s current residence and bed on the patient chart without a direct cross-core dependency on RH tables. Returns null if patient is not currently admitted in any RH facility. Request:
GET /api/v1/rh/current-residence?patient_id={patient_id}
Authorization: Bearer {jwt_token}
Query Parameters:
  • patient_id (required): UUID of the patient (from pf_patient_identities)
Response Schema (200 OK):
{
  episode_id: string;
  residence_id: string;
  residence_name: string;
  facility_type: 'recovery_housing' | 'psychiatric_residential' | 'inpatient_unit';
  bed_label: string;
  room_number: string | null;
  unit_label: string | null;   // Non-null for inpatient_unit
  admission_date: string;      // ISO date
} | null  // null if not currently admitted
Notes:
  • RLS enforces org-level isolation; patient_id must belong to the caller’s organization.
  • CL must not import directly from @/cores/rh/...; use this Platform Integration Layer endpoint.
Content-Type: application/json (response); no request body (GET). Authentication & Permissions: PII/PHI Logging Guidance (current-residence):
  • Allowed to log: Non-identifying metadata only — request timestamps, response status (e.g. 200, 404, 403, 429), residence_id (redacted or hashed), facility_type.
  • Forbidden: Any patient identifiers (patient_id, pf_patient_identities, names, DOB), exact admission_date beyond date-only if considered PHI, and JWT or token contents.
  • Practice: Redact or use hashed IDs for any IDs in logs; never log raw JWT or patient identifiers.
  • Example log line: GET /api/v1/rh/current-residence 200 residence_id=<redacted> facility_type=recovery_housing ts=2026-02-22T12:00:00Z
Tenant Context Enforcement:
  • RLS policies on rh_episodes, rh_beds, and rh_residences enforce organization_id ownership.
  • patient_id (from pf_patient_identities) must belong to the caller’s organization; requests for cross-org patients return 404 Not Found (not 403) to avoid leaking existence.
Rate Limits:
  • Limit: 60 requests / minute per authenticated user.
  • Response headers: X-RateLimit-Limit: 60, X-RateLimit-Remaining: {n}, X-RateLimit-Reset: {unix_ts}.
  • Exceeded: 429 Too Many Requests with Retry-After header.
Error Response Schema:
{
  error: {
    code: string;       // e.g. 'PATIENT_NOT_FOUND', 'FORBIDDEN', 'RATE_LIMIT_EXCEEDED'
    message: string;    // Human-readable description
    resolution?: string; // Suggested remediation for 4xx errors
  };
}
HTTP Statuserror.codeCondition
400INVALID_PATIENT_IDpatient_id is not a valid UUID
403FORBIDDENCaller lacks rh.beds.view permission
404PATIENT_NOT_FOUNDpatient_id not found in caller’s org
429RATE_LIMIT_EXCEEDEDPer-user rate limit reached
500INTERNAL_ERRORUnexpected server-side failure
TypeScript usage example (GET current-residence with TanStack Query):
type CurrentResidenceResponse = {
  episode_id: string;
  residence_id: string;
  residence_name: string;
  facility_type: 'recovery_housing' | 'psychiatric_residential' | 'inpatient_unit';
  bed_label: string;
  room_number: string | null;
  unit_label: string | null;
  admission_date: string;
} | null;

type CurrentResidenceError = {
  error: { code: string; message: string; resolution?: string };
};

async function fetchCurrentResidence(
  patientId: string,
  accessToken: string
): Promise<CurrentResidenceResponse> {
  const res = await fetch(
    `${API_BASE}/api/v1/rh/current-residence?patient_id=${encodeURIComponent(patientId)}`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
  );
  if (res.status === 200) return res.json();
  const body = (await res.json()) as CurrentResidenceError;
  if (res.status === 403) throw new Error(body.error?.message ?? 'Forbidden');
  if (res.status === 404) return null;
  if (res.status === 429) {
    const retryAfter = res.headers.get('Retry-After');
    throw new Error(`Rate limited${retryAfter ? `; retry after ${retryAfter}s` : ''}`);
  }
  throw new Error(body.error?.message ?? `Request failed ${res.status}`);
}

// TanStack Query: recommended staleTime/gcTime and retry
const { data } = useQuery({
  queryKey: ['rh', 'current-residence', patientId],
  queryFn: () => fetchCurrentResidence(patientId, session.access_token),
  enabled: !!patientId && !!session?.access_token,
  staleTime: 30_000,
  gcTime: 300_000,
  retry: (failureCount, error) => {
    if (failureCount >= 3) return false;
    // Retry on 429/503/504; respect Retry-After when available
    return /rate limit|503|504/i.test(String(error));
  },
});
Performance SLA:
  • p50: < 100 ms · p95: < 500 ms · p99: < 1 000 ms.
  • Server-side query timeout: 5 000 ms; returns 504 Gateway Timeout if exceeded.
  • This endpoint reads cached census data; no heavy aggregates are executed.
Retry & Idempotency:
  • Safe to retry: GET is idempotent; callers may retry on 429 (after Retry-After) and 503/504.
  • No write semantics; no idempotency key required.
Caching Strategy:
  • Response is cacheable; recommended client Cache-Control: max-age=30, stale-while-revalidate=60.
  • Server sets Cache-Control: public, max-age=30 on 200 responses.
  • Callers should use TanStack Query with staleTime: 30_000 and gcTime: 300_000.

PF-30: Permission Check API

Endpoint: Database function pf_has_permission()
Provider: PF (Platform Foundation)
Consumer: All Cores
Status: 🟡 In Progress (Phase 1 Complete)
Spec Reference: PF-30 Permissions System V2
Implementation Notes (Phase 1 Complete - 2025-12-11):
  • Database schema created: pf_custom_roles, pf_module_permissions, pf_role_permissions, pf_user_role_assignments
  • RLS policies implemented with proper WITH CHECK clauses
  • ~168 module permissions seeded across 7 modules + system
  • Helper function pf_get_user_permissions() available for permission lookup
Purpose: Check if a user has a specific module permission, integrating with PF-26 for three-tier permission checks. Function Signature:
pf_has_permission(
  p_user_id UUID,
  p_organization_id UUID,
  p_permission_key TEXT,
  p_site_id UUID DEFAULT NULL
) RETURNS BOOLEAN
Parameters:
  • p_user_id (required): UUID of the user to check
  • p_organization_id (required): UUID of the organization context
  • p_permission_key (required): Permission key in format {module}.{entity}.{action}
  • p_site_id (optional): UUID of site for site-scoped permission checks
Return Value:
  • TRUE: User has the permission
  • FALSE: User does not have the permission
Permission Check Hierarchy:
1. Check system role permissions (pf_user_roles)

2. Check custom role permissions (pf_user_role_assignments → pf_role_permissions)

3. If site_id provided, validate site scope from role assignment

4. Return TRUE if any role grants the permission, FALSE otherwise
Integration with PF-26: When checking access to a specific record, the full permission check hierarchy is:
1. Module Permission (PF-30): pf_has_permission('hr.employees.view')
   ↓ (if TRUE)
2. Object Permission (PF-26): pf_object_permissions check
   ↓ (if TRUE)
3. Field Permission (PF-26): pf_field_permissions check
Usage Example:
-- Check if user can view employees in org
SELECT pf_has_permission(
  auth.uid(),
  'org-uuid-here',
  'hr.employees.view'
);

-- Check if user can approve bills at specific site
SELECT pf_has_permission(
  auth.uid(),
  'org-uuid-here',
  'fa.bills.approve',
  'site-uuid-here'
);
Frontend Hook:
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { useOrganization } from '@/platform/organizations/OrganizationContext';

export function useHasPermission(permissionKey: string, siteId?: string) {
  const { currentOrganization } = useOrganization();

  return useQuery({
    queryKey: ['permission', permissionKey, currentOrganization?.id, siteId],
    queryFn: async () => {
      const { data, error } = await supabase.rpc('pf_has_permission', {
        p_user_id: (await supabase.auth.getUser()).data.user?.id,
        p_organization_id: currentOrganization?.id,
        p_permission_key: permissionKey,
        p_site_id: siteId ?? null,
      });
      
      if (error) throw error;
      return data as boolean;
    },
    enabled: !!currentOrganization?.id,
    staleTime: 5 * 60 * 1000, // 5 minutes per constitution
    gcTime: 10 * 60 * 1000,   // 10 minutes per constitution
  });
}
Security Notes:
  • Function is SECURITY DEFINER to avoid RLS recursion
  • Always uses SET search_path = public for security
  • Logs permission checks to audit log when enabled
  • Returns FALSE on any error (fail closed)
Performance:
  • p50 < 20ms, p95 < 50ms, p99 < 100ms
  • Optimized with database indexes on role and permission tables
  • Frontend caching reduces database load
Version: v1.0.0 (Phase 1 complete, Phase 2 planned)
Last Updated: 2025-12-11

HR-01: Employee Lookup

Endpoint: /api/v1/hr/employees/{employee_id} Method: GET Provider: HR (Workforce) Consumer: RH (Recovery Housing); optional CL consumer for provider lookup — see PM-17 integration doc. Status: 📝 Planned (RH-06 Implementation, Q2 2026) Spec Reference: RH-06 (Compliance & Staff Operations) Tenant isolation: organization_id enforced by RLS; JWT required. All queries scoped by authenticated session/claims. Purpose: Lookup employee details for staff assignments in Recovery Housing Request:
GET /api/v1/hr/employees/{employee_id}
Authorization: Bearer {jwt_token}
Path Parameters:
  • employee_id (required): UUID of employee
Response Schema (200 OK):
{
  employee_id: uuid;
  employee_number: string;
  full_name: string;
  job_title: string;
  department: string;
  hire_date: date;
  employment_status: 'active' | 'inactive' | 'terminated';
  contact_email: string;
  contact_phone: string;
}
Error Codes:
  • 404 Not Found: Employee not found
  • 403 Forbidden: User does not have access to employee
  • 500 Internal Server Error: Internal HR error
Authentication: JWT required, RLS enforces organization isolation
Rate Limiting: 100 requests/minute per organization
Version: v1.0.0 (planned)

Endpoint: /api/v1/hr/employees Method: GET Provider: HR (Workforce) Consumer: RH (Recovery Housing); optional CL consumer for provider lookup — see PM-17 integration doc. Status: 📝 Planned (RH-06 Implementation, Q2 2026) Spec Reference: RH-06 (Compliance & Staff Operations) Tenant isolation: organization_id enforced by RLS; JWT required. All queries scoped by authenticated session/claims. Purpose: Search employees for staff assignments in Recovery Housing Request:
GET /api/v1/hr/employees?department=Recovery Housing&status=active&page=1&page_size=50
Authorization: Bearer {jwt_token}
Query Parameters:
  • department (optional): Filter by department name
  • status (optional): Filter by employment status (active, inactive, terminated)
  • job_title (optional): Filter by job title
  • page (optional): Page number (default: 1)
  • page_size (optional): Results per page (default: 50, max: 100)
Response Schema (200 OK):
{
  employees: [{
    employee_id: uuid;
    employee_number: string;
    full_name: string;
    job_title: string;
    department: string;
    hire_date: date;
    employment_status: 'active' | 'inactive' | 'terminated';
  }];
  total_count: number;
  page: number;
  page_size: number;
  has_next_page: boolean;
}
Error Codes:
  • 400 Bad Request: Invalid query parameters
  • 403 Forbidden: User does not have access
  • 500 Internal Server Error: Internal HR error
Authentication: JWT required, RLS enforces organization isolation
Rate Limiting: 100 requests/minute per organization
Version: v1.0.0 (planned)

HR-06 ↔ HR-11: Benefits-Enabled Leave Integration

Integration Type: API Contract (Pattern 3)
Status: 📝 Planned (HR-06 Phase 2)
Version: 1.0
Provider: HR-11 (Benefits Administration)
Consumer: HR-06 Phase 2 (Leave Management)
Purpose: Link leave policies to benefit plans and track benefits-eligible leave. Enables HR-06 to check employee benefit enrollment status and subscribe to enrollment changes. API Endpoints:

Query: Employee Benefit Enrollment Status

Hook: useEmployeeBenefitEnrollment(employeeId, planType)
Location: src/cores/hr/hooks/useEmployeeBenefitEnrollment.ts
Provider: HR-11
Parameters:
  • employeeId (required): UUID of employee
  • planType (required): Benefit plan type - 'std' | 'ltd' | 'disability' | 'health' | 'dental' | 'vision' | 'retirement' | 'life'
Response Schema:
{
  id: uuid;
  employee_id: uuid;
  plan_id: uuid;
  plan: {
    id: uuid;
    name: string;
    plan_type: string;
    plan_category?: string;
  };
  status: 'pending' | 'approved' | 'active' | 'terminated' | 'cancelled';
  effective_date: date;
  termination_date?: date;
  employee_contribution: number;
  employer_contribution: number;
}
Usage Example:
import { useEmployeeBenefitEnrollment } from '@/cores/hr/hooks/useEmployeeBenefitEnrollment';

// In leave request component
const { data: stdEnrollment } = useEmployeeBenefitEnrollment(employeeId, 'std');
const isBenefitsEligible = stdEnrollment?.status === 'active';

Query: Leave Policy Benefits

Hook: useLeavePolicyBenefits(leavePolicyId)
Location: src/cores/hr/hooks/useLeavePolicyBenefits.ts
Provider: HR-06 Phase 2
Parameters:
  • leavePolicyId (required): UUID of leave policy
Response Schema:
[{
  id: uuid;
  leave_policy_id: uuid;
  benefit_plan_id: uuid;
  benefit_plan: {
    id: uuid;
    name: string;
    plan_type: string;
  };
  alignment_status: 'aligned' | 'misaligned' | 'pending' | 'not_applicable';
  alignment_verified_at?: timestamp;
  requires_enrollment: boolean;
  enrollment_impact?: string;
}]
Data Flow:
  1. HR Admin links leave policy to benefit plan (creates hr_leave_policy_benefits record with FK to hr_benefits_plans)
  2. Employee enrolls in benefit (HR-11 creates hr_benefits_enrollments record)
  3. HR-11 publishes benefits_enrollment_approved event
  4. HR-06 Phase 2 subscribes to event, updates leave eligibility
  5. Employee views leave request form, sees benefits-eligible status via useEmployeeBenefitEnrollment hook
Events:
  • benefits_enrollment_approved - HR-11 → HR-06 (enrollment status change to ‘approved’ or ‘active’)
  • benefits_enrollment_terminated - HR-11 → HR-06 (enrollment termination)
See Also: Version: v1.0.0 (planned)
Last Updated: 2026-01-09

FW-19: Workflow Query Action

Endpoint: Edge function workflow-query-action
Method: POST
Provider: FW (Forms & Workflow)
Consumer: Automation Executor
Status: 📝 Planned (FW-19 Implementation)
Spec Reference: FW-19 Data Query & Integration Actions
Purpose: Execute parameterized queries against whitelisted database tables within workflow execution. Request:
POST /functions/v1/workflow-query-action
Authorization: Bearer {jwt_token}
Content-Type: application/json
Request Body:
{
  organization_id: uuid;
  table_name: string;           // Must be in org's whitelist
  select_columns?: string[];    // Columns to return (null = all allowed)
  filters: [{
    column: string;
    operator: 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' | 'like' | 'in' | 'is_null' | 'is_not_null';
    value: any;
  }];
  order_by?: { column: string; direction: 'asc' | 'desc' };
  limit?: number;               // Default: 100, max: from whitelist
  execution_id: uuid;           // For audit logging
}
Response Schema (200 OK):
{
  data: any[];                  // Query results
  count: number;                // Number of rows returned
  query_time_ms: number;        // Query execution time
}
Error Response Schema:
{
  error: {
    code: 'TABLE_NOT_ALLOWED' | 'INVALID_FILTER' | 'QUERY_TIMEOUT' | 'ACCESS_DENIED' | 'SERVER_ERROR';
    message: string;
    details?: {
      table_name?: string;
      organization_id?: uuid;
    };
  }
}
Error Codes:
  • 400 Bad Request: Invalid query parameters or filters
  • 403 Forbidden: Table not in organization’s whitelist
  • 408 Request Timeout: Query exceeded 30s timeout
  • 500 Internal Server Error: Database error
Authentication: JWT required, RLS enforces organization isolation
Rate Limiting: 100 requests/minute per organization
Version: v1.0.0 (planned)

FW-19: Workflow API Action

Endpoint: Edge function workflow-api-action
Method: POST
Provider: FW (Forms & Workflow)
Consumer: Automation Executor
Status: 📝 Planned (FW-19 Implementation)
Spec Reference: FW-19 Data Query & Integration Actions
Purpose: Execute external API calls with authentication and retry logic within workflow execution. Request:
POST /functions/v1/workflow-api-action
Authorization: Bearer {jwt_token}
Content-Type: application/json
Request Body:
{
  organization_id: uuid;
  config: {
    connection_id?: string;       // Optional: use saved connection
    method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
    url: string;                  // Can use {{variables}}
    headers: Record<string, string>;
    body?: any;                   // JSON or template
    auth: {
      type: 'none' | 'api_key' | 'bearer' | 'basic' | 'oauth2';
      config: any;                // Type-specific auth config
    };
    retry: {
      max_attempts: number;       // 1-5
      backoff: 'linear' | 'exponential';
    };
    timeout_ms: number;           // Default: 30000
  };
  variables: Record<string, any>; // For template resolution
  execution_id: uuid;             // For audit logging
}
Response Schema (200 OK):
{
  status: number;               // HTTP status from external API
  data: any;                    // Parsed response body
  headers: Record<string, string>;
  duration_ms: number;          // Total request time
  retries_attempted: number;    // Number of retry attempts
}
Error Response Schema:
{
  error: {
    code: 'CONNECTION_FAILED' | 'AUTH_FAILED' | 'TIMEOUT' | 'MAX_RETRIES_EXCEEDED' | 'SERVER_ERROR';
    message: string;
    details?: {
      status_code?: number;
      external_error?: string;
      retries_attempted?: number;
    };
  }
}
Error Codes:
  • 400 Bad Request: Invalid config or URL
  • 401 Unauthorized: External API authentication failed
  • 408 Request Timeout: External API exceeded timeout
  • 502 Bad Gateway: External API returned error after all retries
  • 500 Internal Server Error: Internal function error
Authentication: JWT required, organization context required for credential lookup
Rate Limiting: 50 requests/minute per organization (lower due to external API calls)
Version: v1.0.0 (planned)

FW-22: Workflow Debug Control

Endpoint: Edge function workflow-debug-control
Method: POST
Provider: FW (Forms & Workflow)
Consumer: Workflow Developer, Operations Engineer
Status: ✅ Implemented (FW-22 Phase 4)
Spec Reference: FW-22 Workflow Execution Monitoring & Debugging
Purpose: Control debug sessions for workflow executions - start, pause, resume, step through. Request:
POST /functions/v1/workflow-debug-control
Authorization: Bearer {jwt_token}
Content-Type: application/json
Request Body:
{
  organization_id: uuid;
  action: 'start' | 'pause' | 'resume' | 'step' | 'stop' | 'set_breakpoint' | 'remove_breakpoint';
  execution_id: uuid;
  session_id?: uuid;              // Required for pause/resume/step/stop
  node_id?: string;               // Required for set_breakpoint/remove_breakpoint
  breakpoint_condition?: string;  // Optional: condition expression for breakpoint
  modified_variables?: Record<string, any>; // Optional: modify variables during step
}
Response Schema (200 OK):
{
  session_id: uuid;
  status: 'active' | 'paused' | 'stepping' | 'completed' | 'error';
  current_node_id?: string;
  variables: Record<string, any>;
  breakpoints: string[];
  step_history: [{
    node_id: string;
    variables: Record<string, any>;
    timestamp: string;
  }];
}
Error Response Schema:
{
  error: {
    code: 'EXECUTION_NOT_FOUND' | 'SESSION_NOT_FOUND' | 'PERMISSION_DENIED' | 'EXECUTION_COMPLETED' | 'SESSION_CONFLICT' | 'SERVER_ERROR';
    message: string;
    details?: {
      execution_id?: uuid;
      session_id?: uuid;
      active_session_user?: string;
    };
  }
}
Error Codes:
  • 400 Bad Request: Invalid action or missing required fields
  • 403 Forbidden: User lacks debug permissions for this execution
  • 404 Not Found: Execution or session not found
  • 409 Conflict: Another user has an active debug session
  • 500 Internal Server Error: Debug control error
Authentication: JWT required, user must have org_admin or workflow owner role
Rate Limiting: 100 requests/minute per organization
Version: v1.0.0 (released 2025-12-08)

FW-24: Sandbox Execute

Endpoint: Edge function sandbox-execute
Method: POST
Provider: FW (Forms & Workflow)
Consumer: Workflow Developer, QA Engineer
Status: 📝 Planned (FW-24 Implementation)
Spec Reference: FW-24 Workflow Testing & Sandbox
Purpose: Execute workflow in an isolated sandbox environment for testing. Request:
POST /functions/v1/sandbox-execute
Authorization: Bearer {jwt_token}
Content-Type: application/json
Request Body:
{
  organization_id: uuid;
  rule_id: uuid;
  input_data: Record<string, any>;
  test_dataset_id?: uuid;              // Optional: use test dataset
}
Response Schema (200 OK):
{
  execution_id: uuid;
  status: 'completed' | 'failed';
  output_data: Record<string, any>;
  duration_ms: number;
  execution_logs: [{
    timestamp: string;
    level: 'debug' | 'info' | 'warn' | 'error';
    node_id?: string;
    message: string;
  }];
}
Error Response Schema:
{
  error: {
    code: 'RULE_NOT_FOUND' | 'DATASET_NOT_FOUND' | 'PERMISSION_DENIED' | 'EXECUTION_FAILED' | 'SERVER_ERROR';
    message: string;
    details?: {
      rule_id?: uuid;
      dataset_id?: uuid;
      execution_error?: string;
    };
  }
}
Error Codes:
  • 400 Bad Request: Invalid input data or rule_id
  • 403 Forbidden: User lacks workflow edit permissions
  • 404 Not Found: Rule or test dataset not found
  • 500 Internal Server Error: Sandbox execution error
Authentication: JWT required, user must have workflow edit permissions
Rate Limiting: 50 requests/minute per organization
Version: v1.0.0 (planned)

FW-24: Test Datasets Import

Endpoint: Edge function test-datasets-import
Method: POST
Provider: FW (Forms & Workflow)
Consumer: Workflow Developer, QA Engineer
Status: 📝 Planned (FW-24 Implementation)
Spec Reference: FW-24 Workflow Testing & Sandbox
Purpose: Import test data from CSV or JSON files. Request:
POST /functions/v1/test-datasets-import
Authorization: Bearer {jwt_token}
Content-Type: multipart/form-data
Request Body (multipart):
  • file: File (CSV or JSON, max 10MB)
  • organization_id: UUID
  • name: string
  • description: string (optional)
Response Schema (200 OK):
{
  dataset_id: uuid;
  row_count: number;
  columns: string[];
  validation_warnings: [{
    row?: number;
    column?: string;
    message: string;
  }];
}
Error Codes:
  • 400 Bad Request: Invalid file format
  • 413 Payload Too Large: File exceeds 10MB limit
  • 422 Unprocessable Entity: Data validation errors
Authentication: JWT required, user must have workflow edit permissions
Rate Limiting: 10 requests/minute per organization
Version: v1.0.0 (planned)

FW-24: Test Cases Run

Endpoint: Edge function test-cases-run
Method: POST
Provider: FW (Forms & Workflow)
Consumer: Workflow Developer, QA Engineer
Status: 📝 Planned (FW-24 Implementation)
Spec Reference: FW-24 Workflow Testing & Sandbox
Purpose: Execute test cases against a workflow in sandbox. Request:
POST /functions/v1/test-cases-run
Authorization: Bearer {jwt_token}
Content-Type: application/json
Request Body:
{
  organization_id: uuid;
  rule_id: uuid;
  test_case_ids?: uuid[];              // Optional: run specific tests (all if omitted)
}
Response Schema (200 OK):
{
  total: number;
  passed: number;
  failed: number;
  duration_ms: number;
  results: [{
    test_case_id: uuid;
    name: string;
    status: 'pass' | 'fail' | 'error';
    duration_ms: number;
    error_message?: string;
    actual_output?: Record<string, any>;
    expected_output?: Record<string, any>;
  }];
}
Error Codes:
  • 400 Bad Request: Invalid test case IDs
  • 403 Forbidden: User lacks workflow edit permissions
  • 404 Not Found: Rule or test cases not found
  • 500 Internal Server Error: Test execution error
Authentication: JWT required, user must have workflow edit permissions
Rate Limiting: 20 requests/minute per organization
Version: v1.0.0 (planned)
Usage Example:
// Start a debug session
const { data: session } = await supabase.functions.invoke('workflow-debug-control', {
  body: {
    organization_id: orgId,
    action: 'start',
    execution_id: executionId,
  },
});

// Pause at current node
await supabase.functions.invoke('workflow-debug-control', {
  body: {
    organization_id: orgId,
    action: 'pause',
    execution_id: executionId,
    session_id: session.session_id,
  },
});

// Step to next node
await supabase.functions.invoke('workflow-debug-control', {
  body: {
    organization_id: orgId,
    action: 'step',
    execution_id: executionId,
    session_id: session.session_id,
    modified_variables: { overrideValue: 'test' }, // Optional
  },
});

FW-45: Evaluate Decision Table

Endpoint: Edge function evaluate-decision-table
Method: POST
Provider: FW (Forms & Workflow)
Consumer: Workflow steps, server-side validation, integrations
Status: ✅ Implemented
Spec Reference: FW-45 Decision Tables
Integration: Server-side evaluation + fw_log_decision_table_evaluation RPC
Request body (logical): table_id, facts (record), optional version_id, evaluation_context (workflow_step | form_validation | api_request), optional source_execution_id, source_step_id. Response (200): success, matched, matchedRuleCount, outputs, evaluationId, versionNumber, hitPolicy, optional error. Authentication: JWT; org access verified per _shared/auth.ts.

FW-46: Workflow Executor Worker

Endpoint: Edge function workflow-executor-worker
Method: POST (typically invoked by pg_cron via pg_net, not direct client)
Provider: FW
Consumer: Platform scheduler / automation infrastructure
Status: ✅ Implemented
Spec Reference: FW-46 Durable Execution Worker
Purpose: Dequeue from pgmq workflow_execution_queue (primary) or claim queued rows (fallback); advance workflow execution with checkpoint integration (FW-48); route permanent failures toward DLQ (FW-47). Request Body: Typically empty (cron invocation). Optional JSON body for manual testing:
interface WorkflowExecutorWorkerRequest {
  // Optional: for manual testing only
  organization_id?: string;  // Process specific org only
  batch_size?: number;      // Override default batch size
}
Success Response (200):
interface WorkflowExecutorWorkerResponse {
  success: true;
  correlationId: string;
  results: Record<string, ProcessingResult>;  // Key: organization_id
}

interface ProcessingResult {
  messagesProcessed: number;  // Total messages dequeued/claimed
  succeeded: number;           // Successfully processed
  failed: number;              // Failed during processing
  dlqRouted: number;           // Routed to dead letter queue
  errors: string[];            // Error messages (if any)
}
Error Response:
interface ErrorResponse {
  success: false;
  error: string;              // Human-readable error message
  category: ErrorCategory;    // 'config' | 'validation' | 'authorization' | 'not_found' | 'runtime' | 'timeout' | 'external_service'
  correlationId?: string;     // Request correlation ID for tracing
  details?: Record<string, unknown>;  // Additional error context (sanitized, no PHI/PII)
}
HTTP Status Codes:
  • 200 OK - Success (may include partial failures in results)
  • 400 Bad Request - Invalid request body
  • 500 Internal Server Error - Runtime error (e.g., queue read failure, semaphore acquisition failure)
Side Effects:
  • Updates fw_module_settings.worker_running and worker_last_run_at (semaphore pattern)
  • Updates fw_module_settings.worker_last_batch_size with processed count
  • Routes failed executions to workflow_dlq (FW-47) after max retries
  • Writes checkpoint data via FW-48 integration
  • Emits domain events for execution state changes (if configured)
Authentication: Service role / scheduled invocation pattern per deployment; not a general user-facing API.

FW-49: Workflow Timeout Checker

Endpoint: Edge function workflow-timeout-checker
Method: POST
Provider: FW
Consumer: pg_cron–scheduled watchdog
Status: ✅ Implemented
Spec Reference: FW-49 Execution Timeout & Watchdog
Integration: FW-49-execution-timeout-watchdog-INTEGRATION.md
Purpose: Scan overdue executions, apply on_timeout action (from timeout_config), emit workflow.execution.timed_out, integrate with FW-47 and PF-10. Request Body: Empty (cron invocation via createCronHandler). Optional JSON for manual testing:
interface WorkflowTimeoutCheckerRequest {
  // Optional: for manual testing only
  scan_window?: string;  // ISO 8601 duration (e.g., "PT1H")
  limit?: number;        // Max executions to process (default: 100 expired, 200 at-risk)
}
Success Response (200):
interface WorkflowTimeoutCheckerResponse {
  timedOutCount: number;      // Executions marked as timed_out/cancelled
  warningsSent: number;        // At-risk warning notifications sent
  errorsCount: number;         // Errors encountered during processing
  expiredChecked: number;      // Total expired executions scanned
  atRiskChecked: number;       // Total at-risk executions scanned
}
Error Response:
  • Errors are logged but do not cause HTTP error responses (cron handler pattern)
  • Individual execution processing errors increment errorsCount and continue processing
  • Database query errors are logged with correlation ID
HTTP Status Codes:
  • 200 OK - Processing completed (may include errorsCount > 0)
Side Effects:
  • Updates fw_workflow_executions.status to 'timed_out' or 'cancelled' based on timeout_config.on_timeout
  • Sets fw_workflow_executions.completed_at and error_message for timed-out executions
  • Sends notifications via PF-10 (createNotificationIfNew) for:
    • Timeout events (normal or high priority for escalate)
    • At-risk warnings (when elapsedPercent >= warning_threshold_percent)
  • Updates fw_workflow_executions.timeout_warning_sent = true after warning sent
  • Emits workflow.execution.timed_out domain event (if configured)
Authentication: Service role / cron invocation; not a user-facing API.

FW-49: Extend Execution Deadline

Endpoint: Edge function extend-execution-deadline
Method: POST
Provider: FW
Consumer: FW UI (admin), execution detail
Status: ✅ Implemented
Spec Reference: FW-49 Execution Timeout & Watchdog
Request Body:
interface ExtendExecutionDeadlineRequest {
  execution_id: string;        // UUID of workflow execution
  organization_id: string;      // UUID of organization (must match execution)
  extension_minutes: number;    // Minutes to extend (1-1440, inclusive)
  reason: string;              // Justification for extension (5-500 characters)
}
Success Response (200):
interface ExtendExecutionDeadlineResponse {
  success: true;
  correlationId: string;
  execution_id: string;
  previous_deadline: string;    // ISO 8601 timestamp
  new_deadline: string;         // ISO 8601 timestamp
  extension_minutes: number;
}
Error Response:
interface ErrorResponse {
  success: false;
  error: string;              // Human-readable error message
  category: ErrorCategory;    // 'validation' | 'authorization' | 'not_found' | 'runtime'
  correlationId?: string;     // Request correlation ID for tracing
  details?: Record<string, unknown>;  // Additional error context (e.g., { field: 'status', actual: 'completed' })
}
HTTP Status Codes:
  • 200 OK - Deadline extended successfully
  • 400 Bad Request - Validation error:
    • Missing required fields (execution_id, organization_id, extension_minutes, reason)
    • extension_minutes outside valid range (1-1440)
    • reason length invalid (5-500 characters)
    • Execution status not active (running, paused, pending, queued)
  • 401 Unauthorized - Missing or invalid JWT token
  • 403 Forbidden - Insufficient permissions (workflow admin required)
  • 404 Not Found - Execution not found or organization_id mismatch
  • 500 Internal Server Error - Database update failure
Side Effects:
  • Updates fw_workflow_executions.deadline_at with new deadline
  • Resets fw_workflow_executions.timeout_warning_sent = false to allow new warnings
  • Logs audit event with correlation ID, user ID, extension details, and timestamps
Authentication: JWT required; validates via validateAuth() from _shared/auth.ts. Requires fw.workflows.admin permission or equivalent org admin role.

FW-52: Workflow Export (Edge Function)

Endpoint: Edge function fw-workflow-export
Method: POST
Provider: FW
Consumer: FW UI, fw-promote CLI
Status: 📝 Planned
Spec Reference: FW-52 Workflow Import/Export & Portability
Integration Doc: FW-52-workflow-import-export-portability-INTEGRATION.md
Request Body (logical):
interface FwWorkflowExportRequest {
  workflow_id: string;
  organization_id: string;
  bundle?: boolean; // include forms + decision tables when true
}
Success Response (200): WorkflowExportPackage JSON (see spec schema). Authentication: JWT; verifyOrgAccess; permission fw.workflows.export.

FW-52: Workflow Import (Edge Function)

Endpoint: Edge function fw-workflow-import
Method: POST
Provider: FW
Consumer: FW UI, fw-promote CLI
Status: 📝 Planned
Spec Reference: FW-52 Workflow Import/Export & Portability
Integration Doc: FW-52-workflow-import-export-portability-INTEGRATION.md
Request Body (logical):
interface FwWorkflowImportRequest {
  organization_id: string;
  mode: 'dry_run' | 'apply';
  package: WorkflowExportPackage; // or raw JSON string + content-type
  resolution?: Record<string, 'rename' | 'skip' | 'overwrite'>; // apply only
}
Success Response (200): Dry-run: analysis summary JSON. Apply: { success: true, import_log_id: string }. Authentication: JWT; verifyOrgAccess; permission fw.workflows.import.

API Implementation Guidelines

Authentication

All API endpoints MUST:
  • Require JWT authentication in Authorization header
  • Validate JWT token and extract user context
  • Enforce RLS policies based on user’s organization access
  • Return 401 Unauthorized for missing/invalid tokens
  • Return 403 Forbidden for insufficient permissions

Rate Limiting

All API endpoints MUST:
  • Implement per-organization rate limits
  • Default limit: 100 requests/minute per organization
  • Return 429 Too Many Requests when limit exceeded
  • Include rate limit headers in response:
    • X-RateLimit-Limit: Maximum requests per window
    • X-RateLimit-Remaining: Remaining requests in window
    • X-RateLimit-Reset: Time when limit resets

Error Handling

All API endpoints MUST:
  • Use standard HTTP status codes
  • Return consistent error response format:
    &#123;
      error: &#123;
        code: string;        // Error code (e.g., 'EPISODE_NOT_FOUND')
        message: string;      // Human-readable message
        details?: any;        // Additional error details
      &#125;
    &#125;
    
  • Log all errors for debugging
  • Never expose sensitive data in error messages

Response Format

All successful responses MUST:
  • Return JSON with consistent structure
  • Include metadata when applicable (pagination, timestamps)
  • Use camelCase for field names
  • Include organization_id for multi-tenant context

Planned API Contracts (IT Module)

The IT (Information Technology) module defines 4 planned API contracts for asset, vendor, and license management. Full IT API documentation: IT Integration Contracts

IT-01: Asset Lookup

Endpoint: /api/v1/it/assets/{asset_id}
Method: GET
Provider: IT (IT-01 Asset Management)
Consumer: IT-02 (Support Ticketing), IT-05 (Security)
Status: 📝 Planned
Spec Reference: IT-01 IT Asset Management
Purpose: Lookup IT asset details for ticket linking and security tracking. Response Schema (200 OK):
{
  asset_id: uuid;
  asset_tag: string;
  asset_name: string;
  asset_type: 'desktop' | 'laptop' | 'tablet' | 'phone' | 'server' | 'network_equipment' | 'peripheral';
  model: string;
  serial_number: string;
  status: 'available' | 'assigned' | 'in_repair' | 'retired' | 'disposed';
  assigned_to_employee_id?: uuid;
  assigned_to_name?: string;
  location: { site_id: uuid; building?: string; room?: string };
  warranty_expiration_date?: date;
  organization_id: uuid;
}
Error Codes: 404 Not Found, 403 Forbidden, 500 Internal Server Error
Authentication: JWT required, RLS enforces organization isolation
Rate Limiting: 100 requests/minute per organization

Endpoint: /api/v1/it/assets
Method: GET
Provider: IT (IT-01 Asset Management)
Consumer: IT-02, IT-04, IT-05, IT-06
Status: 📝 Planned
Query Parameters:
  • asset_type (optional): Filter by asset type
  • status (optional): Filter by status
  • site_id (optional): Filter by site
  • search (optional): Search by asset tag, name, or serial number
  • page, page_size: Pagination
Authentication: JWT required, RLS enforces organization isolation
Rate Limiting: 100 requests/minute per organization

IT-03: Vendor Lookup

Endpoint: /api/v1/it/vendors/{vendor_id}
Method: GET
Provider: IT (IT-03 Vendor Management)
Consumer: IT-01, IT-04, IT-06
Status: 📝 Planned
Spec Reference: IT-03 IT Vendor Management
Purpose: Lookup IT vendor details for asset, license, and procurement linking. Response Schema (200 OK):
{
  vendor_id: uuid;
  vendor_name: string;
  vendor_type: 'hardware' | 'software' | 'service' | 'consulting' | 'reseller';
  contact_name?: string;
  contact_email?: string;
  status: 'active' | 'inactive' | 'suspended';
  contract_count: number;
  active_contracts: [{ contract_id: uuid; contract_type: string; expiration_date: date }];
  organization_id: uuid;
}
Authentication: JWT required, RLS enforces organization isolation
Rate Limiting: 100 requests/minute per organization

IT-04: License Compliance Check

Endpoint: Database function it_check_license_compliance()
Provider: IT (IT-04 Software License Management)
Consumer: IT-04 Dashboard, scheduled compliance checks
Status: 📝 Planned
Spec Reference: IT-04 Software License Management
Purpose: Check license compliance for a software license or all licenses. Function Signature:
it_check_license_compliance(
  p_organization_id UUID,
  p_license_id UUID DEFAULT NULL
) RETURNS TABLE (
  license_id UUID,
  software_name TEXT,
  license_count INTEGER,
  usage_count INTEGER,
  compliance_status TEXT,
  variance INTEGER,
  variance_percent NUMERIC
)
Security: Function is SECURITY DEFINER with SET search_path = public

FA-19: Financial Close Management APIs

Status: ✅ Implemented
Spec Reference: FA-19-financial-close-management.md
Last Verified: 2026-01-19

API 1: Close Period Approval

Endpoint: /api/v1/fa/close/periods/{period_id}/approve
Method: POST
Provider: FA (Finance & Accounting)
Consumer: FW-03 (Approval Workflow), FA-19 Internal
Status: ✅ Implemented
Purpose: Approve or reject a close period after all tasks are complete. This is a critical control point in the financial close process. Tenant Context Enforcement:
  • The close period’s organization_id MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the period belongs to an organization the caller cannot access
  • User must have fa.close.approve permission
PII/PHI Logging Guidelines:
  • NEVER log: User names, comments containing PII, financial amounts
  • ALLOWED in logs: UUIDs (period_id, user_id), error codes, timestamps, organization_id, status codes
Request Schema:
{
  approved: boolean;           // true = approve, false = reject
  comments?: string;           // Optional approval/rejection notes
}
Response Schema (200 OK):
{
  close_period_id: uuid;
  status: 'approved' | 'rejected';
  approved_at: timestamp;
  approved_by: uuid;
  comments?: string;
}
Error Response Schema:
{
  error: {
    code: 'PERIOD_NOT_FOUND' | 'ACCESS_DENIED' | 'INCOMPLETE_TASKS' | 
          'INVALID_STATUS' | 'PERMISSION_DENIED' | 'SERVER_ERROR';
    message: string;
    details?: {
      period_id?: uuid;
      incomplete_task_count?: number;
      current_status?: string;
    };
  }
}
Error Codes:
  • 400 Bad Request: Invalid request body
  • 403 Forbidden: User lacks fa.close.approve permission or organization access
    • Error code: ACCESS_DENIED or PERMISSION_DENIED
  • 404 Not Found: Close period not found
    • Error code: PERIOD_NOT_FOUND
  • 409 Conflict: Period not in pending_approval status or has incomplete tasks
    • Error code: INVALID_STATUS or INCOMPLETE_TASKS
  • 500 Internal Server Error: Unexpected error
    • Error code: SERVER_ERROR
Usage Example:
// Approve a close period
const { data: { session } } = await supabase.auth.getSession();
if (!session) throw new Error('Not authenticated');

const response = await fetch(`/api/v1/fa/close/periods/${periodId}/approve`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${session.access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    approved: true,
    comments: 'All tasks verified. Month-end close approved.',
  }),
});

if (!response.ok) {
  const error = await response.json();
  if (error.error?.code === 'INCOMPLETE_TASKS') {
    throw new Error(`Cannot approve: ${error.error.details.incomplete_task_count} tasks incomplete`);
  }
  throw new Error(error.error?.message || 'Failed to approve close period');
}

const result = await response.json();
Implementation Notes:
  • Period must be in pending_approval status
  • All tasks must be completed or skipped before approval
  • Approval triggers close_period_approved event
  • Rejection triggers status change back to in_progress
  • Approval locks period from further modifications

API 2: Close Period Status Query

Endpoint: /api/v1/fa/close/periods/{period_id}/status
Method: GET
Provider: FA (Finance & Accounting)
Consumer: FA-07 (Financial Reporting), FA-02 (General Ledger)
Status: ✅ Implemented
Purpose: Query the current status and progress of a close period. Used by Financial Reporting to verify close status before generating final reports. Tenant Context Enforcement:
  • The close period’s organization_id MUST be validated against the authenticated session using pf_has_org_access(organization_id, auth.uid())
  • Return 403 ACCESS_DENIED if the period belongs to an organization the caller cannot access
PII/PHI Logging Guidelines:
  • NEVER log: User names, task descriptions containing PHI
  • ALLOWED in logs: UUIDs (period_id), error codes, timestamps, organization_id, task counts
Response Schema (200 OK):
{
  close_period_id: uuid;
  period_name: string;
  period_type: 'month' | 'quarter' | 'year';
  status: 'not_started' | 'in_progress' | 'pending_approval' | 
          'approved' | 'rejected' | 'completed' | 'locked';
  fiscal_period_id: uuid | null;        // Linked fiscal period
  period_start_date: date;
  period_end_date: date;
  task_summary: {
    total: number;                      // Total tasks in period
    completed: number;                  // Completed tasks
    skipped: number;                    // Skipped tasks
    in_progress: number;                // Tasks being worked
    blocked: number;                    // Blocked by dependencies
    not_started: number;                // Not yet started
  };
  completion_percentage: number;        // 0-100 based on completed+skipped
  started_at: timestamp | null;
  started_by: uuid | null;
  completed_at: timestamp | null;
  completed_by: uuid | null;
  approved_at: timestamp | null;
  approved_by: uuid | null;
}
Error Codes:
  • 403 Forbidden: User does not have access to period’s organization
    • Error code: ACCESS_DENIED
  • 404 Not Found: Close period not found
    • Error code: PERIOD_NOT_FOUND
Usage Example:
// Query close period status
import { useQuery } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';

export function useClosePeriodStatus(periodId: string) {
  return useQuery({
    queryKey: ['close-period-status', periodId],
    queryFn: async () => {
      const { data: { session } } = await supabase.auth.getSession();
      if (!session) throw new Error('Not authenticated');

      const response = await fetch(
        `/api/v1/fa/close/periods/${periodId}/status`,
        {
          headers: {
            Authorization: `Bearer ${session.access_token}`,
            'Content-Type': 'application/json',
          },
        }
      );

      if (!response.ok) {
        const error = await response.json();
        throw new Error(error.error?.message || 'Failed to fetch status');
      }

      return response.json();
    },
    staleTime: 5 * 60 * 1000,  // 5 minutes
    gcTime: 10 * 60 * 1000,    // 10 minutes
  });
}
Implementation Notes:
  • Used by FA-07 to verify period is closed before final reporting
  • Completion percentage = (completed + skipped) / total * 100
  • Task summary aggregated from fa_close_tasks
  • Returns fiscal_period_id if close period is linked to fiscal period

FA-19 Consumed APIs

From FA-02 (General Ledger):
  • fa_fiscal_periods table query for linking close periods to fiscal periods
  • Period status validation before close approval
  • Fiscal period closed status check
From FA-07 (Financial Reporting):
  • Close status check before generating final reports
  • fa_report_definitions query for close-related reports

PF-59: AI Edge Functions (OpenRouter)

Status: 🔄 Updated (PF-59 migration from Lovable AI Gateway)
Implemented: 2025-12-06 (PF-27), Updated 2026-01-28 (PF-59)
Spec Reference:

ai-assistant

Endpoint: /functions/v1/ai-assistant
Method: POST
Provider: PF (Platform Foundation)
Consumer: All cores via @/platform/ai hooks
Status: ✅ Implemented (PF-27), 🔄 Updated (PF-59)
Purpose: Streaming AI chat with module-specific system prompts and automatic model selection based on module context. Request Schema:
{
  messages: Array<{
    role: 'user' | 'assistant' | 'system';
    content: string;
  }>;
  moduleContext?: {
    module: 'pf' | 'rh' | 'fa' | 'hr' | 'gr' | 'fw' | 'fm' | 'lo';
    feature?: string;
  };
  stream?: boolean; // default: true
}
Response Schema (200 OK - Streaming):
  • Server-Sent Events (SSE) stream with chunks:
data: {"content": "chunk text", "done": false}
data: {"content": "more text", "done": false}
data: {"done": true}
Response Schema (200 OK - Non-Streaming):
{
  message: string;
  correlationId: string;
}
Model Selection:
  • Automatically selected based on moduleContext.module:
    • gr, hranthropic/claude-3.5-sonnet
    • fa, fw, rh, pfopenai/gpt-4o
    • lo, fmopenai/gpt-4o-mini
    • Unknown/default → openai/gpt-4o
Error Codes:
  • 400 Bad Request: Invalid request format or PHI detected
    • Error code: INVALID_REQUEST or PHI_DETECTED
  • 401 Unauthorized: Missing or invalid JWT token
    • Error code: UNAUTHORIZED
  • 402 Payment Required: AI credits depleted
    • Error code: CREDITS_DEPLETED
    • User message: “AI credits depleted”
  • 429 Too Many Requests: Rate limited
    • Error code: RATE_LIMITED
    • User message: “Rate limited, please try again”
    • Automatic exponential backoff retry (max 3 retries)
  • 500 Internal Server Error: OpenRouter service error
    • Error code: SERVICE_ERROR
    • User message: “AI service error. Please try again.”
Authentication:
  • JWT required in Authorization: Bearer {token} header
  • Token validated against Supabase Auth
  • Organization ID derived from authenticated user profile
Tenant Context Enforcement:
  • The organization_id is derived from the authenticated user via auth.uid() and user profile metadata
  • If moduleContext.organization_id is provided, it MUST be validated using pf_has_org_access(organization_id, auth.uid())
  • If validation fails (user does not have access to the specified organization), return 403 Forbidden with error code ACCESS_DENIED
  • Never trust client-provided organization IDs without validation
PII/PHI Logging Guidelines:
  • FORBIDDEN: Do NOT log user messages, AI responses, conversation history, or moduleContext identifiers
  • ALLOWED: Log UUIDs (correlation_id, user_id), error codes, timestamps, organization_id (UUID only), model selection, and aggregated token counts
  • Usage logging to pf_ai_usage_logs and telemetry MUST exclude message content and AI responses
  • Only log metadata required for billing, debugging, and analytics (token counts, model used, success/failure status)
Rate Limiting:
  • OpenRouter enforces rate limits
  • Exponential backoff: 1s initial, max 30s delay, max 3 retries
  • Retry only on 429 errors
Environment Variables:
  • OPENROUTER_API_KEY (required) - OpenRouter API key
  • APP_URL (optional) - Application URL for referrer header (defaults to https://encoreos.io)
Implementation Notes:
  • Streaming responses use Server-Sent Events (SSE)
  • PHI detection runs on user messages before sending to AI
  • Module context enables specialized system prompts
  • Usage logged to pf_ai_usage_logs table (metadata only, no content)
Content-Type Headers:
  • Streaming: text/event-stream; charset=utf-8
  • Non-streaming: application/json
Performance SLA Targets:
  • p50 latency: <500ms (time-to-first-token for streaming)
  • p95 latency: <2s (time-to-first-token for streaming)
  • p99 latency: <5s (time-to-first-token for streaming)
  • Non-streaming response: <3s p95
Request/Processing Timeouts:
  • Default timeout: 30s (configurable via OPENROUTER_TIMEOUT environment variable)
  • Streaming timeout: 60s (longer for streaming responses)
  • Client should set appropriate timeout based on expected response length
Idempotency Semantics:
  • Non-idempotent: Identical requests may produce different responses due to:
    • Model non-determinism (temperature > 0)
    • Context window state differences
    • Rate limiting and retry behavior
  • Clients should not retry identical requests expecting identical results
  • Use correlationId for tracking related requests
Retry Semantics:
  • Non-retryable errors (do not retry):
    • 400 Bad Request (INVALID_REQUEST, PHI_DETECTED) - Client error, retry won’t help
    • 401 Unauthorized - Authentication issue, retry won’t help
    • 402 Payment Required (CREDITS_DEPLETED) - Billing issue, retry won’t help
    • 403 Forbidden (ACCESS_DENIED) - Permission issue, retry won’t help
  • Retry on 429 (Rate Limited):
    • Exponential backoff: 1s initial delay, max 30s delay, max 3 retries
    • Retry-After header should be respected if present
    • Client should implement jitter to avoid thundering herd
  • Client guidance for 500 (Internal Server Error):
    • Retry with exponential backoff (1s, 2s, 4s delays)
    • Max 3 retries for 500 errors
    • If all retries fail, show user-friendly error message
    • Log correlationId for support
Caching Strategy:
  • No caching: Responses are not cached
  • No TTL: Each request is processed fresh
  • Clients may cache responses locally if needed, but server does not cache
Testing Requirements:
  • Unit Tests:
    • Model selection logic based on moduleContext.module
    • PHI detection function (positive and negative cases)
    • Error handling for all error codes (400, 401, 402, 403, 429, 500)
    • Request validation (message format, required fields)
  • Integration/E2E Tests:
    • Streaming behavior (SSE format validation, chunk parsing)
    • Non-streaming response format
    • Authentication flow (JWT validation)
    • Rate limiting behavior (429 responses)
    • Timeout handling
  • Tenant Isolation/RLS Tests:
    • Exercise pf_has_org_access() function for organization_id validation
    • Verify cross-tenant access is denied (403 Forbidden)
    • Test with users from different organizations
    • Verify RLS policies prevent data leakage
Version: v1.1.0 (PF-59 - OpenRouter migration)
Last Updated: 2026-01-28

ai-document-analyze

Endpoint: /functions/v1/ai-document-analyze
Method: POST
Provider: PF (Platform Foundation)
Consumer: All cores via @/platform/ai hooks
Status: ✅ Implemented (PF-27), 🔄 Updated (PF-59)
Purpose: Analyze documents using AI with structured JSON output. Optimized for document extraction and analysis tasks. Request Schema:
{
  documentId: uuid;
  analysisPrompt?: string; // Optional custom prompt
  schema?: Record<string, unknown>; // JSON schema for structured output
}
Response Schema (200 OK):
{
  analysis: Record<string, unknown>; // Structured analysis results
  confidence?: number; // Analysis confidence score (0-1)
  extractedFields?: Record<string, string>; // Key-value pairs extracted
}
Model: openai/gpt-4o (optimized for structured output) Error Codes:
  • Same as ai-assistant (400, 401, 402, 429, 500)
  • 403 Forbidden: Access denied when document’s organization_id validation fails
    • Error code: ACCESS_DENIED
    • User message: “Access denied to this document”
Authentication: Same as ai-assistant (JWT required) Tenant Context Enforcement:
  • The document’s organization_id (from pf_documents table) MUST be validated using pf_has_org_access(document.organization_id, auth.uid())
  • If validation fails, return 403 Forbidden with error code ACCESS_DENIED
  • Never process documents from organizations the user cannot access
PII/PHI Logging Guidelines:
  • FORBIDDEN: Do NOT log document content, analysis results containing PHI/PII, or schema outputs with sensitive fields
  • ALLOWED: Log UUIDs (document_id, correlation_id), error codes, timestamps, confidence scores (without content), and non-sensitive metadata
  • Analysis results and extracted fields may contain PHI/PII and MUST NOT be logged to application logs, telemetry, or audit trails
  • Only log metadata required for debugging and analytics (document_id, success/failure, processing time, model used)
Environment Variables: Same as ai-assistant Content-Type Headers:
  • application/json (non-streaming only)
Performance SLA Targets:
  • p50 latency: <2s
  • p95 latency: <5s
  • p99 latency: <10s
Request/Processing Timeouts:
  • Default timeout: 30s (configurable via OPENROUTER_TIMEOUT environment variable)
  • Client should set appropriate timeout based on document size
Idempotency Semantics:
  • Non-idempotent: Identical requests may produce different analysis results due to:
    • Model non-determinism (temperature > 0)
    • Document content changes (if document is updated between requests)
  • Clients should not retry identical requests expecting identical results
  • Use correlationId for tracking related requests
Retry Semantics:
  • Non-retryable errors (do not retry):
    • 400 Bad Request (INVALID_REQUEST, PHI_DETECTED) - Client error, retry won’t help
    • 401 Unauthorized - Authentication issue, retry won’t help
    • 402 Payment Required (CREDITS_DEPLETED) - Billing issue, retry won’t help
    • 403 Forbidden (ACCESS_DENIED) - Permission issue, retry won’t help
  • Retry on 429 (Rate Limited):
    • Exponential backoff: 1s initial delay, max 30s delay, max 3 retries
    • Retry-After header should be respected if present
    • Client should implement jitter to avoid thundering herd
  • Client guidance for 500 (Internal Server Error):
    • Retry with exponential backoff (1s, 2s, 4s delays)
    • Max 3 retries for 500 errors
    • If all retries fail, show user-friendly error message
    • Log correlationId for support
Caching Strategy:
  • No caching: Responses are not cached
  • No TTL: Each request is processed fresh
  • Clients may cache analysis results locally if needed, but server does not cache
Testing Requirements:
  • Unit Tests:
    • Model selection (should use openai/gpt-4o for structured output)
    • PHI detection function (positive and negative cases)
    • Error handling for all error codes (400, 401, 402, 403, 429, 500)
    • Request validation (documentId format, schema validation)
  • Integration/E2E Tests:
    • Response format validation (analysis, confidence, extractedFields)
    • Authentication flow (JWT validation)
    • Rate limiting behavior (429 responses)
    • Timeout handling
  • Tenant Isolation/RLS Tests:
    • Exercise pf_has_org_access() function for document organization_id validation
    • Verify cross-tenant access is denied (403 Forbidden)
    • Test with users from different organizations accessing same document
    • Verify RLS policies prevent data leakage
Version: v1.1.0 (PF-59 - OpenRouter migration)
Last Updated: 2026-01-28

generate-report-narrative

Endpoint: /functions/v1/generate-report-narrative
Method: POST
Provider: PF (Platform Foundation)
Consumer: All cores via @/platform/ai hooks
Status: ✅ Implemented (PF-27), 🔄 Updated (PF-59)
Purpose: Generate narrative text for reports based on data and context. Optimized for narrative generation tasks. Request Schema:
{
  reportData: Record<string, unknown>; // Report data to summarize
  narrativePrompt?: string; // Optional custom prompt
  context?: {
    module?: string;
    reportType?: string;
  };
}
Response Schema (200 OK):
{
  narrative: string; // Generated narrative text
  summary?: string; // Optional summary
}
Model: openai/gpt-4o (optimized for narrative generation) Error Codes:
  • Same as ai-assistant (400, 401, 402, 429, 500)
  • 403 Forbidden: Access denied when report’s organization_id validation fails
    • Error code: ACCESS_DENIED
    • User message: “Access denied to this report”
Authentication: Same as ai-assistant (JWT required) Tenant Context Enforcement:
  • The incoming request.reportData MUST include organization_id
  • The server MUST validate access using pf_has_org_access(reportData.organization_id, auth.uid())
  • If validation fails (user does not have access to the specified organization), return 403 Forbidden with error code ACCESS_DENIED
  • Never generate narratives for reports from organizations the user cannot access
PII/PHI Logging Guidelines:
  • FORBIDDEN: Do NOT log report content, generated narrative text, or summaries containing identifiers or financial data
  • ALLOWED: Log UUIDs (report_id, correlation_id), error codes, timestamps, and non-content metadata (reportType as enum, not content)
  • Report data, narratives, and summaries may contain PHI/PII and financial information and MUST NOT be logged to application logs, telemetry, or audit trails
  • Only log metadata required for debugging and analytics (report_id, reportType enum, success/failure, processing time, model used)
Environment Variables: Same as ai-assistant Content-Type Headers:
  • application/json (non-streaming only)
Performance SLA Targets:
  • p50 latency: <3s
  • p95 latency: <8s
  • p99 latency: <15s
Request/Processing Timeouts:
  • Default timeout: 30s (configurable via OPENROUTER_TIMEOUT environment variable)
  • Client should set appropriate timeout based on report data size
Idempotency Semantics:
  • Non-idempotent: Identical requests may produce different narrative text due to:
    • Model non-determinism (temperature > 0)
    • Report data changes (if report is updated between requests)
  • Clients should not retry identical requests expecting identical results
  • Use correlationId for tracking related requests
Retry Semantics:
  • Non-retryable errors (do not retry):
    • 400 Bad Request (INVALID_REQUEST, PHI_DETECTED) - Client error, retry won’t help
    • 401 Unauthorized - Authentication issue, retry won’t help
    • 402 Payment Required (CREDITS_DEPLETED) - Billing issue, retry won’t help
    • 403 Forbidden (ACCESS_DENIED) - Permission issue, retry won’t help
  • Retry on 429 (Rate Limited):
    • Exponential backoff: 1s initial delay, max 30s delay, max 3 retries
    • Retry-After header should be respected if present
    • Client should implement jitter to avoid thundering herd
  • Client guidance for 500 (Internal Server Error):
    • Retry with exponential backoff (1s, 2s, 4s delays)
    • Max 3 retries for 500 errors
    • If all retries fail, show user-friendly error message
    • Log correlationId for support
Caching Strategy:
  • No caching: Responses are not cached
  • No TTL: Each request is processed fresh
  • Clients may cache narrative text locally if needed, but server does not cache
Testing Requirements:
  • Unit Tests:
    • Model selection (should use openai/gpt-4o for narrative generation)
    • PHI detection function (positive and negative cases)
    • Error handling for all error codes (400, 401, 402, 403, 429, 500)
    • Request validation (reportData format, context validation)
  • Integration/E2E Tests:
    • Response format validation (narrative, summary)
    • Authentication flow (JWT validation)
    • Rate limiting behavior (429 responses)
    • Timeout handling
  • Tenant Isolation/RLS Tests:
    • Exercise pf_has_org_access() function for reportData organization_id validation
    • Verify cross-tenant access is denied (403 Forbidden)
    • Test with users from different organizations accessing same report
    • Verify RLS policies prevent data leakage
Version: v1.1.0 (PF-59 - OpenRouter migration)
Last Updated: 2026-01-28

PF-65: Gusto Embedded Payroll Proxy

Endpoint: POST /functions/v1/gusto-proxy
Status: ✅ Implemented
Auth: Bearer JWT (Supabase session)
Provider: PF (Platform Foundation)
Consumer: HR core, Gusto Embedded SDK components

Purpose

Forward Gusto Embedded SDK requests to https://api.gusto.com with server-injected OAuth token and x-gusto-client-ip header. No Gusto credentials exposed to frontend.

Request Schema

{
  path: string;      // Gusto API path (e.g., "/v1/companies/{id}/employees")
  method: string;    // HTTP method (GET, POST, PUT, DELETE)
  body?: object;     // Request body for POST/PUT/DELETE
}

Response

  • Success: Same status and body as Gusto API response
  • 401 Unauthorized: Missing or invalid JWT
  • 403 Forbidden: Organization not found or no Gusto connection
  • 502 Bad Gateway: Proxy error (token refresh failed, Gusto unreachable)

Error Response Schema

{
  error: {
    code: 'UNAUTHORIZED' | 'ACCESS_DENIED' | 'NO_GUSTO_CONNECTION' | 'TOKEN_REFRESH_FAILED' | 'PROXY_ERROR';
    message: string;
  }
}

Client IP Resolution

Proxy sets x-gusto-client-ip from:
  1. X-Forwarded-For (rightmost trusted proxy entry)
  2. X-Real-IP (fallback)
  3. "unknown" if no trusted IP available
Only enabled when TRUST_PROXY_HEADERS=true environment variable is set.

Token Refresh Flow

On 401 response from Gusto API:
  1. Read refresh_token from pf_oauth_tokens
  2. Exchange refresh_token for new access_token
  3. Persist both new access_token AND refresh_token (if returned)
  4. Retry original request once with new access_token
  5. On failure: return 502 with sanitized error

Security Requirements

  • No tokens in client responses
  • No stack traces or internal details in errors
  • All error messages sanitized before return
  • PKCE verification for initial OAuth flow

PII/PHI Logging Guidelines

  • NEVER log: Access tokens, refresh tokens, employee data, SSN fragments
  • ALLOWED in logs: UUIDs (organization_id, integration_id), error codes, timestamps, HTTP status codes

Rate Limiting

Inherits Gusto API rate limits. No additional rate limiting applied. Version: v1.0.0 (PF-65)
Last Updated: 2026-02-05

PF-65: Gusto Connection Status

Endpoint: GET /functions/v1/gusto-connection-status
Status: ✅ Implemented
Auth: Bearer JWT (Supabase session)
Provider: PF (Platform Foundation)
Consumer: Integration Hub UI, HR core

Purpose

Return current Gusto integration status for the caller’s organization.

Response Schema (200 OK)

{
  status: 'connected' | 'disconnected' | 'error' | 'expired';
  gustoCompanyId: string | null;   // From configuration.gusto_company_uuid
  lastHealthCheck: string | null;  // ISO timestamp
  error: string | null;            // If status = 'error'
}

Error Codes

  • 401 Unauthorized: Missing or invalid JWT
  • 403 Forbidden: User not a member of any organization

Security Requirements

  • Only returns status, never credentials
  • Organization resolved from JWT, not query params
Version: v1.0.0 (PF-65)
Last Updated: 2026-02-05

IT Module APIs (Implemented)

IT-07: Dashboard Summary RPC

Endpoint: Database function it_get_dashboard_summary(p_organization_id UUID)
Provider: IT (IT-07 Dashboard & Reporting)
Consumer: IT Dashboard
Status: ✅ Implemented
Last Verified: 2026-02-15
Purpose: Return aggregated IT dashboard metrics in a single call. Invocation Pattern:
const { data, error } = await supabase.rpc('it_get_dashboard_summary', {
  p_organization_id: organizationId,
});
Function Signature:
it_get_dashboard_summary(p_organization_id UUID)
RETURNS JSON
SECURITY DEFINER SET search_path = public
Response Schema:
{
  open_tickets: number;
  critical_tickets: number;
  assets_total: number;
  assets_assigned: number;
  licenses_expiring_30d: number;
  contracts_expiring_30d: number;
  pending_onboarding: number;
  pending_offboarding: number;
}
React Hook: useITDashboardSummary(organizationId) in src/cores/it/hooks/useITDashboardSummary.ts Authentication: JWT required. Validated via it_has_org_access(p_organization_id, auth.uid()) SECURITY DEFINER check.
Tenant Enforcement: p_organization_id validated against caller’s JWT via it_has_org_access(). Returns only data for the specified organization.
Permissions: it.dashboard.view
Error Response Schema:
{
  error: {
    code: 'ACCESS_DENIED' | 'INVALID_INPUT' | 'SERVER_ERROR';
    message: string;
  }
}
Rate Limiting: 100 req/min per organization (standard). Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Caching: staleTime: 5min, gcTime: 10min (configured in React hook).
Performance SLA: p95 < 500ms.
Idempotency: Read-only RPC — safe to retry.

PII/PHI Logging Guidelines (IT-07)

The it_get_dashboard_summary RPC and its React hook useITDashboardSummary handle only aggregated counts — no individual-level PII is returned or logged.
CategoryRule
Allowed log fieldsorganization_id, timestamp, aggregated metric names (e.g., open_tickets, assets_total), request duration, HTTP status
Prohibited dataFull names, SSNs, DOB, medical details, credentials, API tokens, IP addresses of end-users
Masking / hashingNot applicable — this RPC returns no PII fields. If future enhancements add user-level data, mask email to j***@example.com and phone to ***-**-1234
Retention & accessLogs retained ≤ 30 days; access restricted to org_admin and platform operators via ACL
Log levelsINFO: high-level invocation events (org_id, latency). DEBUG: scrubbed query parameters only. ERROR: non-PII diagnostics (error code, org_id, timestamp)
Compliant log line:
INFO  it_get_dashboard_summary org=abc-123 latency=42ms status=200
Non-compliant log line:
ERROR it_get_dashboard_summary user=jbloom@example.com org=abc-123 failed
Reference: See FA-10/FA-11 PII/PHI Logging Policy in this document for the canonical policy. IT-07 follows the same standards.

IT-09: Change Request List/Detail RPCs

Endpoint 1: Database function it_get_changes(p_organization_id, p_status, p_limit)
Endpoint 2: Database function it_get_change_detail(p_organization_id, p_change_id)
Provider: IT (IT-09 Change Management)
Consumer: IT Change Management UI
Status: ✅ Implemented
Last Verified: 2026-02-15
Purpose: Query change requests with requester/implementer names, and get full detail with approvals and implementations. Invocation Pattern:
// List changes
const { data, error } = await supabase.rpc('it_get_changes', {
  p_organization_id: organizationId,
  p_status: status,    // optional filter
  p_limit: limit,      // optional, default 50, max 500
});

// Get change detail
const { data, error } = await supabase.rpc('it_get_change_detail', {
  p_organization_id: organizationId,
  p_change_id: changeId,
});
React Hooks:
  • useITChanges(organizationId, status?, limit?) in src/cores/it/hooks/useITChanges.ts
  • useITChangeDetail(organizationId, changeId) in src/cores/it/hooks/useITChanges.ts
Authentication: JWT required. Validated via it_has_org_access(p_organization_id, auth.uid()) SECURITY DEFINER check.
Tenant Enforcement: p_organization_id validated against caller’s JWT via it_has_org_access().
Permissions: it.changes.view
Error Response Schema:
{
  error: {
    code: 'ACCESS_DENIED' | 'NOT_FOUND' | 'INVALID_INPUT' | 'SERVER_ERROR';
    message: string;
  }
}
Rate Limiting: 100 req/min per organization (standard). Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Caching: staleTime: 5min, gcTime: 10min (configured in React hooks).
Performance SLA: p95 < 500ms.
Idempotency: Read-only RPCs — safe to retry. it_get_change_detail returns NULL when change not found.

PII/PHI Logging Guidelines (IT-09)

The it_get_changes and it_get_change_detail RPCs (hooks: useITChanges, useITChangeDetail) return change request metadata including requester/implementer display names joined from pf_profiles.
CategoryRule
Allowed log fieldsorganization_id, change_id, status, priority, timestamp, request duration, HTTP status
Prohibited dataFull names, SSNs, DOB, medical details, credentials, API tokens, raw email addresses
Masking / hashingRequester/implementer names must NOT appear in logs. Use requester_id (UUID) instead. Mask email to j***@domain.com, phone to ***-**-1234
Retention & accessLogs retained ≤ 30 days; access restricted to org_admin and platform operators via ACL
Log levelsINFO: invocation events (org_id, change_id, latency). DEBUG: scrubbed filter params (status, limit). ERROR: non-PII diagnostics (error code, change_id, org_id)
Compliant log line:
INFO  it_get_change_detail org=abc-123 change_id=def-456 latency=28ms status=200
Non-compliant log line:
DEBUG it_get_change_detail requester="John Bloom" email=jbloom@example.com
Reference: See FA-10/FA-11 PII/PHI Logging Policy in this document for the canonical policy. IT-09 follows the same standards.

IT-07: Report Generation (Documented)

Endpoint: supabase.functions.invoke('generate-it-report', { body })
Provider: IT (IT-07 Dashboard & Reporting)
Consumer: IT Report Builder
Status: ✅ Implemented (existing edge function)
Last Verified: 2026-02-15
Invocation Pattern:
const { data, error } = await supabase.functions.invoke('generate-it-report', {
  body: { run_id, organization_id, report_type, parameters, format },
});
Request Schema:
{
  run_id: uuid;
  organization_id: uuid;
  report_definition_id?: uuid;
  report_type?: 'asset_inventory' | 'ticket_summary' | 'license_compliance' | 'vendor_summary' | 'change_summary' | 'security_posture';
  parameters?: Record<string, unknown>;
  format?: 'csv' | 'pdf' | 'xlsx';
}
Response Schema:
{
  success: boolean;
  run_id: uuid;
  row_count: number;
  file_url: string; // Points to it-reports storage bucket
}
Download: Use file_url from the response to download the generated report. Authentication: JWT required (Authorization header). organization_id validated server-side.
Tenant Enforcement: organization_id in body validated against caller’s JWT.
Permissions: it.reports.generate
Error Response Schema:
{
  error: {
    code: 'ACCESS_DENIED' | 'INVALID_INPUT' | 'SERVER_ERROR';
    message: string;
  }
}
Rate Limiting: 100 req/min per organization (standard). Headers: X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset.
Performance SLA: p95 < 2s for report generation.
Idempotency: Idempotent by run_id — re-invoking with the same run_id returns cached result.

IT-08: Onboarding/Offboarding Hooks (Documented)

Provider: IT (IT-08 Onboarding/Offboarding)
Status: ✅ Implemented (existing hooks)
Last Verified: 2026-02-15
Hooks:
  • usePendingOnboarding(organizationId) — filters it_onboarding_instances by workflow_type = 'onboarding', status IN ('pending', 'in_progress')
  • usePendingOffboarding(organizationId) — filters it_onboarding_instances by workflow_type = 'offboarding', status IN ('pending', 'in_progress')
Location: src/cores/it/hooks/useOnboardingInstances.ts Authentication: JWT required. RLS enforces it_has_org_access(organization_id, auth.uid()).
Tenant Enforcement: organization_id filter applied in query + RLS policy.
Permissions: it.onboarding.view
Caching: staleTime: 5min, gcTime: 10min (configured in React hooks).
Performance SLA: p95 < 500ms.

FA-10/FA-11: Cross-Core Payroll Read Integrations (W-2 / Form 941)

Endpoint 1: Database function fa_generate_w2_forms(p_org_id UUID, p_tax_year INT) Endpoint 2: Database function fa_generate_form_941(p_org_id UUID, p_tax_year INT, p_quarter INT) Provider: FA (FA-10 Tax Compliance / FA-11 Payroll Tax) Consumer: FA Tax Compliance UI, 1099/W-2 Generation Status: ✅ Implemented Last Verified: 2026-02-16 Purpose: Generate W-2 and Form 941 data by reading cross-core HR payroll tables. These functions run with SECURITY DEFINER to access HR tables from the FA core via the Platform Integration Layer pattern. Cross-Core Tables Read (HR → FA):
  • hr_payroll_line_items — individual pay line items (wages, deductions, taxes)
  • hr_payroll_runs — payroll run metadata (period, status, finalized date)
  • hr_employees — employee demographic data (name, SSN for W-2 filing)
Data Fields Accessed:
  • wages_tips_compensation, federal_tax_withheld, ss_tax_withheld, medicare_tax_withheld, state_tax_withheld
  • social_security_wages, medicare_wages
  • Employee identifiers (for W-2 box population)
Tenant Isolation: All queries filter by organization_id = p_org_id. Only finalized payroll runs (status = 'finalized') for the specified tax year are included. RLS is bypassed via SECURITY DEFINER; the function validates org access internally. Security: Both functions use SECURITY DEFINER SET search_path = public. Caller’s org membership is validated before execution.

PII/PHI Logging Guidelines (FA-10/FA-11 Payroll)

CategoryRule
Allowed log fieldsorganization_id, tax_year, quarter, aggregated counts (e.g., w2_count, total_wages_sum), request duration
Prohibited dataEmployee names, SSNs, DOB, individual wage amounts, bank account numbers, medical deductions
Masking / hashingEmployee references in logs must use employee_id (UUID) only. Never log SSN, even partially. Aggregate wage totals are acceptable
Retention & accessLogs retained ≤ 30 days; access restricted to org_admin and platform operators
Log levelsINFO: generation events (org_id, tax_year, record_count). ERROR: non-PII diagnostics only
Architecture Reference: These functions follow constitution.md §1.3 (Cross-Core Integration via SECURITY DEFINER RPCs). The FA core does not import HR code; it reads HR tables through documented database functions only.

Planned API Contracts (CL/PM EHR & Practice Management)

Source: EHR_PM_PLANNING_BUNDLE.md
Status: 📝 Planned (stubs); request/response schemas to be defined in respective CL/PM specs.

Eligibility Check

Endpoint: GET /api/v1/pm/eligibility or POST /api/v1/pm/eligibility (platform layer may wrap external 270/271)
Method: GET (by patient_id + payer_id) or POST (batch)
Content-Type: application/json (request/response)
Provider: PM (or platform layer wrapping external 270/271)
Consumers: CL, PM-07, PM-08
Status: 📝 Planned
Spec Reference: PM-02 – Insurance & Eligibility Verification
Purpose: Synchronous eligibility verification for a patient and payer. Request/response schema to be defined in PM-02; may wrap clearinghouse 270/271. Authentication & tenant context: JWT required; organization_id MUST be validated against the authenticated session/claims (do not trust client-provided org_id). All queries scoped by tenant. No cross-tenant access. TypeScript interfaces (required vs optional):
/** Request body for POST /api/v1/pm/eligibility */
interface EligibilityCheckRequest {
  /** Required. Patient UUID. */
  patient_id: string;
  /** Required. Payer or insurance record UUID. */
  payer_id: string;
  /** Optional. Service date in ISO 8601 (YYYY-MM-DD). */
  service_date?: string;
  /** Required. Tenant isolation (must be validated against session org). */
  organization_id: string;
}

/** Response (200) for eligibility check */
interface EligibilityCheckResponse {
  /** Required. Whether the patient is eligible under the payer. */
  eligible: boolean;
  /** Optional. Benefit summary from 271. */
  benefit_summary?: Record<string, unknown>;
  /** Optional. Eligibility period. */
  eligibility_dates?: { start: string; end: string };
  /** Optional. Reference to raw 271 for audit. */
  raw_271_ref?: string;
}
Rate limiting: 60 requests/minute per organization. Response headers:
  • X-RateLimit-Limit: 60
  • X-RateLimit-Remaining: remaining in current window
  • X-RateLimit-Reset: Unix timestamp (seconds) when the window resets
SLA & timeout: p50 < 2s, p95 < 5s, p99 < 10s; client timeout 15s. 504 if backend exceeds 15s. Retry & idempotency: GET is idempotent. For POST: send Idempotency-Key: <uuid> header to allow safe retries; same key returns same response. Retry 5xx and 429 with exponential backoff (e.g. 1s, 2s, 4s); do not retry 4xx (except 429). Error response: { error: { code, message, details? } }; status codes: 400 (invalid params), 401 (unauthorized), 403 (forbidden), 404 (patient/payer not found), 429 (rate limit), 500.
Permissions: pm.eligibility.read or equivalent; authorization rules in PM-02.
PII/PHI logging: Log only organization_id, request_id, duration; never log member ID, DOB, or payer-specific identifiers in plain text.
Caching: Short TTL (e.g. 5 min) keyed by organization_id + patient_id + payer_id + service_date; invalidate on eligibility update.
Example (consumer):
import type { EligibilityCheckRequest, EligibilityCheckResponse } from '@/integrations/pm/eligibility-types';

async function checkEligibility(
  token: string,
  request: EligibilityCheckRequest
): Promise<EligibilityCheckResponse> {
  const res = await fetch('/api/v1/pm/eligibility', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': crypto.randomUUID(),
    },
    body: JSON.stringify(request),
    signal: AbortSignal.timeout(15_000),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    throw new Error(err?.error?.message ?? `Eligibility check failed: ${res.status}`);
  }
  const data: EligibilityCheckResponse = await res.json();
  return data;
}

// Usage
const data = await checkEligibility(token, {
  patient_id: patientId,
  payer_id: payerId,
  organization_id: orgId,
  service_date: '2026-02-17',
});
if (data.eligible) {
  // proceed with benefit_summary / eligibility_dates
}

Claim Status Query

Endpoint: GET /api/v1/pm/claim-status?claim_id={id} or POST /api/v1/pm/claim-status (platform layer may wrap 276/277)
Method: GET or POST
Content-Type: application/json
Provider: PM (or platform layer wrapping clearinghouse 276/277)
Consumers: PM-08, PM-11
Status: 📝 Planned
Spec Reference: PM-15 – Clearinghouse Integration
Purpose: Query claim status from clearinghouse. Request/response to be defined in PM-15. Authentication & tenant context: JWT required; organization_id enforced; RLS/defense-in-depth on claim access.
Request: ClaimStatusRequestclaim_id: uuid, organization_id: uuid.
Response (200): ClaimStatusResponseclaim_id, status, adjudication_date?, denial_reason?, payer_ref?.
Error response: Same structure as above; 400, 401, 403, 404, 429, 500.
Permissions: pm.claims.read; scope by org and optional site.
PII/PHI logging: No claim numbers or patient identifiers in logs; only org_id, claim_id (UUID), duration.
Rate limiting & SLA: TBD (e.g. 120/min); p50/p95/p99 and timeout TBD in PM-15.
Retry & idempotency: GET idempotent; retry 5xx/429 with backoff.
Caching: Optional short TTL for status by claim_id.
Example:
const res = await fetch(\/api/v1/pm/claim-status?claim_id=&#123;claimId&#125;\`, { headers: { 'Authorization': \`Bearer {token}` } });
const data: ClaimStatusResponse = await res.json();`

Resident/Patient Lookup (RH–CL Boundary)

Endpoint: GET /api/v1/platform/patient-lookup?resident_id={uuid} or GET /api/v1/platform/patient-lookup?organization_id={uuid}&external_id={string}
Method: GET
Content-Type: application/json
Provider: PM or PF (platform layer)
Consumers: RH, CL
Status: 📝 Planned
Spec Reference: Cross-core; no direct RH→CL imports. See EHR_PM_PLANNING_BUNDLE.md §3 (API Contracts List).
Purpose: Resolve resident (RH) to patient (PM/CL) identity for cross-core workflows. Schema to be defined in integration spec. Authentication & tenant context: JWT required; organization_id enforced; caller must have access to resident and patient context.
Request: Query params resident_id or organization_id + external_id (e.g. RH resident id).
Response (200): PatientLookupResponsepatient_id?: uuid, resident_id?: uuid, match_type: 'exact'|'none', demographics_ref?: string.
Error response: 400, 401, 403, 404, 429, 500.
Permissions: platform.patient_lookup.read or equivalent; RH/CL scoped.
PII/PHI logging: Log only org_id, resident_id (UUID), match_type; no names or DOB.
Rate limiting & SLA: TBD; conservative limits for lookup.
Retry & idempotency: GET idempotent.
Caching: Optional by resident_id + org_id.
Example:
const res = await fetch(\/api/v1/platform/patient-lookup?resident_id=&#123;residentId&#125;\`, { headers: { 'Authorization': \`Bearer {token}` } });
const data: PatientLookupResponse = await res.json();`

Patient Demographics (Read)

Endpoint: GET /api/v1/pm/patients/:id (or platform layer equivalent)
Method: GET
Content-Type: application/json
Provider: PM
Consumers: CL, PM-02, PM-03, PM-07, PM-08
Status: 📝 Planned
Spec Reference: PM-01 – Patient Registration & Demographics
Purpose: Read-only patient demographics (USCDI v3–aligned). No PII in logs. Authentication & tenant context: JWT required; organization_id and RLS enforce tenant scope; path param :id is patient UUID.
Request: Path only; optional query fields for field set.
Response (200): PatientDemographicsResponse — USCDI v3–aligned demographics (name, DOB, gender, identifiers, etc.); shape in PM-01.
Error response: 400, 401, 403, 404, 429, 500.
Permissions: pm.patients.read or equivalent; org/site scoped.
PII/PHI logging: Do not log PHI; only org_id, patient_id (UUID), request_id, duration.
Rate limiting & SLA: 200 req/min per org; p95 < 500ms; timeout 10s.
Retry & idempotency: GET idempotent; retry 5xx/429 with backoff.
Caching: Short TTL for demographics by patient_id (e.g. 60s).
Example:
const res = await fetch(\/api/v1/pm/patients/&#123;patientId&#125;\`, { headers: { 'Authorization': \`Bearer {token}` } });
const data: PatientDemographicsResponse = await res.json();`

Endpoint: POST /api/v1/platform/consent/check (or internal CL service)
Method: POST
Content-Type: application/json
Provider: CL or platform consent service
Consumers: CL, PM (billing), FW
Status: 📝 Planned
Spec Reference: CL-11 – Consent Management & 42 CFR Part 2
Purpose: Evaluate whether a given access (e.g. TPO, SUD counseling notes, legal proceedings) is permitted for the current user/context. Request/response to be defined in CL-11. Authentication & tenant context: JWT required; organization_id and patient/resource context required; tenant-scoped policy evaluation.
Request: ConsentCheckRequestpatient_id: uuid, purpose_of_use: string, resource_type?: string, encounter_id?: uuid, organization_id: uuid, user_id?: uuid.
Response (200): ConsentCheckResponseallowed: boolean, obligations?: string[], denial_reason?: string, audit_id?: string.
Error response: 400, 401, 403, 404, 429, 500.
Permissions: Caller must have access to patient and consent service; rules in CL-11.
PII/PHI logging: Log only org_id, patient_id (UUID), purpose_of_use, allowed/denied, audit_id; no consent text or PHI.
Rate limiting & SLA: 300 req/min per org; p95 < 200ms; timeout 5s. Retry 5xx/429 with backoff.
Rate limiting & SLA: TBD; low latency required for UI/API gating.
Retry & idempotency: POST non-idempotent; use idempotency key if supported.
Caching: Policy result cache with short TTL; invalidate on consent change.
Example:
const res = await fetch('/api/v1/platform/consent/check', { method: 'POST', headers: { 'Authorization': \Bearer ${token}`, ‘Content-Type’: ‘application/json’ }, body: JSON.stringify({ patient_id, purpose_of_use: ‘TPO’, organization_id }) });
const data: ConsentCheckResponse = await res.json();`

CL-16-EN-01: TEFCA/QHIN Connectivity API (Planned) \

Provider: CL (Clinical & EHR) — CL-16-EN-01
Consumers: CL clinical workflows, compliance/audit services, downstream interoperability consumers
Status: 📝 Planned
Ownership: CL-16-EN-01 owns TEFCA/QHIN request orchestration and contract versioning.
Spec Reference: CL-16-EN-01
Integration Doc: CL-16-EN-01-tefca-qhin-connectivity-INTEGRATION.md
Contract requirements (all CL-16-EN-01 TEFCA endpoints):
  • Authentication: JWT required (Authorization: Bearer <token>).
  • Scopes: cl.tefca.view required for query/history endpoints; cl.tefca.exchange required for outbound exchange.
  • Tenant isolation: organization_id in request MUST match the authenticated session organization; mismatches fail with 403 TENANT_MISMATCH.
  • Correlation: every request/response includes correlation_id for traceability.
  • PHI/PII logging restrictions: log only correlation_id, organization_id, purpose_of_use, and status; never log patient identifiers, patient match attributes, or clinical payload content.
  • Error model: standardized responses include one of 400, 401, 403, 404, 429, 500 with stable machine-readable error codes.
  • Rate limiting/retry: high-volume exchanges are rate-limited per org; clients retry only 429/5xx with exponential backoff + jitter and honor Retry-After when present.
Standard error code expectations:
  • 400 INVALID_REQUEST - malformed payload, missing required fields, or unsupported enum values.
  • 401 UNAUTHENTICATED - missing/invalid JWT.
  • 403 CONSENT_DENIED - CL-11 consent is missing/restricted for requested disclosure.
  • 403 TENANT_MISMATCH - organization_id does not match authenticated tenant context.
  • 404 NOT_FOUND - requested exchange/query artifact does not exist in tenant scope.
  • 429 RATE_LIMITED - per-org throughput limit exceeded.
  • 500 QHIN_CONNECTIVITY_FAILURE - downstream QHIN/transport/service failure.

Endpoint: POST /tefca/qhin/query \

  • Purpose: Submit a TEFCA/QHIN patient query request.
  • Required fields: organization_id, patient match attributes, purpose_of_use.
  • Response shape: { status, correlation_id, match_candidates[] }.
  • Ownership: CL-16-EN-01.
  • TODO (schema/version): request/response JSON schema, enum normalization, version lifecycle.

Endpoint: POST /tefca/qhin/exchange \

  • Purpose: Execute outbound TEFCA/QHIN exchange with CL-11 consent gates.
  • Required fields: organization_id, patient_id, purpose_of_use, exchange payload reference.
  • Response shape: { status, correlation_id, exchange_log_id }.
  • Consent binding (required): request MUST execute a CL-11 ConsentCheckRequest with patient_id, purpose_of_use, and organization_id before outbound disclosure.
  • Fail-closed behavior: if consent is absent/restricted, return 403 CONSENT_DENIED and do not send any outbound payload.
  • Ownership: CL-16-EN-01.
  • TODO (schema/version): consent obligations schema, redisclosure notice metadata, versioned error model.

Endpoint: GET /tefca/qhin/exchanges \

  • Purpose: Retrieve org-scoped TEFCA/QHIN exchange history and compliance statuses.
  • Required fields: organization_id and filter parameters (status, direction, date range, qhin_identifier).
  • Response shape: { status, correlation_id, items[], pagination }.
  • Ownership: CL-16-EN-01.
  • TODO (schema/version): pagination contract, filter grammar, response projection profile by API version.

CL-16: FHIR R4 API — Edge Function Facade

Invocation: supabase.functions.invoke('fhir-r4', { body })
Content-Type: application/json (request) / application/fhir+json (response)
Provider: CL (Clinical & EHR)
Consumers: PM-12 (patient access), external FHIR clients, HIE partners
Status: 🟡 In Progress (Phase 1)
Spec Reference: CL-16 – FHIR Interoperability & Data Exchange
Integration Doc: CL-16-fhir-interoperability-data-exchange-INTEGRATION.md
Purpose: FHIR R4 facade exposing US Core resources (Patient, Condition, MedicationRequest, AllergyIntolerance, Encounter, Observation) via Supabase Edge Functions. Translates internal Encore Health OS data model to FHIR R4 JSON. Consent-gated (CL-11 Part 2). All calls logged to cl_data_exchange_log. Architecture Decision (Errata E-1): Supabase Edge Functions as FHIR facade for Phase 1, with migration path to Hybrid (Edge Functions + external FHIR server) for ONC certification.

Endpoint 1: GET /fhir/r4/Patient/:id

Action: get-patient
Required Fields: organization_id, patient_id
Source Table: pm_patients
FHIR Profile: US Core Patient (http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient)
Request:
{
  action: 'get-patient';
  organization_id: string; // uuid
  patient_id: string;      // uuid
}
Response (200):
// FHIR R4 Patient resource
{
  resourceType: 'Patient';
  id: string;
  meta: { profile: string[]; lastUpdated: string };
  identifier: Array<{ system: string; value: string }>;
  name: Array<{ family: string; given: string[]; use: string }>;
  gender: 'male' | 'female' | 'other' | 'unknown';
  birthDate: string;
  telecom?: Array<{ system: string; value: string; use: string }>;
  address?: Array<{ line: string[]; city: string; state: string; postalCode: string }>;
}
Error Responses:
CodeOperationOutcome IssueWhen
400invalidMissing required params
401securityNo/invalid JWT
403forbiddenCross-tenant access
404not-foundPatient not in org
429throttledRate limit exceeded
500exceptionInternal error
All errors return FHIR OperationOutcome JSON with Content-Type: application/fhir+json.

Endpoint 2: GET /fhir/r4/Condition

Action: search-condition
Required Fields: organization_id, patient_id
Optional Fields: chart_id
Source Table: cl_problems
FHIR Profile: US Core Condition (http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-problems-health-concerns)
Request:
{
  action: 'search-condition';
  organization_id: string;
  patient_id: string;
  chart_id?: string; // optional filter
}
Response (200): FHIR Bundle of Condition resources. Consent Gating: SUD-related conditions (ICD-10 F10–F19) are excluded if patient does not have active Part 2 consent (CL-11). Non-SUD conditions are always returned.

Endpoint 3: GET /fhir/r4/MedicationRequest

Action: search-medication-request
Required Fields: organization_id, patient_id
Source Table: cl_medications
FHIR Profile: US Core MedicationRequest (http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest)
Request:
{
  action: 'search-medication-request';
  organization_id: string;
  patient_id: string;
}
Response (200): FHIR Bundle of MedicationRequest resources. Consent Gating: SUD medications (e.g., buprenorphine, methadone, naltrexone) are excluded without Part 2 consent.

Endpoint 4: GET /fhir/r4/Patient/:id/$everything

Action: patient-everything
Required Fields: organization_id, patient_id
Source Tables: pm_patients, cl_problems, cl_medications, cl_allergies, pm_appointments, cl_order_results
FHIR Profile: FHIR Bundle (type: searchset)
Request:
{
  action: 'patient-everything';
  organization_id: string;
  patient_id: string;
}
Response (200): FHIR Bundle containing all available resources for the patient: Patient, Condition(s), MedicationRequest(s), AllergyIntolerance(s), Encounter(s), Observation(s). Consent-gated per CL-11. Performance Note: For large charts, response may be paginated in future phases. Phase 1 returns all resources in a single bundle with withTimeout() protection.

Cross-Cutting Concerns (All CL-16 Endpoints)

Authentication: JWT required via Authorization: Bearer {token}. Validated by validateAuth().
Tenant Enforcement: organization_id required in every request. Verified via verifyOrgAccess() against pf_user_organizations. No cross-tenant data access.
RBAC: Caller must have cl.fhir.read permission (or equivalent clinical access).
Consent Enforcement (CL-11): Part 2 SUD data gated by checkSudConsent(). Fail-closed: if consent check fails, SUD data is excluded.
Rate Limiting: 100 req/min per organization for reads; 10 req/min for $everything. Enforced at Edge Function level.
Performance SLA: p95 < 500ms for single-resource reads; p95 < 2s for $everything.
Timeout: 25s per request via withTimeout().
Retry & Idempotency: All GET endpoints are idempotent. Clients may retry on 5xx/429 with exponential backoff.
Caching: No cache for live clinical data. cl_fhir_resources table available for pre-computed FHIR cache in future phases.
PII/PHI Logging Guidelines:
  • Allowed in logs: organization_id, patient_id (UUID), resource_type, record_count, exchange_type, status, correlation_id, consent_status (boolean), duration_ms
  • Prohibited in logs: Patient names, DOB, SSN, diagnosis codes, medication names, clinical note content, address details
  • Masking: No PHI fields are logged; only IDs and operational metadata
  • Retention: Exchange log entries in cl_data_exchange_log retained per org retention policy; Edge Function stdout/stderr logged for 7 days (Supabase default)
Audit Trail: Every API call writes one record to cl_data_exchange_log with: exchange_type, direction (outbound), resource_types, record_count, status, created_by. Example:
const { data, error } = await supabase.functions.invoke('fhir-r4', {
  body: {
    action: 'get-patient',
    organization_id: orgId,
    patient_id: patientId,
  },
});
// data: FHIR Patient resource (application/fhir+json)

PM-49 API Contracts

Provider: PM-49 (Revenue Cycle Automation Rules Engine) Consumers: PM-49 edge functions (rcm-execute-rules) Status: 📝 Planned Integration Doc: PM-49-revenue-cycle-automation-rules-engine-INTEGRATION.md Spec Reference: PM-49 spec Purpose: Synchronous API contracts for PM-49 automated rule actions that interact with PM-09 (Payment Posting), PM-29 (Denial Management), and PM-45 (Collections).

PM-49 → PM-09: Post Adjustment

Callable Interface:
  • HTTP Endpoint: POST /rest/v1/rpc/pm_post_adjustment (SECURITY DEFINER RPC)
  • Alternative: Edge function at POST /functions/v1/pm-post-adjustment
  • Required Auth Scope: pm.adjustments.create
  • Expected Status Codes: 200 (success), 400 (validation error), 403 (forbidden), 404 (not found), 409 (duplicate), 500 (server error)
  • Idempotency Key Field: rule_execution_id (optional UUID)
Purpose: Auto-post small balance adjustments and contractual adjustments Request Schema:
interface PM49PostAdjustmentRequest {
  organization_id: string;      // UUID, required for tenant isolation
  claim_id: string;             // UUID
  adjustment_code: string;      // e.g., "SB-WRITEOFF", "CONTR-ADJ"
  adjustment_amount: number;    // Decimal (positive value)
  reason_text: string;          // Human-readable reason (min 5 chars)
  rule_execution_id?: string;   // UUID (for audit trail and idempotency)
}
Response Schema (200 OK):
interface PM49PostAdjustmentResponse {
  success: true;
  adjustment_id: string;        // UUID of created adjustment
}
Error Response (4xx/5xx):
interface PM49PostAdjustmentError {
  success: false;
  error_code: string;
  error_message: string;
}
Error Codes:
  • CLAIM_NOT_FOUND (404): Claim ID does not exist
  • INSUFFICIENT_BALANCE (400): Balance lower than adjustment amount
  • INVALID_ADJUSTMENT_CODE (400): Unrecognized adjustment code
  • ORG_MISMATCH (403): Claim does not belong to organization
  • DUPLICATE_REQUEST (409): Idempotent duplicate (returns existing adjustment_id in error_message)
Idempotency: Use rule_execution_id as idempotency key; duplicate requests return 409 with existing adjustment_id Authentication: Service role or JWT with pm.adjustments.create permission

PM-49 → PM-29: Trigger Denial Resubmission

Callable Interface:
  • HTTP Endpoint: POST /rest/v1/rpc/pm_trigger_denial_resubmission (SECURITY DEFINER RPC)
  • Alternative: Edge function at POST /functions/v1/pm-trigger-resubmission
  • Alternative Path: POST /pm/denials/{denial_id}/resubmit (RESTful route)
  • Required Auth Scope: pm.denials.resubmit
  • Expected Status Codes: 200 (success), 403 (forbidden), 404 (not found), 409 (already resubmitted), 422 (deadline passed), 500 (server error)
  • Idempotency Key Field: rule_execution_id (optional UUID)
Purpose: Auto-trigger denial re-submissions for correctable denial codes Request Schema:
interface PM49TriggerResubmissionRequest {
  organization_id: string;      // UUID, required
  denial_id: string;            // UUID
  resubmission_reason: string;  // e.g., "auto-resubmit CO-4 modifier"
  correction_notes?: string;    // Optional correction instructions
  rule_execution_id?: string;   // UUID (for audit trail and idempotency)
}
Response Schema (200 OK):
interface PM49TriggerResubmissionResponse {
  success: true;
  resubmission_id: string;      // UUID of created resubmission
}
Error Response (4xx/5xx):
interface PM49TriggerResubmissionError {
  success: false;
  error_code: string;
  error_message: string;
}
Error Codes:
  • DENIAL_NOT_FOUND (404): Denial ID does not exist
  • FILING_DEADLINE_PASSED (422): Resubmission past filing deadline (resolved via PF-96)
  • ALREADY_RESUBMITTED (409): Denial already has pending resubmission
  • ORG_MISMATCH (403): Denial does not belong to organization
  • JURISDICTION_PROFILE_MISSING (500): Unable to resolve filing deadline via PF-96
Idempotency: Use rule_execution_id as idempotency key Filing Deadline Resolution: Uses PF-96 pf_resolve_jurisdiction_profile(org_id, site_id) to validate resubmission window. If profile unavailable, fails closed with JURISDICTION_PROFILE_MISSING error. Authentication: Service role or JWT with pm.denials.resubmit permission

PM-49 → PM-45: Transfer to Collections

Callable Interface:
  • HTTP Endpoint: POST /rest/v1/rpc/pm_transfer_to_collections (SECURITY DEFINER RPC)
  • Alternative: Edge function at POST /functions/v1/pm-transfer-collections
  • Alternative Path: POST /pm/collections/transfer (RESTful route)
  • Required Auth Scope: pm.collections.create
  • Expected Status Codes: 200 (success), 400 (invalid tier), 403 (forbidden), 404 (not found), 409 (already in collections), 422 (ineligible payer), 500 (server error)
  • Idempotency Key Field: rule_execution_id (optional UUID)
Purpose: Auto-transfer accounts to collections tiers based on aging rules Request Schema:
interface PM49TransferCollectionsRequest {
  organization_id: string;      // UUID, required
  account_id: string;           // UUID (claim_id or patient account ID)
  collections_tier: string;     // e.g., "early", "standard", "final"
  transfer_reason: string;      // Human-readable reason (min 10 chars)
  rule_execution_id?: string;   // UUID (for audit trail and idempotency)
}
Response Schema (200 OK):
interface PM49TransferCollectionsResponse {
  success: true;
  collections_entry_id: string; // UUID of created collections entry
}
Error Response (4xx/5xx):
interface PM49TransferCollectionsError {
  success: false;
  error_code: string;
  error_message: string;
}
Error Codes:
  • ACCOUNT_NOT_FOUND (404): Account/claim ID does not exist
  • ALREADY_IN_COLLECTIONS (409): Account already assigned to collections tier
  • INELIGIBLE_PAYER (422): Payer type excluded from collections (e.g., Medicaid in some states)
  • ORG_MISMATCH (403): Account does not belong to organization
  • INVALID_TIER (400): Unrecognized collections tier
Idempotency: Use rule_execution_id as idempotency key Authentication: Service role or JWT with pm.collections.create permission

PM-49 Published Event: pm_rcm_rule_executed

Purpose: PM-11 dashboard consumes this event for RCM automation metrics Event Schema:
interface PM49RuleExecutedEvent {
  rule_id: string;              // UUID
  rule_type: string;            // e.g., "small_balance_adjust", "denial_resubmit"
  action_taken: string;         // e.g., "post_adjustment", "trigger_resubmission"
  target_entity_id: string;     // UUID (claim_id, denial_id, account_id)
  dollar_amount?: number;       // Decimal (if financial action)
  organization_id: string;      // UUID
  executed_at: string;          // ISO 8601 timestamp
}
Consumers: PM-11 (Revenue Cycle Dashboard) Event Contract Reference: See EVENT_CONTRACTS.md for full event contract details

PF-63: Entra ID Deep Integration Edge Functions

Status: ✅ Implemented
Spec Reference: PF-63 (Entra ID Integration)
Last Updated: 2026-02-21

Edge Function 1: entra-employee-presence

Invocation: supabase.functions.invoke('entra-employee-presence', { body })
Provider: PF (Platform Foundation)
Consumer: HR employee views, dashboards
JWT Required: Yes
Actions:
ActionRequired FieldsResponse
get-singleorganization_id, employee_id{ presence: { id, availability, activity } }
get-batchorganization_id, employee_ids[]{ presenceMap: Record<empId, presence> }
get-oooorganization_id, employee_id{ ooo: { isOOO, endDate } }
Security: JWT + verifyOrgAccess(). OOO message content redacted (PHI).
Performance SLA: p95 < 2s (single), < 5s (batch up to 650).
Rate Limit: Per-organization, delegated to Graph API limits.

Edge Function 2: entra-teams-notify

Invocation: supabase.functions.invoke('entra-teams-notify', { body })
Provider: PF (Platform Foundation)
Consumer: event-consumer, internal services
JWT Required: Yes (typically service-role)
Request:
{
  organization_id: uuid;
  team_id: string;
  channel_id: string;
  message_type: string;
  payload?: Record<string, unknown>;
  message_template?: string;
}
Response: { success: true, correlationId }
Security: JWT + verifyOrgAccess(). HTML escaping on all template values.
Performance SLA: p95 < 3s.

Edge Function 3: entra-sharepoint-documents

Invocation: supabase.functions.invoke('entra-sharepoint-documents', { body })
Provider: PF (Platform Foundation)
Consumer: SharePoint document browser UI
JWT Required: Yes
Actions:
ActionRequired FieldsResponse
listorganization_id, site_id, folder_id?{ items: SharePointItem[] }
uploadorganization_id, site_id, file_name, file_content (base64){ item }
downloadorganization_id, site_id, item_id{ downloadUrl }
create-folderorganization_id, site_id, folder_name{ folder }
searchorganization_id, site_id, query{ items: SharePointItem[] }
Security: JWT + verifyOrgAccess(). Max upload: 50MB. Action parameter sanitized.
Performance SLA: p95 < 3s (list/search), < 10s (upload).

Edge Function 4: entra-teams-activity

Invocation: supabase.functions.invoke('entra-teams-activity', { body })
Provider: PF (Platform Foundation)
Consumer: Notification pipeline
JWT Required: Yes
Request:
{
  organization_id: uuid;
  user_id: uuid;
  title: string;
  body?: string;
  web_url?: string;
  activity_type?: string;
  priority?: 'low' | 'normal' | 'high' | 'critical';
}
Response: { success: true } or { success: true, skipped: true, reason: string }
Skip Conditions: Feature disabled, below priority threshold, no Entra ID for user.
Security: JWT + verifyOrgAccess(). Requires Azure Bot registration.
Performance SLA: p95 < 3s.

HR-34: Contractor 1099 Totals RPC

Provider: HR (Workforce) — HR-34 Consumer: HR-PAY-04 (Tax Forms), HR-34 UI (1099 dashboard) Status: ✅ Implemented Integration Doc: HR-34 Integration Database function: hr_get_contractor_1099_totals(p_organization_id uuid, p_tax_year integer) Returns: SETOF RECORD (contractor_id uuid, total_amount numeric, entry_count bigint) Purpose: Aggregates all approved (approval_status = 'approved') time entry amounts for contractors in a given organization and tax year. Used by HR-PAY-04 to produce 1099-NEC input data. Auth: authenticated role + hr_has_org_access(p_organization_id, auth.uid()) enforced inside the SECURITY DEFINER function. Callers must also hold hr.contractor.tax_id.read permission for tax ID display in the UI. Request (logical):
  • p_organization_id (UUID): Tenant context
  • p_tax_year (INTEGER): Calendar year to aggregate (e.g. 2025)
Response: One row per contractor with approved time entries in the given year. total_amount is the sum of hr_contractor_time_entries.amount where approval_status = 'approved'. entry_count is the count of such entries. Idempotency: Read-only function; re-runs return same results for same inputs. Hook: useContractor1099Totals(taxYear) in src/cores/hr/hooks/contractors/useContractor1099Totals.ts
Next Review: 2026-06-30 (Quarterly)