> ## 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.

# API Contracts

> Version: 2.1.4 Last Updated: 2026-05-18 Constitution Reference: Section 1.3 (Integration Patterns - Pattern 3)

**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](../../../specs/pf/specs/PF-97-multi-tenant-organization-api-access.md)**. 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](./multi-tenant-organization-api-access-integration.md)** once implementation is available.

**Related:** [PF-30-EN-01 machine principals & scopes](../../../specs/pf/specs/PF-30-EN-01-machine-principals-scopes.md), [PF-89-EN-01 machine client versioning](../../../specs/pf/specs/PF-89-EN-01-machine-client-versioning.md).

***

## 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](../../../specs/cl/specs/CL-68-bhrf-clinical-residential-program-documentation.md); full contract [CL-GR-PM-BHRF-EPISODE-LIFECYCLE](./CL-GR-PM-BHRF-EPISODE-LIFECYCLE.md)

**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:**

```typescript theme={null}
// @/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](../../../specs/gr/specs/GR-20-external-lms-bridge.md)

**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:**

```typescript theme={null}
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](./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](./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.

| Function                       | Auth pattern                                                     | Request (logical)                                                    | Response (logical)                                        |
| ------------------------------ | ---------------------------------------------------------------- | -------------------------------------------------------------------- | --------------------------------------------------------- |
| `compliance-run-checks`        | Service 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-scan`          | Service role / cron                                              | Optional `organization_id`                                           | `{ columns_scanned, rows_upserted, capped: boolean }`     |
| `generate-compliance-evidence` | User JWT + `pf.compliance.evidence.generate`                     | `organization_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](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](./medicaid-state-compliance-configuration-integration.md)
**Spec Reference:** [PF-96 spec](../../../specs/pf/specs/PF-96-medicaid-state-compliance-configuration.md)

**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:

```typescript theme={null}
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:**

```typescript theme={null}
import { useJurisdictionProfile } from '@/platform/jurisdiction';

const { data: profile, isLoading, error } = useJurisdictionProfile(siteId?);
// Accesses: profile?.clinical, profile?.billing, profile?.compliance
```

**Edge Function Helper:**

```typescript theme={null}
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:**

* [PLATFORM\_INTEGRATION\_LAYERS.md#pf-96-jurisdiction-profile-integration-layer](./PLATFORM_INTEGRATION_LAYERS.md) for hook usage
* [EVENT\_CONTRACTS.md#pf-96-jurisdiction-profile-changed](./EVENT_CONTRACTS.md) for cache invalidation event

***

### 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](./google-workspace-integration-integration.md)
**Spec Reference:** [PF-101 spec](../../../specs/pf/specs/PF-101-google-workspace-integration.md)

**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:**

```typescript theme={null}
{
  organization_id: uuid;
  connection_id: uuid;
  capabilities?: ('directory' | 'gmail' | 'calendar' | 'drive' | 'chat' | 'licensing' | 'reports')[];
}
```

**Response:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  organization_id: uuid;
  connection_id: uuid;
  sync_mode: 'full' | 'incremental';
}
```

**Response:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  organization_id: uuid;
  employee_id: uuid;
  profile_id?: uuid;
  department_id?: uuid;
  position_id?: uuid;
  correlation_id: string;
}
```

**Response:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  organization_id: uuid;
  employee_id: uuid;
  termination_date: timestamp;
  correlation_id: string;
}
```

**Response:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  organization_id: uuid;
  connection_id: uuid;
  report_types?: ('admin' | 'login' | 'drive' | 'calendar')[];
  start_time?: timestamp;
  end_time?: timestamp;
}
```

