Skip to main content

Documentation Index

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

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

Version: 1.0.0
Last Updated: 2026-01-28
Constitution Reference: Section 1.3 (Integration Patterns - Pattern 3: API Contracts)
Status: 📝 Planned

Overview

This document describes the integration contract between FA-UX-05 (Purchase Order Creation Wizard) and FW-03 (Approval Workflows) for submitting purchase orders for approval. Related Specifications:
  • FA-UX-05: Purchase Order Creation Wizard (specs/fa/ux/FA-UX-05-purchase-order-creation-wizard.md)
  • FW-03: Approval Workflows (referenced as dependency in FA-UX-05)
  • FA-04: Purchase Orders & Procurement (specs/fa/specs/FA-04-purchase-orders.md)

Integration Pattern

Pattern: Pattern 3 - API Contracts (Synchronous Request-Response) Rationale: Purchase order submission for approval requires synchronous confirmation that the approval request was created and the PO status was updated. This is a request-response interaction, not an event-driven workflow.

API Contracts

Submit Purchase Order for Approval

Endpoint: /api/v1/fw/approval-requests/submit
Method: POST
Provider: FW (Forms & Workflow) - FW-03 Approval Workflows
Consumer: FA (Finance & Accounting) - FA-UX-05 Purchase Order Creation Wizard
Status: 📝 Planned

Purpose

Submit a purchase order created via FA-UX-05 wizard for approval workflow processing. The approval workflow determines approvers based on PO amount, department, and organization approval chain configuration.

Request

Endpoint:
POST /api/v1/fw/approval-requests/submit
Authorization: Bearer {jwt_token}
Content-Type: application/json
Request Body:
interface SubmitPOApprovalRequest {
  organization_id: uuid;           // REQUIRED - Multi-tenant isolation
  source_type: 'purchase_order';   // REQUIRED - Identifies source entity type
  source_id: uuid;                 // REQUIRED - Purchase order ID (fa_purchase_orders.id)
  source_table: 'fa_purchase_orders'; // REQUIRED - Table name for entity type
  chain_id?: uuid;                 // OPTIONAL - Specific approval chain ID (if not provided, system selects based on rules)
  title: string;                   // REQUIRED - Approval request title (e.g., "PO-2026-001: Office Supplies")
  description?: string;            // OPTIONAL - Approval request description
  request_data: {                  // REQUIRED - Snapshot of PO data at time of submission
    po_number: string;
    vendor_id: uuid;
    vendor_name: string;
    total_amount: number;
    line_count: number;
    delivery_date?: string;        // ISO date
    ship_to_site_id?: uuid;
    department_id?: uuid;
    fund_id?: uuid;
    project_id?: uuid;
  };
  priority?: 'low' | 'normal' | 'high' | 'urgent'; // OPTIONAL - Default: 'normal'
  submitted_by: uuid;              // REQUIRED - User ID submitting the request
}

Response

Success (201 Created):
interface SubmitPOApprovalResponse {
  approval_request_id: uuid;       // Created approval request ID
  organization_id: uuid;
  chain_id: uuid;                  // Approval chain ID used
  current_step: number;            // Current approval step (starts at 1)
  status: 'pending' | 'in_progress'; // Initial status
  next_approver?: {                // Next approver (if known)
    user_id: uuid;
    user_name: string;
    role?: string;
  };
  estimated_completion?: string;   // ISO date - Estimated completion based on SLA
  submitted_at: string;           // ISO timestamp
}
Error Response:
interface ErrorResponse {
  error: {
    code: 'INVALID_REQUEST' | 'UNAUTHORIZED' | 'ACCESS_DENIED' | 'CHAIN_NOT_FOUND' | 'VALIDATION_ERROR' | 'SERVER_ERROR';
    message: string;                // Human-readable error message
    details?: {
      field?: string;               // Field name if validation error
      constraint?: string;          // Constraint violated
    };
  }
}

Error Codes

StatusCodeDescription
400INVALID_REQUESTInvalid request parameters (missing required fields, invalid UUIDs)
401UNAUTHORIZEDMissing or invalid JWT token
403ACCESS_DENIEDUser does not have permission to submit approval requests
404CHAIN_NOT_FOUNDNo approval chain configured for this PO type/amount
422VALIDATION_ERRORBusiness rule validation failed (e.g., PO already submitted, invalid status)
429RATE_LIMITEDToo many requests (rate limit exceeded)
500SERVER_ERRORInternal server error

Security

  • Authentication: JWT required in Authorization: Bearer {token} header
  • Organization Validation: organization_id validated against user’s accessible organizations
  • RLS Enforcement: RLS policies enforce tenant isolation on approval requests
  • Rate Limiting: 100 requests/minute per organization
  • Permission Check: User must have fw.approval.submit permission (PF-30)

Usage Example

// In FA-UX-05 wizard completion handler
import { supabase } from '@/integrations/supabase/client';