**Response:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  organization_id: uuid;
  connection_id: uuid;
  space_id: string;
  message: {
    text: string;
    cards?: unknown[];
  };
  correlation_id?: string;
}
```

**Response:**

```typescript theme={null}
{
  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:**

* [PF-101-google-workspace-integration-INTEGRATION.md](./google-workspace-integration-integration.md) for detailed integration contracts
* [EVENT\_CONTRACTS.md](./EVENT_CONTRACTS.md) for published events (`pf_google_workspace_user_provisioned`, `pf_google_workspace_connector_degraded`)

***

### 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](./managed-care-authorization-tracking-integration.md)
**Spec Reference:** [PM-47 spec](../../../specs/pm/specs/PM-47-managed-care-authorization-tracking.md)

**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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  error: {
    code: string;           // Canonical error code
    message: string;        // User-friendly error message
    details?: any;          // Optional additional context
  }
}
```

**Status Code Mappings:**

* `400 Bad Request` → `INVALID_REQUEST` - Invalid input (missing fields, malformed data)
* `403 Forbidden` → `ACCESS_DENIED` - Auth/permission failure (missing `pm.managed_care_auth.validate` permission or organization\_id mismatch)
* `404 Not Found` → `CLAIM_NOT_FOUND` - Missing claim\_id in request
* `500 Internal Server Error` → `SERVER_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](./jurisdiction-aware-assessment-requirements-integration.md)
**Spec Reference:** [CL-02-EN-60 spec](../../../specs/cl/specs/CL-02-EN-60-jurisdiction-aware-assessment-requirements.md)

**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:

```json theme={null}
{
  "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_problem` → `chief_complaint IS NOT NULL AND <> ''`
* `history_present_illness`, `history_of_present_illness` → `history_of_present_illness IS NOT NULL AND <> ''`
* `medical_history` → `medical_history IS NOT NULL AND <> ''`
* `mental_health_history`, `psychiatric_history` → `mental_health_history IS NOT NULL AND <> ''`
* `substance_use_history` → `substance_use_history IS NOT NULL AND <> ''`
* `social_history` → `social_history IS NOT NULL AND <> ''`
* `preliminary_diagnoses`, `diagnostic_formulation` → `preliminary_diagnoses IS NOT NULL AND jsonb_array_length(preliminary_diagnoses) > 0`
* `sdoh_screening_completed` → `sdoh_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:**

* [PLATFORM\_INTEGRATION\_LAYERS.md#pf-96-jurisdiction-profile-integration-layer](./PLATFORM_INTEGRATION_LAYERS.md) for profile-driven element lists
* [EVENT\_CONTRACTS.md](./EVENT_CONTRACTS.md) for related event patterns

***

### 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:**

* [RH-01 Census, Beds & Episodes](../../../specs/rh/specs/RH-01-census-beds-episodes.md) — Consumer specification
* [RH-01.1 Bed Board & Facility Types](../../../specs/rh/specs/RH-01.1-bed-board-facility-types.md) — Bed board usage
* FA-01 (Chart of Accounts) — Provider foundation
  **Integration Doc:** [RH-01-bed-board-census-INTEGRATION.md](./bed-board-census-integration.md)

**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):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  organization_id: uuid;
  tax_year_id: uuid;
  form_type?: '1099-MISC' | '1099-NEC' | 'both';
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  generated_count: number;
  forms: [{
    form_id: uuid;
    vendor_id: uuid;
    vendor_name: string;
    total_payments: number;
  }];
}
```

**Usage Example (Write Endpoint):**

```typescript theme={null}
// 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):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  organization_id: uuid;
  depreciation_month: date; // YYYY-MM-01 format
  asset_ids?: uuid[]; // Optional: specific assets, otherwise all active
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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):**

```typescript theme={null}
// 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:**

```typescript theme={null}
{
  approved: boolean;
  comments?: string;
  approved_amount?: number;
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  expense_report_id: uuid;
  status: 'approved' | 'rejected';
  approved_at: timestamp;
}
```

**Usage Example (Write Endpoint):**

```typescript theme={null}
// 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):**

```typescript theme={null}
{
  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):**

```typescript theme={null}
{
  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):**

```typescript theme={null}
// 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:**

```typescript theme={null}
{
  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):**

```typescript theme={null}
{
  position_id: uuid;
  organization_id: uuid;
  position_date: date;
  total_cash: number;
  updated_at: timestamp;
}
```

**Usage Example (Write Endpoint):**

```typescript theme={null}
// 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:**

```typescript theme={null}
{
  organization_id: uuid;
  investment_type: string;
  amount: number;
  maturity_date: date;
  interest_rate: number;
  notes?: string;
}
```

**Response Schema (201 Created):**

```typescript theme={null}
{
  investment_id: uuid;
  investment_number: string;
  organization_id: uuid;
  status: 'active';
  created_at: timestamp;
}
```