async function submitPOForApproval(
  poId: string,
  organizationId: string,
  userId: string,
  poData: PurchaseOrderCreationWizardData
) {
  const { data, error } = await supabase.functions.invoke('fw-submit-approval', {
    body: {
      organization_id: organizationId,
      source_type: 'purchase_order',
      source_id: poId,
      source_table: 'fa_purchase_orders',
      title: `PO-${poData.po_number}: ${poData.vendor_name}`,
      description: `Purchase order for ${poData.line_items.length} line items`,
      request_data: {
        po_number: poData.po_number,
        vendor_id: poData.vendor_id,
        vendor_name: poData.vendor_name,
        total_amount: poData.total_amount,
        line_count: poData.line_items.length,
        delivery_date: poData.delivery_date,
        ship_to_site_id: poData.ship_to_site_id,
        department_id: poData.department_id,
        fund_id: poData.fund_id,
        project_id: poData.project_id,
      },
      priority: poData.total_amount > 10000 ? 'high' : 'normal',
      submitted_by: userId,
    },
  });

  if (error) {
    throw new Error(`Failed to submit for approval: ${error.message}`);
  }

  return data; // SubmitPOApprovalResponse
}

Platform-Layer Usage

Services/Hooks Calling FW-03

Primary Hook: useSubmitPurchaseOrderApproval Location: src/cores/fa/hooks/usePurchaseOrders.ts (or similar) Implementation:
import { supabase } from '@/integrations/supabase/client';
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useSubmitPurchaseOrderApproval(organizationId: string) {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (params: {
      poId: string;
      poData: PurchaseOrderCreationWizardData;
    }) => {
      const { data: user } = await supabase.auth.getUser();
      if (!user.user) throw new Error('User not authenticated');

      const { data, error } = await supabase.functions.invoke('fw-submit-approval', {
        body: {
          organization_id: organizationId,
          source_type: 'purchase_order',
          source_id: params.poId,
          source_table: 'fa_purchase_orders',
          title: `PO-${params.poData.po_number}: ${params.poData.vendor_name}`,
          request_data: {
            po_number: params.poData.po_number,
            vendor_id: params.poData.vendor_id,
            vendor_name: params.poData.vendor_name,
            total_amount: params.poData.total_amount,
            line_count: params.poData.line_items.length,
            // ... other fields
          },
          submitted_by: user.user.id,
        },
      });

      if (error) throw error;
      return data;
    },
    onSuccess: () => {
      // Invalidate PO queries to refresh status
      queryClient.invalidateQueries({ queryKey: ['purchase-orders', organizationId] });
    },
  });
}

Expected Payloads

Request Payload Structure:
  • organization_id: UUID (required)
  • source_type: 'purchase_order' (required)
  • source_id: UUID of fa_purchase_orders.id (required)
  • source_table: 'fa_purchase_orders' (required)
  • title: String with PO number and vendor name (required)
  • request_data: JSONB snapshot of PO data (required)
  • submitted_by: UUID of submitting user (required)
Response Payload Structure:
  • approval_request_id: UUID of created approval request
  • chain_id: UUID of approval chain used
  • current_step: Number (starts at 1)
  • status: 'pending' or 'in_progress'
  • next_approver: Optional approver information
  • submitted_at: ISO timestamp

Retry/Timeout Semantics

Retry Policy:
  • Max Retries: 3 attempts
  • Retry Conditions: Network errors (5xx), timeout errors
  • No Retry: 4xx errors (client errors), validation errors
  • Backoff: Exponential backoff (1s, 2s, 4s)
Timeout:
  • Request Timeout: 30 seconds
  • Connection Timeout: 10 seconds
Error Handling:
  • Network errors: Retry with exponential backoff
  • Validation errors: Show user-friendly error message, allow correction
  • Permission errors: Show access denied message, redirect if needed
  • Server errors: Show generic error, log for support

Sequencing and Workflow State Transitions

Purchase Order Status Transitions

Before Submission:
  • status: 'draft' - PO created but not submitted
On Submission (via FW-03):
  • status: 'pending_approval' - PO submitted, awaiting approval
  • approval_request_id stored in fa_purchase_orders.approval_request_id
During Approval:
  • status: 'pending_approval' - Remains until approval/rejection
  • Approval status tracked in fw_approval_requests.status
After Approval:
  • status: 'approved' - PO approved, ready for goods receipt
  • approved_at timestamp set
  • approved_by user ID set
After Rejection:
  • status: 'rejected' - PO rejected, cannot proceed
  • rejected_at timestamp set
  • rejected_by user ID set
  • rejection_reason stored

Approval Workflow State Transitions

Request Creation (FA-UX-05 → FW-03):
  1. FA-UX-05 wizard completes, creates PO with status: 'draft'
  2. FA-UX-05 calls FW-03 API to submit for approval
  3. FW-03 creates fw_approval_requests record with status: 'pending'
  4. FW-03 determines approval chain based on PO amount, department, organization rules
  5. FW-03 assigns first approver(s) based on chain configuration
  6. FW-03 updates PO status to 'pending_approval' and sets approval_request_id
  7. FW-03 publishes approval_request_created event
  8. FW-03 sends notification to first approver(s) via PF-10