**Usage Example (Write Endpoint):**

```typescript theme={null}
// 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:**

```typescript theme={null}
{
  fiscal_period_id: uuid;
  project_ids?: uuid[]; // Optional: specific projects, otherwise all active
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  consolidation_id: uuid;
  fiscal_period_id: uuid;
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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](./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:**

```typescript theme={null}
{
  recognition_date: date;
  fiscal_period_id: uuid;
  schedule_ids?: uuid[]; // Optional: specific schedules, otherwise all due for the period
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  approval_notes?: string;
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  close_period_id: uuid;
  status: 'locked';
  approved_at: timestamp;
  approved_by: uuid;
}
```

**Usage Example:**

```typescript theme={null}
// 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:**

* [RH-01.1 Bed Board & Facility Types](../../../specs/rh/specs/RH-01.1-bed-board-facility-types.md)
* [CL-01 Patient Chart & Clinical Summary](../../../specs/cl/specs/CL-01-patient-chart-clinical-summary.md)
  **Integration Doc:** [RH-01-bed-board-census-INTEGRATION.md](./bed-board-census-integration.md)

**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:**

```http theme={null}
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):**

```typescript theme={null}
{
  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:**

* Scheme: Bearer JWT (Supabase session token).
* Required role/scope: `rh.beds.view` — callers without this permission receive `403 Forbidden`.
* Spec refs: [RH-01.1 Bed Board & Facility Types](../../../specs/rh/specs/RH-01.1-bed-board-facility-types.md) · [CL-01 Patient Chart](../../../specs/cl/specs/CL-01-patient-chart-clinical-summary.md).

**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:**

```typescript theme={null}
{
  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 Status | `error.code`          | Condition                              |
| ----------- | --------------------- | -------------------------------------- |
| 400         | `INVALID_PATIENT_ID`  | `patient_id` is not a valid UUID       |
| 403         | `FORBIDDEN`           | Caller lacks `rh.beds.view` permission |
| 404         | `PATIENT_NOT_FOUND`   | `patient_id` not found in caller's org |
| 429         | `RATE_LIMIT_EXCEEDED` | Per-user rate limit reached            |
| 500         | `INTERNAL_ERROR`      | Unexpected server-side failure         |

**TypeScript usage example (GET current-residence with TanStack Query):**

```typescript theme={null}
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](../../../specs/pf/specs/PF-30-permissions-system-v2.md)

> **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:**

```sql theme={null}
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:**

```sql theme={null}
-- 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:**

```typescript theme={null}
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](provider-credentialing-enrollment-verification-integration.md).
**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):**

```typescript theme={null}
{
  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)

***

### HR-01: Employee Search

**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](provider-credentialing-enrollment-verification-integration.md).
**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):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
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:**

```typescript theme={null}
[{
  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:**

* [Event Contracts](./EVENT_CONTRACTS.md) - Complete event schema
* [HR-06 Phase 2-3 Expansion](../../../specs/hr/specs/HR-06-PHASE-2-3-EXPANSION.md) - Consumer specification
* [HR-11 Benefits Administration](../../../specs/hr/specs/HR-11-benefits-administration.md) - Provider specification

**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](../../../specs/fw/specs/FW-19-data-query-integration.md)

**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:**

```typescript theme={null}
{
  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):**

```typescript theme={null}
{
  data: any[];                  // Query results
  count: number;                // Number of rows returned
  query_time_ms: number;        // Query execution time
}
```

**Error Response Schema:**

```typescript theme={null}
{
  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](../../../specs/fw/specs/FW-19-data-query-integration.md)

**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:**

```typescript theme={null}
{
  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):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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](../../../specs/fw/archive/FW-22-workflow-execution-monitoring.md)

**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:**

```typescript theme={null}
{
  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):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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](../../../specs/fw/archive/FW-24-workflow-testing-sandbox.md)

**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:**

```typescript theme={null}
{
  organization_id: uuid;
  rule_id: uuid;
  input_data: Record<string, any>;
  test_dataset_id?: uuid;              // Optional: use test dataset
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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](../../../specs/fw/archive/FW-24-workflow-testing-sandbox.md)

**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):**

```typescript theme={null}
{
  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](../../../specs/fw/archive/FW-24-workflow-testing-sandbox.md)