Approval Processing (FW-03 Internal):
  1. Approver reviews and approves/rejects
  2. If approved and more steps: Move to next step, notify next approver
  3. If approved and final step: Complete approval, publish approval_request_approved event
  4. If rejected: Complete rejection, publish approval_request_rejected event
Approval Completion (FW-03 → FA):
  1. FW-03 publishes approval_request_approved or approval_request_rejected event
  2. FA event handler receives event
  3. FA updates PO status to 'approved' or 'rejected'
  4. FA updates approved_at/rejected_at and approved_by/rejected_by
  5. FA sends notification to PO creator via PF-10

Event Contracts

Event: approval_request_created Publisher: FW-03 (Approval Workflows)
Subscribers: PF-10 (Notifications), Event Consumers
Payload:
interface ApprovalRequestCreatedPayload {
  event_type: 'approval_request_created';
  organization_id: uuid;
  timestamp: string; // ISO timestamp
  approval_request_id: uuid;
  source_type: 'purchase_order';
  source_id: uuid; // PO ID
  submitted_by: uuid;
  next_approver?: uuid;
}
Event: approval_request_approved Publisher: FW-03 (Approval Workflows)
Subscribers: FA (Purchase Orders), PF-10 (Notifications)
Payload:
interface ApprovalRequestApprovedPayload {
  event_type: 'approval_request_approved';
  organization_id: uuid;
  timestamp: string; // ISO timestamp
  approval_request_id: uuid;
  source_type: 'purchase_order';
  source_id: uuid; // PO ID
  approved_by: uuid;
  completed_at: string; // ISO timestamp
}
Event: approval_request_rejected Publisher: FW-03 (Approval Workflows)
Subscribers: FA (Purchase Orders), PF-10 (Notifications)
Payload:
interface ApprovalRequestRejectedPayload {
  event_type: 'approval_request_rejected';
  organization_id: uuid;
  timestamp: string; // ISO timestamp
  approval_request_id: uuid;
  source_type: 'purchase_order';
  source_id: uuid; // PO ID
  rejected_by: uuid;
  rejection_reason?: string;
  completed_at: string; // ISO timestamp
}

Integration Examples

Example 1: Submit PO for Approval

// In FA-UX-05 wizard completion handler
const { mutate: submitForApproval, isLoading } = useSubmitPurchaseOrderApproval(organizationId);

// After PO creation
const poId = await createPurchaseOrder(poData);

// Submit for approval
submitForApproval(
  {
    poId,
    poData,
  },
  {
    onSuccess: (response) => {
      // Update PO with approval request ID
      updatePurchaseOrder(poId, {
        approval_request_id: response.approval_request_id,
        status: 'pending_approval',
      });
      
      // Show success message
      toast.success('Purchase order submitted for approval');
      
      // Navigate to PO detail page
      navigate(`/fa/purchase-orders/${poId}`);
    },
    onError: (error) => {
      // Show error message
      toast.error(`Failed to submit for approval: ${sanitizeErrorMessage(error)}`);
    },
  }
);

Example 2: Handle Approval Event

// In FA event handler (edge function or client subscription)
supabase
  .channel('approval_events')
  .on('postgres_changes', {
    event: 'INSERT',
    schema: 'public',
    table: 'fw_approval_events',
    filter: `event_type=eq.approval_request_approved,source_type=eq.purchase_order`,
  }, (payload) => {
    const event = payload.new as ApprovalRequestApprovedPayload;
    
    // Update PO status
    await supabase
      .from('fa_purchase_orders')
      .update({
        status: 'approved',
        approved_at: event.completed_at,
        approved_by: event.approved_by,
      })
      .eq('id', event.source_id)
      .eq('organization_id', event.organization_id);
    
    // Send notification to PO creator
    await sendNotification({
      user_id: poData.created_by,
      type: 'po_approved',
      title: 'Purchase Order Approved',
      message: `PO ${poData.po_number} has been approved`,
    });
  })
  .subscribe();

Testing Requirements

Unit Tests

  • Request payload validation
  • Response payload parsing
  • Error handling (all error codes)
  • Retry logic (network errors, timeouts)
  • Organization validation

Integration Tests

  • Submit PO for approval (happy path)
  • Approval chain selection based on PO amount
  • PO status update on submission
  • Event publishing on approval/rejection
  • PO status update on approval completion

E2E Tests

  • Complete wizard flow → Submit for approval → Approval workflow → PO status update
  • Error scenarios (invalid PO, missing chain, permission denied)
  • Retry scenarios (network failure, timeout)

  • FA-UX-05 Spec: specs/fa/ux/FA-UX-05-purchase-order-creation-wizard.md
  • FA-04 Spec: specs/fa/specs/FA-04-purchase-orders.md
  • FW-03/FW-34 Spec: specs/fw/specs/FW-34-approval-workflows.md (or FW-03 if different)
  • Event Contracts: docs/architecture/integrations/EVENT_CONTRACTS.md
  • API Contracts: docs/architecture/integrations/API_CONTRACTS.md
  • Integration Matrix: docs/architecture/integrations/CROSS_CORE_INTEGRATIONS.md

Last Updated: 2026-01-28
Next Review: After FW-03/FW-34 implementation