**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:**

```typescript theme={null}
{
  organization_id: uuid;
  rule_id: uuid;
  test_case_ids?: uuid[];              // Optional: run specific tests (all if omitted)
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
// 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](../../../specs/fw/archive/FW-45-decision-tables.md)\
**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](../../../specs/fw/archive/FW-46-durable-execution-worker.md)

**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:

```typescript theme={null}
interface WorkflowExecutorWorkerRequest {
  // Optional: for manual testing only
  organization_id?: string;  // Process specific org only
  batch_size?: number;      // Override default batch size
}
```

**Success Response (200):**

```typescript theme={null}
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:**

```typescript theme={null}
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](../../../specs/fw/archive/FW-49-execution-timeout-watchdog.md)\
**Integration:** [FW-49-execution-timeout-watchdog-INTEGRATION.md](./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:

```typescript theme={null}
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):**

```typescript theme={null}
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](../../../specs/fw/archive/FW-49-execution-timeout-watchdog.md)

**Request Body:**

```typescript theme={null}
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):**

```typescript theme={null}
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:**

```typescript theme={null}
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](../../../specs/fw/specs/FW-52-workflow-import-export-portability.md)\
**Integration Doc:** [FW-52-workflow-import-export-portability-INTEGRATION.md](./workflow-import-export-portability-integration.md)

**Request Body (logical):**

```typescript theme={null}
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](../../../specs/fw/specs/FW-52-workflow-import-export-portability.md)\
**Integration Doc:** [FW-52-workflow-import-export-portability-INTEGRATION.md](./workflow-import-export-portability-integration.md)

**Request Body (logical):**

```typescript theme={null}
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:
  ```typescript theme={null}
  &#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_INTEGRATION_CONTRACTS.md)

### 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](../../../specs/it/specs/IT-01-it-asset-management.md)

**Purpose:** Lookup IT asset details for ticket linking and security tracking.

**Response Schema (200 OK):**

```typescript theme={null}
{
  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

***

### IT-01: Asset Search

**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](../../../specs/it/specs/IT-03-it-vendor-management.md)

**Purpose:** Lookup IT vendor details for asset, license, and procurement linking.

**Response Schema (200 OK):**

```typescript theme={null}
{
  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](../../../specs/it/specs/IT-04-software-license-management.md)

**Purpose:** Check license compliance for a software license or all licenses.

**Function Signature:**

```sql theme={null}
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](../../../specs/fa/specs/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:**

```typescript theme={null}
{
  approved: boolean;           // true = approve, false = reject
  comments?: string;           // Optional approval/rejection notes
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  close_period_id: uuid;
  status: 'approved' | 'rejected';
  approved_at: timestamp;
  approved_by: uuid;
  comments?: string;
}
```

**Error Response Schema:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
// 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):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
// 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:**

* [PF-27-platform-ai.md](../../../specs/pf/specs/PF-27-platform-ai.md) - Original specification
* [PF-59-ai-provider-migration.md](../../../specs/pf/specs/PF-59-ai-provider-migration.md) - OpenRouter migration

### 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:**

```typescript theme={null}
{
  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:

```typescript theme={null}
data: {"content": "chunk text", "done": false}
data: {"content": "more text", "done": false}
data: {"done": true}
```

**Response Schema (200 OK - Non-Streaming):**

```typescript theme={null}
{
  message: string;
  correlationId: string;
}
```

**Model Selection:**

* Automatically selected based on `moduleContext.module`:
  * `gr`, `hr` → `anthropic/claude-3.5-sonnet`
  * `fa`, `fw`, `rh`, `pf` → `openai/gpt-4o`
  * `lo`, `fm` → `openai/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](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:**

```typescript theme={null}
{
  documentId: uuid;
  analysisPrompt?: string; // Optional custom prompt
  schema?: Record<string, unknown>; // JSON schema for structured output
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  reportData: Record<string, unknown>; // Report data to summarize
  narrativePrompt?: string; // Optional custom prompt
  context?: {
    module?: string;
    reportType?: string;
  };
}
```

**Response Schema (200 OK):**

```typescript theme={null}
{
  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](https://api.gusto.com) with server-injected OAuth token and `x-gusto-client-ip` header. No Gusto credentials exposed to frontend.

### Request Schema

```typescript theme={null}
{
  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

```typescript theme={null}
{
  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)

```typescript theme={null}
{
  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:**

```typescript theme={null}
const { data, error } = await supabase.rpc('it_get_dashboard_summary', {
  p_organization_id: organizationId,
});
```

**Function Signature:**

```sql theme={null}
it_get_dashboard_summary(p_organization_id UUID)
RETURNS JSON
SECURITY DEFINER SET search_path = public
```

**Response Schema:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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.

| Category               | Rule                                                                                                                                                            |
| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Allowed log fields** | `organization_id`, `timestamp`, aggregated metric names (e.g., `open_tickets`, `assets_total`), request duration, HTTP status                                   |
| **Prohibited data**    | Full names, SSNs, DOB, medical details, credentials, API tokens, IP addresses of end-users                                                                      |
| **Masking / hashing**  | Not 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 & access** | Logs retained ≤ 30 days; access restricted to `org_admin` and platform operators via ACL                                                                        |
| **Log levels**         | `INFO`: 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:**

```typescript theme={null}
// 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:**

```typescript theme={null}
{
  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`.

| Category               | Rule                                                                                                                                                                      |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Allowed log fields** | `organization_id`, `change_id`, `status`, `priority`, `timestamp`, request duration, HTTP status                                                                          |
| **Prohibited data**    | Full names, SSNs, DOB, medical details, credentials, API tokens, raw email addresses                                                                                      |
| **Masking / hashing**  | Requester/implementer names must NOT appear in logs. Use `requester_id` (UUID) instead. Mask email to `j***@domain.com`, phone to `***-**-1234`                           |
| **Retention & access** | Logs retained ≤ 30 days; access restricted to `org_admin` and platform operators via ACL                                                                                  |
| **Log levels**         | `INFO`: 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:**

```typescript theme={null}
const { data, error } = await supabase.functions.invoke('generate-it-report', {
  body: { run_id, organization_id, report_type, parameters, format },
});
```

**Request Schema:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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)

| Category               | Rule                                                                                                                                |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Allowed log fields** | `organization_id`, `tax_year`, `quarter`, aggregated counts (e.g., `w2_count`, `total_wages_sum`), request duration                 |
| **Prohibited data**    | Employee names, SSNs, DOB, individual wage amounts, bank account numbers, medical deductions                                        |
| **Masking / hashing**  | Employee references in logs must use `employee_id` (UUID) only. Never log SSN, even partially. Aggregate wage totals are acceptable |
| **Retention & access** | Logs retained ≤ 30 days; access restricted to `org_admin` and platform operators                                                    |
| **Log levels**         | `INFO`: 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](../../../specs/cl/planning/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](../../../specs/pm/specs/PM-02-insurance-eligibility-verification.md)

**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):**

```typescript theme={null}
/** 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):**

```typescript theme={null}
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](../../../specs/pm/specs/PM-15-clearinghouse-integration.md)

**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:** `ClaimStatusRequest` — `claim_id: uuid`, `organization_id: uuid`.\
**Response (200):** `ClaimStatusResponse` — `claim_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](../../../specs/cl/planning/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):** `PatientLookupResponse` — `patient_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](../../../specs/pm/specs/PM-01-patient-registration-demographics.md)

**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();\`

***

### Consent Check (Part 2 / TPO)

**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](../../../specs/cl/specs/CL-11-consent-management-42cfr-part2.md)

**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:** `ConsentCheckRequest` — `patient_id: uuid`, `purpose_of_use: string`, `resource_type?: string`, `encounter_id?: uuid`, `organization_id: uuid`, `user_id?: uuid`.\
**Response (200):** `ConsentCheckResponse` — `allowed: 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();\`

***

<h3 id="cl-16-en-01-tefca-qhin-api">
  CL-16-EN-01: TEFCA/QHIN Connectivity API (Planned) \\
</h3>

**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](../../../specs/cl/specs/CL-16-EN-01-tefca-qhin-connectivity.md)\
**Integration Doc:** [CL-16-EN-01-tefca-qhin-connectivity-INTEGRATION.md](./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.

<h4 id="cl-16-en-01-api-post-tefca-qhin-query">
  Endpoint: `POST /tefca/qhin/query` \\
</h4>

* **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.

<h4 id="cl-16-en-01-api-post-tefca-qhin-exchange">
  Endpoint: `POST /tefca/qhin/exchange` \\
</h4>

* **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.

<h4 id="cl-16-en-01-api-get-tefca-qhin-exchanges">
  Endpoint: `GET /tefca/qhin/exchanges` \\
</h4>

* **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](../../../specs/cl/specs/CL-16-fhir-interoperability-data-exchange.md)\
**Integration Doc:** [CL-16-fhir-interoperability-data-exchange-INTEGRATION.md](./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](http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient))

**Request:**

```typescript theme={null}
{
  action: 'get-patient';
  organization_id: string; // uuid
  patient_id: string;      // uuid
}
```

**Response (200):**

```typescript theme={null}
// 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:**

| Code | OperationOutcome Issue | When                    |
| ---- | ---------------------- | ----------------------- |
| 400  | `invalid`              | Missing required params |
| 401  | `security`             | No/invalid JWT          |
| 403  | `forbidden`            | Cross-tenant access     |
| 404  | `not-found`            | Patient not in org      |
| 429  | `throttled`            | Rate limit exceeded     |
| 500  | `exception`            | Internal 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](http://hl7.org/fhir/us/core/StructureDefinition/us-core-condition-problems-health-concerns))

**Request:**

```typescript theme={null}
{
  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](http://hl7.org/fhir/us/core/StructureDefinition/us-core-medicationrequest))

**Request:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
{
  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:**

```typescript theme={null}
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](./revenue-cycle-automation-rules-engine-integration.md)
**Spec Reference:** [PM-49 spec](../../../specs/pm/specs/PM-49-revenue-cycle-automation-rules-engine.md)

**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:**

```typescript theme={null}
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):**

```typescript theme={null}
interface PM49PostAdjustmentResponse {
  success: true;
  adjustment_id: string;        // UUID of created adjustment
}
```

**Error Response (4xx/5xx):**

```typescript theme={null}
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:**

```typescript theme={null}
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):**

```typescript theme={null}
interface PM49TriggerResubmissionResponse {
  success: true;
  resubmission_id: string;      // UUID of created resubmission
}
```

**Error Response (4xx/5xx):**

```typescript theme={null}
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:**

```typescript theme={null}
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):**

```typescript theme={null}
interface PM49TransferCollectionsResponse {
  success: true;
  collections_entry_id: string; // UUID of created collections entry
}
```

**Error Response (4xx/5xx):**

```typescript theme={null}
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:**

```typescript theme={null}
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](./EVENT_CONTRACTS.md) for full event contract details

***

## Related Documentation

* [Platform Integration Layers](./PLATFORM_INTEGRATION_LAYERS.md) - Platform layer integrations
* [Event Contracts](./EVENT_CONTRACTS.md) - Event-based integrations
* [IT Integration Contracts](./IT_INTEGRATION_CONTRACTS.md) - IT module integration contracts
* [Integration Examples](../patterns/INTEGRATION_EXAMPLES.md) - Code examples
* [Constitution](../../../constitution.md) - Engineering guardrails

***

## 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:**

| Action       | Required Fields                     | Response                                       |
| ------------ | ----------------------------------- | ---------------------------------------------- |
| `get-single` | `organization_id`, `employee_id`    | `{ presence: { id, availability, activity } }` |
| `get-batch`  | `organization_id`, `employee_ids[]` | `{ presenceMap: Record<empId, presence> }`     |
| `get-ooo`    | `organization_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:**

```typescript theme={null}
{
  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:**

| Action          | Required Fields                                                    | Response                      |
| --------------- | ------------------------------------------------------------------ | ----------------------------- |
| `list`          | `organization_id`, `site_id`, `folder_id?`                         | `{ items: SharePointItem[] }` |
| `upload`        | `organization_id`, `site_id`, `file_name`, `file_content` (base64) | `{ item }`                    |
| `download`      | `organization_id`, `site_id`, `item_id`                            | `{ downloadUrl }`             |
| `create-folder` | `organization_id`, `site_id`, `folder_name`                        | `{ folder }`                  |
| `search`        | `organization_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:**

```typescript theme={null}
{
  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](./contractor-contingent-workforce-integration.md)

**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)
