openapi: 3.1.0
info:
  title: WorkSkedge Webhook API
  version: 2.25.0
  description: |
    **Last Updated: 2026-05-01**

    ### What's New in 2.25.0
    - **Trainer Chat Takeover (Restricted)**: New iframe surface allowing 3rd-party apps to replace the built-in WorkSkedge Trainer chat. **No webhook events are emitted by this surface** — communication is iframe-direct via the App Bridge (`?token=<jwt>&install_id=<uuid>&surface=trainer`). Requires `supports_trainer_provider = true` and `trainer_provider_url` set on the app's catalog entry by a WorkSkedge administrator. See `docs/trainer-provider-developer-guide.md`.

    ### What's New in 2.24.0
    - **Project Overhead Work Orders**: Every new project auto-creates a "Project Overhead" work order (`is_overhead: true`, `status: complete`). These are excluded from webhook dispatches by default. The `is_overhead` field is now included in all work order webhook payloads.
    
    ### 2.24.0
    - Added `sync_status`, `sync_message`, `sync_url` to vendor credit webhook payloads
    
    ### 2.23.0
    - Added `vendor_credit.created`, `vendor_credit.updated`, `vendor_credit.applied` webhook events
    
    ### 2.22.0
    - Added `settings.updated` webhook event for company, finance, timesheet, and module settings changes
    
    ### 2.22.0 (payload_version 1.1.0) — Cost link reverse-echo
    - **Additive**: `invoice.*` events gain `data.invoice.cost_links[]` — a unified projection of every consumed cost source (timesheet entries, PO lines, header-level POs, usage records, vendor credits) regardless of `billing_mode` or hidden state. See `InvoiceCostLink` schema.
    - **Additive**: `purchase_order.*`, `vendor_credit.*`, `usage_record.*` events gain optional `linked_invoice` (see `LinkedInvoiceRef`); `timesheet.submitted` / `timesheet.updated` carry `linked_invoice` on each entry.
    - Same fields are returned by `GET /v1/invoices/:id`, `GET /v1/purchase-orders/:id`, `GET /v1/timesheets/:id` (per entry), `GET /v1/usage-records/:id`, `GET /v1/vendor-credits/:id`. List endpoints unchanged.
    - `data.payload_version` bumps from `1.0.0` to `1.1.0` on all affected event families. Receivers may ignore the new fields.

    ### 2.21.0 / 2.20.0
    - Added `is_taxable` and `tax_code_id` fields to PO line items and POBill line items
    
    
    ### 2.19.0
    - Added `tax_inclusive`, `finalized_at` fields to POBill schema
    - Added `cost_code`, `product_id` fields to POBill line_items
    
    ### 2.18.0
    - **BREAKING**: `tax_inclusion` removed from individual tax line objects in webhook payloads. Tax inclusion is now controlled at the document level via `tax_inclusive` (boolean, default `false`) on the PO and Invoice root objects.
    
    ### What's New in 2.17.0
    - **Migration Data Support**: Records imported via the Data Migration wizard are flagged with `is_migration: true`. Migration records do **not** trigger outbound webhooks. REST API list endpoints support `exclude_migration` query parameter (default: `true`) to filter out legacy data.
    
    ## Overview
    The WorkSkedge Webhook API supports **bidirectional webhook integration**:
    
    ### Inbound Webhooks
    External systems can send events **TO** WorkSkedge to create, update, or trigger actions on work orders, projects, and assignments.
    
    ### Outbound Webhooks  
    WorkSkedge sends real-time events **FROM** WorkSkedge to your registered endpoints when work orders are created, employees are assigned, reports are submitted, and more.
    
    ---
    
    ## For Integration Developers
    
    **Important:** If your code generator or API tool doesn't see all webhook events:
    1. Clear your OpenAPI spec cache
    2. Re-fetch from https://app.workskedge.com/openapi.yaml
    3. Look for the `AllOutboundWebhookEvents` schema in components/schemas
    4. Check that the `webhooks` section exists at the root level
    
    This spec uses OpenAPI 3.0 webhooks with discriminated unions for comprehensive event discoverability.
    
    ---
    
    ## Authentication
    All requests must include an API key in the `X-API-Key` header. Generate API keys in WorkSkedge under **Settings → API & Webhooks**.
    
    ```
    X-API-Key: wsk_live_abc123xyz...
    ```
    
    ---
    
    ## Rate Limiting
    - **100 requests per minute** per API key
    - Rate limit headers included in all responses:
      - `X-RateLimit-Limit`: Total requests allowed per minute
      - `X-RateLimit-Remaining`: Requests remaining in current window
      - `X-RateLimit-Reset`: Timestamp when the limit resets
    
    ---
    
    ## Webhook Signature Verification
    
    ### For Inbound Webhooks (Your System → WorkSkedge)
    When sending webhooks to WorkSkedge, include these headers:
    - `X-Webhook-Signature`: HMAC-SHA256 signature of the payload
    - `X-Webhook-Timestamp`: Unix timestamp (seconds since epoch)
    - `X-Webhook-Source`: Identifier for your system (e.g., "dynamics365")
    
    ### For Outbound Webhooks (WorkSkedge → Your System)
    WorkSkedge automatically includes these headers in all outbound webhooks:
    - `X-WorkSkedge-Signature`: HMAC-SHA256 signature for verification
    - `X-WorkSkedge-Event`: Event type (e.g., "work_order.created")
    - `X-WorkSkedge-Delivery-Id`: Unique delivery attempt ID
    - `X-WorkSkedge-Install-Id`: App install ID or subscription identifier (always present)
    
    #### Verification Steps
    
    1. **Extract signature header from the incoming request:**
       ```
       signature = request.headers['X-WorkSkedge-Signature']
       ```
    
    2. **Get the raw request body (before parsing as JSON):**
       ```javascript
       const rawBody = request.rawBody; // Must be raw string, not parsed JSON
       ```
    
    3. **Compute HMAC-SHA256 using your webhook secret:**
       ```javascript
       const crypto = require('crypto');
       const expectedSignature = crypto
         .createHmac('sha256', webhookSecret)
         .update(rawBody)
         .digest('hex');
       ```
    
    4. **Compare signatures using constant-time comparison:**
       ```javascript
       const isValid = crypto.timingSafeEqual(
         Buffer.from(signature),
         Buffer.from(expectedSignature)
       );
       ```
    
    #### Code Examples
    
    **Node.js:**
    ```javascript
    const crypto = require('crypto');
    
    function verifyWebhookSignature(req, webhookSecret) {
      const signature = req.headers['x-workskedge-signature'];
      const rawBody = req.rawBody; // Use raw body, not parsed JSON
      
      // Compute expected signature
      const expectedSignature = crypto
        .createHmac('sha256', webhookSecret)
        .update(rawBody)
        .digest('hex');
      
      // Constant-time comparison
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expectedSignature)
      );
    }
    ```
    
    **Python:**
    ```python
    import hmac
    import hashlib
    
    def verify_webhook_signature(signature, raw_body, webhook_secret):
        # Compute expected signature
        expected_signature = hmac.new(
            webhook_secret.encode(),
            raw_body.encode(),
            hashlib.sha256
        ).hexdigest()
        
        # Constant-time comparison
        return hmac.compare_digest(signature, expected_signature)
    ```
    
    **C# (.NET):**
    ```csharp
    using System;
    using System.Security.Cryptography;
    using System.Text;
    
    public bool VerifyWebhookSignature(string signature, string rawBody, string webhookSecret)
    {
        // Compute expected signature
        using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret)))
        {
            var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
            var expectedSignature = BitConverter.ToString(hash).Replace("-", "").ToLower();
            return signature == expectedSignature;
        }
    }
    ```
    
    ---
    
    ## API Integrations & Echo Storm Prevention
    
    ### What are API Integrations?
    WorkSkedge organizes bidirectional sync around **API Integrations**. Each integration represents a connection with an external system (e.g., "QuickBooks Integration", "Dynamics 365 Integration") and groups together:
    - **API Key**: Used for authentication when the external system sends inbound webhooks
    - **Inbound Webhook URL**: The endpoint where external systems send data TO WorkSkedge
    - **Outbound Endpoints**: URLs where WorkSkedge sends data FROM WorkSkedge to the external system
    
    ### Echo Storm Prevention (Automatic)
    When external systems send changes via inbound webhooks, WorkSkedge **automatically prevents echo storms** by:
    1. Tracking which API key originated each inbound webhook
    2. Filtering outbound webhook subscriptions to exclude the originating system
    3. Still notifying all other systems about the change
    
    **This happens automatically** - no configuration needed!
    
    ### Example Scenario
    ```
    1. QuickBooks sends inbound webhook: "Create Employee #1234"
       → WorkSkedge creates the employee
       → WorkSkedge dispatches outbound webhook: employee.created
       → Filtering logic: Skip subscriptions linked to QuickBooks API key
       → Result: Dynamics 365 receives the webhook, QuickBooks does NOT
    
    2. User creates Employee #5678 in WorkSkedge UI
       → WorkSkedge dispatches outbound webhook: employee.created
       → No filtering (user-initiated, not from API)
       → Result: ALL systems (QuickBooks, Dynamics 365, etc.) receive the webhook
    ```
    
    ### Benefits
    ✅ **No infinite loops** - Changes from external systems won't echo back  
    ✅ **Multi-system sync** - Other systems are still notified  
    ✅ **No configuration** - Works automatically when you link outbound endpoints to API keys  
    ✅ **Auditable** - All webhook deliveries are logged for troubleshooting
    
    ### Best Practices for Bidirectional Integration
    - **Link outbound endpoints** to the same API key used for inbound webhooks
    - **Test with multiple systems** to verify each system doesn't receive its own changes back
    - **Monitor webhook logs** to ensure expected filtering behavior
    - **Handle user-initiated changes** - All systems will receive these webhooks
    
    ---
    
    ## Webhook Best Practices
    
    ### Receiving Webhooks
    - **Respond quickly:** Always return a `2xx` status code within 5 seconds
    - **Process asynchronously:** Queue webhooks for background processing
    - **Verify signatures:** Always verify HMAC signatures before processing
    - **Handle duplicates:** Use `event_id` or `X-WorkSkedge-Delivery-Id` for idempotency
    - **Store raw payloads:** Keep raw webhook bodies for debugging and reprocessing
    - **Echo prevention:** Link outbound endpoints to API keys to prevent receiving your own changes back (see "API Integrations & Echo Storm Prevention" above)
    
    ### Error Handling
    - **5xx errors:** Temporary failures - WorkSkedge will retry
    - **4xx errors:** Permanent failures - No retry will be attempted
    - **Retry strategy:** WorkSkedge retries failed webhooks up to 5 times with exponential backoff:
      - Attempt 1: Immediate
      - Attempt 2: After 30 seconds
      - Attempt 3: After 2 minutes
      - Attempt 4: After 10 minutes
      - Attempt 5 (final): After 1 hour
    - **Permanent failures:** After 5 failed attempts, the delivery is marked as `permanent_failure` and visible in Settings → API Webhooks
    
    ### Security
    - **HTTPS only:** Webhook endpoints must use HTTPS
    - **Verify signatures:** Never skip signature verification in production
    - **Rotate secrets:** Periodically rotate webhook secrets (quarterly recommended)
    - **Monitor patterns:** Watch for unusual activity or repeated failures
    - **IP allowlisting:** Consider allowlisting WorkSkedge IPs for additional security
    
    ### Testing
    - Use the WorkSkedge dashboard to send test webhooks to your endpoints
    - Monitor webhook delivery logs to troubleshoot failures
    - Test signature verification with known payloads
    - Implement health check endpoints for webhook reliability monitoring
    
  contact:
    name: WorkSkedge API Support
    email: support@workskedge.com
    url: https://workskedge.com/support

servers:
  - url: https://app.workskedge.com
    description: Production API

security:
  - ApiKeyAuth: []

webhooks:
  outboundWebhook:
    post:
      summary: Outbound Webhook Callback
      description: |
        WorkSkedge will POST event notifications to your configured webhook endpoints when events occur in the system.
        
        **How it works:**
        1. Configure webhook endpoints in WorkSkedge dashboard under Settings → API & Webhooks
        2. Subscribe to specific event types (e.g., `work_order.created`, `employee.updated`)
        3. WorkSkedge automatically POSTs events to your endpoints in real-time
        
        **Event Discrimination:**
        All webhook payloads include an `event_type` field that identifies the specific event schema.
        Use this field to determine which event occurred and parse the payload accordingly.
        
        **Security:**
        - All webhooks include an `X-WorkSkedge-Signature` header with HMAC-SHA256 signature
        - Verify signatures using your webhook secret from the WorkSkedge dashboard
        - Use `X-WorkSkedge-Delivery-Id` to detect and handle duplicate deliveries
        
        **Echo Prevention:**
        When you link an outbound endpoint to an API key, that endpoint will NOT receive webhooks 
        for changes that originated from that same API key. This prevents infinite loops in bidirectional sync.
      operationId: receiveOutboundWebhook
      tags:
        - Outbound Webhooks
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AllOutboundWebhookEvents'
            examples:
              workOrderCreated:
                summary: Work Order Created
                value:
                  event_id: "evt_wo_created_001"
                  event_type: "work_order.created"
                  timestamp: "2025-01-15T10:30:00Z"
                  company_id: "123e4567-e89b-12d3-a456-426614174000"
                  data:
                    id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    work_order_number: "WO-2025-001"
                    project_name: "Office Renovation"
                    phase_name: "Electrical"
                    status: "draft"
              employeeUpdated:
                summary: Employee Updated
                value:
                  event_id: "evt_emp_updated_001"
                  event_type: "employee.updated"
                  timestamp: "2025-01-15T14:20:00Z"
                  company_id: "123e4567-e89b-12d3-a456-426614174000"
                  data:
                    id: "emp_123e4567-e89b-12d3-a456-426614174001"
                    emp_number: "EMP-001"
                    name: "John Smith"
                    email: "john.smith@example.com"
              timesheetSubmitted:
                summary: Timesheet Submitted
                value:
                  event_id: "evt_ts_submitted_001"
                  event_type: "timesheet.submitted"
                  timestamp: "2025-01-15T17:00:00Z"
                  company_id: "123e4567-e89b-12d3-a456-426614174000"
                  data:
                    id: "ts_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    emp_number: "EMP-001"
                    timesheet_date: "2025-01-15"
                    total_hours: 8.5
      responses:
        '200':
          description: Webhook received and processed successfully
        '400':
          description: Invalid webhook payload or signature verification failed
        '401':
          description: Signature verification failed
        '500':
          description: Internal server error processing webhook

tags:
  - name: OAuth 2.0
    description: |
      **OAuth 2.0 Authorization Code flow** for app installations.
      
      Use this flow when users connect to WorkSkedge from your application.
      
      **Endpoints:**
      - Authorization: `https://app.workskedge.com/v1/oauth-authorize`
      - Token Exchange: `https://app.workskedge.com/v1/oauth-token`
      
      **Flow Overview:**
      1. Redirect user to authorization URL with your `client_id`
      2. User logs in and approves permissions
      3. User is redirected to your `redirect_uri` with an authorization `code`
      4. Exchange the `code` for credentials using the token endpoint
      5. Receive `access_token` (API key) and `session_signing_secret` for embedded apps
      6. Use the API key for all subsequent API calls

  - name: Webhook Scope Requirements
    description: |
      ## Overview
      
      When creating an app in WorkSkedge, you must configure both **API Scopes** and **Webhook Events**.
      Each webhook event requires a corresponding API scope to be granted.
      
      **Important:** Webhook events only require `:read` access to their resource. You do not need 
      `:write` or `:delete` scopes to receive event notifications.
      
      ## Event-to-Scope Mapping
      
      | Webhook Events | Required Scope | Event Count |
      |----------------|----------------|-------------|
      | `work_order.*` | `work_orders:read` | 5 events |
      | `project.*` | `projects:read` | 5 events |
      | `employee.*` | `employees:read` | 3 events |
      | `timesheet.*` | `timesheets:read` | 5 events |
      | `customer.*` | `customers:read` | 3 events |
      | `location.*` | `locations:read` | 3 events |
      | `contact.*` | `customers:read` | 3 events ¹ |
      | `vendor.*` | `customers:read` | 9 events ² |
      | `assignment.*` | `assignments:read` | 3 events |
      | `schedule.*` | `schedules:read` | 3 events |
      | `custom_booking.*` | `custom_bookings:read` | 3 events |
      | `time_off.*` | `schedules:read` | 3 events ³ |
      | `report.*` | `reports:read` | 4 events |
      | `po.*` | `purchase_orders:read` | 11 events |
      | `invoice.*` | `invoices:read` | 7 events |
      | `equipment.*` | `equipment:read` | 4 events |
      
      **Notes:**
      - ¹ Contact events use `customers:read` because contacts belong to customers
      - ² Vendor events use `customers:read` because vendors share the customer data model
      - ³ Time-off events use `schedules:read` because time-off is part of scheduling
      - Write/delete scopes (`assignments:write`, `assignments:delete`, `schedules:write`, `schedules:delete`, `custom_bookings:write`, `custom_bookings:delete`) gate the matching REST endpoints — see `rest-api.yaml` and `docs/api/scheduling.md` for the full write surface.
      
      ## App Lifecycle Events
      
      These events are sent automatically to apps with a `webhook_url` configured:
      - `app.uninstalled` - Sent when a company uninstalls your app
      - `app.updated` - Sent when app configuration changes
      
      **No scope required** - these are implicit lifecycle events.
      
      ## Example: Configuring an Accounting Integration
      
      If your app needs to receive purchase order and invoice webhooks:
      
      **Required Scopes:**
      - `purchase_orders:read`
      - `invoices:read`
      
      **Subscribable Events:**
      - `po.created`, `po.approved`, `po.received`, etc.
      - `invoice.created`, `invoice.sent`, `invoice.paid`, etc.

  - name: Inbound Webhooks
    description: |
      **Send events TO WorkSkedge** from your external systems.
      
      Use these endpoints to create or update work orders, projects, and employee assignments in WorkSkedge from external systems like Dynamics 365, QuickBooks, or custom integrations.
      
      **Authentication Required**: Include your API key in the `X-API-Key` header.
      
      **Rate Limits**: 100 requests per minute per API key.
      
      **Echo Prevention**: Changes made via inbound webhooks will NOT trigger outbound webhooks back to your system (when outbound endpoints are linked to your API key). Other systems will still be notified.
      
  - name: Outbound Webhooks
    description: |
      **Receive events FROM WorkSkedge** at your registered endpoints.
      
      WorkSkedge automatically sends real-time notifications to your configured webhook URLs when events occur (work orders created, employees assigned, reports submitted, etc.).
      
      **Setup**: Configure webhook endpoints in WorkSkedge under Settings → API & Webhooks.
      
      **Security**: All outbound webhooks include HMAC-SHA256 signatures for verification.
      
      **Echo Prevention**: When you link an outbound endpoint to an API key, that endpoint will NOT receive webhooks for changes that originated from that same API key. This prevents infinite loops in bidirectional sync. Other endpoints (unlinked or linked to different API keys) will still receive all events normally.
      
      **Note**: The schemas below represent the event payloads WorkSkedge will POST to your endpoints. There are no API endpoints to call - these are events sent automatically by WorkSkedge.
  
  - name: Webhook Management
    description: |
      **Programmatically manage webhook subscriptions**.
      
      Use these endpoints to register, list, and remove webhook endpoints from your integration code.
      Perfect for auto-registration from platforms like Zapier, Make.com, and custom integrations.
      
      **Authentication**: Include your API key in the `X-API-Key` header (same key used for inbound webhooks).
      
      **Echo Prevention**: Subscriptions created via this API are automatically linked to your API key,
      enabling echo storm prevention (your endpoint won't receive webhooks for changes you initiated).

paths:
  # ==========================================
  # INBOUND WEBHOOKS (External → WorkSkedge)
  # ==========================================
  
  /webhooks/inbound:
    post:
      summary: Send webhook event to WorkSkedge
      description: |
        **Inbound webhook endpoint** for external systems to send events TO WorkSkedge.
        Events can create, update, or trigger actions on work orders, projects, and employee assignments.
        
        Use this endpoint to integrate external systems like Dynamics 365, QuickBooks, or custom applications.
        
        #### Example: Submit Daily Report
        ```bash
        curl -X POST 'https://app.workskedge.com/webhooks/inbound' \
          -H 'Content-Type: application/json' \
          -H 'X-API-Key: wsk_live_abc123...' \
          -d '{
            "event_type": "report.daily_submitted",
            "event_id": "daily_report_12345",
            "data": {
              "work_order_number": "WO-00123",
              "report_date": "2025-01-15",
              "progress_notes": "Completed foundation work",
              "hours_worked": 8.5,
              "weather_conditions": "Sunny, 72°F"
            }
          }'
        ```
        
        #### Example: Submit Completion Report
        ```bash
        curl -X POST 'https://app.workskedge.com/webhooks/inbound' \
          -H 'Content-Type: application/json' \
          -H 'X-API-Key: wsk_live_abc123...' \
          -d '{
            "event_type": "report.completion_submitted",
            "data": {
              "work_order_number": "WO-00123",
              "completion_date": "2025-01-20",
              "completion_notes": "Project completed successfully",
              "customer_satisfaction": 5
            }
          }'
        ```
        
        #### Example: Approve Completion Report
        ```bash
        curl -X POST 'https://app.workskedge.com/webhooks/inbound' \
          -H 'Content-Type: application/json' \
          -H 'X-API-Key: wsk_live_abc123...' \
          -d '{
            "event_type": "report.completion_approved",
            "data": {
              "work_order_number": "WO-00123"
            }
          }'
        ```
      operationId: receiveWebhook
      tags:
        - Inbound Webhooks
      parameters:
        - name: X-API-Key
          in: header
          required: true
          schema:
            type: string
          description: Your WorkSkedge API key (REQUIRED)
        - name: X-Webhook-Timestamp
          in: header
          required: false
          schema:
            type: integer
            format: int64
          description: Unix timestamp when the webhook was sent (optional, for signature verification)
        - name: X-Webhook-Signature
          in: header
          required: false
          schema:
            type: string
          description: HMAC-SHA256 signature of the request body (optional, recommended for security)
        - name: X-Webhook-Source
          in: header
          required: false
          schema:
            type: string
            enum: [custom, square, quickbooks, dynamics365]
            default: custom
          description: Source system for signature verification
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookEvent'
            examples:
              # ==================== TEST/AUTHENTICATION EVENTS ====================
              testConnection:
                summary: Test Connection (Authentication Test)
                description: |
                  Use this event type to test authentication and verify your API key is working.
                  This is commonly used by integration platforms like Zapier during setup.
                value:
                  event_type: "test.connection"
                  timestamp: "2025-01-15T10:00:00Z"
                  source: "zapier"
                  data:
                    test: true
              workOrderCreated:
                summary: Work Order Created (Comprehensive)
                value:
                  event_type: "work_order.created"
                  timestamp: "2025-01-15T14:30:00Z"
                  source: "erp_system"
                  data:
                    project_code: "PRJ-2025-001"
                    phase_name: "Foundation Installation"
                    status: "scheduled"
                    priority: "high"
                    start_date: "2025-02-01"
                    end_date: "2025-02-05"
                    start_time: "08:00"
                    end_time: "17:00"
                    expected_start_date: "2025-02-01"
                    expected_end_date: "2025-02-05"
                    expected_start_time: "08:00"
                    expected_end_time: "17:00"
                    people_required: 5
                    required_headcount: 5
                    estimated_hours: 200
                    travel_time_hours: 1.5
                    additional_labor_cost: 5000.00
                    location_id: "loc-uuid-001"
                    site_address: null
                    location:
                      id: "loc-uuid-001"
                      name: "Building C"
                      address_line_1: "456 Construction Way"
                      city: "Vancouver"
                      province: "BC"
                      postal_code: "V5K 0A1"
                      country: "Canada"
                    customer_name: "ABC Property Development"
                    customer_contact_name: "Jane Smith"
                    customer_phone: "555-123-4567"
                    customer_email: "jane.smith@abcproperty.com"
                    site_lead_id: "employee-123"
                    site_lead_name: "John Foreman"
                    required_skills: ["Concrete pouring", "Form setting"]
                    scope_of_work:
                      checklists:
                        - id: "c1a2b3c4-d5e6-7890-abcd-ef1234567890"
                          name: "Foundation Work"
                          is_table_view: false
                          tasks:
                            - id: "t0000001-0000-0000-0000-000000000001"
                              text: "Install foundation footings"
                              completed: false
                            - id: "t0000001-0000-0000-0000-000000000002"
                              text: "Pour concrete slab"
                              completed: false
                            - id: "t0000001-0000-0000-0000-000000000003"
                              text: "Set anchor bolts"
                              completed: false
                    equipment: ["Concrete mixer", "Vibrator", "Laser level"]
                    specialty_materials: ["High-strength concrete", "Rebar grade 60"]
                    scheduler_notes:
                      subtrade_required: "Concrete"
                      scheduling_insights: "Weather-dependent, requires 3-day cure time"
                    custom_fields:
                      permit_number: "BUILD-2025-001"
                      inspection_required: "yes"
                    display_order: 1
                    require_daily_report: true
                    require_completion_report: true
              workOrderCreatedMinimal:
                summary: Work Order Created (Minimal Required Fields)
                value:
                  event_type: "work_order.created"
                  timestamp: "2025-01-15T15:00:00Z"
                  source: "scheduling_app"
                  data:
                    project_code: "PRJ-2025-002"
                    phase_name: "Electrical Rough-In"
                    start_date: "2025-02-10"
                    end_date: "2025-02-12"

              workOrderUpdated:
                summary: Work Order Updated
                value:
                  event_type: "work_order.updated"
                  timestamp: "2025-01-15T16:00:00Z"
                  source: "project_management"
                  data:
                    work_order_number: "WO-2025-001"
                    project_code: "PROJ-2025-001"
                    phase_name: "Foundation Installation - Revised"
                    priority: "high"
                    required_headcount: 7
                    end_date: "2025-02-06"
                    estimated_hours: 560
                    scheduler_notes:
                      subtrade_required: "Concrete"
                      scheduling_insights: "Added crew due to weather delays"

              workOrderDeleted:
                summary: Work Order Deleted
                value:
                  event_type: "work_order.deleted"
                  timestamp: "2025-01-15T17:00:00Z"
                  source: "erp_system"
                  data:
                    work_order_number: "WO-2025-003"

              # ==================== PROJECT EVENTS ====================
              projectCreated:
                summary: Project Created (Comprehensive - New Customer/Location System)
                value:
                  event_type: "project.created"
                  timestamp: "2025-01-15T10:00:00Z"
                  source: "crm_system"
                  data:
                    project_name: "Skyline Tower Construction"
                    project_code: "PRJ-2025-100"
                    description: "15-story mixed-use building with retail and residential units"
                    customer_name: "Skyline Development Corp"
                    location_name: "Broadway Tower Site"
                    customer_type: "Commercial"
                    customer_email: "info@skylinedev.com"
                    customer_phone: "+1-555-0100"
                    customer_website: "https://www.skylinedev.com"
                    customer_status: "Active"
                    customer_notes: "Major commercial developer - prefers morning meetings"
                    location_address_line_1: "789 Broadway"
                    location_city: "New York"
                    location_province: "NY"
                    location_postal_code: "10003"
                    location_country: "USA"
                    location_notes: "Main construction site for tower project"
                    account_manager_emp_number: "EMP-2001"
                    project_owner_emp_numbers: ["EMP-2001", "EMP-2002"]
                    priority: "high"
                    color_index: 5
                    equipment: ["Tower crane", "Concrete pump", "Material hoist"]
                    custom_fields:
                      building_permit: "NYC-BUILD-2025-5678"
                      insurance_policy: "GLI-2025-9876"
                      contract_value: "50000000"
                    docs:
                      - name: "Architectural Plans.pdf"
                        url: "https://storage.example.com/skyline/arch-plans.pdf"
                      - name: "Structural Drawings.pdf"
                        url: "https://storage.example.com/skyline/struct-draw.pdf"

              projectCreatedMinimal:
                summary: Project Created (Minimal Required Fields)
                value:
                  event_type: "project.created"
                  timestamp: "2025-01-15T11:00:00Z"
                  source: "sales_system"
                  data:
                    project_name: "Office Renovation"
                    project_code: "PRJ-2025-101"
                    customer_name: "Acme Corporation"
                    location_name: "Main Office"

              projectUpdated:
                summary: Project Updated
                value:
                  event_type: "project.updated"
                  timestamp: "2025-01-15T12:00:00Z"
                  source: "project_management"
                  data:
                    project_code: "PRJ-2025-100"
                    description: "15-story mixed-use building - updated scope includes rooftop amenities"
                    priority: "urgent"
                    custom_fields:
                      contract_value: "52000000"
                      change_order: "CO-001"

              projectDeleted:
                summary: Project Deleted
                value:
                  event_type: "project.deleted"
                  timestamp: "2025-01-15T13:00:00Z"
                  source: "erp_system"
                  data:
                    project_code: "PRJ-2024-999"

              projectClosed:
                summary: Project Closed (Default - Fails if Open Work Orders)
                description: |
                  Close a project. By default, this will fail if there are incomplete work orders.
                  Use `close_work_orders: true` to force-close all work orders first.
                value:
                  event_type: "project.closed"
                  timestamp: "2025-01-15T14:00:00Z"
                  source: "erp_system"
                  data:
                    project_code: "PRJ-2025-100"

              projectClosedForceWorkOrders:
                summary: Project Closed (Force Close All Work Orders)
                description: |
                  Force-close a project and all its incomplete work orders.
                  When `close_work_orders: true`, all work orders in states other than 'closed' 
                  will be automatically closed before the project is closed.
                value:
                  event_type: "project.closed"
                  timestamp: "2025-01-15T14:00:00Z"
                  source: "erp_system"
                  data:
                    project_code: "PRJ-2025-100"
                    close_work_orders: true

              # ==================== SEARCH EVENTS ====================
              projectSearchExact:
                summary: Project Search (Exact Match)
                description: Search for projects by exact project code or name
                value:
                  event_type: "project.search"
                  timestamp: "2025-01-15T13:30:00Z"
                  source: "integration_platform"
                  data:
                    search_term: "PRJ-2025-100"
                    search_field: "project_code"
                    exact_match: true

              projectSearchFuzzy:
                summary: Project Search (Fuzzy Match)
                description: Search for projects using partial name matching
                value:
                  event_type: "project.search"
                  timestamp: "2025-01-15T13:35:00Z"
                  source: "integration_platform"
                  data:
                    search_term: "Skyline"
                    search_field: "project_name"
                    exact_match: false

              workOrderSearchExact:
                summary: Work Order Search (Exact Match)
                description: Search for work orders by exact work order number
                value:
                  event_type: "work_order.search"
                  timestamp: "2025-01-15T13:40:00Z"
                  source: "integration_platform"
                  data:
                    search_term: "WO-2025-001"
                    search_field: "work_order_number"
                    exact_match: true

              workOrderSearchFuzzy:
                summary: Work Order Search (Fuzzy Match)
                description: Search for work orders by phase name
                value:
                  event_type: "work_order.search"
                  timestamp: "2025-01-15T13:45:00Z"
                  source: "integration_platform"
                  data:
                    search_term: "Foundation"
                    search_field: "phase_name"
                    exact_match: false

              workOrderSearchWithProject:
                summary: Work Order Search (With Project Filter)
                description: Search for work orders within a specific project
                value:
                  event_type: "work_order.search"
                  timestamp: "2025-01-15T13:50:00Z"
                  source: "integration_platform"
                  data:
                    search_term: "Electrical"
                    search_field: "phase_name"
                    project_code: "PRJ-2025-100"
                    exact_match: false

              # ==================== EMPLOYEE EVENTS ====================
              employeeCreated:
                summary: Employee Created (with employee_type)
                description: Create employee using single employee_type designation
                value:
                  event_type: "employee.created"
                  timestamp: "2025-01-15T09:00:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12345"
                    name: "John Martinez"
                    email: "john.martinez@company.com"
                    phone: "+1-555-1234"
                    department_name: "Field Operations"
                    role_name: "Site Supervisor"
                    employee_type: "site_lead"
                    employee_skills: ["Concrete", "Framing", "Safety Management"]

              employeeCreatedWithRoles:
                summary: Employee Created (with roles array - Multi-Role)
                description: |
                  Create employee using roles array for multi-role support.
                  Use this when an employee needs multiple roles (e.g., both site_lead and site_worker).
                  If both employee_type and roles are provided, roles takes precedence.
                value:
                  event_type: "employee.created"
                  timestamp: "2025-01-15T09:15:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12347"
                    name: "Maria Garcia"
                    email: "maria.garcia@company.com"
                    phone: "+1-555-9999"
                    department_name: "Field Operations"
                    role_name: "Lead Technician"
                    roles: ["site_lead", "site_worker"]
                    start_date: "2024-06-01"
                    birthday: "1990-03-15"
                    address:
                      street: "456 Oak Avenue"
                      city: "Houston"
                      state: "TX"
                      zip: "77001"
                      country: "USA"
                    emergency_contact_name: "Carlos Garcia"
                    emergency_contact_relationship: "Spouse"
                    emergency_phone_1: "+1-555-8888"
                    employee_skills: ["Electrical", "HVAC", "Plumbing"]

              employeeCreatedMinimal:
                summary: Employee Created (Minimal Required Fields)
                value:
                  event_type: "employee.created"
                  timestamp: "2025-01-15T09:30:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12346"
                    name: "Sarah Johnson"
                    email: "sarah.johnson@company.com"
                    employee_type: "site_worker"

              employeeUpdated:
                summary: Employee Updated (with employee_type)
                value:
                  event_type: "employee.updated"
                  timestamp: "2025-01-15T10:30:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12345"
                    phone: "+1-555-5678"
                    department_name: "Project Management"
                    employee_type: "project_owner"
                    employee_skills: ["Concrete", "Framing", "Safety Management", "Project Scheduling"]

              employeeUpdatedWithRoles:
                summary: Employee Updated (with roles array - Promote to Multi-Role)
                description: |
                  Update employee to have multiple roles using the roles array.
                  This example promotes an employee to be both a site_lead and scheduler.
                value:
                  event_type: "employee.updated"
                  timestamp: "2025-01-15T10:45:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12345"
                    roles: ["site_lead", "scheduler"]
                    department_name: "Field Operations"

              employeeDeleted:
                summary: Employee Deleted/Deactivated
                value:
                  event_type: "employee.deleted"
                  timestamp: "2025-01-15T11:30:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-99999"

              # ==================== TIME OFF EVENTS ====================
              timeOffCreated:
                summary: Time Off Created
                value:
                  event_type: "time_off.created"
                  timestamp: "2025-01-15T14:30:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12345"
                    start_date: "2025-01-20"
                    end_date: "2025-01-24"
                    reason: "Vacation"
                    notes: "Annual family vacation to Hawaii. Will be completely unavailable."

              timeOffCreatedMinimal:
                summary: Time Off Created (Minimal)
                value:
                  event_type: "time_off.created"
                  timestamp: "2025-01-15T14:45:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12346"
                    start_date: "2025-02-10"
                    end_date: "2025-02-11"

              timeOffDeleted:
                summary: Time Off Cancelled/Deleted
                value:
                  event_type: "time_off.deleted"
                  timestamp: "2025-01-16T10:00:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-12346"
                    start_date: "2025-02-10"
                    end_date: "2025-02-11"

              # ==================== VENDOR EVENTS ====================
              vendorCreated:
                summary: Vendor Created (Comprehensive)
                value:
                  event_type: "vendor.created"
                  timestamp: "2025-01-15T08:00:00Z"
                  source: "procurement_system"
                  data:
                    vendor_name: "Superior HVAC Solutions"
                    trade_specialty: ["HVAC", "Mechanical", "Refrigeration"]
                    primary_contact_name: "Robert Williams"
                    primary_contact_email: "robert@superiorhvac.com"
                    primary_contact_phone: "+1-555-HVAC-100"
                    address: "1500 Industrial Parkway, Suite 200"
                    city: "Chicago"
                    state: "IL"
                    zip_code: "60601"
                    country: "USA"
                    rating: 5
                    regions: ["Midwest", "Great Lakes"]
                    notes: "Preferred vendor for commercial HVAC projects. 15% discount on materials."
                    custom_fields:
                      license_number: "HVAC-IL-2025-5432"
                      insurance_expiry: "2025-12-31"
                      w9_on_file: "yes"

              vendorCreatedMinimal:
                summary: Vendor Created (Minimal)
                value:
                  event_type: "vendor.created"
                  timestamp: "2025-01-15T08:30:00Z"
                  source: "procurement_system"
                  data:
                    vendor_name: "Quick Electric Supply"
                    trade_specialty: "Electrical"
                    primary_contact_name: "John Doe"

              vendorUpdated:
                summary: Vendor Updated
                value:
                  event_type: "vendor.updated"
                  timestamp: "2025-01-15T09:00:00Z"
                  source: "procurement_system"
                  data:
                    vendor_name: "Superior HVAC Solutions"
                    primary_contact_email: "r.williams@superiorhvac.com"
                    rating: 4
                    notes: "Updated contact info. Recent project had minor delays."
                    custom_fields:
                      insurance_expiry: "2026-12-31"

              vendorUpdatedCanada:
                summary: Vendor Updated (Canadian with Province Code)
                value:
                  event_type: "vendor.updated"
                  timestamp: "2025-01-15T09:15:00Z"
                  source: "procurement_system"
                  data:
                    vendor_name: "Elite HVAC Solutions"
                    address: "456 Main Street"
                    address_line2: "Suite 101"
                    city: "Winnipeg"
                    state: "MB"
                    zip_code: "R3C 0V8"
                    country: "Canada"
                    primary_contact_name: "Jane Smith"
                    primary_contact_email: "jane@elitehvac.ca"
                    rating: 5

              vendorDeleted:
                summary: Vendor Deleted/Deactivated
                value:
                  event_type: "vendor.deleted"
                  timestamp: "2025-01-15T09:30:00Z"
                  source: "procurement_system"
                  data:
                    vendor_name: "Old Plumbing Co"

              # ==================== PURCHASE ORDER EVENTS ====================
              poCreated:
                summary: Purchase Order Created (with Budget Tracking)
                value:
                  event_type: "po.created"
                  timestamp: "2025-01-15T10:00:00Z"
                  source: "procurement_system"
                  data:
                    po_number: "PO-2025-001"
                    work_order_number: "WO-00123"
                    vendor_name: "Superior HVAC Solutions"
                    description: "HVAC equipment for second floor installation"
                    terms: "Net 30"
                    budget_amount: 15000.00
                    not_to_exceed_amount: 17500.00
                    total_amount: 12500.00
                    line_items:
                      - description: "Copper piping 1/2 inch"
                        quantity: 100
                        unit: "ft"
                        unit_price: 12.50
                        max_amount: 1500.00
                        notes: "Type L copper, schedule 40"
                      - description: "HVAC condensing unit"
                        quantity: 2
                        unit: "ea"
                        unit_price: 5000.00
                        max_amount: 11000.00
                        notes: "Carrier 24ACC636A003"

              poCreatedMinimal:
                summary: Purchase Order Created (Minimal)
                value:
                  event_type: "po.created"
                  timestamp: "2025-01-15T10:30:00Z"
                  source: "procurement_system"
                  data:
                    work_order_number: "WO-00124"
                    vendor_name: "Quick Electric Supply"
                    description: "Electrical supplies"
                    line_items:
                      - description: "12-2 Romex wire"
                        quantity: 500
                        unit: "ft"
                        unit_price: 0.85

              poUpdated:
                summary: Purchase Order Updated
                value:
                  event_type: "po.updated"
                  timestamp: "2025-01-15T14:00:00Z"
                  source: "procurement_system"
                  data:
                    po_number: "PO-2025-001"
                    budget_amount: 18000.00
                    not_to_exceed_amount: 20000.00
                    line_items:
                      - description: "Copper piping 1/2 inch"
                        quantity: 150
                        unit: "ft"
                        unit_price: 12.50
                        max_amount: 2000.00
                        notes: "Increased quantity per site requirements"

              poSubmitted:
                summary: Purchase Order Submitted for Approval
                value:
                  event_type: "po.submitted"
                  timestamp: "2025-01-15T15:00:00Z"
                  source: "procurement_system"
                  data:
                    po_number: "PO-2025-001"
                    submitted_by: "123e4567-e89b-12d3-a456-426614174010"
                    submitted_at: "2025-01-15T15:00:00Z"

              poApproved:
                summary: Purchase Order Approved
                value:
                  event_type: "po.approved"
                  timestamp: "2025-01-15T16:00:00Z"
                  source: "procurement_system"
                  data:
                    po_number: "PO-2025-001"
                    approved_by: "123e4567-e89b-12d3-a456-426614174011"
                    approved_at: "2025-01-15T16:00:00Z"

              poRejected:
                summary: Purchase Order Rejected
                value:
                  event_type: "po.rejected"
                  timestamp: "2025-01-15T16:30:00Z"
                  source: "procurement_system"
                  data:
                    po_number: "PO-2025-002"
                    rejected_by: "123e4567-e89b-12d3-a456-426614174011"
                    rejected_at: "2025-01-15T16:30:00Z"
                    rejection_reason: "Budget exceeded for this quarter"

              poReceived:
                summary: Purchase Order Received
                value:
                  event_type: "po.received"
                  timestamp: "2025-01-20T09:00:00Z"
                  source: "procurement_system"
                  data:
                    po_number: "PO-2025-001"
                    received_by: "123e4567-e89b-12d3-a456-426614174012"
                    received_at: "2025-01-20T09:00:00Z"

              poSearch:
                summary: Purchase Order Search (Dynamic Dropdown)
                description: Search for purchase orders for Zapier dropdown support
                value:
                  event_type: "po.search"
                  timestamp: "2025-01-15T13:55:00Z"
                  source: "integration_platform"
                  data:
                    search_term: "PO-2025"
                    search_field: "po_number"
                    exact_match: false

              poSearchWithFilters:
                summary: Purchase Order Search (With Filters)
                description: Search for POs filtered by status and project
                value:
                  event_type: "po.search"
                  timestamp: "2025-01-15T14:00:00Z"
                  source: "integration_platform"
                  data:
                    search_term: "ACME"
                    search_field: "vendor_name"
                    status: "approved"
                    project_code: "PRJ-2025-100"
                    exact_match: false

              # ==================== REPORT EVENTS ====================
              dailyReportSubmitted:
                summary: Daily Progress Report Submitted
                value:
                  event_type: "report.daily_submitted"
                  timestamp: "2025-01-15T17:00:00Z"
                  source: "field_management_system"
                  data:
                    report_id: "123e4567-e89b-12d3-a456-426614174000"
                    work_order_id: "123e4567-e89b-12d3-a456-426614174001"
                    work_order_number: "WO-00123"
                    report_date: "2025-01-15"
                    submitted_by: "EMP-001"
                    is_auto_approved: false
                    project_name: "Downtown Office Renovation"
                    phase_name: "Electrical Installation"
                    notes: "Completed wiring for second floor"
                    photos:
                      - url: "https://example.com/photo1.jpg"
                        caption: "Second floor wiring complete"
                    submitted_at: "2025-01-15T17:00:00Z"

              completionReportSubmitted:
                summary: Completion Report Submitted
                value:
                  event_type: "report.completion_submitted"
                  timestamp: "2025-01-20T16:00:00Z"
                  source: "field_management_system"
                  data:
                    report_id: "123e4567-e89b-12d3-a456-426614174002"
                    work_order_id: "123e4567-e89b-12d3-a456-426614174001"
                    work_order_number: "WO-00123"
                    report_type: "completion"
                    report_date: "2025-01-20"
                    submitted_by:
                      user_id: "123e4567-e89b-12d3-a456-426614174003"
                      employee_id: "123e4567-e89b-12d3-a456-426614174004"
                      name: "John Smith"
                    project_name: "Downtown Office Renovation"
                    phase_name: "Electrical Installation"
                    scope_checklist:
                      items:
                        - id: "task_1"
                          name: "Install outlets"
                          completed: true
                      changes: []
                    completion_checklist:
                      items:
                        - item: "Final walkthrough completed"
                          completed: true
                          notes: ""
                        - item: "All tools removed from site"
                          completed: true
                          notes: ""
                    notes: "Project completed successfully. All items addressed."
                    photos:
                      - "https://example.com/photos/final-1.jpg"
                      - "https://example.com/photos/final-2.jpg"
                    is_auto_approved: false
                    requires_approval: true
                    submitted_at: "2025-01-20T16:00:00Z"

              completionReportApproved:
                summary: Completion Report Approved
                value:
                  event_type: "report.completion_approved"
                  timestamp: "2025-01-21T10:00:00Z"
                  source: "project_management_system"
                  data:
                    report_id: "123e4567-e89b-12d3-a456-426614174002"
                    work_order_id: "123e4567-e89b-12d3-a456-426614174001"
                    work_order_number: "WO-00123"
                    approved_by: "123e4567-e89b-12d3-a456-426614174005"
                    approver_name: "Jane Manager"
                    approved_at: "2025-01-21T10:00:00Z"
                    project_name: "Downtown Office Renovation"
                    phase_name: "Electrical Installation"

              # ==================== TIMESHEET EVENTS ====================
              timesheetCreated:
                summary: Timesheet Created (Daily - With Mixed Work Types)
                value:
                  event_type: "timesheet.created"
                  timestamp: "2025-01-15T08:00:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-1001"
                    timesheet_date: "2025-01-21"
                    status: "draft"
                    entries:
                      - entry_date: "2025-01-21"
                        work_order_number: "WO-00012"
                        hours: 8.0
                        work_type_code: "REG"
                        notes: "HVAC installation - regular hours"

              timesheetCreatedMinimal:
                summary: Timesheet Created (Daily - Minimal, No Entries)
                value:
                  event_type: "timesheet.created"
                  timestamp: "2025-01-15T08:30:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-1002"
                    timesheet_date: "2025-01-22"

              timesheetUpdated:
                summary: Timesheet Updated (Daily - With Work Type Changes)
                value:
                  event_type: "timesheet.updated"
                  timestamp: "2025-01-15T17:00:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-1001"
                    timesheet_date: "2025-01-21"
                    status: "submitted"
                    notes: "Day completed with overtime"
                    entries:
                      - entry_date: "2025-01-21"
                        work_order_number: "WO-00012"
                        hours: 8.0
                        work_type_code: "REG"
                      - entry_date: "2025-01-21"
                        work_order_number: "WO-00012"
                        hours: 2.0
                        work_type_code: "OT"
                        notes: "Overtime to meet deadline"

              timesheetDeleted:
                summary: Timesheet Deleted (Daily)
                value:
                  event_type: "timesheet.deleted"
                  timestamp: "2025-01-15T18:00:00Z"
                  source: "hr_system"
                  data:
                    emp_number: "EMP-1001"
                    timesheet_date: "2025-01-21"

              # ==================== CUSTOMER EVENTS ====================
              customerCreated:
                summary: Customer Created (Comprehensive)
                value:
                  event_type: "customer.created"
                  timestamp: "2025-01-30T10:00:00Z"
                  source: "crm_system"
                  data:
                    name: "Acme Corporation"
                    type: "Commercial"
                    email: "accounts@acmecorp.com"
                    phone: "555-123-4567"
                    website: "https://www.acmecorp.com"
                    status: "Active"
                    account_manager_emp_number: "EMP-2001"
                    notes: "Major client - requires dedicated account manager"

              customerCreatedMinimal:
                summary: Customer Created (Minimal Required Fields)
                value:
                  event_type: "customer.created"
                  timestamp: "2025-01-30T10:15:00Z"
                  source: "crm_system"
                  data:
                    name: "Quick Services LLC"

              customerUpdated:
                summary: Customer Updated
                value:
                  event_type: "customer.updated"
                  timestamp: "2025-01-30T11:00:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    email: "billing@acmecorp.com"
                    phone: "555-123-4568"
                    status: "Inactive"
                    notes: "Contract ended - moved to inactive"

              customerDeleted:
                summary: Customer Deleted
                value:
                  event_type: "customer.deleted"
                  timestamp: "2025-01-30T12:00:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Old Customer Inc"

              # ==================== LOCATION EVENTS ====================
              locationCreated:
                summary: Location Created (Comprehensive)
                value:
                  event_type: "location.created"
                  timestamp: "2025-01-30T10:30:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    name: "Acme Headquarters"
                    address_line_1: "123 Main Street"
                    address_line_2: "Suite 400"
                    city: "New York"
                    province: "NY"
                    postal_code: "10001"
                    country: "USA"
                    notes: "Main office building"

              locationCreatedMinimal:
                summary: Location Created (Minimal Required Fields)
                value:
                  event_type: "location.created"
                  timestamp: "2025-01-30T10:45:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    name: "Acme Warehouse"

              locationUpdated:
                summary: Location Updated
                value:
                  event_type: "location.updated"
                  timestamp: "2025-01-30T11:15:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    location_name: "Acme Headquarters"
                    address_line_1: "123 Main Street"
                    address_line_2: "Suite 500"
                    phone: "555-123-4570"

              locationDeleted:
                summary: Location Deleted
                value:
                  event_type: "location.deleted"
                  timestamp: "2025-01-30T12:15:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    location_name: "Old Warehouse"

              # ==================== CONTACT EVENTS ====================
              contactCreated:
                summary: Contact Created (Comprehensive)
                value:
                  event_type: "contact.created"
                  timestamp: "2025-01-30T10:30:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    location_name: "Acme Headquarters"
                    first_name: "John"
                    last_name: "Smith"
                    title: "Operations Manager"
                    email: "john.smith@acmecorp.com"
                    phone: "555-123-4571"
                    phone_ext: "101"
                    mobile: "555-987-6543"
                    is_primary: true
                    contact_tags: ["operations", "billing"]
                    notes: "Primary contact for all project coordination"

              contactCreatedMinimal:
                summary: Contact Created (Minimal Required Fields)
                value:
                  event_type: "contact.created"
                  timestamp: "2025-01-30T10:45:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    first_name: "Jane"
                    last_name: "Doe"

              contactUpdated:
                summary: Contact Updated
                value:
                  event_type: "contact.updated"
                  timestamp: "2025-01-30T11:30:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    location_name: "Acme Headquarters"
                    first_name: "John"
                    last_name: "Smith"
                    email: "j.smith@acmecorp.com"
                    phone: "555-123-4572"
                    phone_ext: "102"
                    is_primary: false
                    contact_tags: ["operations"]

              contactDeleted:
                summary: Contact Deleted
                value:
                  event_type: "contact.deleted"
                  timestamp: "2025-01-30T12:30:00Z"
                  source: "crm_system"
                  data:
                    customer_name: "Acme Corporation"
                    location_name: "Acme Headquarters"
                    first_name: "John"
                    last_name: "Smith"
      responses:
        '200':
          description: Webhook processed successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResponse'
              examples:
                success:
                  value:
                    success: true
                    message: Webhook processed successfully
                    event_id: evt_1234567890
                    record_id: rec_abcdef123456
                projectClosedSuccess:
                  summary: Project Closed Successfully (No Work Orders)
                  value:
                    success: true
                    action: closed
                    project_id: "550e8400-e29b-41d4-a716-446655440000"
                    work_orders_closed: 0
                projectClosedForceSuccess:
                  summary: Project Closed with Force-Closed Work Orders
                  value:
                    success: true
                    action: closed
                    project_id: "550e8400-e29b-41d4-a716-446655440000"
                    work_orders_closed: 3
                projectClosedError:
                  summary: Project Close Failed - Open Work Orders
                  value:
                    success: false
                    error: "Cannot close project with incomplete work orders"
                    incomplete_work_orders:
                      - work_order_number: "WO-00001"
                        phase_name: "Electrical Phase 1"
                        status: "active"
                      - work_order_number: "WO-00002"
                        phase_name: "HVAC Installation"
                        status: "assigned"
                searchResults:
                  summary: Search Results
                  description: Response format for project.search and work_order.search events
                  value:
                    success: true
                    action: search
                    results:
                      - id: "550e8400-e29b-41d4-a716-446655440000"
                        project_name: "Skyline Tower Development"
                        project_code: "PRJ-2025-100"
                        description: "15-story mixed-use building"
                        start_date: "2025-02-01"
                        end_date: "2025-12-31"
                    count: 1
        '400':
          description: Bad request - Invalid payload or missing required fields
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidPayload:
                  value:
                    error: Invalid payload
                    message: Missing required field 'event_type'
        '401':
          description: Unauthorized - Invalid or missing API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidApiKey:
                  value:
                    error: Unauthorized
                    message: Invalid API key
        '403':
          description: Forbidden - Signature verification failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidSignature:
                  value:
                    error: Forbidden
                    message: Invalid webhook signature
        '409':
          description: Conflict - Duplicate event (already processed)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                duplicate:
                  value:
                    error: Duplicate event
                    message: Event already processed
        '429':
          description: Rate limit exceeded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RateLimitResponse'
              examples:
                rateLimited:
                  value:
                    error: Rate limit exceeded
                    message: Too many requests. Please try again later.
                    retry_after: 60
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ==========================================
  # WEBHOOK MANAGEMENT (Programmatic Subscriptions)
  # ==========================================
  
  /webhooks/subscribe:
    post:
      summary: Register webhook subscription
      description: |
        **Programmatically register a webhook endpoint** to receive events from WorkSkedge.
        
        This enables auto-registration from platforms like Zapier, Make.com, and custom integrations.
        
        **How it works:**
        1. Call this endpoint with your desired `event_type` and `target_url`
        2. WorkSkedge creates a subscription and returns a unique `id` and `secret`
        3. WorkSkedge will automatically POST events to your `target_url` when they occur
        4. The subscription is linked to your API key for echo storm prevention
        
        **Echo Prevention:**
        Subscriptions created with this API are automatically linked to your API key.
        Your endpoint will NOT receive webhooks for changes that originated from your API key.
      operationId: subscribeWebhook
      tags:
        - Webhook Management
      parameters:
        - name: X-API-Key
          in: header
          required: true
          schema:
            type: string
          description: Your WorkSkedge API key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - event_type
                - target_url
              properties:
                event_type:
                  type: string
                  description: Type of event to subscribe to
                  example: "work_order.created"
                target_url:
                  type: string
                  format: uri
                  description: HTTPS URL where WorkSkedge will POST events
                  example: "https://your-app.com/webhooks/workskedge"
                description:
                  type: string
                  description: Optional description for this subscription
                  example: "Production integration - QuickBooks sync"
      responses:
        '201':
          description: Subscription created successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                    description: Unique subscription ID
                  secret:
                    type: string
                    description: Secret for HMAC signature verification (store this securely)
                  status:
                    type: string
                    enum: [active]
                  event_type:
                    type: string
                  target_url:
                    type: string
                  created_at:
                    type: string
                    format: date-time
              examples:
                generic:
                  summary: Generic integration
                  value:
                    id: "550e8400-e29b-41d4-a716-446655440000"
                    secret: "wsk_sec_abc123xyz..."
                    status: "active"
                    event_type: "work_order.created"
                    target_url: "https://your-app.com/webhooks/workskedge"
                    created_at: "2025-01-15T10:30:00Z"
                zapier:
                  summary: Zapier integration
                  value:
                    id: "550e8400-e29b-41d4-a716-446655440001"
                    secret: "wsk_sec_def456uvw..."
                    status: "active"
                    event_type: "employee.created"
                    target_url: "https://hooks.zapier.com/hooks/standard/1234567/abcd"
                    created_at: "2025-01-15T10:35:00Z"
        '400':
          description: Invalid request (bad event type or invalid URL)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                invalidEventType:
                  value:
                    error: "Invalid event_type"
                    message: "event_type must be one of: work_order.created, employee.updated, etc."
                    valid_types:
                      - "work_order.created"
                      - "work_order.updated"
                      - "employee.created"
                invalidUrl:
                  value:
                    error: "Invalid target_url"
                    message: "target_url must be HTTPS and not point to private/localhost addresses"
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /webhooks/unsubscribe/{subscriptionId}:
    delete:
      summary: Remove webhook subscription
      description: |
        **Delete a webhook subscription** to stop receiving events.
        
        This is typically called by integration platforms (like Zapier) when a user turns off their Zap,
        or by custom integrations that no longer need to receive events.
        
        **Important:** You can only delete subscriptions that belong to your API key.
      operationId: unsubscribeWebhook
      tags:
        - Webhook Management
      parameters:
        - name: X-API-Key
          in: header
          required: true
          schema:
            type: string
          description: Your WorkSkedge API key
        - name: subscriptionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: The subscription ID to delete (returned from subscribe endpoint)
      responses:
        '200':
          description: Subscription deleted successfully
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  deleted_id:
                    type: string
                    format: uuid
                  event_type:
                    type: string
              examples:
                success:
                  value:
                    success: true
                    deleted_id: "550e8400-e29b-41d4-a716-446655440000"
                    event_type: "work_order.created"
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: Not authorized to delete this subscription
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                notAuthorized:
                  value:
                    error: "Not authorized to delete this subscription"
                    message: "This subscription belongs to a different API key"
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                notFound:
                  value:
                    error: "Subscription not found"
                    message: "No subscription exists with this ID"
        '500':
          description: Internal server error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /webhooks/subscriptions:
    get:
      summary: List webhook subscriptions
      description: |
        **List all webhook subscriptions** for your API key.
        
        This is useful for debugging, auditing, or displaying active integrations in your UI.
        
        **Filtering:** Optionally filter by `event_type` to see subscriptions for a specific event.
      operationId: listWebhookSubscriptions
      tags:
        - Webhook Management
      parameters:
        - name: X-API-Key
          in: header
          required: true
          schema:
            type: string
          description: Your WorkSkedge API key
        - name: event_type
          in: query
          required: false
          schema:
            type: string
          description: Optional filter by event type
          example: "work_order.created"
      responses:
        '200':
          description: List of subscriptions
          content:
            application/json:
              schema:
                type: object
                properties:
                  subscriptions:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: string
                          format: uuid
                        event_type:
                          type: string
                        target_url:
                          type: string
                        is_active:
                          type: boolean
                        description:
                          type: string
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                  count:
                    type: integer
              examples:
                success:
                  value:
                    subscriptions:
                      - id: "550e8400-e29b-41d4-a716-446655440000"
                        event_type: "work_order.created"
                        target_url: "https://hooks.zapier.com/hooks/standard/1234567/abcd"
                        is_active: true
                        description: "Zapier: Create QuickBooks Invoice"
                        created_at: "2025-01-15T10:30:00Z"
                        updated_at: "2025-01-15T10:30:00Z"
                      - id: "660e8400-e29b-41d4-a716-446655440001"
                        event_type: "employee.updated"
                        target_url: "https://hooks.zapier.com/hooks/standard/9876543/wxyz"
                        is_active: true
                        description: "API-created: employee.updated"
                        created_at: "2025-01-16T14:00:00Z"
                        updated_at: "2025-01-16T14:00:00Z"
                    count: 2
                empty:
                  value:
                    subscriptions: []
                    count: 0
        '401':
          description: Invalid API key
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '500':
          description: Internal server error
          content:
            application/json:
              schema:

  # ==========================================
  # TIMESHEETS REST API (v1)
  # ==========================================

  /v1/timesheets:
    get:
      tags: [Timesheets]
      summary: List timesheets
      description: |
        Returns a paginated list of timesheets for the authenticated company.
        Requires scope `timesheets:read`.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: employee_id
          schema: { type: string, format: uuid }
        - in: query
          name: employee_ids
          description: Comma-separated list of employee UUIDs.
          schema: { type: string }
        - in: query
          name: status
          schema:
            type: string
            enum: [draft, submitted, locked, processed]
        - in: query
          name: start_date
          schema: { type: string, format: date }
        - in: query
          name: end_date
          schema: { type: string, format: date }
        - in: query
          name: updated_since
          schema: { type: string, format: date-time }
        - in: query
          name: ids
          description: Comma-separated list of timesheet UUIDs.
          schema: { type: string }
        - in: query
          name: exclude_migration
          schema: { type: boolean }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TimesheetResource' }
                  total: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Timesheets]
      summary: Create timesheet
      description: |
        Creates a timesheet, optionally with entries. When `status` is `submitted`
        (the default), the timesheet is immediately submitted and `timesheet.submitted`
        is emitted. Requires scope `timesheets:write`.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimesheetCreateRequest' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheets/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Timesheets]
      summary: Get timesheet
      description: Requires scope `timesheets:read`. Includes entries with `linked_invoice` echo.
      security: [ { ApiKeyAuth: [] } ]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Timesheets]
      summary: Update timesheet
      description: |
        Updates editable fields only. Sending `status`, `total_hours`,
        `submitted_at`, `submitted_by`, `locked_at`, `locked_by`, `processed_at`,
        or `processed_by` returns **400** — status transitions must use the
        lifecycle endpoints. Requires scope `timesheets:write`.
      security: [ { ApiKeyAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimesheetUpdateRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Timesheets]
      summary: Delete timesheet
      description: |
        Rejected with **409** when the timesheet is `locked` or `processed`.
        Emits `timesheet.deleted`. Requires scope `timesheets:delete`.
      security: [ { ApiKeyAuth: [] } ]
      responses:
        '204': { description: Deleted }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheets/{id}/submit:
    post:
      tags: [Timesheets]
      summary: Submit timesheet
      description: |
        Transitions `draft → submitted`. Re-submitting a `submitted` timesheet
        refreshes entries and re-emits `timesheet.submitted`. Returns **409**
        when the current status is `locked` or `processed`. Requires scope
        `timesheets:write`.
      security: [ { ApiKeyAuth: [] } ]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheets/{id}/lock:
    post:
      tags: [Timesheets]
      summary: Lock timesheet
      description: Transitions `submitted → locked`. Requires scope `timesheets:lock`.
      security: [ { ApiKeyAuth: [] } ]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheets/{id}/unlock:
    post:
      tags: [Timesheets]
      summary: Unlock timesheet
      description: |
        Transitions `locked → submitted`. Optional `reason` is recorded in the audit
        trail. Requires scope `timesheets:lock`.
      security: [ { ApiKeyAuth: [] } ]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: { type: string, maxLength: 500 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheets/{id}/process:
    post:
      tags: [Timesheets]
      summary: Process timesheet
      description: |
        Transitions `submitted | locked → processed`. **Permanent** — there is no
        revert from `processed`. Requires scope `timesheets:process`.
      security: [ { ApiKeyAuth: [] } ]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheets/{id}/revert:
    post:
      tags: [Timesheets]
      summary: Revert timesheet to draft
      description: |
        Transitions `submitted | locked → draft`. Rejected with **409** when the
        timesheet is `processed`. Requires scope `timesheets:write`.
      security: [ { ApiKeyAuth: [] } ]
      parameters:
        - in: path
          name: id
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheets/batch-lock:
    post:
      tags: [Timesheets]
      summary: Batch lock timesheets
      description: |
        Locks up to 500 submitted timesheets in one call. Timesheets not in
        `submitted` status are returned in `skipped_ids`. Requires scope
        `timesheets:lock`.
      security: [ { ApiKeyAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimesheetBatchIdsRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetBatchLockResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/timesheets/batch-process:
    post:
      tags: [Timesheets]
      summary: Batch process timesheets
      description: |
        Processes up to 500 submitted/locked timesheets. Timesheets not eligible
        are returned in `skipped_ids`. Requires scope `timesheets:process`.
      security: [ { ApiKeyAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimesheetBatchIdsRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetBatchProcessResponse' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/timesheet-entries:
    get:
      tags: [Timesheet Entries]
      summary: List timesheet entries
      description: Requires scope `timesheet_entries:read` (implied by `timesheets:read`).
      security: [ { ApiKeyAuth: [] } ]
      parameters:
        - in: query
          name: timesheet_id
          schema: { type: string, format: uuid }
        - in: query
          name: employee_id
          schema: { type: string, format: uuid }
        - in: query
          name: work_order_id
          schema: { type: string, format: uuid }
        - in: query
          name: start_date
          schema: { type: string, format: date }
        - in: query
          name: end_date
          schema: { type: string, format: date }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TimesheetEntryResource' }
                  total: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Timesheet Entries]
      summary: Create timesheet entry
      description: |
        Creates an entry on an existing timesheet. Rejected with **409** when the
        parent timesheet is `locked` or `processed`. After mutation, the parent's
        `total_hours` is recomputed and `timesheet.updated` is re-emitted.
        Requires scope `timesheet_entries:write` (implied by `timesheets:write`).
      security: [ { ApiKeyAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimesheetEntryCreateRequest' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetEntryResource' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/timesheet-entries/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Timesheet Entries]
      summary: Get timesheet entry
      security: [ { ApiKeyAuth: [] } ]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetEntryResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Timesheet Entries]
      summary: Update timesheet entry
      description: |
        Rejected with **409** when the parent timesheet is `locked` or `processed`.
        Requires scope `timesheet_entries:write`.
      security: [ { ApiKeyAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimesheetEntryUpdateRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimesheetEntryResource' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }
    delete:
      tags: [Timesheet Entries]
      summary: Delete timesheet entry
      description: |
        Rejected with **409** when the parent timesheet is `locked` or `processed`.
        Requires scope `timesheet_entries:delete`.
      security: [ { ApiKeyAuth: [] } ]
      responses:
        '204': { description: Deleted }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { $ref: '#/components/responses/Conflict' }

  /v1/time-off:
    get:
      tags: [Time Off]
      summary: List time-off entries
      description: |
        Returns a paginated list of employee time-off records for the
        authenticated company. Requires scope `time_off:read`.
      security:
        - ApiKeyAuth: []
      parameters:
        - in: query
          name: employee_id
          description: Single UUID or comma-separated list of employee UUIDs.
          schema: { type: string }
        - in: query
          name: employee_ids
          description: Comma-separated list of employee UUIDs.
          schema: { type: string }
        - in: query
          name: emp_number
          description: Employee number (string match).
          schema: { type: string, maxLength: 50 }
        - in: query
          name: status
          schema:
            type: string
            enum: [approved, pending, denied]
        - in: query
          name: start_date
          description: Inclusive lower bound (matches records whose `end_date >= start_date`).
          schema: { type: string, format: date }
        - in: query
          name: end_date
          description: Inclusive upper bound (matches records whose `start_date <= end_date`).
          schema: { type: string, format: date }
        - in: query
          name: updated_since
          description: ISO 8601 timestamp for delta sync.
          schema: { type: string, format: date-time }
        - in: query
          name: ids
          description: Comma-separated list of time-off UUIDs.
          schema: { type: string }
        - in: query
          name: limit
          schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
        - in: query
          name: offset
          schema: { type: integer, minimum: 0, default: 0 }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/TimeOffResource' }
                  total: { type: integer }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [Time Off]
      summary: Create time-off entry
      description: |
        Creates a time-off record for an employee. Emits `time_off.created`.
        Requires scope `time_off:write`.
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimeOffCreateRequest' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimeOffResource' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /v1/time-off/{id}:
    parameters:
      - in: path
        name: id
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [Time Off]
      summary: Get time-off entry
      description: Requires scope `time_off:read`.
      security: [ { ApiKeyAuth: [] } ]
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimeOffResource' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [Time Off]
      summary: Update time-off entry
      description: |
        Updates editable fields only. Emits `time_off.updated`.
        Requires scope `time_off:write`.
      security: [ { ApiKeyAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimeOffUpdateRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimeOffResource' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [Time Off]
      summary: Update time-off entry (alias for PATCH)
      description: |
        Functionally identical to `PATCH /v1/time-off/{id}`. Requires scope `time_off:write`.
      security: [ { ApiKeyAuth: [] } ]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TimeOffUpdateRequest' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TimeOffResource' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [Time Off]
      summary: Delete time-off entry
      description: |
        Permanently removes the record and emits `time_off.deleted`.
        Requires scope `time_off:delete`.
      security: [ { ApiKeyAuth: [] } ]
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      success: { type: boolean }
                      id: { type: string, format: uuid }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

components:

  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key for authentication. Get your API key from WorkSkedge dashboard settings.

  schemas:
    # ==========================================
    # INBOUND WEBHOOK SCHEMAS
    # ==========================================
    
    WebhookEvent:
      type: object
      required:
        - event_type
        - data
      properties:
        event_type:
          type: string
          description: Type of event being sent
          enum:
            - test.connection
            - work_order.created
            - work_order.updated
            - work_order.deleted
            - work_order.search
            - project.created
            - project.updated
            - project.deleted
            - project.closed
            - project.search
            - employee.created
            - employee.updated
            - employee.deleted
            - time_off.created
            - time_off.updated
            - time_off.deleted
            - vendor.created
            - vendor.updated
            - vendor.deleted
            - vendor.contact.created
            - vendor.contact.updated
            - vendor.contact.deleted
            - vendor.location.created
            - vendor.location.updated
            - vendor.location.deleted
            - report.daily_submitted
            - report.completion_submitted
            - report.completion_approved
            - report.updated
            - timesheet.created
            - timesheet.updated
            - customer.created
            - customer.updated
            - customer.deleted
            - location.created
            - location.updated
            - location.deleted
            - contact.created
            - contact.updated
            - contact.deleted
            - po.created
            - po.updated
            - po.submitted
            - po.approved
            - po.rejected
            - po.received
            - po.attachment.added
            - po.attachment.removed
            - po.search
            - settings.updated
            - task.created
            - task.completed
            - task.reassigned
        event_id:
          type: string
          description: Unique identifier for this event (for idempotency)
        id:
          type: string
          description: Alternative unique identifier (for idempotency)
        data:
          type: object
          description: Event payload - varies by event_type
          additionalProperties: true

    WorkOrderData:
      type: object
      description: |
        Comprehensive data for work order create/update/delete events.
        
        **Important for Updates/Deletes**: Use `work_order_number` (NOT `id`) to identify the work order.
        
        **Field Name Aliases**: The following aliases are supported for convenience:
        - `required_headcount` → maps to `people_required`
        - `estimated_hours` → maps to `days_hours_required`
      required:
        - project_code
        - phase_name
      properties:
        work_order_number:
          type: string
          description: |
            Work order number (e.g., "WO-00001") - **REQUIRED for updates/deletes**.
            This is the public identifier used to match work orders across systems.
          example: "WO-2025-001"
        project_code:
          type: string
          description: |
            Project code (e.g., "PROJ-2025-001") - **REQUIRED for creates, optional for updates**.
            For updates, if provided, it will change the work order's associated project.
            This is the human-readable project identifier, NOT a UUID.
          example: "PROJ-2025-001"
        phase_name:
          type: string
          description: Name/title of the work order (REQUIRED for creates)
        status:
          type: string
          enum: [created, draft, ready, assigned, active, on-hold, complete-pending, complete, closed, cancelled]
          default: created
          description: Work order status
        priority:
          type: string
          enum: [low, normal, high, urgent]
          default: normal
          description: Priority level
        start_date:
          type: string
          format: date
          nullable: true
          description: Actual scheduled start date (YYYY-MM-DD)
        end_date:
          type: string
          format: date
          nullable: true
          description: Actual scheduled end date (YYYY-MM-DD)
        start_time:
          type: string
          format: time
          nullable: true
          description: Actual scheduled start time (HH:MM:SS)
        end_time:
          type: string
          format: time
          nullable: true
          description: Actual scheduled end time (HH:MM:SS)
        expected_start_date:
          type: string
          format: date
          nullable: true
          description: Expected start date (YYYY-MM-DD)
        expected_end_date:
          type: string
          format: date
          nullable: true
          description: Expected end date (YYYY-MM-DD)
        expected_start_time:
          type: string
          format: time
          nullable: true
          description: Expected start time (HH:MM:SS)
        expected_end_time:
          type: string
          format: time
          nullable: true
          description: Expected end time (HH:MM:SS)
        requested_start_date:
          type: string
          format: date
          nullable: true
          description: |
            Alias for `expected_start_date`. Use this field name for clarity.
            Will be automatically mapped to expected_start_date in the database.
        requested_end_date:
          type: string
          format: date
          nullable: true
          description: |
            Alias for `expected_end_date`. Use this field name for clarity.
            Will be automatically mapped to expected_end_date in the database.
        requested_start_time:
          type: string
          format: time
          nullable: true
          description: |
            Alias for `expected_start_time`. Use this field name for clarity.
            Will be automatically mapped to expected_start_time in the database.
        requested_end_time:
          type: string
          format: time
          nullable: true
          description: |
            Alias for `expected_end_time`. Use this field name for clarity.
            Will be automatically mapped to expected_end_time in the database.
        people_required:
          type: integer
          nullable: true
          description: Number of people needed
        required_headcount:
          type: integer
          nullable: true
          description: |
            Alias for `people_required`. Use this field name for clarity.
            Will be automatically mapped to people_required in the database.
        people_skill_requirements:
          type: object
          nullable: true
          description: |
            Required skills with counts (object with skill names as keys and integer counts as values).
            Use this for advanced scenarios where you need to specify different counts per skill.
          additionalProperties:
            type: integer
          example:
            "HVAC Technician": 2
            "Helper": 1
        required_employee_skills:
          type: array
          items:
            type: string
          nullable: true
          description: |
            Simpler alternative to `people_skill_requirements`. Provide an array of skill names.
            Each skill will be set as required with a count of 1.
            If any skill doesn't exist in the company, the webhook will return an error.
          example: ["HVAC Installation", "Refrigeration", "Electrical Systems"]
        days_hours_required:
          type: string
          nullable: true
          description: Time estimate description
        estimated_hours:
          type: number
          nullable: true
          description: |
            Alias for `days_hours_required`. Use this field name for numeric hour estimates.
            Will be automatically mapped to days_hours_required in the database.
        performed_by:
          type: string
          enum: [company, vendor]
          nullable: true
          description: Who performs the work
        vendor_id:
          type: string
          nullable: true
          description: Vendor name performing the work (will be matched to existing vendor)
          example: "Acme HVAC Services"
        site_lead_id:
          type: string
          nullable: true
          description: Site lead employee number (use emp_number, not UUID)
          example: "EMP-1001"
        site_lead_emp_number:
          type: string
          nullable: true
          description: Alternative to `site_lead_id` — resolve site lead by human-readable emp_number (company-scoped). Provide either the UUID or the emp_number, not both.
        scope_of_work:
          type: object
          default:
            checklists: []
          description: |
            Scope of work checklist structure. The webhook accepts this format and automatically
            converts it to the internal storage format.
            
            Input format (what you send):
            - `checklist_name` → stored as `name`
            - `items` → stored as `tasks`
            - `item_name` → stored as `text`
            - `status` ("pending"/"completed") → stored as `completed` (boolean)
          properties:
            checklists:
              type: array
              items:
                type: object
                properties:
                  checklist_name:
                    type: string
                    description: Name of the checklist
                  items:
                    type: array
                    description: List of checklist items
                    items:
                      type: object
                      properties:
                        item_name:
                          type: string
                          description: Name of the checklist item
                        status:
                          type: string
                          enum: [pending, complete]
                          description: Status of the item (use "pending" or "complete", not "completed")
          example:
            checklists:
              - checklist_name: "Site Preparation"
                items:
                  - item_name: "Clear and level site"
                    status: "pending"
                  - item_name: "Set up formwork"
                    status: "pending"
              - checklist_name: "Pour and Finish"
                items:
                  - item_name: "Pour concrete"
                    status: "pending"
        equipment:
          type: array
          items:
            type: string
          default: []
          description: Equipment needed
        specialty_materials:
          type: array
          items:
            type: string
          default: []
          description: Materials needed
        scheduler_notes:
          type: object
          nullable: true
          description: |
            Scheduling metadata object (NOT a string). Must contain at least one of the properties below.
          properties:
            subtrade_required:
              type: string
              nullable: true
              description: Required subtrade/specialty
            scheduling_insights:
              type: string
              nullable: true
              description: Additional scheduling notes or insights
        custom_fields:
          type: object
          additionalProperties: true
          default: {}
          description: Company-specific custom fields
        display_order:
          type: integer
          default: 0
          description: Display order for UI
        require_daily_report:
          type: boolean
          default: true
          description: Require daily progress reports
        require_completion_report:
          type: boolean
          default: true
          description: Require completion report

    ProjectData:
      type: object
      description: |
        Comprehensive data for project create/update events.
        
        **New Customer/Location System:**
        Projects now reference existing customers and locations by name, rather than embedding customer details.
        
        **Required Fields:**
        - `project_name`: Name of the project
        - `project_code`: Unique project identifier
        - `customer_name`: Name of existing customer (creates if doesn't exist)
        - `location_name`: Name of location under the customer (creates if doesn't exist)
        
        **Customer/Location Resolution:**
        - If customer doesn't exist, it will be created with provided details
        - If location doesn't exist under that customer, it will be created
        - Project will be linked to both customer and location
        
        **Backward Compatibility:**
        The old embedded customer fields (customer_first_name, customer_last_name, etc.) 
        are deprecated but still supported for transitions. New integrations should use 
        customer_name and location_name instead.
      required:
        - project_name
        - project_code
        - customer_name
        - location_name
      properties:
        project_name:
          type: string
          description: Project name (REQUIRED for creates)
        project_code:
          type: string
          description: Unique project code (REQUIRED for creates)
        description:
          type: string
          nullable: true
          description: Project description
        customer_name:
          type: string
          description: |
            **REQUIRED** - Name of the customer (individual or business).
            - For existing customers: Must match exactly (case-insensitive)
            - For new customers: Will be created with this name
            - This replaces the old customer_first_name/customer_last_name fields
          example: "Acme Corporation"
        location_name:
          type: string
          description: |
            **REQUIRED** - Name of the location under the customer.
            - For existing locations: Must match exactly (case-insensitive) within the customer
            - For new locations: Will be created with this name
            - Each customer can have multiple locations
          example: "Main Office"
        customer_type:
          type: string
          enum: [Commercial, Residential, Industrial, GC, Subcontractor, Developer, Government]
          nullable: true
          description: Type of customer (used when creating new customer)
        customer_email:
          type: string
          format: email
          nullable: true
          description: Customer email address (used when creating new customer)
        customer_phone:
          type: string
          nullable: true
          description: Customer phone number (used when creating new customer)
        customer_website:
          type: string
          nullable: true
          description: Customer website (used when creating new customer)
        customer_status:
          type: string
          enum: [Active, Inactive]
          default: Active
          description: Customer status (used when creating new customer)
        customer_notes:
          type: string
          nullable: true
          description: Notes about the customer (used when creating new customer)
        location_address_line_1:
          type: string
          nullable: true
          description: Street address for the location (used when creating new location)
          example: "123 Main Street"
        location_address_line_2:
          type: string
          nullable: true
          description: Additional address line (used when creating new location)
          example: "Suite 400"
        location_city:
          type: string
          nullable: true
          description: City (used when creating new location)
          example: "New York"
        location_province:
          type: string
          nullable: true
          description: State/Province (used when creating new location)
          example: "NY"
        location_postal_code:
          type: string
          nullable: true
          description: Postal/ZIP code (used when creating new location)
          example: "10001"
        location_country:
          type: string
          nullable: true
          default: USA
          description: Country (used when creating new location)
        location_notes:
          type: string
          nullable: true
          description: Notes about the location (used when creating new location)
        account_manager_emp_number:
          type: string
          nullable: true
          description: Account manager employee number for the customer (e.g., "EMP-2001")
          example: "EMP-2001"
        project_owner_emp_numbers:
          type: array
          items:
            type: string
          maxItems: 2
          nullable: true
          description: |
            **Canonical field.** Employee numbers (e.g., `["EMP-2001", "EMP-2002"]`) for the
            employees to assign as project owners. Maximum 2. Use employee numbers, not UUIDs.
            This is the recommended field name and matches the REST API and outbound webhook payloads.
          example: ["EMP-2001", "EMP-2002"]
        internal_manager_emp_number:
          oneOf:
            - type: string
            - type: array
              items:
                type: string
              maxItems: 2
          nullable: true
          deprecated: true
          description: |
            **Deprecated alias** for `project_owner_emp_numbers`. Accepts a single employee
            number string or an array of up to 2. Kept for backward compatibility — new
            integrations should use `project_owner_emp_numbers`.
          example: ["EMP-2001", "EMP-2002"]
        internal_manager_emp_numbers:
          type: array
          items:
            type: string
          maxItems: 2
          nullable: true
          deprecated: true
          description: |
            **Deprecated alias** for `project_owner_emp_numbers`. New integrations should use
            `project_owner_emp_numbers`.
          example: ["EMP-2001", "EMP-2002"]
        priority:
          type: string
          enum: [normal, high]
          default: normal
          description: Project priority level
        status:
          type: string
          description: |
            Initial project status. Depends on company's project_status_mode:
            - **Simple mode**: "active" (default), "closed"
            - **Advanced mode**: "lead" (default), "estimating", "pending_approval", "approved", "active", "complete", "closed"
        estimation_due_date:
          type: string
          format: date
          nullable: true
          description: Due date for the estimation. Required when status is "estimating".
        color_index:
          type: integer
          minimum: 0
          maximum: 8
          nullable: true
          description: Color code for UI (0-8)
        equipment:
          type: array
          items:
            type: string
          default: []
          description: Equipment required for project
        custom_fields:
          type: object
          additionalProperties: true
          default: {}
          description: Company-specific custom fields
        docs:
          type: array
          items:
            type: object
            properties:
              name:
                type: string
              url:
                type: string
          default: []
          description: Project documents
        contacts:
          type: array
          items:
            type: object
            properties:
              first_name:
                type: string
                description: Contact first name
              last_name:
                type: string
                description: Contact last name
              title:
                type: string
                nullable: true
                description: Contact title/position
              email:
                type: string
                format: email
                nullable: true
                description: Contact email address
              phone:
                type: string
                nullable: true
                description: Contact phone number
              mobile:
                type: string
                nullable: true
                description: Contact mobile number
              phone_ext:
                type: string
                nullable: true
                description: Phone extension
              is_primary:
                type: boolean
                default: false
                description: Whether this is the primary contact
              contact_tags:
                type: array
                items:
                  type: string
                nullable: true
                description: Tags for categorizing the contact
              notes:
                type: string
                nullable: true
                description: Additional notes about the contact
            required:
              - first_name
          description: |
            Optional array of contacts to create/link to this project's customer and location.
            - Existing contacts are matched by email (if provided) or first_name + last_name
            - New contacts are automatically created if no match is found
            - Contacts are linked to both the customer and location (if location_name provided)
            - If multiple contacts provided, the first one with is_primary=true becomes the primary contact

    EmployeeData:
      type: object
      description: Data for employee create/update/delete events
      required:
        - emp_number
      properties:
        emp_number:
          type: string
          description: Employee ID/Number (REQUIRED - this is the public identifier, NOT the UUID)
          example: "EMP001"
        name:
          type: string
          description: Full employee name (REQUIRED for creates)
        email:
          type: string
          format: email
          description: Employee email address (REQUIRED for creates)
        phone:
          type: string
          nullable: true
          description: Employee phone number
        department_name:
          type: string
          nullable: true
          description: Department name - will be created if it doesn't exist, or matched to existing department
        role_name:
          type: string
          nullable: true
          description: Role name - will be created if it doesn't exist, or matched to existing role
        employee_type:
          type: string
          enum: [site_worker, site_lead, project_owner, office]
          description: |
            Employee type designation (REQUIRED for creates, optional for updates):
            - **site_worker**: Field worker who can be assigned to work orders. When the timesheets module is enabled, site workers can be invited to create a user account to submit daily timesheets.
            - **site_lead**: Field supervisor who leads work orders and has site_lead role
            - **project_owner**: Manager who oversees projects and has project_owner role
            - **office**: Office/administrative staff (not assigned to field work)
          example: "site_worker"
        roles:
          type: array
          items:
            type: string
            enum: [site_worker, site_lead, project_owner, scheduler, manager, administrator]
          description: |
            Array of role designations for multi-role employees. Use this instead of employee_type 
            when you need to assign multiple roles (e.g., ["site_worker", "site_lead"] for an employee 
            who is both a field worker and site supervisor).
            
            **Note:** If both `employee_type` and `roles` are provided, `roles` takes precedence.
            
            Available roles:
            - **site_worker**: Field worker who can be assigned to work orders
            - **site_lead**: Field supervisor who leads work orders
            - **project_owner**: Manager who oversees projects
            - **scheduler**: Can manage scheduling and assignments
            - **manager**: Management role with elevated permissions
            - **administrator**: Full administrative access
          example: ["site_worker", "site_lead"]
        start_date:
          type: string
          format: date
          nullable: true
          description: Date when the employee started working for the company (YYYY-MM-DD format)
          example: "2024-01-15"
        birthday:
          type: string
          format: date
          nullable: true
          description: Employee's date of birth (YYYY-MM-DD format)
          example: "1985-06-15"
        address:
          type: object
          nullable: true
          description: Employee's home address
          properties:
            street:
              type: string
              description: Street address
              example: "123 Main Street"
            city:
              type: string
              description: City
              example: "Austin"
            state:
              type: string
              description: State/Province
              example: "TX"
            zip:
              type: string
              description: ZIP/Postal code
              example: "78701"
            country:
              type: string
              description: Country
              example: "USA"
        emergency_contact_name:
          type: string
          nullable: true
          description: Name of emergency contact person
          example: "Jane Doe"
        emergency_contact_relationship:
          type: string
          nullable: true
          description: Relationship to emergency contact (e.g., Spouse, Parent, Sibling)
          example: "Spouse"
        emergency_phone_1:
          type: string
          nullable: true
          description: Primary emergency contact phone number
          example: "555-999-8888"
        emergency_phone_2:
          type: string
          nullable: true
          description: Secondary emergency contact phone number
          example: "555-777-6666"
        emergency_email:
          type: string
          format: email
          nullable: true
          description: Emergency contact email address
          example: "jane.doe@email.com"
        employee_skills:
          type: array
          items:
            type: string
          nullable: true
          description: |
            Array of skill names for the employee. 
            **Important**: All skill names must already exist in your company's skill library. 
            If any skill doesn't exist, the webhook will return an error listing the missing skills.
            Use the skill management endpoints to create skills before assigning them to employees.
          example: ["Concrete", "Framing", "Safety Management", "Project Scheduling"]

    VendorData:
      type: object
      description: Data for vendor/subcontractor create/update/delete events
      required:
        - vendor_name
      properties:
        vendor_name:
          type: string
          description: |
            Vendor/company name (REQUIRED - this is the public identifier, NOT the UUID).
            **Important**: For `vendor.updated` and `vendor.deleted` events, this field is used to identify the vendor. 
            The vendor name is immutable and cannot be changed through webhooks.
          example: "Acme HVAC Services"
        type:
          type: string
          enum: [vendor, distributor, manufacturer, subcontractor, rental_provider, supplier, partner, operations_facility, professional_services, technology_saas, admin_office, fleet_equipment, marketing_growth, hr_culture]
          description: Vendor classification type
          default: vendor
          example: "subcontractor"
        tags:
          type: array
          items:
            type: string
          description: Freeform tags for categorization and filtering
          example: ["preferred", "local", "union"]
        trade_specialty:
          oneOf:
            - type: string
              description: Single trade/specialty as comma-separated string
              example: "HVAC, Mechanical, Refrigeration"
            - type: array
              description: Multiple trades/specialties as array
              items:
                type: string
              example: ["HVAC", "Mechanical", "Refrigeration"]
          description: Primary trade/specialty (REQUIRED for creates). Can be a string or array of strings.
        primary_contact_name:
          type: string
          description: Primary contact person name (REQUIRED for creates)
        primary_contact_email:
          type: string
          format: email
          nullable: true
          description: Primary contact email
        primary_contact_phone:
          type: string
          nullable: true
          description: Primary contact phone
        address:
          type: string
          nullable: true
          description: Vendor street address (line 1)
          example: "1500 Industrial Parkway, Suite 200"
        address_line2:
          type: string
          nullable: true
          description: Additional address line (optional)
          example: "Building C, Floor 3"
        city:
          type: string
          nullable: true
          description: City
          example: "Chicago"
        state:
          type: string
          nullable: true
          description: "State/Province - accepts both 2-letter codes (e.g., 'MB', 'IL') and full names (e.g., 'Manitoba', 'Illinois')"
          example: "IL"
        zip_code:
          type: string
          nullable: true
          description: ZIP/Postal code
          example: "60601"
        approval_status:
          type: string
          enum: [pending, approved, expired]
          default: pending
          description: Vendor approval status
        insurance_expiry_date:
          type: string
          format: date
          nullable: true
          description: Insurance expiration date (YYYY-MM-DD)
        compliance_expiry_date:
          type: string
          format: date
          nullable: true
          description: Compliance expiration date (YYYY-MM-DD)
        rating:
          type: number
          minimum: 0
          maximum: 5
          nullable: true
          description: Vendor rating (0-5)
        regions:
          type: array
          items:
            type: string
          nullable: true
          description: Geographic regions or service areas covered by the vendor
          example: ["Midwest", "Great Lakes Region"]
        notes:
          type: string
          nullable: true
          description: Additional notes about the vendor
        custom_fields:
          type: object
          additionalProperties: true
          default: {}
          description: Company-specific custom fields
        contacts:
          type: array
          description: Additional vendor contacts (optional, for bulk creation)
          items:
            type: object
            required:
              - first_name
            properties:
              first_name:
                type: string
                description: Contact first name
              last_name:
                type: string
                description: Contact last name
              email:
                type: string
                format: email
                nullable: true
              phone:
                type: string
                nullable: true
              mobile:
                type: string
                nullable: true
              role:
                type: string
                nullable: true
                description: Contact's role/title
              is_primary:
                type: boolean
                default: false
              notes:
                type: string
                nullable: true
        locations:
          type: array
          description: Vendor locations (optional, for bulk creation)
          items:
            type: object
            required:
              - name
            properties:
              name:
                type: string
                description: Location name
              address:
                type: object
                nullable: true
                properties:
                  street:
                    type: string
                  city:
                    type: string
                  state:
                    type: string
                  zip:
                    type: string
                  country:
                    type: string
              phone:
                type: string
                nullable: true
              is_default:
                type: boolean
                default: false

    CustomerData:
      type: object
      description: |
        Data for customer create/update/delete events.
        
        **Identification:**
        - For creates: `name` is required
        - For updates/deletes: Use `customer_name` or `id` to identify the customer
      properties:
        id:
          type: string
          format: uuid
          description: Customer UUID (for updates/deletes)
        name:
          type: string
          description: Customer name (REQUIRED for creates)
        customer_name:
          type: string
          description: Customer name for lookup (for updates/deletes)
        type:
          type: string
          enum: [Commercial, Residential, Government, Non-Profit]
          nullable: true
          description: Customer type classification
        email:
          type: string
          format: email
          nullable: true
        phone:
          type: string
          nullable: true
        website:
          type: string
          nullable: true
        status:
          type: string
          enum: [Active, Inactive]
          default: Active
        account_manager_emp_number:
          type: string
          nullable: true
          description: Employee number of the account manager (e.g., "EMP-2001")
        notes:
          type: string
          nullable: true
        custom_fields:
          type: object
          additionalProperties: true
          description: Company-specific custom fields
      required:
        - name

    LocationData:
      type: object
      description: |
        Data for location create/update/delete events.
        
        **Identification:**
        - For creates: `customer_name` or `customer_id` + `name` required
        - For updates/deletes: Use `location_name` or `id` to identify the location
      properties:
        id:
          type: string
          format: uuid
          description: Location UUID (for updates/deletes)
        customer_id:
          type: string
          format: uuid
          description: Customer UUID
        customer_name:
          type: string
          description: Customer name for lookup
        name:
          type: string
          description: Location name (REQUIRED for creates)
        location_name:
          type: string
          description: Location name for lookup (for updates/deletes)
        address_line_1:
          type: string
          nullable: true
        address_line_2:
          type: string
          nullable: true
        city:
          type: string
          nullable: true
        province:
          type: string
          nullable: true
        postal_code:
          type: string
          nullable: true
        country:
          type: string
          nullable: true
          default: USA
        notes:
          type: string
          nullable: true
        default_contact_id:
          type: string
          format: uuid
          nullable: true
          description: Default contact UUID for this location
      required:
        - name

    ContactData:
      type: object
      description: |
        Data for contact create/update/delete events.
        
        **Identification:**
        - For creates: `customer_name` or `customer_id` + `first_name` required (`last_name` optional)
        - For updates: Use `contact_id` or (`customer_name` + `first_name` + `last_name`)
        - For deletes: Use `contact_id` or (`customer_name` + `first_name` + `last_name`)
      properties:
        contact_id:
          type: string
          format: uuid
          description: Contact UUID (for updates/deletes)
        customer_id:
          type: string
          format: uuid
          description: Customer UUID
        customer_name:
          type: string
          description: Customer name for lookup
        location_id:
          type: string
          format: uuid
          nullable: true
          description: Location UUID (optional)
        location_name:
          type: string
          nullable: true
          description: Location name for lookup (optional)
        first_name:
          type: string
          description: Contact first name (REQUIRED)
        last_name:
          type: string
          description: Contact last name
        title:
          type: string
          nullable: true
          description: Job title
        email:
          type: string
          format: email
          nullable: true
        phone:
          type: string
          nullable: true
        phone_ext:
          type: string
          nullable: true
        mobile:
          type: string
          nullable: true
        is_primary:
          type: boolean
          default: false
          description: Whether this is the primary contact for the customer
        contact_tags:
          type: array
          items:
            type: string
          nullable: true
          description: Tags for categorization (e.g., ["billing", "operations"])
        notes:
          type: string
          nullable: true
      required:
        - first_name

    TimeOffData:
      type: object
      description: |
        Data for employee time off events (create, update, and delete).
        
        **For Creates**: Requires emp_number, start_date, end_date
        **For Updates**: Requires either id OR (emp_number, start_date, end_date) for lookup
        **For Deletes**: Requires emp_number, start_date, end_date
      required:
        - emp_number
        - start_date
        - end_date
      properties:
        id:
          type: string
          format: uuid
          description: Time off record ID (preferred for updates, optional)
        emp_number:
          type: string
          description: Employee number (e.g., "EMP-1001") - REQUIRED to identify the employee
        start_date:
          type: string
          format: date
          description: Start date of time off (YYYY-MM-DD) - REQUIRED for create/delete, used for lookup in updates
        end_date:
          type: string
          format: date
          description: End date of time off (YYYY-MM-DD) - REQUIRED for create/delete, used for lookup in updates
        start_time:
          type: string
          pattern: '^\d{2}:\d{2}$'
          nullable: true
          description: Start time (HH:MM) for partial day off
        end_time:
          type: string
          pattern: '^\d{2}:\d{2}$'
          nullable: true
          description: End time (HH:MM) for partial day off
        time_off_type:
          type: string
          enum: [vacation, sick_leave, personal, unpaid, training, other]
          default: vacation
          description: Type of time off
        status:
          type: string
          enum: [approved, pending, denied]
          default: approved
          description: Approval status
        reason:
          type: string
          nullable: true
          description: Reason for time off (alias for notes)
        notes:
          type: string
          nullable: true
          description: Additional notes

    TimesheetData:
      type: object
      description: |
        Data for employee daily timesheet events.
        
        **Work Type Support:**
        Each timesheet entry can specify a work type to categorize hours:
        - **Regular Time (REG)**: Standard working hours with 1.0x multiplier
        - **Overtime (OT)**: Hours beyond standard schedule, typically 1.5x multiplier
        - **Vacation (VAC)**: Paid time off, non-billable
        - **Sick Leave (SICK)**: Sick time, non-billable
        - **Holiday (HOL)**: Company holiday, non-billable
        - **Custom Types**: Organizations can define additional work types
        
        **How to Specify Work Types:**
        1. **Preferred:** Use `work_type_code` (e.g., "OT", "VAC") - more reliable
        2. **Alternative:** Use `work_type_name` (e.g., "Overtime", "Vacation")
        3. **Default:** If neither provided, system uses company's default work type
        
        **Work Order Requirements:**
        - Billable work types (REG, OT) typically require `work_order_number`
        - Non-billable types (VAC, SICK, HOL) can have `work_order_number: null`
        - System validates based on work type's `requires_work_order` setting
        
        **Resolution Logic:**
        - System first tries to match `work_type_code` against active work types
        - If not found, tries `work_type_name`
        - If still not found, falls back to company's default work type
        - Invalid work types are logged but don't fail the request
      required:
        - emp_number
        - timesheet_date
      properties:
        emp_number:
          type: string
          description: Employee number (e.g., "EMP-1001") - REQUIRED to identify the employee
        timesheet_date:
          type: string
          format: date
          description: Date of the daily timesheet (YYYY-MM-DD)
        status:
          type: string
          enum: [draft, submitted, locked, processed]
          default: draft
          description: Timesheet status (optional - defaults to 'draft' if not provided)
        total_hours:
          type: number
          minimum: 0
          description: Total hours for the timesheet
        notes:
          type: string
          nullable: true
          description: Additional notes
        entries:
          type: array
          description: Array of timesheet entries
          items:
            $ref: '#/components/schemas/TimesheetEntryData'

    TimesheetEntryData:
      type: object
      description: |
        Individual timesheet entry.
        
        **Time-Based Entry Mode:**
        When the company uses time_in_out entry mode, entries include start_time and end_time.
        Hours are calculated automatically from these times.
        When auto_break is enabled, separate break entries are inserted with a specific work type.
      required:
        - entry_date
        - hours
      properties:
        entry_date:
          type: string
          format: date
          description: Date of work (YYYY-MM-DD)
        work_order_number:
          type: string
          nullable: true
          description: |
            Work order number (e.g., "WO-00012") - NOT a UUID.
            Optional for non-billable work types (vacation, sick leave).
            Required for billable work types unless work type specifies otherwise.
            Note: Break entries typically have no work order.
        hours:
          type: number
          minimum: 0
          maximum: 24
          description: Hours worked on this date (calculated from start/end times in time_in_out mode)
        start_time:
          type: string
          format: time
          nullable: true
          description: |
            Start time in HH:MM format (e.g., "08:00").
            Included when company uses time_in_out entry mode.
          example: "08:00"
        end_time:
          type: string
          format: time
          nullable: true
          description: |
            End time in HH:MM format (e.g., "17:00").
            Included when company uses time_in_out entry mode.
          example: "17:00"
        work_type_code:
          type: string
          maxLength: 20
          nullable: true
          description: |
            Work type short code (e.g., "REG", "OT", "VAC", "SICK", "BREAK").
            Preferred method for specifying work type.
            If not provided, falls back to work_type_name, then company default.
          example: "OT"
        work_type_name:
          type: string
          maxLength: 100
          nullable: true
          description: |
            Work type full name (e.g., "Regular Time", "Overtime", "Vacation", "Break").
            Used if work_type_code is not provided.
            System will match against active work types in WorkSkedge.
          example: "Overtime"
        notes:
          type: string
          nullable: true
          description: Notes for this entry

    # ==========================================
    # REPORT DATA SCHEMAS (INBOUND)
    # ==========================================

    DailyReportData:
      type: object
      description: |
        Data for submitting a daily progress report via inbound webhook.
        
        **Required Fields**: work_order_number, report_date
        
        **Scope Checklist**: Optional - if provided, updates the scope of work with checked items.
        Structure should match the work order's existing scope_of_work format.
        
        **Photos**: Optional array of photo objects with url and optional caption.
      required:
        - work_order_number
        - report_date
      properties:
        work_order_number:
          type: string
          description: Work order number (e.g., "WO-00123") - REQUIRED to identify the work order
          example: "WO-00123"
        report_date:
          type: string
          format: date
          description: Date of the report (YYYY-MM-DD)
          example: "2025-01-15"
        progress_notes:
          type: string
          nullable: true
          description: Daily progress notes (alias for notes)
          example: "Completed wiring for second floor"
        notes:
          type: string
          nullable: true
          description: Daily progress notes (alias for progress_notes)
        scope_checklist:
          type: object
          nullable: true
          description: |
            Scope of work changes. Structure varies by implementation but typically contains
            arrays of checked/completed items that update the work order's scope_of_work field.
          additionalProperties: true
        photos:
          type: array
          nullable: true
          description: Array of photo attachments
          items:
            type: object
            properties:
              url:
                type: string
                format: uri
                description: URL of the photo
              caption:
                type: string
                nullable: true
                description: Optional caption for the photo

    CompletionReportData:
      type: object
      description: |
        Data for submitting a completion report via inbound webhook.
        
        **Required Fields**: work_order_number, completion_date
        
        **Scope Checklist**: Optional - if provided, updates the scope of work with final item statuses.
        
        **Completion Checklist**: Optional - standard checklist items that must be verified before
        work order can be marked complete (e.g., "Final walkthrough completed", "All tools removed").
        
        **Photos**: Final photos can be provided as either `final_photos` or `photos` array.
      required:
        - work_order_number
        - completion_date
      properties:
        work_order_number:
          type: string
          description: Work order number (e.g., "WO-00123") - REQUIRED to identify the work order
          example: "WO-00123"
        completion_date:
          type: string
          format: date
          description: Date of completion (YYYY-MM-DD)
          example: "2025-01-20"
        completion_notes:
          type: string
          nullable: true
          description: Completion notes (alias for notes)
          example: "Project completed successfully. All items addressed."
        notes:
          type: string
          nullable: true
          description: Completion notes (alias for completion_notes)
        scope_checklist:
          type: object
          nullable: true
          description: Final scope of work status with completed items
          additionalProperties: true
        completion_checklist:
          type: object
          nullable: true
          description: |
            Standard completion checklist with items that must be verified.
            Typically contains an items array with item name, completed status, and optional notes.
          additionalProperties: true
          example:
            items:
              - item: "Final walkthrough completed"
                completed: true
                notes: ""
              - item: "All tools removed from site"
                completed: true
                notes: ""
        final_photos:
          type: array
          nullable: true
          description: Array of final completion photos (alias for photos)
          items:
            type: object
            properties:
              url:
                type: string
                format: uri
              caption:
                type: string
                nullable: true
        photos:
          type: array
          nullable: true
          description: Array of final completion photos (alias for final_photos)
          items:
            type: object
            properties:
              url:
                type: string
                format: uri
              caption:
                type: string
                nullable: true

    CompletionReportApprovalData:
      type: object
      description: |
        Data for approving a completion report via inbound webhook.
        
        **Required Fields**: work_order_number
        
        This event triggers approval of the most recent pending completion report for the
        specified work order. The work order status will typically transition to 'closed'.
      required:
        - work_order_number
      properties:
        work_order_number:
          type: string
          description: Work order number to approve (e.g., "WO-00123")
          example: "WO-00123"

    # ==========================================
    # PURCHASE ORDER DATA SCHEMAS (INBOUND)
    # ==========================================

    POData:
      type: object
      description: |
        Data for purchase order create/update events via inbound webhooks.
        
        **For Creates**: At least one line item is **required**.
        **For Updates**: Requires po_number to identify the PO to update
        
        **Linking**: Purchase orders can be linked to:
        - A work order (using work_order_number)
        - A project (using project_code)
        - A vendor (using vendor_name)
        
        **Budget Tracking Fields**:
        - `budget_amount`: Approved budget for this PO
        - `not_to_exceed_amount`: Maximum amount that cannot be exceeded
        - Line items can have `max_amount` to set maximum per line
        
        **Validation Rules**:
        - When `destination_type` is "job", `work_order_number` or `work_order_id` is **required**
        - When work order is provided without destination_type, it defaults to "job"
      required:
        - line_items
      properties:
        po_number:
          type: string
          maxLength: 100
          nullable: true
          description: |
            Purchase order number. Auto-generated if not provided on create.
            REQUIRED for updates to identify the PO.
          example: "PO-2025-001"
        work_order_number:
          type: string
          maxLength: 100
          nullable: true
          description: |
            Work order number to link this PO to.
            **Required when destination_type is 'job'** (unless work_order_id is provided).
          example: "WO-00123"
        work_order_id:
          type: string
          format: uuid
          nullable: true
          description: |
            Work order UUID (alternative to work_order_number).
            **Required when destination_type is 'job'** (unless work_order_number is provided).
        project_code:
          type: string
          maxLength: 100
          nullable: true
          description: Project code to link this PO to (alternative to work_order_number)
          example: "PRJ-2025-100"
        vendor_name:
          type: string
          maxLength: 255
          nullable: true
          description: Vendor name (will be matched to existing vendor or can specify for reference)
          example: "Superior HVAC Solutions"
        description:
          type: string
          maxLength: 5000
          nullable: true
          description: Description of the purchase order
          example: "HVAC equipment for second floor installation"
        terms:
          type: string
          maxLength: 1000
          nullable: true
          description: Payment terms or conditions
          example: "Net 30"
        budget_amount:
          type: number
          minimum: 0
          nullable: true
          description: Approved budget amount for this PO
          example: 15000.00
        not_to_exceed_amount:
          type: number
          minimum: 0
          nullable: true
          description: Maximum amount that cannot be exceeded (NTE)
          example: 17500.00
        total_amount:
          type: number
          minimum: 0
          nullable: true
          description: Total amount (calculated from line items if not provided)
          example: 12500.00
        po_type:
          type: string
          enum: [standard, service, blanket]
          nullable: true
          description: |
            Type of purchase order:
            - **standard**: Itemized materials/equipment with quantities (qty × price)
            - **service**: Lump sum for subcontractor/service work
            - **blanket**: Open account with NTE limit (line items added as costs incur)
          example: "standard"
        destination_type:
          type: string
          enum: [job, inventory, overhead]
          nullable: true
          description: |
            Cost allocation destination.
            - **job**: Costs allocated to a specific work order (**requires work_order_number or work_order_id**)
            - **inventory**: Costs allocated to inventory
            - **overhead**: General company overhead expenses
            
            Defaults to 'job' if work_order_number is provided.
          example: "job"
        paid_by:
          type: string
          enum: [vendor_invoice, cash_debit, credit_card, reimbursement]
          default: vendor_invoice
          description: |
            How this expense was or will be paid:
            - **vendor_invoice**: Vendor will send an invoice (traditional PO flow)
            - **cash_debit**: Expense paid by cash or debit card
            - **credit_card**: Expense paid by company credit card (requires credit_card_id)
            - **reimbursement**: Employee paid out-of-pocket and needs reimbursement (requires reimbursement_employee_id)
          example: "vendor_invoice"
        reimbursement_employee_id:
          type: string
          format: uuid
          nullable: true
          description: |
            ID of the employee to be reimbursed.
            **Required when paid_by is 'reimbursement'**.
        reimbursement_emp_number:
          type: string
          nullable: true
          description: Alternative to `reimbursement_employee_id` — resolve reimbursement employee by human-readable emp_number (company-scoped). Provide either the UUID or the emp_number, not both.
        equipment_asset_id:
          type: string
          format: uuid
          nullable: true
          description: |
            ID of an equipment asset to link to this PO for expense tracking.
        line_items:
          type: array
          description: Array of line items for this PO. At least one line item is required.
          minItems: 1
          items:
            $ref: '#/components/schemas/POLineItemData'

    POLineItemData:
      type: object
      description: Individual purchase order line item
      required:
        - description
        - quantity
        - unit
        - unit_price
      properties:
        description:
          type: string
          maxLength: 500
          description: Line item description
          example: "Copper piping 1/2 inch"
        quantity:
          type: number
          minimum: 0
          description: Quantity ordered
          example: 100
        unit:
          type: string
          maxLength: 50
          description: Unit of measure
          example: "ft"
        unit_price:
          type: number
          minimum: 0
          description: Price per unit
          example: 12.50
        max_amount:
          type: number
          minimum: 0
          nullable: true
          description: Maximum amount for this line item (budget cap)
          example: 1500.00
        notes:
          type: string
          maxLength: 1000
          nullable: true
          description: Additional notes for this line item
          example: "Type L copper, schedule 40"
        is_taxable:
          type: boolean
          default: true
          description: Whether this line item is subject to tax. Set to false for tax-exempt items.
        tax_code_id:
          type: string
          format: uuid
          nullable: true
          description: Override tax code for this line item. When set, this line uses the specified tax code instead of document-level taxes.

    POStatusData:
      type: object
      description: |
        Data for purchase order status change events (submitted, approved, rejected, received, billed).
        
        **Required**: po_number to identify the PO
      required:
        - po_number
      properties:
        po_number:
          type: string
          maxLength: 100
          description: Purchase order number
          example: "PO-2025-001"
        submitted_by:
          type: string
          format: uuid
          description: User ID who submitted (for po.submitted)
        submitted_at:
          type: string
          format: date-time
          description: Timestamp of submission (for po.submitted)
        approved_by:
          type: string
          format: uuid
          description: User ID who approved (for po.approved)
        approved_at:
          type: string
          format: date-time
          description: Timestamp of approval (for po.approved)
        rejected_by:
          type: string
          format: uuid
          description: User ID who rejected (for po.rejected)
        rejected_at:
          type: string
          format: date-time
          description: Timestamp of rejection (for po.rejected)
        rejection_reason:
          type: string
          maxLength: 1000
          nullable: true
          description: Reason for rejection (for po.rejected)
          example: "Budget exceeded for this quarter"
        received_by:
          type: string
          format: uuid
          description: User ID who marked as received (for po.received)
        received_at:
          type: string
          format: date-time
          description: Timestamp when marked received (for po.received)
        billed_at:
          type: string
          format: date-time
          description: Timestamp when marked as billed (for po.billed)
        invoice_number:
          type: string
          maxLength: 100
          nullable: true
          description: Vendor invoice number (for po.billed)
          example: "INV-2025-001234"
        invoice_amount:
          type: number
          minimum: 0
          nullable: true
          description: Actual invoice amount from vendor (for po.billed)
          example: 12450.00
        invoice_date:
          type: string
          format: date
          nullable: true
          description: Date on the vendor invoice (for po.billed)
          example: "2025-01-20"
        invoice_variance:
          type: number
          nullable: true
          description: Difference between invoice_amount and total_amount (invoice - expected)
          example: -50.00

    POAttachmentData:
      type: object
      description: Data for purchase order attachment events
      required:
        - po_number
        - attachment_id
        - file_name
        - file_path
      properties:
        po_number:
          type: string
          maxLength: 100
          description: Purchase order number
          example: "PO-2025-001"
        attachment_id:
          type: string
          format: uuid
          description: Unique attachment ID
        file_name:
          type: string
          maxLength: 255
          description: Original file name
          example: "invoice-001.pdf"
        file_path:
          type: string
          maxLength: 1000
          description: Storage path for the attachment
        file_size:
          type: integer
          minimum: 0
          nullable: true
          description: File size in bytes
        content_type:
          type: string
          maxLength: 255
          nullable: true
          description: MIME type of the file
          example: "application/pdf"

    TestConnectionData:
      type: object
      description: |
        Data for testing webhook authentication and connectivity.
        Used by integration platforms like Zapier during setup to verify API keys and endpoint availability.
        No specific fields are required - any valid JSON object is accepted.
      properties:
        test:
          type: boolean
          description: Optional test flag
          example: true
      additionalProperties: true

    ProjectSearchData:
      type: object
      description: |
        Search for projects by name or code.
        Returns an array of matching projects with their details.
      required:
        - search_term
      properties:
        search_term:
          type: string
          minLength: 1
          maxLength: 255
          description: Text to search for in project names or codes
          example: "Skyline"
        search_field:
          type: string
          enum: [project_name, project_code]
          default: project_name
          description: Field to search in
          example: "project_name"
        exact_match:
          type: boolean
          default: false
          description: Whether to perform exact match (true) or fuzzy match (false)
          example: false

    WorkOrderSearchData:
      type: object
      description: |
        Search for work orders by phase name or work order number.
        Optionally filter by project code.
        Returns an array of matching work orders with project details.
      required:
        - search_term
      properties:
        search_term:
          type: string
          minLength: 1
          maxLength: 255
          description: Text to search for in work order names or numbers
          example: "Foundation"
        search_field:
          type: string
          enum: [phase_name, work_order_number]
          default: phase_name
          description: Field to search in
          example: "phase_name"
        project_code:
          type: string
          nullable: true
          description: Optional project code to filter work orders to a specific project
          example: "PRJ-2025-100"
        exact_match:
          type: boolean
          default: false
          description: Whether to perform exact match (true) or fuzzy match (false)
          example: false

    POSearchData:
      type: object
      description: |
        Search for purchase orders by PO number or vendor name.
        Optionally filter by status or project code.
        Returns an array of matching POs with details for Zapier dropdown support.
      required:
        - search_term
      properties:
        search_term:
          type: string
          minLength: 1
          maxLength: 255
          description: Text to search for in PO numbers or vendor names
          example: "PO-2025"
        search_field:
          type: string
          enum: [po_number, vendor_name]
          default: po_number
          description: Field to search in
          example: "po_number"
        status:
          type: string
          enum: [draft, submitted, approved, rejected, received, cancelled]
          nullable: true
          description: Optional status filter to narrow results
          example: "approved"
        project_code:
          type: string
          nullable: true
          description: Optional project code to filter POs to a specific project
          example: "PRJ-2025-100"
        exact_match:
          type: boolean
          default: false
          description: Whether to perform exact match (true) or fuzzy match (false)
          example: false

    WebhookResponse:
      type: object
      properties:
        success:
          type: boolean
          description: Whether the webhook was processed successfully
        message:
          type: string
          description: Human-readable response message
        event_id:
          type: string
          description: Echo of the event ID received
        record_id:
          type: string
          description: ID of the created/updated record in WorkSkedge
        action:
          type: string
          description: Action performed (e.g., "created", "updated", "closed")
        project_id:
          type: string
          format: uuid
          description: ID of the affected project (for project events)
        work_orders_closed:
          type: integer
          description: Number of work orders that were force-closed (only present in project.closed responses when close_work_orders=true)

    ErrorResponse:
      type: object
      properties:
        error:
          type: string
          description: Error type
        message:
          type: string
          description: Human-readable error message
        details:
          type: object
          description: Additional error details
          additionalProperties: true

    RateLimitResponse:
      type: object
      properties:
        error:
          type: string
          example: rate_limit_exceeded
        message:
          type: string
          example: Rate limit exceeded. Please try again later.
        retry_after:
          type: integer
          description: Seconds until rate limit resets
          example: 60
    
    # ==========================================
    # OUTBOUND WEBHOOK EVENT SCHEMAS
    # ==========================================
    # These are the event payloads WorkSkedge sends TO your webhook endpoints
    # Configure subscriptions in WorkSkedge dashboard under Settings → API & Webhooks
    
    # Base outbound event structure
    OutboundWebhookEvent:
      type: object
      required:
        - event_id
        - event_type
        - timestamp
        - company_id
        - data
      properties:
        event_id:
          type: string
          format: uuid
          description: Unique identifier for this event (use for idempotency)
          example: 550e8400-e29b-41d4-a716-446655440000
        event_type:
          type: string
          description: Type of event
          example: work_order.created
        timestamp:
          type: string
          format: date-time
          description: When the event occurred (ISO 8601)
          example: '2024-01-15T14:30:00Z'
        company_id:
          type: string
          format: uuid
          description: Company ID this event belongs to
          example: 123e4567-e89b-12d3-a456-426614174000
        data:
          type: object
          description: Event-specific payload (varies by event_type)
    
    # ==========================================
    # WORK ORDER EVENTS
    # ==========================================
    
    WorkOrderCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [work_order.created]
            data:
              type: object
              description: Wrapped in work_order key per standard
              required:
                - work_order
              properties:
                work_order:
                  type: object
                  required:
                    - id
                    - work_order_id
                    - company_id
                  properties:
                    id:
                      type: string
                      format: uuid
                      description: Primary work order ID
                    work_order_id:
                      type: string
                      format: uuid
                      description: Alias for id (backward compatibility)
                    company_id:
                      type: string
                      format: uuid
                    work_order_number:
                      type: string
                      example: WO-2025-001
                    phase_name:
                      type: string
                      example: Install HVAC System
                    status:
                      type: string
                      enum: [created, draft, ready, assigned, active, on-hold, complete-pending, complete, closed, cancelled]
                    priority:
                      type: string
                      enum: [low, medium, high, urgent]
                      nullable: true
                    project_id:
                      type: string
                      format: uuid
                    project:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        code:
                          type: string
                        name:
                          type: string
                    site_lead_id:
                      type: string
                      format: uuid
                      nullable: true
                    site_lead_emp_number:
                      type: string
                      nullable: true
                      description: Alternative to `site_lead_id` — resolve site lead by human-readable emp_number (company-scoped). Provide either the UUID or the emp_number, not both.
                    site_lead:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        email:
                          type: string
                    vendor_id:
                      type: string
                      format: uuid
                      nullable: true
                    vendor:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                    start_date:
                      type: string
                      format: date
                      nullable: true
                    end_date:
                      type: string
                      format: date
                      nullable: true
                    location_id:
                      type: string
                      format: uuid
                      nullable: true
                      description: Customer location ID for the job site
                    site_address:
                      type: object
                      nullable: true
                      description: Ad-hoc site address (takes priority over location_id). Accepts an optional `gps_coordinates` string ("lat, lng") in decimal degrees.
                      properties:
                        address_line_1:
                          type: string
                        address_line_2:
                          type: string
                        city:
                          type: string
                        province:
                          type: string
                        postal_code:
                          type: string
                        country:
                          type: string
                        gps_coordinates:
                          type: string
                          nullable: true
                          example: "50.145580, -95.788568"
                          description: 'Optional GPS coordinates as "latitude, longitude" (decimal degrees, lat -90..90, lng -180..180). Paste directly from a maps app.'
                    location:
                      type: object
                      nullable: true
                      description: Resolved location from location_id join
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        address_line_1:
                          type: string
                        address_line_2:
                          type: string
                        city:
                          type: string
                        province:
                          type: string
                        postal_code:
                          type: string
                        country:
                          type: string
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
                    is_migration:
                      type: boolean
                      default: false
                      description: "`true` for records imported via the Data Migration wizard"
                    is_overhead:
                      type: boolean
                      default: false
                      description: "`true` for auto-created Project Overhead work orders used for pre-setup/estimating costs"
          example:
            event_id: "evt_wo_created_001"
            event_type: "work_order.created"
            timestamp: "2025-01-15T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              work_order:
                id: "wo_12345678-1234-1234-1234-123456789abc"
                work_order_id: "wo_12345678-1234-1234-1234-123456789abc"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                work_order_number: "WO-2025-001"
                phase_name: "Install HVAC System"
                status: "created"
                priority: "medium"
                project_id: "proj_12345678-1234-1234-1234-123456789abc"
                project:
                  id: "proj_12345678-1234-1234-1234-123456789abc"
                  code: "PRJ-2025-001"
                  name: "Main Street Building"
                site_lead:
                  id: "emp_12345678-1234-1234-1234-123456789abc"
                  name: "John Smith"
                  email: "john@example.com"
                start_date: "2025-01-20"
                end_date: "2025-01-25"
                location_id: "loc-uuid-001"
                site_address: null
                location:
                  id: "loc-uuid-001"
                  name: "Main Office"
                  address_line_1: "123 Main St"
                  city: "Dallas"
                  province: "TX"
                  postal_code: "75201"
                  country: "US"
                created_at: "2025-01-15T10:00:00Z"
                updated_at: "2025-01-15T10:00:00Z"
    
    WorkOrderUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [work_order.updated]
            data:
              type: object
              required:
                - work_order
                - changes
              properties:
                work_order:
                  type: object
                  description: Complete current state of the work order (same structure as WorkOrderCreatedEvent)
                  additionalProperties: true
                changes:
                  type: object
                  description: Object containing fields that changed, each with old and new values
                  additionalProperties:
                    type: object
                    properties:
                      old:
                        description: Previous value
                      new:
                        description: New value
                  example:
                    status:
                      old: planning
                      new: assigned
                    end_date:
                      old: '2024-01-25'
                      new: '2024-01-27'
                    people_required:
                      old: 5
                      new: 7
    
    WorkOrderStatusChangedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [work_order.status_changed]
            data:
              type: object
              required:
                - work_order
                - status_change
              properties:
                work_order:
                  type: object
                  description: Complete current state of the work order (same structure as WorkOrderCreatedEvent). Includes is_overhead field.
                  additionalProperties: true
                  properties:
                    is_overhead:
                      type: boolean
                      description: Whether this is an auto-created Project Overhead work order
                status_change:
                  type: object
                  required:
                    - old
                    - new
                    - trigger
                  properties:
                    old:
                      type: string
                      example: assigned
                    new:
                      type: string
                      example: active
                    changed_at:
                      type: string
                      format: date-time
                      description: Timestamp when the status change occurred
                    trigger:
                      type: string
                      description: What caused the status transition
                      enum:
                        - manual
                        - daily_report
                        - completion_report
                        - completion_report_auto_approved
                        - close
                        - direct_complete
    
    WorkOrderProgressUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [work_order.progress_updated]
            data:
              type: object
              description: |
                Lightweight work order payload with scope_of_work and progress_cache.
                Triggered when daily/completion reports update task completion states.
              required:
                - work_order_id
                - scope_of_work
                - progress_cache
                - trigger_source
                - trigger_report_id
              properties:
                work_order_id:
                  type: string
                  format: uuid
                  description: Work order ID
                work_order_number:
                  type: string
                  example: WO-001
                phase_name:
                  type: string
                  example: Install Phase 1
                status:
                  type: string
                  example: active
                project_id:
                  type: string
                  format: uuid
                project_code:
                  type: string
                  example: PRJ-001
                project_name:
                  type: string
                  example: Customer Building A
                site_lead_id:
                  type: string
                  format: uuid
                site_lead_emp_number:
                  type: string
                  nullable: true
                  description: Alternative to `site_lead_id` — resolve site lead by human-readable emp_number (company-scoped). Provide either the UUID or the emp_number, not both.
                site_lead_name:
                  type: string
                  example: John Smith
                scope_of_work:
                  type: object
                  description: Full scope of work JSON with checklists and tasks
                  properties:
                    checklists:
                      type: array
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          name:
                            type: string
                          tasks:
                            type: array
                            items:
                              type: object
                              properties:
                                id:
                                  type: string
                                  format: uuid
                                text:
                                  type: string
                                completed:
                                  type: boolean
                                completed_at:
                                  type: string
                                  format: date-time
                                  nullable: true
                                  description: ISO timestamp when task was marked complete
                                fieldValues:
                                  type: array
                                  items:
                                    type: object
                progress_cache:
                  type: object
                  description: Pre-computed progress metrics
                  properties:
                    total_tasks:
                      type: integer
                      example: 10
                    completed_tasks:
                      type: integer
                      example: 7
                    progress_percentage:
                      type: integer
                      example: 70
                    last_report_id:
                      type: string
                      format: uuid
                    last_report_date:
                      type: string
                      format: date
                    last_report_type:
                      type: string
                      enum: [daily, completion]
                    last_reported_by_name:
                      type: string
                    updated_at:
                      type: string
                      format: date-time
                trigger_source:
                  type: string
                  enum: [daily_report, completion_report, report_update, completion_approved]
                  description: What triggered this progress update
                trigger_report_id:
                  type: string
                  format: uuid
                  description: ID of the report that caused this update
                updated_at:
                  type: string
                  format: date-time

    # ==========================================
    # PROJECT EVENTS
    # ==========================================
    
    ProjectCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [project.created]
            data:
              type: object
              required:
                - project
              properties:
                project:
                  type: object
                  description: |
                    Canonical project payload built by `buildProjectPayload` (server-side).
                    Internal-only columns (`company_id`, `is_migration`, `is_sample`,
                    `created_by_employee_id`, `updated_by_employee_id`, `app_metadata`)
                    are stripped. The same shape is returned by the REST API
                    (`GET/POST/PATCH /v1/projects`) and emitted on every project webhook
                    event (`created`, `updated`, `deleted`, `closed`, `status_changed`).
                  required:
                    - id
                  properties:
                    id:
                      type: string
                      format: uuid
                    project_code:
                      type: string
                      example: PRJ-2025-001
                    project_name:
                      type: string
                      example: Downtown Office Renovation
                    description:
                      type: string
                      nullable: true
                    status:
                      type: string
                      description: |
                        Project status. Values depend on company's `project_status_mode`:
                        - Simple mode: "active", "closed"
                        - Advanced mode: "lead", "estimating", "pending_approval", "approved", "active", "complete", "closed", "job_lost"
                    priority:
                      type: string
                      enum: [low, normal, high, urgent]
                    closed_at:
                      type: string
                      format: date-time
                      nullable: true
                    job_lost_reason:
                      type: string
                      nullable: true
                      description: Populated only when status is `job_lost`.
                    estimation_due_date:
                      type: string
                      format: date
                      nullable: true
                      description: Populated when status is `estimating`.
                    customer_id:
                      type: string
                      format: uuid
                      nullable: true
                    location_id:
                      type: string
                      format: uuid
                      nullable: true
                    estimator_employee_id:
                      type: string
                      format: uuid
                      nullable: true
                    estimator_emp_number:
                      type: string
                      nullable: true
                      description: Alternative to `estimator_employee_id` — resolve estimator by human-readable emp_number (company-scoped). Provide either the UUID or the emp_number, not both.
                    color_index:
                      type: integer
                      nullable: true
                    customer_po_numbers:
                      type: array
                      items:
                        type: string
                      description: External customer PO numbers tracked on this project.
                    equipment:
                      type: array
                      items:
                        type: string
                      nullable: true
                    custom_fields:
                      type: object
                      additionalProperties: true
                      nullable: true
                    docs:
                      type: array
                      items:
                        type: object
                      description: Project documents (links and storage file references).
                    site_address:
                      type: object
                      nullable: true
                      description: Ad-hoc job site address, independent of the customer location. Accepts an optional `gps_coordinates` string ("lat, lng") in decimal degrees.
                      properties:
                        address_line_1: { type: string }
                        address_line_2: { type: string }
                        city: { type: string }
                        province: { type: string }
                        postal_code: { type: string }
                        country: { type: string }
                        gps_coordinates: { type: string, nullable: true, example: "50.145580, -95.788568", description: 'Optional GPS coordinates as "latitude, longitude" (decimal degrees).' }
                    customer:
                      type: object
                      nullable: true
                      properties:
                        id: { type: string, format: uuid }
                        name: { type: string }
                        email: { type: string, nullable: true }
                        phone: { type: string, nullable: true }
                    location:
                      type: object
                      nullable: true
                      properties:
                        id: { type: string, format: uuid }
                        name: { type: string }
                        address_line_1: { type: string, nullable: true }
                        address_line_2: { type: string, nullable: true }
                        city: { type: string, nullable: true }
                        province: { type: string, nullable: true }
                        postal_code: { type: string, nullable: true }
                        country: { type: string, nullable: true }
                    estimator:
                      type: object
                      nullable: true
                      description: Populated when `estimator_employee_id` is set.
                      properties:
                        id: { type: string, format: uuid }
                        name: { type: string }
                        email: { type: string, nullable: true }
                    project_owners:
                      type: array
                      description: |
                        Employees assigned as project owners (max 2). Each entry wraps the
                        employee under an `employee` key (mirrors the DB join shape).
                      items:
                        type: object
                        properties:
                          employee:
                            type: object
                            properties:
                              id: { type: string, format: uuid }
                              name: { type: string }
                              email: { type: string, nullable: true }
                    project_contacts:
                      type: array
                      description: Customer contacts linked to the project.
                      items:
                        type: object
                        properties:
                          is_primary:
                            type: boolean
                          contact:
                            type: object
                            properties:
                              id: { type: string, format: uuid }
                              first_name: { type: string }
                              last_name: { type: string }
                              email: { type: string, nullable: true }
                              phone: { type: string, nullable: true }
                              mobile: { type: string, nullable: true }
                              title: { type: string, nullable: true }
                              notes: { type: string, nullable: true }
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time

    ProjectUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [project.updated]
            data:
              type: object
              required:
                - project
                - changes
              properties:
                project:
                  type: object
                  description: Complete current state of the project (same canonical shape as `ProjectCreatedEvent.data.project`).
                  additionalProperties: true
                changes:
                  type: object
                  description: Object containing fields that changed (currently empty, will be implemented)
                  additionalProperties:
                    type: object
                    properties:
                      old:
                        description: Previous value
                      new:
                        description: New value
    
    ProjectDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [project.deleted]
            data:
              type: object
              required:
                - project
              properties:
                project:
                  type: object
                  required:
                    - id
                    - company_id
                    - deleted_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owned this project
                    project_name:
                      type: string
                    deleted_at:
                      type: string
                      format: date-time
    
    ProjectClosedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [project.closed]
            data:
              type: object
              required:
                - project
              properties:
                project:
                  type: object
                  required:
                    - id
                    - company_id
                    - closed_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owns this project
                    project_name:
                      type: string
                    project_code:
                      type: string
                    closed_at:
                      type: string
                      format: date-time
          example:
            event_type: "project.closed"
            event_id: "evt_abc123"
            timestamp: "2025-01-15T14:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              project:
                id: "550e8400-e29b-41d4-a716-446655440000"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                project_code: "PRJ-2025-100"
                project_name: "Skyline Tower Construction"
                closed_at: "2025-01-15T14:00:00Z"
    
    ProjectStatusChangedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [project.status_changed]
            data:
              type: object
              required:
                - project
                - status_change
              properties:
                project:
                  type: object
                  description: |
                    Complete current state of the project — same canonical shape as
                    `ProjectCreatedEvent.data.project` (built by `buildProjectPayload`).
                    Includes `customer`, `location`, `estimator`, `project_owners`,
                    `project_contacts`, `site_address`, `customer_po_numbers`, etc.
                  additionalProperties: true
                  required:
                    - id
                    - status
                  properties:
                    id:
                      type: string
                      format: uuid
                    status:
                      type: string
                      description: Current status after the change.
                status_change:
                  type: object
                  required:
                    - old
                    - new
                    - changed_at
                    - trigger
                  properties:
                    old:
                      type: string
                      description: Previous status value
                    new:
                      type: string
                      description: New status value
                    changed_at:
                      type: string
                      format: date-time
                      description: When the status change occurred
                    trigger:
                      type: string
                      description: What caused the status transition
                      enum:
                        - manual
                        - work_order_created
                        - auto_complete
                        - close
                        - reopen
          example:
            event_type: "project.status_changed"
            event_id: "evt_status_123"
            timestamp: "2026-01-19T12:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              project:
                id: "550e8400-e29b-41d4-a716-446655440000"
                project_code: "PRJ-2026-001"
                project_name: "Downtown Office Complex"
                status: "active"
                priority: "normal"
                job_lost_reason: null
                customer:
                  id: "c1111111-1111-1111-1111-111111111111"
                  name: "Acme Construction"
                location:
                  id: "l2222222-2222-2222-2222-222222222222"
                  name: "Main Site"
                project_owners:
                  - employee:
                      id: "e3333333-3333-3333-3333-333333333333"
                      name: "Jane Smith"
                      email: "jane@company.com"
                project_contacts: []
              status_change:
                old: "approved"
                new: "active"
                changed_at: "2026-01-19T12:00:00Z"
                trigger: "work_order_created"
    
    # ==========================================
    # WORK ORDER EVENTS (ADDITIONAL)
    # ==========================================
    
    
    # ==========================================
    # REPORT EVENTS
    # ==========================================
    
    DailyReportSubmittedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [report.daily_submitted]
            data:
              type: object
              required:
                - work_order_id
                - report_date
              properties:
                work_order_id:
                  type: string
                  description: Work order number
                report_date:
                  type: string
                  format: date
                submitted_by:
                  type: string
                  description: Employee number (emp_number) who submitted the report
                is_auto_approved:
                  type: boolean
                project_name:
                  type: string
                  nullable: true
                phase_name:
                  type: string
                work_order_number:
                  type: string
                  nullable: true
                notes:
                  type: string
                  nullable: true
                photos:
                  type: array
                  items:
                    type: object
                    properties:
                      url:
                        type: string
                      caption:
                        type: string
                      uploadedAt:
                        type: string
                        format: date-time
                  nullable: true
                submitted_at:
                  type: string
                  format: date-time
    
    CompletionReportSubmittedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [report.completion_submitted]
            data:
              type: object
              required:
                - report
              properties:
                report:
                  type: object
                  required:
                    - id
                    - work_order_id
                    - report_type
                    - submitted_by
                    - submitted_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    work_order_id:
                      type: string
                      format: uuid
                    report_type:
                      type: string
                      enum: [completion]
                    report_date:
                      type: string
                      format: date
                    submitted_by:
                      type: object
                      properties:
                        user_id:
                          type: string
                          format: uuid
                        employee_id:
                          type: string
                          format: uuid
                          nullable: true
                        name:
                          type: string
                    notes:
                      type: string
                    scope_checklist:
                      type: object
                      properties:
                        items:
                          type: array
                          items:
                            type: object
                        changes:
                          type: array
                          items:
                            type: object
                            properties:
                              task_id:
                                type: string
                              completed:
                                type: boolean
                              timestamp:
                                type: string
                                format: date-time
                    completion_checklist:
                      type: object
                      properties:
                        items:
                          type: array
                          items:
                            type: object
                            properties:
                              item:
                                type: string
                              completed:
                                type: boolean
                              notes:
                                type: string
                    photos:
                      type: array
                      items:
                        type: string
                    submitted_at:
                      type: string
                      format: date-time
                    is_auto_approved:
                      type: boolean
                    requires_approval:
                      type: boolean
                    work_order:
                      type: object
                      properties:
                        phase_name:
                          type: string
                        work_order_number:
                          type: string
                        project_name:
                          type: string

    CompletionReportApprovedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [report.completion_approved]
            data:
              type: object
              required:
                - report_id
                - work_order_id
                - approved_by
                - approved_at
              properties:
                report_id:
                  type: string
                  format: uuid
                work_order_id:
                  type: string
                  format: uuid
                approved_by:
                  type: string
                  format: uuid
                approver_name:
                  type: string
                project_name:
                  type: string
                  nullable: true
                phase_name:
                  type: string
                work_order_number:
                  type: string
                  nullable: true
                approved_at:
                  type: string
                  format: date-time

    ReportUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [report.updated]
            data:
              type: object
              required:
                - report
                - updated_fields
                - updated_at
              properties:
                report:
                  type: object
                  required:
                    - id
                    - work_order_id
                    - report_type
                    - report_date
                  properties:
                    id:
                      type: string
                      format: uuid
                    work_order_id:
                      type: string
                      format: uuid
                    report_type:
                      type: string
                      enum: [daily, completion]
                    report_date:
                      type: string
                      format: date
                    notes:
                      type: string
                    scope_checklist:
                      type: object
                    photos:
                      type: array
                      items:
                        type: string
                    work_order:
                      type: object
                      properties:
                        phase_name:
                          type: string
                        work_order_number:
                          type: string
                        project_name:
                          type: string
                updated_fields:
                  type: array
                  items:
                    type: string
                  description: List of field names that were modified
                  example: ["notes", "scope_checklist"]
                updated_at:
                  type: string
                  format: date-time
                updated_by:
                  type: object
                  properties:
                    user_id:
                      type: string
                      format: uuid
                    employee_id:
                      type: string
                      format: uuid
                      nullable: true
                    name:
                      type: string

    WorkOrderDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [work_order.deleted]
            data:
              type: object
              required:
                - work_order_id
                - deleted_at
              properties:
                work_order_id:
                  type: string
                  format: uuid
                phase_name:
                  type: string
                work_order_number:
                  type: string
                project_id:
                  type: string
                  format: uuid
                project_name:
                  type: string
                deleted_at:
                  type: string
                  format: date-time

    # ==========================================
    # CUSTOMER EVENTS
    # ==========================================
    
    CustomerCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [customer.created]
            data:
              type: object
              required:
                - customer
              properties:
                customer:
                  type: object
                  required:
                    - id
                    - company_id
                    - name
                    - created_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owns this customer
                    name:
                      type: string
                    type:
                      type: string
                      enum: [Commercial, Residential, Government, Non-Profit]
                      nullable: true
                    email:
                      type: string
                      format: email
                      nullable: true
                    phone:
                      type: string
                      nullable: true
                    website:
                      type: string
                      nullable: true
                    status:
                      type: string
                      description: Customer status (Active, Inactive)
                    notes:
                      type: string
                      nullable: true
                    account_manager:
                      type: object
                      nullable: true
                      description: Assigned account manager details
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        emp_number:
                          type: string
                    custom_fields:
                      type: object
                      additionalProperties: true
                      nullable: true
                    tax_exemptions:
                      type: array
                      description: Tax exemption records for this customer
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          exemption_type:
                            type: string
                            enum: [resale, government, non_profit, tribal, export, other]
                          jurisdiction_level:
                            type: string
                            enum: [global, federal, regional]
                          jurisdiction_name:
                            type: string
                            nullable: true
                          id_number:
                            type: string
                          certificate_name:
                            type: string
                            nullable: true
                          expires_at:
                            type: string
                            format: date
                            nullable: true
                          status:
                            type: string
                            enum: [active, expired]
                            description: Computed from expires_at
                          notes:
                            type: string
                            nullable: true
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
    
    CustomerUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [customer.updated]
            data:
              type: object
              required:
                - customer
              properties:
                customer:
                  type: object
                  required:
                    - id
                    - company_id
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owns this customer
                    additionalProperties: true
                    description: Complete current state of the customer including tax_exemptions array
    
    CustomerDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [customer.deleted]
            data:
              type: object
              required:
                - customer
              properties:
                customer:
                  type: object
                  required:
                    - id
                    - company_id
                    - deleted_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owned this customer
                    name:
                      type: string
                    deleted_at:
                      type: string
                      format: date-time
    
    # ==========================================
    # LOCATION EVENTS
    # ==========================================
    
    LocationCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [location.created]
            data:
              type: object
              additionalProperties: true
              properties:
                id:
                  type: string
                  format: uuid
                customer_id:
                  type: string
                  format: uuid
                customer_name:
                  type: string
                name:
                  type: string
                address_line_1:
                  type: string
                address_line_2:
                  type: string
                  nullable: true
                city:
                  type: string
                province:
                  type: string
                postal_code:
                  type: string
                  nullable: true
                country:
                  type: string
                created_at:
                  type: string
                  format: date-time
    
    LocationUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [location.updated]
            data:
              type: object
              additionalProperties: true
              description: Complete current state of the location
    
    LocationDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [location.deleted]
            data:
              type: object
              required:
                - id
                - deleted_at
              properties:
                id:
                  type: string
                  format: uuid
                customer_id:
                  type: string
                  format: uuid
                name:
                  type: string
                deleted_at:
                  type: string
                  format: date-time
    
    # ==========================================
    # CONTACT EVENTS
    # ==========================================
    
    ContactCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [contact.created]
            data:
              type: object
              additionalProperties: true
              properties:
                id:
                  type: string
                  format: uuid
                customer_id:
                  type: string
                  format: uuid
                customer_name:
                  type: string
                location_id:
                  type: string
                  format: uuid
                  nullable: true
                location_name:
                  type: string
                  nullable: true
                first_name:
                  type: string
                last_name:
                  type: string
                email:
                  type: string
                  format: email
                  nullable: true
                phone:
                  type: string
                  nullable: true
                title:
                  type: string
                  nullable: true
                phone_ext:
                  type: string
                  nullable: true
                mobile:
                  type: string
                  nullable: true
                contact_tags:
                  type: array
                  items:
                    type: string
                  nullable: true
                notes:
                  type: string
                  nullable: true
                updated_at:
                  type: string
                  format: date-time
                is_primary:
                  type: boolean
                created_at:
                  type: string
                  format: date-time
    
    ContactUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [contact.updated]
            data:
              type: object
              additionalProperties: true
              properties:
                id:
                  type: string
                  format: uuid
                customer_id:
                  type: string
                  format: uuid
                customer_name:
                  type: string
                location_id:
                  type: string
                  format: uuid
                  nullable: true
                location_name:
                  type: string
                  nullable: true
                first_name:
                  type: string
                last_name:
                  type: string
                email:
                  type: string
                  format: email
                  nullable: true
                phone:
                  type: string
                  nullable: true
                title:
                  type: string
                  nullable: true
                phone_ext:
                  type: string
                  nullable: true
                mobile:
                  type: string
                  nullable: true
                contact_tags:
                  type: array
                  items:
                    type: string
                  nullable: true
                notes:
                  type: string
                  nullable: true
                is_primary:
                  type: boolean
                updated_at:
                  type: string
                  format: date-time
    
    ContactDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [contact.deleted]
            data:
              type: object
              required:
                - id
                - deleted_at
              properties:
                id:
                  type: string
                  format: uuid
                customer_id:
                  type: string
                  format: uuid
                customer_name:
                  type: string
                location_id:
                  type: string
                  format: uuid
                  nullable: true
                location_name:
                  type: string
                  nullable: true
                first_name:
                  type: string
                last_name:
                  type: string
                email:
                  type: string
                  format: email
                  nullable: true
                phone:
                  type: string
                  nullable: true
                title:
                  type: string
                  nullable: true
                phone_ext:
                  type: string
                  nullable: true
                mobile:
                  type: string
                  nullable: true
                contact_tags:
                  type: array
                  items:
                    type: string
                  nullable: true
                notes:
                  type: string
                  nullable: true
                is_primary:
                  type: boolean
                deleted_at:
                  type: string
                  format: date-time
    
    # ==========================================
    # EMPLOYEE EVENTS
    # ==========================================
    
    EmployeeCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [employee.created]
            data:
              type: object
              required:
                - employee
              properties:
                employee:
                  type: object
                  required:
                    - id
                    - company_id
                    - emp_number
                    - name
                    - email
                  properties:
                    id:
                      type: string
                      format: uuid
                      description: Unique identifier for the employee
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owns this employee
                    emp_number:
                      type: string
                      description: Employee number/code
                    name:
                      type: string
                      description: Employee full name
                    email:
                      type: string
                      format: email
                      description: Employee email address
                    phone:
                      type: string
                      nullable: true
                      description: Employee phone number
                    role:
                      type: string
                      nullable: true
                      description: Resolved role name (e.g. "Site Supervisor"). The underlying DB column is `role_id`; outbound webhooks always emit the resolved name string, never the UUID.
                    department:
                      type: string
                      nullable: true
                      description: Resolved department name. The underlying DB column is `department_id`; outbound webhooks always emit the resolved name string, never the UUID.

                    app_roles:
                      type: array
                      items:
                        type: string
                        enum: [site_worker, site_lead, project_owner, scheduler, manager, administrator]
                      description: Array of role designations (e.g., ["site_worker", "site_lead"])
                    start_date:
                      type: string
                      format: date
                      nullable: true
                      description: Date when employee started working (YYYY-MM-DD)
                    birthday:
                      type: string
                      format: date
                      nullable: true
                      description: Employee's date of birth (YYYY-MM-DD)
                    address:
                      type: object
                      nullable: true
                      description: Employee's address
                      properties:
                        street:
                          type: string
                        city:
                          type: string
                        state:
                          type: string
                        zip:
                          type: string
                        country:
                          type: string
                    emergency_contact_name:
                      type: string
                      nullable: true
                      description: Name of emergency contact
                    emergency_contact_relationship:
                      type: string
                      nullable: true
                      description: Relationship to emergency contact
                    emergency_phone_1:
                      type: string
                      nullable: true
                      description: Primary emergency contact phone number
                    emergency_phone_2:
                      type: string
                      nullable: true
                      description: Secondary emergency contact phone number
                    emergency_email:
                      type: string
                      format: email
                      nullable: true
                      description: Emergency contact email address
                    created_at:
                      type: string
                      format: date-time
                      description: Timestamp when the employee was created
                    updated_at:
                      type: string
                      format: date-time
                      description: Timestamp when the employee was last updated
          example:
            event_id: "evt_employee_created_001"
            event_type: "employee.created"
            timestamp: "2025-01-15T09:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              employee:
                id: "123e4567-e89b-12d3-a456-426614174000"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                emp_number: "EMP-12345"
                name: "John Martinez"
                email: "john.martinez@company.com"
                phone: "+1-555-1234"
                role: "Site Supervisor"
                department: "Field Operations"
                app_roles: ["site_worker"]
                start_date: "2024-01-15"
                birthday: "1985-06-20"
                address:
                  street: "123 Main St"
                  city: "Austin"
                  state: "TX"
                  zip: "78701"
                  country: "USA"
                created_at: "2025-01-15T09:00:00Z"
                updated_at: "2025-01-15T09:00:00Z"
    
    EmployeeUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [employee.updated]
            data:
              type: object
              required:
                - employee
              properties:
                employee:
                  type: object
                  required:
                    - id
                    - company_id
                    - emp_number
                    - name
                    - email
                  properties:
                    id:
                      type: string
                      format: uuid
                      description: Unique identifier for the employee
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owns this employee
                    emp_number:
                      type: string
                      description: Employee number/code
                    name:
                      type: string
                      description: Employee full name
                    email:
                      type: string
                      format: email
                      description: Employee email address
                    phone:
                      type: string
                      nullable: true
                      description: Employee phone number
                    role:
                      type: string
                      nullable: true
                      description: Resolved role name. The underlying DB column is `role_id`; outbound webhooks always emit the resolved name string, never the UUID.
                    department:
                      type: string
                      nullable: true
                      description: Resolved department name. The underlying DB column is `department_id`; outbound webhooks always emit the resolved name string, never the UUID.

                    app_roles:
                      type: array
                      items:
                        type: string
                        enum: [site_worker, site_lead, project_owner, scheduler, manager, administrator]
                      description: Array of role designations (e.g., ["site_worker", "site_lead"])
                    start_date:
                      type: string
                      format: date
                      nullable: true
                    birthday:
                      type: string
                      format: date
                      nullable: true
                    address:
                      type: object
                      nullable: true
                      properties:
                        street:
                          type: string
                        city:
                          type: string
                        state:
                          type: string
                        zip:
                          type: string
                        country:
                          type: string
                    emergency_contact_name:
                      type: string
                      nullable: true
                    emergency_contact_relationship:
                      type: string
                      nullable: true
                    emergency_phone_1:
                      type: string
                      nullable: true
                    emergency_phone_2:
                      type: string
                      nullable: true
                    emergency_email:
                      type: string
                      format: email
                      nullable: true
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_employee_updated_001"
            event_type: "employee.updated"
            timestamp: "2025-01-16T14:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              employee:
                id: "123e4567-e89b-12d3-a456-426614174000"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                emp_number: "EMP-12345"
                name: "John Martinez"
                email: "john.martinez@company.com"
                role: "Senior Site Supervisor"
                department: "Field Operations"
                app_roles: ["site_lead", "site_worker"]
                updated_at: "2025-01-16T14:30:00Z"
    
    EmployeeDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [employee.deleted]
            data:
              type: object
              required:
                - employee
              properties:
                employee:
                  type: object
                  required:
                    - id
                    - company_id
                    - deleted_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owned this employee
                    emp_number:
                      type: string
                    name:
                      type: string
                    email:
                      type: string
                      format: email
                      nullable: true
                    deleted_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_employee_deleted_001"
            event_type: "employee.deleted"
            timestamp: "2025-01-17T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              employee:
                id: "123e4567-e89b-12d3-a456-426614174000"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                emp_number: "EMP-12345"
                name: "John Martinez"
                email: "john.martinez@company.com"
                deleted_at: "2025-01-17T10:00:00Z"
    
    # ==========================================
    # TIME OFF EVENTS
    # ==========================================
    
    TimeOffCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [time_off.created]
            data:
              type: object
              required:
                - id
                - employee_id
                - emp_number
                - start_date
                - end_date
              properties:
                id:
                  type: string
                  format: uuid
                  description: Unique time off record ID
                employee_id:
                  type: string
                  format: uuid
                  description: Employee ID
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                  description: Employee name
                start_date:
                  type: string
                  format: date
                  description: Start date of time off
                end_date:
                  type: string
                  format: date
                  description: End date of time off
                start_time:
                  type: string
                  nullable: true
                  description: Start time (HH:MM) for partial day
                end_time:
                  type: string
                  nullable: true
                  description: End time (HH:MM) for partial day
                time_off_type:
                  type: string
                  enum: [vacation, sick_leave, personal, unpaid, training, other]
                status:
                  type: string
                  enum: [approved, pending, denied]
                notes:
                  type: string
                  nullable: true
                created_at:
                  type: string
                  format: date-time
          example:
            event_id: "evt_timeoff_created_001"
            event_type: "time_off.created"
            timestamp: "2025-01-15T09:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              start_date: "2025-02-01"
              end_date: "2025-02-05"
              start_time: null
              end_time: null
              time_off_type: "vacation"
              status: "approved"
              notes: "Annual vacation"
              created_at: "2025-01-15T09:00:00Z"
    
    TimeOffUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [time_off.updated]
            data:
              type: object
              required:
                - id
                - employee_id
                - emp_number
              properties:
                id:
                  type: string
                  format: uuid
                  description: Unique time off record ID
                employee_id:
                  type: string
                  format: uuid
                  description: Employee ID
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                  description: Employee name
                start_date:
                  type: string
                  format: date
                  description: Start date of time off
                end_date:
                  type: string
                  format: date
                  description: End date of time off
                start_time:
                  type: string
                  nullable: true
                  description: Start time (HH:MM) for partial day
                end_time:
                  type: string
                  nullable: true
                  description: End time (HH:MM) for partial day
                time_off_type:
                  type: string
                  enum: [vacation, sick_leave, personal, unpaid, training, other]
                status:
                  type: string
                  enum: [approved, pending, denied]
                notes:
                  type: string
                  nullable: true
                updated_at:
                  type: string
                  format: date-time
          example:
            event_id: "evt_timeoff_updated_001"
            event_type: "time_off.updated"
            timestamp: "2025-01-15T10:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              start_date: "2025-02-01"
              end_date: "2025-02-07"
              time_off_type: "vacation"
              status: "approved"
              notes: "Extended vacation"
              updated_at: "2025-01-15T10:30:00Z"
    
    TimeOffDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [time_off.deleted]
            data:
              type: object
              required:
                - id
                - employee_id
                - emp_number
                - deleted_at
              properties:
                id:
                  type: string
                  format: uuid
                  description: Unique time off record ID
                employee_id:
                  type: string
                  format: uuid
                  description: Employee ID
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                  description: Employee name
                start_date:
                  type: string
                  format: date
                  description: Start date of deleted time off
                end_date:
                  type: string
                  format: date
                  description: End date of deleted time off
                deleted_at:
                  type: string
                  format: date-time
          example:
            event_id: "evt_timeoff_deleted_001"
            event_type: "time_off.deleted"
            timestamp: "2025-01-15T11:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              start_date: "2025-02-01"
              end_date: "2025-02-05"
              deleted_at: "2025-01-15T11:00:00Z"

    # ==========================================
    # VENDOR EVENTS
    # ==========================================
    
    VendorCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.created]
            data:
              type: object
              required:
                - vendor
              properties:
                vendor:
                  type: object
                  required:
                    - id
                    - company_id
                    - vendor_name
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owns this vendor
                    vendor_name:
                      type: string
                    legal_name:
                      type: string
                      nullable: true
                    type:
                      type: string
                      enum: [vendor, distributor, manufacturer, subcontractor, rental_provider, supplier, partner, operations_facility, professional_services, technology_saas, admin_office, fleet_equipment, marketing_growth, hr_culture]
                      description: Vendor classification type
                    tags:
                      type: array
                      items:
                        type: string
                      description: Freeform tags for categorization
                    trade_specialty:
                      oneOf:
                        - type: string
                        - type: array
                          items:
                            type: string
                    approval_status:
                      type: string
                      enum: [pending, approved, expired]
                      nullable: true
                    insurance_expiry_date:
                      type: string
                      format: date
                      nullable: true
                    compliance_expiry_date:
                      type: string
                      format: date
                      nullable: true
                    contacts:
                      type: array
                      description: Vendor contacts
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          name:
                            type: string
                          email:
                            type: string
                            format: email
                            nullable: true
                          phone:
                            type: string
                            nullable: true
                          role:
                            type: string
                            nullable: true
                          is_primary:
                            type: boolean
                    locations:
                      type: array
                      description: Vendor locations
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          name:
                            type: string
                          address:
                            type: object
                            nullable: true
                          phone:
                            type: string
                            nullable: true
                          is_default:
                            type: boolean
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_vendor_created_001"
            event_type: "vendor.created"
            timestamp: "2025-01-15T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              vendor:
                id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                vendor_name: "ABC Construction"
                type: "subcontractor"
                tags: ["preferred", "local"]
                trade_specialty: "General Contracting"
                approval_status: "approved"
                contacts:
                  - id: "c1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    name: "John Smith"
                    email: "john@abc.com"
                    is_primary: true
                locations:
                  - id: "l1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    name: "Main Office"
                    is_default: true
                created_at: "2025-01-15T10:00:00Z"
    
    VendorUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.updated]
            data:
              type: object
              required:
                - vendor
              properties:
                vendor:
                  type: object
                  required:
                    - id
                    - company_id
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owns this vendor
                    vendor_name:
                      type: string
                    type:
                      type: string
                      enum: [vendor, distributor, manufacturer, subcontractor, rental_provider, supplier, partner, operations_facility, professional_services, technology_saas, admin_office, fleet_equipment, marketing_growth, hr_culture]
                    tags:
                      type: array
                      items:
                        type: string
                    trade_specialty:
                      oneOf:
                        - type: string
                        - type: array
                          items:
                            type: string
                    approval_status:
                      type: string
                      enum: [pending, approved, expired]
                      nullable: true
                    legal_name:
                      type: string
                      nullable: true
                    rating:
                      type: number
                      minimum: 0
                      maximum: 5
                      nullable: true
                    website:
                      type: string
                      nullable: true
                    notes:
                      type: string
                      nullable: true
                    regions:
                      type: array
                      items:
                        type: string
                      nullable: true
                    custom_fields:
                      type: object
                      additionalProperties: true
                      nullable: true
                    insurance_expiry_date:
                      type: string
                      format: date
                      nullable: true
                    compliance_expiry_date:
                      type: string
                      format: date
                      nullable: true
                    contacts:
                      type: array
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          name:
                            type: string
                          email:
                            type: string
                            format: email
                            nullable: true
                          phone:
                            type: string
                            nullable: true
                          is_primary:
                            type: boolean
                    locations:
                      type: array
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          name:
                            type: string
                          address:
                            type: object
                            nullable: true
                          is_default:
                            type: boolean
                    updated_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_vendor_updated_001"
            event_type: "vendor.updated"
            timestamp: "2025-01-16T09:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              vendor:
                id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                vendor_name: "ABC Construction"
                type: "subcontractor"
                tags: ["preferred", "local", "certified"]
                approval_status: "approved"
                contacts:
                  - id: "c1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    name: "John Smith"
                    email: "john@abc.com"
                    is_primary: true
                locations:
                  - id: "l1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    name: "Main Office"
                    is_default: true
                updated_at: "2025-01-16T09:00:00Z"
    
    VendorDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.deleted]
            data:
              type: object
              required:
                - vendor
              properties:
                vendor:
                  type: object
                  required:
                    - id
                    - company_id
                    - deleted_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                      description: Company that owned this vendor
                    vendor_name:
                      type: string
                    type:
                      type: string
                      enum: [vendor, distributor, manufacturer, subcontractor, rental_provider, supplier, partner, operations_facility, professional_services, technology_saas, admin_office, fleet_equipment, marketing_growth, hr_culture]
                    deleted_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_vendor_deleted_001"
            event_type: "vendor.deleted"
            timestamp: "2025-01-17T14:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              vendor:
                id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                vendor_name: "ABC Construction"
                type: "subcontractor"
                deleted_at: "2025-01-17T14:00:00Z"

    # ==========================================
    # VENDOR CONTACT EVENTS
    # ==========================================

    VendorContactCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.contact.created]
            data:
              type: object
              required:
                - contact
                - vendor
              properties:
                contact:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                    vendor_id:
                      type: string
                      format: uuid
                    name:
                      type: string
                    email:
                      type: string
                      nullable: true
                    phone:
                      type: string
                      nullable: true
                    role:
                      type: string
                      nullable: true
                    is_primary:
                      type: boolean
                vendor:
                  type: object
                  description: Full parent vendor with contacts and locations
          example:
            event_id: "evt_vc_created_001"
            event_type: "vendor.contact.created"
            timestamp: "2025-01-15T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              contact:
                id: "c1b2c3d4-e5f6-7890-abcd-ef1234567890"
                vendor_id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                name: "John Smith"
                email: "john@abc.com"
                is_primary: true
              vendor:
                id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                vendor_name: "ABC Construction"

    VendorContactUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.contact.updated]
            data:
              type: object
              required:
                - contact
                - vendor
              properties:
                contact:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                    vendor_id:
                      type: string
                      format: uuid
                    name:
                      type: string
                    email:
                      type: string
                      nullable: true
                    phone:
                      type: string
                      nullable: true
                    role:
                      type: string
                      nullable: true
                    is_primary:
                      type: boolean
                vendor:
                  type: object
                  description: Full parent vendor with contacts and locations

    VendorContactDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.contact.deleted]
            data:
              type: object
              required:
                - contact
                - vendor
              properties:
                contact:
                  type: object
                  required:
                    - id
                    - name
                  properties:
                    id:
                      type: string
                      format: uuid
                    name:
                      type: string
                vendor:
                  type: object
                  description: Full parent vendor with contacts and locations

    # ==========================================
    # VENDOR LOCATION EVENTS
    # ==========================================

    VendorLocationCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.location.created]
            data:
              type: object
              required:
                - location
                - vendor
              properties:
                location:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                    vendor_id:
                      type: string
                      format: uuid
                    name:
                      type: string
                    address:
                      type: object
                      nullable: true
                    phone:
                      type: string
                      nullable: true
                    is_default:
                      type: boolean
                vendor:
                  type: object
                  description: Full parent vendor with contacts and locations

    VendorLocationUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.location.updated]
            data:
              type: object
              required:
                - location
                - vendor
              properties:
                location:
                  type: object
                  properties:
                    id:
                      type: string
                      format: uuid
                    vendor_id:
                      type: string
                      format: uuid
                    name:
                      type: string
                    address:
                      type: object
                      nullable: true
                    phone:
                      type: string
                      nullable: true
                    is_default:
                      type: boolean
                vendor:
                  type: object
                  description: Full parent vendor with contacts and locations

    VendorLocationDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [vendor.location.deleted]
            data:
              type: object
              required:
                - location
                - vendor
              properties:
                location:
                  type: object
                  required:
                    - id
                    - name
                  properties:
                    id:
                      type: string
                      format: uuid
                    name:
                      type: string
                vendor:
                  type: object
                  description: Full parent vendor with contacts and locations

    # ==========================================
    # PURCHASE ORDER EVENTS
    # ==========================================
    
    # Reusable PO payload schema for all po.* events
    POWebhookPayload:
      type: object
      description: Standard purchase order payload wrapped in all po.* events
      required:
        - id
        - purchase_order_id
        - company_id
        - po_number
        - status
      properties:
        id:
          type: string
          format: uuid
          description: Primary ID
        purchase_order_id:
          type: string
          format: uuid
          description: Alias for id (backward compatibility)
        company_id:
          type: string
          format: uuid
        po_number:
          type: string
        vendor_id:
          type: string
          format: uuid
          nullable: true
        vendor:
          type: object
          nullable: true
          description: Expanded vendor object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        project_id:
          type: string
          format: uuid
          nullable: true
        project:
          type: object
          nullable: true
          description: Expanded project object
          properties:
            id:
              type: string
              format: uuid
            code:
              type: string
            name:
              type: string
        work_order_id:
          type: string
          format: uuid
          nullable: true
        work_order:
          type: object
          nullable: true
          description: Expanded work order object
          properties:
            id:
              type: string
              format: uuid
            number:
              type: string
            name:
              type: string
        po_type:
          type: string
          enum: [standard, service, blanket]
          nullable: true
        destination_type:
          type: string
          enum: [job, inventory, overhead]
          nullable: true
        paid_by:
          type: string
          enum: [vendor_invoice, cash_debit, credit_card, reimbursement]
          nullable: true
        credit_card_id:
          type: string
          format: uuid
          nullable: true
        credit_card:
          type: object
          nullable: true
          properties:
            id:
              type: string
            name:
              type: string
        reimbursement_employee_id:
          type: string
          format: uuid
          nullable: true
        reimbursement_emp_number:
          type: string
          nullable: true
          description: Alternative to `reimbursement_employee_id` — resolve reimbursement employee by human-readable emp_number (company-scoped). Provide either the UUID or the emp_number, not both.
        reimbursement_employee:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
        equipment_asset_id:
          type: string
          format: uuid
          nullable: true
        equipment_asset:
          type: object
          nullable: true
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            asset_tag:
              type: string
              nullable: true
        description:
          type: string
          nullable: true
          description: Purchase order description/memo
        notes:
          type: string
          nullable: true
        budget_amount:
          type: number
          nullable: true
        not_to_exceed_amount:
          type: number
          nullable: true
        subtotal:
          type: number
          nullable: true
        tax_inclusive:
          type: boolean
          default: false
          description: >
            When false (default), tax is added on top of the subtotal.
            When true, tax is embedded in line item prices and does not increase the total.
        tax_amount:
          type: number
          nullable: true
        total_amount:
          type: number
          nullable: true
        status:
          type: string
          enum: [draft, submitted, approved, rejected, partially_received, received, partially_billed, billed, cancelled]
        line_items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              description:
                type: string
              quantity:
                type: number
              unit:
                type: string
              unit_price:
                type: number
              line_total:
                type: number
              category:
                type: string
                nullable: true
              sku:
                type: string
                nullable: true
              display_order:
                type: integer
              source_type:
                type: string
                enum: [usage_record, purchase_order, labor, po_line_item, timesheet, timesheet_entry, virtual]
                nullable: true
              source_id:
                type: string
                format: uuid
                nullable: true
              purchase_order_id:
                type: string
                format: uuid
                nullable: true
                description: Parent Purchase Order UUID. Present when source_type is po_line_item; null otherwise.
              cost_code:
                type: string
                nullable: true
              is_excluded:
                type: boolean
                default: false
        tax_lines:
          type: array
          description: |
            PO-level tax breakdown. When tax_amount > 0 but no detailed breakdown exists,
            returns a single fallback entry: [{ tax_name: "Tax", tax_rate: 0, tax_amount: <value> }].
          items:
            type: object
            properties:
              tax_name:
                type: string
              tax_rate:
                type: number
              tax_amount:
                type: number
              tax_code_id:
                type: string
                format: uuid
                nullable: true
                description: Reference to the tax code used (from tax_codes table)
              component_amounts:
                type: array
                nullable: true
                description: Per-component tax breakdown when a multi-component tax code is used
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    rate:
                      type: number
                    amount:
                      type: number
                    external_id:
                      type: string
                      nullable: true
                      description: Context-resolved external ID (sales external_id for invoices, purchase_external_id for bills/POs)
        bill_count:
          type: integer
          default: 0
          description: Number of bills linked to this PO (computed from bills array length)
        total_billed_amount:
          type: number
          nullable: true
          description: Aggregate sum of all bill total_amounts. Null when no bills exist.
        bills:
          type: array
          description: |
            Array of individual bills for multi-bill POs. Each bill has its own line items, taxes, and totals.
            Empty array when PO has not been billed.
          items:
            $ref: '#/components/schemas/POBill'
        created_at:
          type: string
          format: date-time
        submitted_at:
          type: string
          format: date-time
          nullable: true
        approved_at:
          type: string
          format: date-time
          nullable: true
        received_at:
          type: string
          format: date-time
          nullable: true
        billed_at:
          type: string
          format: date-time
          nullable: true
        updated_at:
          type: string
          format: date-time
        is_migration:
          type: boolean
          default: false
          description: "`true` for records imported via the Data Migration wizard. Migration records do not trigger outbound webhooks."
        linked_invoice:
          allOf:
            - $ref: '#/components/schemas/LinkedInvoiceRef'
          nullable: true
          description: Most recent invoice that billed this PO (since payload_version 1.1.0). `null` when not billed.



    POBill:
      type: object
      description: An individual bill/invoice linked to a purchase order
      properties:
        id:
          type: string
          format: uuid
        bill_number:
          type: integer
          description: Sequential bill number within the PO
        status:
          type: string
          enum: [draft, finalized]
        is_backorder:
          type: boolean
          default: false
          description: Whether this bill represents backordered items pending delivery
        invoice_number:
          type: string
          nullable: true
        invoice_date:
          type: string
          format: date
          nullable: true
        bill_due_date:
          type: string
          format: date
          nullable: true
        subtotal:
          type: number
          nullable: true
        tax_amount:
          type: number
          nullable: true
        total_amount:
          type: number
          nullable: true
        tax_inclusive:
          type: boolean
          default: false
          description: Whether tax amounts are included in line item prices
        finalized_at:
          type: string
          format: date-time
          nullable: true
          description: Timestamp when the bill was finalized
        line_items:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              description:
                type: string
              quantity:
                type: number
              unit:
                type: string
                nullable: true
              unit_price:
                type: number
              line_total:
                type: number
              cost_code:
                type: string
                nullable: true
                description: Cost code assigned to this line item
              product_id:
                type: string
                format: uuid
                nullable: true
                description: Product/material ID for this line item
              is_taxable:
                type: boolean
                default: true
                description: Whether this line item is subject to tax
              tax_code_id:
                type: string
                format: uuid
                nullable: true
                description: Override tax code for this line item
        tax_lines:
          type: array
          items:
            type: object
            properties:
              tax_name:
                type: string
              tax_rate:
                type: number
              tax_amount:
                type: number
              tax_code_id:
                type: string
                format: uuid
                nullable: true
              component_amounts:
                type: array
                nullable: true
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    rate:
                      type: number
                    amount:
                      type: number
                    external_id:
                      type: string
                      nullable: true
                      description: Context-resolved external ID (purchase_external_id for bill taxes)
        created_at:
          type: string
          format: date-time
    
    POCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.created]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  type: object
                  required:
                    - id
                    - purchase_order_id
                    - company_id
                    - po_number
                    - status
                    - created_at
                  properties:
                    id:
                      type: string
                      format: uuid
                      description: Primary ID
                    purchase_order_id:
                      type: string
                      format: uuid
                      description: Alias for id (backward compatibility)
                    company_id:
                      type: string
                      format: uuid
                    po_number:
                      type: string
                    vendor_id:
                      type: string
                      format: uuid
                      nullable: true
                      description: UUID of the linked vendor
                    vendor:
                      type: object
                      nullable: true
                      description: Expanded vendor object
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                    project_id:
                      type: string
                      format: uuid
                      nullable: true
                    project:
                      type: object
                      nullable: true
                      description: Expanded project object
                      properties:
                        id:
                          type: string
                          format: uuid
                        code:
                          type: string
                        name:
                          type: string
                    work_order_id:
                      type: string
                      format: uuid
                      nullable: true
                    work_order:
                      type: object
                      nullable: true
                      description: Expanded work order object
                      properties:
                        id:
                          type: string
                          format: uuid
                        number:
                          type: string
                        name:
                          type: string
                    po_type:
                      type: string
                      enum: [standard, service, blanket]
                      nullable: true
                      description: Type of purchase order
                    destination_type:
                      type: string
                      enum: [job, inventory, overhead]
                      nullable: true
                      description: Cost allocation destination
                    paid_by:
                      type: string
                      enum: [vendor_invoice, cash_debit, credit_card, reimbursement]
                      nullable: true
                      description: How this expense was or will be paid
                    credit_card_id:
                      type: string
                      format: uuid
                      nullable: true
                    credit_card:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                    reimbursement_employee_id:
                      type: string
                      format: uuid
                      nullable: true
                      description: ID of the employee to be reimbursed
                    reimbursement_emp_number:
                      type: string
                      nullable: true
                      description: Alternative to `reimbursement_employee_id` — resolve reimbursement employee by human-readable emp_number (company-scoped). Provide either the UUID or the emp_number, not both.
                    reimbursement_employee:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                    equipment_asset_id:
                      type: string
                      format: uuid
                      nullable: true
                      description: ID of linked equipment asset
                    equipment_asset:
                      type: object
                      nullable: true
                      description: Expanded equipment asset details
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        asset_tag:
                          type: string
                          nullable: true
                    notes:
                      type: string
                      nullable: true
                      description: PO notes/terms
                    budget_amount:
                      type: number
                      nullable: true
                      description: PO-level budget expectation
                    not_to_exceed_amount:
                      type: number
                      nullable: true
                      description: PO-level maximum spending cap
                    subtotal:
                      type: number
                      nullable: true
                      description: Pre-tax subtotal
                    tax_amount:
                      type: number
                      nullable: true
                      description: Tax amount
                    total_amount:
                      type: number
                      nullable: true
                    delivery_address:
                      type: string
                      nullable: true
                      description: Delivery/shipping address
                    expected_delivery_date:
                      type: string
                      format: date
                      nullable: true
                      description: Expected delivery date
                    internal_notes:
                      type: string
                      nullable: true
                      description: Internal notes (not shared with vendor)
                    status:
                      type: string
                      enum: [draft, submitted, approved, rejected, received, cancelled]
                    line_items:
                      type: array
                      items:
                        type: object
                        properties:
                          id:
                            type: string
                            format: uuid
                          description:
                            type: string
                          quantity:
                            type: number
                          unit:
                            type: string
                          unit_price:
                            type: number
                          line_total:
                            type: number
                          max_amount:
                            type: number
                            nullable: true
                            description: Optional per-item ceiling
                          category:
                            type: string
                            nullable: true
                            description: Item category
                          sku:
                            type: string
                            nullable: true
                            description: Stock keeping unit
                          display_order:
                            type: integer
                            description: Sort order
                          notes:
                            type: string
                            nullable: true
                    created_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_po_created_001"
            event_type: "po.created"
            timestamp: "2025-01-15T10:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                po_number: "PO-2025-001"
                vendor_id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                vendor:
                  id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "ACME Supplies"
                project_id: "p1b2c3d4-e5f6-7890-abcd-ef1234567890"
                project:
                  id: "p1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  code: "PROJ-001"
                  name: "Main Street Renovation"
                work_order_id: "w1b2c3d4-e5f6-7890-abcd-ef1234567890"
                work_order:
                  id: "w1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  number: "WO-00123"
                  name: "Electrical Phase"
                destination_type: "job"
                budget_amount: 5000.00
                not_to_exceed_amount: 5500.00
                total_amount: 5250.00
                status: "draft"
                line_items:
                  - id: "li_001"
                    description: "Copper wire - 12 AWG"
                    quantity: 500
                    unit: "ft"
                    unit_price: 1.50
                    line_total: 750.00
                created_at: "2025-01-15T10:30:00Z"
    
    POUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.updated]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  $ref: '#/components/schemas/POWebhookPayload'
            updated_fields:
              type: array
              description: List of field names that were modified in this update
              items:
                type: string
              example: ["status", "total_amount", "line_items"]
          example:
            event_id: "evt_po_updated_001"
            event_type: "po.updated"
            timestamp: "2025-01-15T14:20:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            updated_fields: ["budget_amount", "total_amount"]
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                po_number: "PO-2025-001"
                vendor:
                  id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "ACME Supplies"
                project:
                  id: "p1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  code: "PROJ-001"
                  name: "Main Street Renovation"
                budget_amount: 5000.00
                total_amount: 5500.00
                status: "draft"
                updated_at: "2025-01-15T14:20:00Z"
    
    POSubmittedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.submitted]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  allOf:
                    - $ref: '#/components/schemas/POWebhookPayload'
                    - type: object
                      properties:
                        submitted_by:
                          type: string
                          format: uuid
                        submitted_at:
                          type: string
                          format: date-time
          example:
            event_id: "evt_po_submitted_001"
            event_type: "po.submitted"
            timestamp: "2025-01-15T15:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                po_number: "PO-2025-001"
                vendor:
                  id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "ACME Supplies"
                total_amount: 5500.00
                status: "submitted"
                submitted_by: "user_123e4567-e89b-12d3-a456-426614174001"
                submitted_at: "2025-01-15T15:00:00Z"
    
    POApprovedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.approved]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  allOf:
                    - $ref: '#/components/schemas/POWebhookPayload'
                    - type: object
                      properties:
                        approved_by:
                          type: string
                          format: uuid
                        approved_at:
                          type: string
                          format: date-time
          example:
            event_id: "evt_po_approved_001"
            event_type: "po.approved"
            timestamp: "2025-01-16T09:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                po_number: "PO-2025-001"
                vendor:
                  id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "ACME Supplies"
                total_amount: 5500.00
                status: "approved"
                approved_by: "mgr_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                approved_at: "2025-01-16T09:00:00Z"
    
    PORejectedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.rejected]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  allOf:
                    - $ref: '#/components/schemas/POWebhookPayload'
                    - type: object
                      properties:
                        rejected_by:
                          type: string
                          format: uuid
                        rejected_at:
                          type: string
                          format: date-time
                        rejection_reason:
                          type: string
                          nullable: true
          example:
            event_id: "evt_po_rejected_001"
            event_type: "po.rejected"
            timestamp: "2025-01-16T10:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                po_number: "PO-2025-001"
                vendor:
                  id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "ACME Supplies"
                total_amount: 5500.00
                status: "rejected"
                rejected_by: "mgr_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                rejected_at: "2025-01-16T10:30:00Z"
                rejection_reason: "Budget exceeded"
    
    POReceivedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.received]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  allOf:
                    - $ref: '#/components/schemas/POWebhookPayload'
                    - type: object
                      properties:
                        received_by:
                          type: string
                          format: uuid
                        received_at:
                          type: string
                          format: date-time
          example:
            event_id: "evt_po_received_001"
            event_type: "po.received"
            timestamp: "2025-01-20T14:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                po_number: "PO-2025-001"
                vendor:
                  id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "ACME Supplies"
                total_amount: 5500.00
                status: "received"
                received_by: "lead_456e7890-a12b-34c5-d678-ef9012345678"
                received_at: "2025-01-20T14:00:00Z"
    
    POBilledEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.billed]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  allOf:
                    - $ref: '#/components/schemas/POWebhookPayload'
                    - type: object
                      properties:
                        invoice_number:
                          type: string
                          nullable: true
                          description: Vendor invoice number
                        invoice_amount:
                          type: number
                          nullable: true
                          description: Actual invoice amount from vendor
                        invoice_date:
                          type: string
                          format: date
                          nullable: true
                          description: Date on the vendor invoice
                        invoice_variance:
                          type: number
                          nullable: true
                          description: Difference between invoice_amount and total_amount
                        billed_at:
                          type: string
                          format: date-time
                        tax_lines:
                          type: array
                          items:
                            type: object
                            properties:
                              tax_name:
                                type: string
                              tax_rate:
                                type: number
                              tax_amount:
                                type: number
                              tax_code_id:
                                type: string
                                format: uuid
                                nullable: true
                              component_amounts:
                                type: array
                                nullable: true
                                items:
                                  type: object
                                  properties:
                                    name:
                                      type: string
                                    rate:
                                      type: number
                                    amount:
                                      type: number
                                    external_id:
                                      type: string
                                      nullable: true
                                      description: Context-resolved external ID (purchase_external_id for bill taxes)
          example:
            event_id: "evt_po_billed_001"
            event_type: "po.billed"
            timestamp: "2025-01-25T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                po_number: "PO-2025-001"
                vendor:
                  id: "v1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "ACME Supplies"
                total_amount: 5500.00
                status: "billed"
                invoice_number: "INV-2025-001234"
                invoice_amount: 5450.00
                invoice_date: "2025-01-24"
                invoice_variance: -50.00
                billed_at: "2025-01-25T10:00:00Z"
                tax_lines:
                  - tax_name: "GST"
                    tax_rate: 5
                    tax_amount: 272.50
    
    POPartiallyBilledEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.partially_billed]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  allOf:
                    - $ref: '#/components/schemas/POWebhookPayload'
                    - type: object
                      properties:
                        invoice_number:
                          type: string
                          nullable: true
                        invoice_amount:
                          type: number
                          nullable: true
                          description: Aggregate billed amount across all bills
                        bill_count:
                          type: integer
                          description: Number of finalized bills
                        bills:
                          type: array
                          items:
                            $ref: '#/components/schemas/POBill'
          example:
            event_id: "evt_po_partial_bill_001"
            event_type: "po.partially_billed"
            timestamp: "2025-01-25T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                po_number: "PO-2025-001"
                status: "partially_billed"
                bill_count: 1
                invoice_amount: 2500.00
                total_amount: 5500.00

    POUnbilledEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.unbilled]
            data:
              type: object
              description: Wrapped in purchase_order key per standard
              required:
                - purchase_order
              properties:
                purchase_order:
                  allOf:
                    - $ref: '#/components/schemas/POWebhookPayload'
                    - type: object
                      properties:
                        bill_count:
                          type: integer
                          description: Number of remaining finalized bills (0 if fully unbilled)
                        bills:
                          type: array
                          items:
                            $ref: '#/components/schemas/POBill'
                        previous_status:
                          type: string
                          description: Status before unbilling (e.g. billed, partially_billed)
          example:
            event_id: "evt_po_unbilled_001"
            event_type: "po.unbilled"
            timestamp: "2025-01-25T12:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              purchase_order:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                po_number: "PO-2025-001"
                status: "received"
                bill_count: 0
                bills: []
                previous_status: "billed"

    POAttachmentAddedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.attachment.added]
            data:
              type: object
              required:
                - po_id
                - po_number
                - attachment_id
                - file_name
              properties:
                po_id:
                  type: string
                  format: uuid
                po_number:
                  type: string
                attachment_id:
                  type: string
                  format: uuid
                file_name:
                  type: string
                file_path:
                  type: string
                file_size:
                  type: integer
                  nullable: true
                content_type:
                  type: string
                  nullable: true
          example:
            event_id: "evt_po_attach_added_001"
            event_type: "po.attachment.added"
            timestamp: "2025-01-15T11:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              po_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              po_number: "PO-2025-001"
              attachment_id: "att_123e4567-e89b-12d3-a456-426614174000"
              file_name: "quote.pdf"
              file_path: "123e4567/po-documents/quote.pdf"
              file_size: 245632
              content_type: "application/pdf"
    
    POAttachmentRemovedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.attachment.removed]
            data:
              type: object
              required:
                - po_id
                - po_number
                - attachment_id
                - file_name
              properties:
                po_id:
                  type: string
                  format: uuid
                po_number:
                  type: string
                attachment_id:
                  type: string
                  format: uuid
                file_name:
                  type: string
                file_path:
                  type: string
          example:
            event_id: "evt_po_attach_removed_001"
            event_type: "po.attachment.removed"
            timestamp: "2025-01-15T16:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              po_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              po_number: "PO-2025-001"
              attachment_id: "att_123e4567-e89b-12d3-a456-426614174000"
              file_name: "old-quote.pdf"
              file_path: "123e4567/po-documents/old-quote.pdf"
    
    POPaymentRecordedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.payment_recorded]
            data:
              type: object
              required:
                - po_id
                - po_number
                - payment_id
                - amount
                - paid_at
              properties:
                po_id:
                  type: string
                  format: uuid
                purchase_order_id:
                  type: string
                  format: uuid
                  description: Alias for po_id (for backward compatibility)
                po_number:
                  type: string
                payment_id:
                  type: string
                  format: uuid
                amount:
                  type: number
                payment_method:
                  type: string
                  nullable: true
                paid_at:
                  type: string
                  format: date
                reference:
                  type: string
                  nullable: true
                external_id:
                  type: string
                  nullable: true
                  description: External system ID (e.g., QuickBooks payment ID)
          example:
            event_id: "evt_po_payment_001"
            event_type: "po.payment_recorded"
            timestamp: "2025-01-15T14:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              po_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              po_number: "PO-2025-001"
              payment_id: "pay_123e4567-e89b-12d3-a456-426614174000"
              amount: 500.00
              payment_method: "check"
              paid_at: "2025-01-15"
              reference: "CHK-12345"
              external_id: "qb_pay_123"

    POPaymentDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.payment_deleted]
            data:
              type: object
              required:
                - po_id
                - po_number
                - payment_id
                - amount
              properties:
                po_id:
                  type: string
                  format: uuid
                purchase_order_id:
                  type: string
                  format: uuid
                  description: Alias for po_id (for backward compatibility)
                po_number:
                  type: string
                payment_id:
                  type: string
                  format: uuid
                amount:
                  type: number
                external_id:
                  type: string
                  nullable: true
          example:
            event_id: "evt_po_payment_del_001"
            event_type: "po.payment_deleted"
            timestamp: "2025-01-15T15:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              po_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              po_number: "PO-2025-001"
              payment_id: "pay_123e4567-e89b-12d3-a456-426614174000"
              amount: 500.00
              external_id: "qb_pay_123"

    POPaidEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [po.paid]
            data:
              type: object
              required:
                - po_id
                - po_number
              properties:
                po_id:
                  type: string
                  format: uuid
                purchase_order_id:
                  type: string
                  format: uuid
                  description: Alias for po_id (for backward compatibility)
                po_number:
                  type: string
                invoice_amount:
                  type: number
                  nullable: true
                amount_paid:
                  type: number
          example:
            event_id: "evt_po_paid_001"
            event_type: "po.paid"
            timestamp: "2025-01-15T16:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              po_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              purchase_order_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              po_number: "PO-2025-001"
              invoice_amount: 1500.00
              amount_paid: 1500.00

    # ==========================================
    # INVOICE EVENTS — Shared Schema
    # ==========================================

    # Standardized invoice webhook payload used by all invoice events.
    # Generated by buildInvoiceWebhookPayload() — see src/lib/buildInvoiceWebhookPayload.ts
    InvoiceCostLink:
      type: object
      description: |
        One entry in `invoice.cost_links[]`. Represents a single consumed cost source,
        regardless of `billing_mode` (`detailed` / `summary` / `fixed-price`) and regardless
        of whether the matching invoice line is hidden or excluded.

        `invoice_line_item_id` is `null` when the source is hidden, fixed-price, or collapsed
        into a summary virtual line — use `ws_id` as the durable source key.

        Invariant: `sum(cost_links[].amount)` ≤ `invoice.subtotal`. Vendor credit amounts are signed negative.
      required: [type, ws_id, amount, invoice_line_item_id]
      properties:
        type:
          type: string
          enum: [timesheet_entry, purchase_order_line, purchase_order, usage_record, vendor_credit]
        ws_id:
          type: string
          format: uuid
          description: Stable WorkSkedge id of the source row (timesheet_entry.id, purchase_order_line_items.id, etc.)
        amount:
          type: number
          description: Signed amount this source contributed to the invoice
        invoice_line_item_id:
          type: string
          format: uuid
          nullable: true
        employee_name: { type: string, nullable: true }
        entry_date: { type: string, format: date, nullable: true }
        hours: { type: number, nullable: true }
        role_name: { type: string, nullable: true }
        po_id: { type: string, format: uuid, nullable: true }
        po_number: { type: string, nullable: true }
        quantity: { type: number, nullable: true }
        category: { type: string, nullable: true }
        cost_code: { type: string, nullable: true }

    LinkedInvoiceRef:
      type: object
      description: |
        Reverse-echo reference from a cost source (PO / timesheet entry / usage record / vendor credit)
        back to the invoice that billed it. Returns the most recent matching invoice when multiple exist.
        `null` when the source has not been billed.
      required: [id, invoice_number, invoice_line_item_id, billed_amount]
      properties:
        id:
          type: string
          format: uuid
        invoice_number:
          type: string
          nullable: true
        invoice_line_item_id:
          type: string
          format: uuid
          nullable: true
        billed_amount:
          type: number

    InvoiceWebhookPayload:
      type: object
      required:
        - id
        - invoice_id
        - company_id
        - invoice_number
        - status
        - invoice_type
      properties:
        id:
          type: string
          format: uuid
          description: Primary invoice identifier
        invoice_id:
          type: string
          format: uuid
          description: Alias for id (backward compatibility)
        company_id:
          type: string
          format: uuid
          description: Company that owns this invoice
        invoice_number:
          type: string
        customer_id:
          type: string
          format: uuid
          nullable: true
        project_id:
          type: string
          format: uuid
          nullable: true
        work_order_id:
          type: string
          format: uuid
          nullable: true
        location_id:
          type: string
          format: uuid
          nullable: true
          description: Service/billing location ID
        contact_ids:
          type: array
          description: Array of contact UUIDs associated with this invoice
          items:
            type: string
            format: uuid
        invoice_type:
          type: string
          enum: [standard, progress, final, credit, retainage]
        status:
          type: string
          enum: [draft, sent, viewed, partial, paid, void]
        billing_mode:
          type: string
          enum: [detailed, summary]
          description: Whether the invoice shows individual line items (detailed) or grouped summaries
        summary_group_by:
          type: string
          enum: [category, date, source_type, employee, role]
          nullable: true
          description: Grouping key when billing_mode is summary
        display_settings:
          type: object
          nullable: true
          description: Per-category display modes, column visibility, and fixed-price category_amounts
          properties:
            category_modes:
              type: object
              additionalProperties:
                type: string
                enum: [header_only, itemized, group_date, group_employee, group_role, group_name]
            column_visibility:
              type: object
              additionalProperties:
                type: object
                properties:
                  qty:
                    type: boolean
                  unit:
                    type: boolean
                  unit_price:
                    type: boolean
                  total:
                    type: boolean
            category_amounts:
              type: object
              additionalProperties:
                type: number
              description: Fixed-price override amounts per category (only present for Fixed Price invoices)
            category_labels:
              type: object
              additionalProperties:
                type: string
              description: Custom display labels per category (e.g., renaming "Materials" to "Management Fee")
            virtual_categories:
              type: array
              items:
                type: string
              description: Keys of virtual (user-created) categories that have no underlying line items. These generate synthetic line items with source_type "virtual" in payloads
        invoice_date:
          type: string
          format: date
        due_date:
          type: string
          format: date
          nullable: true
        subtotal:
          type: number
        tax_inclusive:
          type: boolean
          default: false
          description: >
            When false (default), tax is added on top of the subtotal.
            When true, tax is embedded in line item prices and does not increase the total.
        taxes:
          type: array
          description: Multiple tax lines applied to the invoice
          items:
            type: object
            properties:
              tax_name:
                type: string
              tax_rate:
                type: number
                description: Tax rate as percentage
              tax_amount:
                type: number
              tax_code_id:
                type: string
                format: uuid
                nullable: true
                description: Reference to the tax code used
              component_amounts:
                type: array
                nullable: true
                description: Per-component tax breakdown
                items:
                  type: object
                  properties:
                    name:
                      type: string
                    rate:
                      type: number
                    amount:
                      type: number
                    external_id:
                      type: string
                      nullable: true
                      description: Context-resolved external ID (sales external_id for invoice taxes)
        tax_amount:
          type: number
          description: Total tax amount (sum of all tax lines)
        net_taxable_amount:
          type: number
          description: >
            The effective taxable base after subtracting holdback (pre_tax method) and exempt line items.
            Useful for reconciliation with external accounting systems.
        total_amount:
          type: number
        amount_paid:
          type: number
          description: Total payments recorded against this invoice
        balance_due:
          type: number
          description: Remaining balance (total_amount - amount_paid)
        payment_status:
          type: string
          enum: [unpaid, partially_paid, paid, overpaid]
        holdback_percent:
          type: number
          nullable: true
        holdback_amount:
          type: number
          nullable: true
        holdback_label:
          type: string
          nullable: true
        holdback_tax_method:
          type: string
          nullable: true
          enum: [pre_tax, post_tax]
          description: |
            Tax treatment for holdback. "pre_tax" (Canada): holdback reduces
            the taxable base before tax. "post_tax" (USA): holdback deducted after tax.
        customer_po_numbers:
          type: array
          items:
            type: string
        payment_terms:
          type: string
          nullable: true
        notes:
          type: string
          nullable: true
        internal_notes:
          type: string
          nullable: true
        tax_exemption_snapshot:
          type: array
          nullable: true
          description: Point-in-time snapshot of customer tax exemptions active at invoice creation.
          items:
            type: object
            properties:
              id:
                type: string
                format: uuid
              exemption_type:
                type: string
              jurisdiction_level:
                type: string
                enum: [global, regional]
              jurisdiction_name:
                type: string
                nullable: true
              id_number:
                type: string
              expires_at:
                type: string
                format: date
                nullable: true
        line_items:
          type: array
          description: Invoice line items with full detail
          items:
            $ref: '#/components/schemas/InvoiceWebhookLineItem'
        revision_count:
          type: integer
          description: Number of times this invoice has been revised after initial send
          default: 0
          example: 0
        sent_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        cost_links:
          type: array
          description: Unified projection of every consumed cost source (since payload_version 1.1.0). See `InvoiceCostLink`.
          items:
            $ref: '#/components/schemas/InvoiceCostLink'



    InvoiceWebhookLineItem:
      type: object
      required:
        - id
        - description
        - quantity
        - unit_price
        - line_total
      properties:
        id:
          type: string
          format: uuid
          description: Database-generated line item ID
        description:
          type: string
        quantity:
          type: number
        unit:
          type: string
          nullable: true
        unit_price:
          type: number
        line_total:
          type: number
        category:
          type: string
          nullable: true
        cost_code:
          type: string
          nullable: true
        sort_order:
          type: integer
          description: Display order of the line item
        source_type:
          type: string
          enum: [usage_record, purchase_order, labor, po_line_item, timesheet, timesheet_entry, virtual]
          nullable: true
          description: Origin type linking this line item to a source record
        source_id:
          type: string
          format: uuid
          nullable: true
          description: UUID of the originating source record
        purchase_order_id:
          type: string
          format: uuid
          nullable: true
          description: Parent Purchase Order UUID. Present when source_type is po_line_item; null otherwise.
        employee_name:
          type: string
          nullable: true
          description: Name of the employee associated with this line item
        entry_date:
          type: string
          format: date
          nullable: true
          description: Date of the source entry (e.g., timesheet date)
        role_name:
          type: string
          nullable: true
          description: Role/position of the employee for this line item
        is_excluded:
          type: boolean
          default: false
          description: When true, line item is excluded from invoice subtotal
        tax_code_id:
          type: string
          format: uuid
          nullable: true
          description: Override tax code for this line item. When set, this line uses the specified tax code instead of document-level taxes.
        is_taxable:
          type: boolean
          default: true
          description: Whether this line item is subject to tax. Set to false for tax-exempt items.

    # ==========================================
    # INVOICE EVENT DEFINITIONS
    # ==========================================

    InvoiceCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.created]
            data:
              type: object
              required:
                - invoice
              properties:
                invoice:
                  $ref: '#/components/schemas/InvoiceWebhookPayload'
          example:
            event_id: "evt_invoice_created_001"
            event_type: "invoice.created"
            timestamp: "2025-01-15T10:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                invoice_number: "INV-00001"
                customer_id: "cust_123e4567-e89b-12d3-a456-426614174000"
                project_id: "proj_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                work_order_id: null
                location_id: "loc_123e4567-e89b-12d3-a456-426614174000"
                contact_ids:
                  - "cont_abc123-e89b-12d3-a456-426614174000"
                invoice_type: "standard"
                status: "draft"
                billing_mode: "detailed"
                summary_group_by: null
                display_settings: null
                invoice_date: "2025-01-15"
                due_date: "2025-02-14"
                subtotal: 5000.00
                taxes:
                  - tax_name: "GST"
                    tax_rate: 5
                    tax_amount: 250.00
                  - tax_name: "PST"
                    tax_rate: 7
                    tax_amount: 350.00
                tax_amount: 600.00
                total_amount: 5600.00
                amount_paid: 0
                balance_due: 5600.00
                payment_status: "unpaid"
                holdback_percent: null
                holdback_amount: null
                holdback_label: null
                customer_po_numbers: []
                payment_terms: "Net 30"
                notes: null
                internal_notes: null
                tax_exemption_snapshot: null
                line_items:
                  - id: "li_123e4567-e89b-12d3-a456-426614174000"
                    description: "Professional Services"
                    quantity: 10
                    unit: "hours"
                    unit_price: 500.00
                    line_total: 5000.00
                    category: "labor"
                    cost_code: null
                    sort_order: 0
                    source_type: "labor"
                    source_id: "abc12345-e89b-12d3-a456-426614174000"
                    employee_name: "John Smith"
                    entry_date: "2025-01-14"
                    role_name: "Electrician"
                    is_excluded: false
                revision_count: 0
                sent_at: null
                created_at: "2025-01-15T10:30:00Z"
                updated_at: "2025-01-15T10:30:00Z"

    InvoiceUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.updated]
            data:
              type: object
              required:
                - invoice
              properties:
                invoice:
                  $ref: '#/components/schemas/InvoiceWebhookPayload'
                updated_fields:
                  type: array
                  description: List of field names that were modified in this update
                  items:
                    type: string
                  example: ["status", "subtotal", "taxes"]
          example:
            event_id: "evt_invoice_updated_001"
            event_type: "invoice.updated"
            timestamp: "2025-01-16T14:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                invoice_number: "INV-00001"
                customer_id: "cust_123e4567-e89b-12d3-a456-426614174000"
                project_id: null
                work_order_id: null
                location_id: null
                contact_ids: []
                invoice_type: "standard"
                status: "draft"
                billing_mode: "detailed"
                summary_group_by: null
                display_settings: null
                invoice_date: "2025-01-15"
                due_date: "2025-02-14"
                subtotal: 6000.00
                taxes:
                  - tax_name: "GST"
                    tax_rate: 5
                    tax_amount: 300.00
                  - tax_name: "PST"
                    tax_rate: 7
                    tax_amount: 420.00
                tax_amount: 720.00
                total_amount: 6720.00
                amount_paid: 0
                balance_due: 6720.00
                payment_status: "unpaid"
                holdback_percent: null
                holdback_amount: null
                holdback_label: null
                customer_po_numbers: []
                payment_terms: "Net 30"
                notes: null
                internal_notes: null
                tax_exemption_snapshot: null
                line_items:
                  - id: "li_123e4567-e89b-12d3-a456-426614174000"
                    description: "Professional Services"
                    quantity: 12
                    unit: "hours"
                    unit_price: 500.00
                    line_total: 6000.00
                    category: "labor"
                    cost_code: null
                    sort_order: 0
                    source_type: "labor"
                    source_id: "abc12345-e89b-12d3-a456-426614174000"
                    employee_name: "John Smith"
                    entry_date: "2025-01-14"
                    role_name: "Electrician"
                    is_excluded: false
                revision_count: 1
                sent_at: null
                created_at: "2025-01-15T10:30:00Z"
                updated_at: "2025-01-16T14:00:00Z"
              updated_fields: ["subtotal", "taxes"]

    InvoiceSentEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.sent]
            data:
              type: object
              required:
                - invoice
              properties:
                invoice:
                  $ref: '#/components/schemas/InvoiceWebhookPayload'
          example:
            event_id: "evt_invoice_sent_001"
            event_type: "invoice.sent"
            timestamp: "2025-01-17T09:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                invoice_number: "INV-00001"
                customer_id: "cust_123e4567-e89b-12d3-a456-426614174000"
                project_id: "proj_123e4567-e89b-12d3-a456-426614174000"
                work_order_id: "wo_123e4567-e89b-12d3-a456-426614174000"
                location_id: "loc_123e4567-e89b-12d3-a456-426614174000"
                contact_ids:
                  - "contact_123e4567-e89b-12d3-a456-426614174000"
                invoice_type: "progress"
                status: "sent"
                billing_mode: "detailed"
                summary_group_by: "category"
                display_settings: null
                invoice_date: "2025-01-15"
                due_date: "2025-02-14"
                subtotal: 5000.00
                taxes:
                  - tax_name: "GST"
                    tax_rate: 5
                    tax_amount: 250.00
                  - tax_name: "PST"
                    tax_rate: 7
                    tax_amount: 350.00
                tax_amount: 600.00
                total_amount: 5600.00
                amount_paid: 0
                balance_due: 5600.00
                payment_status: "unpaid"
                holdback_percent: 10
                holdback_amount: 560.00
                holdback_label: "Holdback"
                customer_po_numbers:
                  - "PO-12345"
                payment_terms: "Net 30"
                notes: null
                internal_notes: null
                tax_exemption_snapshot:
                  - id: "te_abc123-e89b-12d3-a456-426614174000"
                    exemption_type: "GST"
                    jurisdiction_level: "global"
                    jurisdiction_name: null
                    id_number: "RT-0001-2345"
                    expires_at: "2026-12-31"
                line_items:
                  - id: "li_abc123-e89b-12d3-a456-426614174000"
                    description: "Phase 1 - Foundation Work"
                    quantity: 1
                    unit: "ea"
                    unit_price: 3500.00
                    line_total: 3500.00
                    category: "labor"
                    cost_code: "01-100"
                    sort_order: 0
                    source_type: "labor"
                    source_id: "abc12345-e89b-12d3-a456-426614174000"
                    employee_name: "John Smith"
                    entry_date: "2025-01-14"
                    role_name: "Foreman"
                    is_excluded: false
                  - id: "li_def456-e89b-12d3-a456-426614174000"
                    description: "Materials - Concrete"
                    quantity: 30
                    unit: "bag"
                    unit_price: 50.00
                    line_total: 1500.00
                    category: "materials"
                    cost_code: "01-200"
                    sort_order: 1
                    source_type: "purchase_order"
                    source_id: "def67890-e89b-12d3-a456-426614174000"
                    employee_name: null
                    entry_date: null
                    role_name: null
                    is_excluded: false
                revision_count: 0
                sent_at: "2025-01-17T09:00:00Z"
                created_at: "2025-01-15T10:30:00Z"
                updated_at: "2025-01-17T09:00:00Z"
    
    InvoicePaidEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.paid]
            data:
              type: object
              required:
                - invoice
              properties:
                invoice:
                  allOf:
                    - $ref: '#/components/schemas/InvoiceWebhookPayload'
                    - type: object
                      properties:
                        paid_at:
                          type: string
                          format: date-time
                          description: Timestamp when invoice was fully paid
          example:
            event_id: "evt_invoice_paid_001"
            event_type: "invoice.paid"
            timestamp: "2025-02-10T11:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                invoice_number: "INV-00001"
                customer_id: "cust_123e4567-e89b-12d3-a456-426614174000"
                project_id: null
                work_order_id: null
                location_id: null
                contact_ids: []
                invoice_type: "standard"
                status: "sent"
                billing_mode: "detailed"
                summary_group_by: null
                display_settings: null
                invoice_date: "2025-01-15"
                due_date: "2025-02-14"
                subtotal: 5000.00
                taxes:
                  - tax_name: "GST"
                    tax_rate: 5
                    tax_amount: 250.00
                tax_amount: 250.00
                total_amount: 5250.00
                amount_paid: 5250.00
                balance_due: 0
                payment_status: "paid"
                holdback_percent: null
                holdback_amount: null
                holdback_label: null
                customer_po_numbers: []
                payment_terms: "Net 30"
                notes: null
                internal_notes: null
                tax_exemption_snapshot: null
                line_items:
                  - id: "li_123"
                    description: "Professional Services"
                    quantity: 10
                    unit: "hours"
                    unit_price: 500.00
                    line_total: 5000.00
                    category: "labor"
                    cost_code: null
                    sort_order: 0
                    source_type: null
                    source_id: null
                    employee_name: null
                    entry_date: null
                    role_name: null
                    is_excluded: false
                revision_count: 0
                sent_at: "2025-01-17T09:00:00Z"
                created_at: "2025-01-15T10:30:00Z"
                updated_at: "2025-02-10T11:30:00Z"
                paid_at: "2025-02-10T11:30:00Z"
    
    InvoiceVoidedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.voided]
            data:
              type: object
              required:
                - invoice
              properties:
                invoice:
                  allOf:
                    - $ref: '#/components/schemas/InvoiceWebhookPayload'
                    - type: object
                      properties:
                        void_reason:
                          type: string
                          nullable: true
                        voided_at:
                          type: string
                          format: date-time
          example:
            event_id: "evt_invoice_voided_001"
            event_type: "invoice.voided"
            timestamp: "2025-01-20T16:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                invoice_number: "INV-00001"
                customer_id: "cust_123e4567-e89b-12d3-a456-426614174000"
                project_id: null
                work_order_id: null
                location_id: null
                contact_ids: []
                invoice_type: "standard"
                status: "void"
                billing_mode: "detailed"
                summary_group_by: null
                display_settings: null
                invoice_date: "2025-01-15"
                due_date: "2025-02-14"
                subtotal: 5000.00
                taxes:
                  - tax_name: "GST"
                    tax_rate: 5
                    tax_amount: 250.00
                tax_amount: 250.00
                total_amount: 5250.00
                amount_paid: 0
                balance_due: 5250.00
                payment_status: "unpaid"
                holdback_percent: null
                holdback_amount: null
                holdback_label: null
                customer_po_numbers: []
                payment_terms: null
                notes: null
                internal_notes: null
                tax_exemption_snapshot: null
                void_reason: "Duplicate invoice created in error"
                voided_at: "2025-01-20T16:00:00Z"
                line_items:
                  - id: "li_123e4567-e89b-12d3-a456-426614174000"
                    description: "Professional Services"
                    quantity: 10
                    unit: "hours"
                    unit_price: 500.00
                    line_total: 5000.00
                    category: "labor"
                    cost_code: null
                    sort_order: 0
                    source_type: "labor"
                    source_id: "abc12345-e89b-12d3-a456-426614174000"
                    employee_name: "John Smith"
                    entry_date: "2025-01-14"
                    role_name: "Electrician"
                    is_excluded: false
                revision_count: 0
                sent_at: null
                created_at: "2025-01-15T10:30:00Z"
                updated_at: "2025-01-20T16:00:00Z"
    
    InvoiceDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.deleted]
            data:
              type: object
              description: >
                Lightweight payload built before the invoice record is deleted.
                Contains only identifying fields — the full invoice object is no longer available.
              required:
                - invoice_id
                - company_id
                - invoice_number
                - customer_id
              properties:
                invoice_id:
                  type: string
                  format: uuid
                invoice_number:
                  type: string
                company_id:
                  type: string
                  format: uuid
                customer_id:
                  type: string
                  format: uuid
          example:
            event_id: "evt_invoice_deleted_001"
            event_type: "invoice.deleted"
            timestamp: "2025-01-21T12:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              invoice_number: "INV-00001"
              company_id: "123e4567-e89b-12d3-a456-426614174000"
              customer_id: "cust_123e4567-e89b-12d3-a456-426614174000"

    InvoicePaymentRecordedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.payment_recorded]
            data:
              type: object
              required:
                - invoice_id
                - invoice_number
                - payment_id
                - amount
                - paid_at
              properties:
                invoice_id:
                  type: string
                  format: uuid
                invoice_number:
                  type: string
                payment_id:
                  type: string
                  format: uuid
                amount:
                  type: number
                payment_method:
                  type: string
                  nullable: true
                reference_number:
                  type: string
                  nullable: true
                paid_at:
                  type: string
                  format: date-time
                external_id:
                  type: string
                  nullable: true
                  description: External system ID for this payment (e.g., QuickBooks payment ID)
                total_paid:
                  type: number
                  description: Total amount paid on invoice after this payment
                balance_due:
                  type: number
                  description: Remaining balance after this payment
          example:
            event_id: "evt_inv_payment_001"
            event_type: "invoice.payment_recorded"
            timestamp: "2025-02-05T14:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              invoice_number: "INV-00001"
              payment_id: "pay_123e4567-e89b-12d3-a456-426614174000"
              amount: 2750.00
              payment_method: "check"
              reference_number: "CHK-12345"
              paid_at: "2025-02-05T14:00:00Z"
              external_id: null
              total_paid: 2750.00
              balance_due: 2750.00
    
    InvoicePaymentDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [invoice.payment_deleted]
            data:
              type: object
              required:
                - invoice_id
                - invoice_number
                - payment_id
                - amount
              properties:
                invoice_id:
                  type: string
                  format: uuid
                invoice_number:
                  type: string
                payment_id:
                  type: string
                  format: uuid
                amount:
                  type: number
                  description: Amount of the deleted payment
                external_id:
                  type: string
                  nullable: true
                  description: External system ID for this payment (e.g., QuickBooks payment ID)
                total_paid:
                  type: number
                  description: Total amount paid on invoice after deletion
                balance_due:
                  type: number
                  description: New balance due after payment deletion
          example:
            event_id: "evt_inv_payment_del_001"
            event_type: "invoice.payment_deleted"
            timestamp: "2025-02-06T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              invoice_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              invoice_number: "INV-00001"
              payment_id: "pay_123e4567-e89b-12d3-a456-426614174000"
              amount: 2750.00
              external_id: null
              total_paid: 0.00
              balance_due: 5500.00

    # ==========================================
    # APP LIFECYCLE EVENTS (Sent to app callback URL)
    # ==========================================
    # These events are sent directly to {developer_website}/webhooks/app
    # when an app is installed, uninstalled, or updated by a company.
    
    AppCompanyProfile:
      type: object
      description: Company profile included in app lifecycle webhooks
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        industry:
          type: string
          nullable: true
        employee_count:
          type: integer
          nullable: true
        timezone:
          type: string
          example: "America/New_York"
        modules_enabled:
          type: object
          additionalProperties:
            type: boolean
        address:
          type: object
          nullable: true
          additionalProperties: true
        contact_info:
          type: object
          nullable: true
          additionalProperties: true

    AppInstalledEvent:
      type: object
      description: Sent to app's callback URL when installed by a company
      required:
        - event
        - timestamp
        - data
      properties:
        event:
          type: string
          enum: [app.installed]
        timestamp:
          type: string
          format: date-time
        data:
          type: object
          required:
            - install_id
            - app_id
            - app_name
            - company
          properties:
            install_id:
              type: string
              format: uuid
              description: Use this ID for API token generation
            app_id:
              type: string
              format: uuid
            app_name:
              type: string
            company:
              $ref: '#/components/schemas/AppCompanyProfile'
            granted_scopes:
              type: array
              items:
                type: string
              description: API scopes granted to the app
            subscribed_events:
              type: array
              items:
                type: string
              description: Webhook events the app will receive
            credentials:
              type: object
              description: |
                API credentials for the app (delivered once at installation).
                Store these securely - they cannot be retrieved again.
              required:
                - api_key
                - session_signing_secret
                - api_base_url
              properties:
                api_key:
                  type: string
                  description: API key for authenticated REST API requests (wsk_live_...)
                session_signing_secret:
                  type: string
                  description: HMAC secret for signing/verifying session tokens (JWT)
                api_base_url:
                  type: string
                  description: Base URL for API calls
                  example: "https://app.workskedge.com/api/v1"
            installed_by:
              type: object
              nullable: true
              properties:
                user_id:
                  type: string
                  format: uuid
                email:
                  type: string
                name:
                  type: string
            installed_at:
              type: string
              format: date-time
            install_mode:
              type: string
              enum: [production, training]
              description: |
                `production` for normal installs, `training` for temporary sandbox installs during training mode.
                Training installs skip billing and are automatically cleaned up when the session ends.
            configuration:
              type: object
              nullable: true
              additionalProperties: true
            allowed_roles:
              type: array
              nullable: true
              items:
                type: string
                enum: [administrator, manager, scheduler, project_owner, site_lead, site_worker]
              description: |
                Roles that can access this app within the company.
                null = all roles can access (default)
                [] = no roles can access (effectively disabled)
                ["administrator", "manager"] = only specified roles can access
      example:
        event: "app.installed"
        timestamp: "2025-01-15T10:30:00Z"
        data:
          install_id: "550e8400-e29b-41d4-a716-446655440000"
          app_id: "660e8400-e29b-41d4-a716-446655440001"
          app_name: "Time Tracker Pro"
          company:
            id: "770e8400-e29b-41d4-a716-446655440002"
            name: "Acme Construction"
            industry: "construction"
            employee_count: 50
            timezone: "America/New_York"
            modules_enabled:
              core: true
              workTracking: true
              timesheets: true
              finances: false
          granted_scopes:
            - "employees:read"
            - "timesheets:read"
            - "timesheets:write"
          subscribed_events:
            - "timesheet.created"
            - "timesheet.submitted"
          credentials:
            api_key: "wsk_live_abc123xyz..."
            session_signing_secret: "secret_xyz789..."
            api_base_url: "https://app.workskedge.com/api/v1"
          allowed_roles:
            - "administrator"
            - "manager"
          installed_by:
            user_id: "880e8400-e29b-41d4-a716-446655440003"
            email: "admin@acme.com"
            name: "John Admin"
          installed_at: "2025-01-15T10:30:00Z"
          install_mode: "production"

    AppUninstalledEvent:
      type: object
      description: |
        Sent to app's callback URL when uninstalled by a company. Also sent automatically
        when a training session ends, with `install_mode: "training"` and `reason: "training_session_ended"`.
      required:
        - event
        - timestamp
        - data
      properties:
        event:
          type: string
          enum: [app.uninstalled]
        timestamp:
          type: string
          format: date-time
        data:
          type: object
          required:
            - install_id
            - app_id
            - app_name
            - company
            - install_mode
            - reason
          properties:
            install_id:
              type: string
              format: uuid
            app_id:
              type: string
              format: uuid
            app_name:
              type: string
            company:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
            install_mode:
              type: string
              enum: [production, training]
              description: Whether the installation was in a production or training environment
            reason:
              type: string
              enum: [user_initiated, training_session_ended]
              description: Why the uninstall occurred
      example:
        event: "app.uninstalled"
        timestamp: "2025-02-15T14:00:00Z"
        data:
          install_id: "550e8400-e29b-41d4-a716-446655440000"
          app_id: "660e8400-e29b-41d4-a716-446655440001"
          app_name: "Time Tracker Pro"
          company:
            id: "770e8400-e29b-41d4-a716-446655440002"
            name: "Acme Construction"
          install_mode: "production"
          reason: "user_initiated"

    AppUpdatedEvent:
      type: object
      description: Sent to app's callback URL when settings are modified
      required:
        - event
        - timestamp
        - data
      properties:
        event:
          type: string
          enum: [app.updated]
        timestamp:
          type: string
          format: date-time
        data:
          type: object
          required:
            - install_id
            - app_id
            - app_name
            - company
          properties:
            install_id:
              type: string
              format: uuid
            app_id:
              type: string
              format: uuid
            app_name:
              type: string
            company:
              $ref: '#/components/schemas/AppCompanyProfile'
            granted_scopes:
              type: array
              items:
                type: string
            subscribed_events:
              type: array
              items:
                type: string
            configuration:
              type: object
              nullable: true
              additionalProperties: true
            allowed_roles:
              type: array
              nullable: true
              items:
                type: string
                enum: [administrator, manager, scheduler, project_owner, site_lead, site_worker]
              description: |
                Roles that can access this app within the company.
                null = all roles can access (default)
                [] = no roles can access (effectively disabled)
                ["administrator", "manager"] = only specified roles can access
            installed_by:
              type: object
              description: User who originally installed the app
              properties:
                user_id:
                  type: string
                  format: uuid
                email:
                  type: string
                  format: email
                name:
                  type: string
            installed_at:
              type: string
              format: date-time
              description: ISO 8601 timestamp of original installation


    # TIMESHEET EVENTS
    # ==========================================
    
    TimesheetCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [timesheet.created]
            data:
              type: object
              required:
                - id
                - timesheet_id
                - emp_number
                - timesheet_date
                - status
                - created_at
              properties:
                id:
                  type: string
                  format: uuid
                  description: Timesheet ID (alias for timesheet_id)
                timesheet_id:
                  type: string
                  format: uuid
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                timesheet_date:
                  type: string
                  format: date
                  description: Date of the daily timesheet
                status:
                  type: string
                  enum: [draft, submitted, locked, processed]
                entry_count:
                  type: integer
                  minimum: 0
                  description: Number of time entries in the timesheet
                created_at:
                  type: string
                  format: date-time
          example:
            event_id: "evt_timesheet_created_001"
            event_type: "timesheet.created"
            timestamp: "2025-01-15T08:00:00Z"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timesheet_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              timesheet_date: "2025-01-15"
              status: "draft"
              entry_count: 0
              created_at: "2025-01-15T08:00:00Z"

    TimesheetUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [timesheet.updated]
            data:
              type: object
              required:
                - timesheet_id
                - update_type
                - updated_at
              properties:
                timesheet_id:
                  type: string
                  format: uuid
                update_type:
                  type: string
                  enum: [entry_added, entry_updated, entry_deleted, notes_updated]
                  description: Type of update made to the timesheet
                updated_at:
                  type: string
                  format: date-time
                entry:
                  type: object
                  description: Full entry details (for entry_added and entry_updated)
                  properties:
                    entry_id:
                      type: string
                      format: uuid
                    entry_date:
                      type: string
                      format: date
                    work_order_id:
                      type: string
                      format: uuid
                      nullable: true
                    work_order_name:
                      type: string
                      nullable: true
                    work_type_id:
                      type: string
                      format: uuid
                    work_type_name:
                      type: string
                    hours:
                      type: number
                      minimum: 0
                    notes:
                      type: string
                      nullable: true
                deleted_entry:
                  type: object
                  description: Basic info about deleted entry (for entry_deleted)
                  properties:
                    entry_date:
                      type: string
                      format: date
                    hours:
                      type: number
                      minimum: 0
                notes:
                  type: string
                  description: New notes text (for notes_updated)
          example:
            event_id: "evt_timesheet_updated_001"
            event_type: "timesheet.updated"
            timestamp: "2025-01-15T14:30:00Z"
            data:
              timesheet_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              update_type: "entry_added"
              updated_at: "2025-01-15T14:30:00Z"
              entry:
                entry_id: "e1f2g3h4-i5j6-7890-klmn-op1234567890"
                entry_date: "2025-01-20"
                work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                work_order_name: "Electrical Phase"
                work_type_id: "wt_123e4567-e89b-12d3-a456-426614174000"
                work_type_name: "Installation"
                hours: 8.0
                notes: "Main panel installation"

    TimesheetLockedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [timesheet.locked]
            data:
              type: object
              required:
                - id
                - timesheet_id
                - employee_id
                - timesheet_date
                - emp_number
                - employee_name
                - total_hours
                - locked_at
                - locked_by
                - locked_by_name
              properties:
                id:
                  type: string
                  format: uuid
                  description: Timesheet ID (alias for timesheet_id)
                timesheet_id:
                  type: string
                  format: uuid
                  description: Unique identifier for the timesheet
                employee_id:
                  type: string
                  format: uuid
                  description: Employee UUID
                timesheet_date:
                  type: string
                  format: date
                  description: Date of the daily timesheet
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                  description: Full name of the employee
                total_hours:
                  type: number
                  format: decimal
                  description: Total hours logged in timesheet
                locked_at:
                  type: string
                  format: date-time
                  description: Timestamp when timesheet was locked
                locked_by:
                  type: string
                  format: uuid
                  description: User ID who locked the timesheet
                locked_by_name:
                  type: string
                  description: Name of manager who locked the timesheet
                entries:
                  type: array
                  description: Full entry details
                  items:
                    type: object
                    properties:
                      entry_id:
                        type: string
                        format: uuid
                      entry_date:
                        type: string
                        format: date
                      work_order_id:
                        type: string
                        format: uuid
                        nullable: true
                      work_order_name:
                        type: string
                      work_type_name:
                        type: string
                      hours:
                        type: number
                      start_time:
                        type: string
                        nullable: true
                      end_time:
                        type: string
                        nullable: true
                      notes:
                        type: string
                        nullable: true
          example:
            event_id: "evt_timesheet_locked_001"
            event_type: "timesheet.locked"
            timestamp: "2025-01-15T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timesheet_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_12345678-1234-1234-1234-123456789abc"
              timesheet_date: "2025-01-15"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              total_hours: 8.5
              locked_at: "2025-01-15T10:00:00Z"
              locked_by: "mgr_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
              locked_by_name: "Manager Smith"
              entries:
                - entry_id: "ent_12345678-1234-1234-1234-123456789abc"
                  entry_date: "2025-01-15"
                  work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  work_order_name: "Electrical Phase"
                  work_type_name: "Labor"
                  hours: 8.0
                  start_time: "08:00"
                  end_time: "16:00"
                  notes: "Main panel installation"

    TimesheetSubmittedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [timesheet.submitted]
            data:
              type: object
              required:
                - id
                - timesheet_id
                - employee_id
                - emp_number
                - timesheet_date
                - total_hours
                - submitted_at
              properties:
                id:
                  type: string
                  format: uuid
                  description: Timesheet ID (alias for timesheet_id)
                timesheet_id:
                  type: string
                  format: uuid
                employee_id:
                  type: string
                  format: uuid
                  description: Employee UUID
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                timesheet_date:
                  type: string
                  format: date
                  description: Date of the daily timesheet
                total_hours:
                  type: number
                  minimum: 0
                submitted_at:
                  type: string
                  format: date-time
                entries:
                  type: array
                  items:
                    type: object
                    properties:
                      entry_id:
                        type: string
                        format: uuid
                      entry_date:
                        type: string
                        format: date
                      work_order_id:
                        type: string
                        format: uuid
                        nullable: true
                      work_order_name:
                        type: string
                      work_type_name:
                        type: string
                      hours:
                        type: number
                      start_time:
                        type: string
                        nullable: true
                      end_time:
                        type: string
                        nullable: true
                      notes:
                        type: string
                        nullable: true
                      linked_invoice:
                        allOf:
                          - $ref: '#/components/schemas/LinkedInvoiceRef'
                        nullable: true
                        description: Most recent invoice that billed this entry (since payload_version 1.1.0). `null` when not billed.
          example:
            event_id: "evt_timesheet_submitted_001"
            event_type: "timesheet.submitted"
            timestamp: "2025-01-15T17:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timesheet_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_12345678-1234-1234-1234-123456789abc"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              timesheet_date: "2025-01-15"
              total_hours: 8.5
              submitted_at: "2025-01-15T17:00:00Z"
              entries:
                - entry_id: "ent_12345678-1234-1234-1234-123456789abc"
                  entry_date: "2025-01-15"
                  work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  work_order_name: "Electrical Phase"
                  work_type_name: "Labor"
                  hours: 8.0
                  start_time: "08:00"
                  end_time: "16:00"
                  notes: "Main panel installation"
                - entry_id: "ent_23456789-2345-2345-2345-234567890abc"
                  entry_date: "2025-01-15"
                  work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  work_order_name: "Electrical Phase"
                  work_type_name: "Overtime"
                  hours: 0.5
                  start_time: "16:00"
                  end_time: "16:30"
                  notes: "Overtime"
    
    TimesheetUnlockedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [timesheet.unlocked]
            data:
              type: object
              required:
                - id
                - timesheet_id
                - employee_id
                - emp_number
                - employee_name
                - timesheet_date
                - unlocked_by
                - unlocked_by_name
                - unlocked_at
              properties:
                id:
                  type: string
                  format: uuid
                  description: Timesheet ID (alias for timesheet_id)
                timesheet_id:
                  type: string
                  format: uuid
                  description: Unique identifier for the timesheet
                employee_id:
                  type: string
                  format: uuid
                  description: Employee UUID
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                  description: Full name of the employee
                timesheet_date:
                  type: string
                  format: date
                  description: Date of the daily timesheet
                total_hours:
                  type: number
                  description: Total hours in the timesheet
                unlocked_by:
                  type: string
                  format: uuid
                  description: User ID who unlocked the timesheet
                unlocked_by_name:
                  type: string
                  description: Name of manager who unlocked the timesheet
                unlocked_at:
                  type: string
                  format: date-time
                  description: Timestamp when timesheet was unlocked
                reason:
                  type: string
                  nullable: true
                  description: Reason provided for unlocking
                entries:
                  type: array
                  description: Entry details at time of unlock
                  items:
                    type: object
                    properties:
                      entry_id:
                        type: string
                        format: uuid
                      entry_date:
                        type: string
                        format: date
                      work_order_id:
                        type: string
                        format: uuid
                        nullable: true
                      work_order_name:
                        type: string
                      work_type_name:
                        type: string
                      hours:
                        type: number
                      start_time:
                        type: string
                        nullable: true
                      end_time:
                        type: string
                        nullable: true
                      notes:
                        type: string
                        nullable: true
          example:
            event_id: "evt_timesheet_unlocked_001"
            event_type: "timesheet.unlocked"
            timestamp: "2025-01-16T09:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timesheet_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_12345678-1234-1234-1234-123456789abc"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              timesheet_date: "2025-01-15"
              total_hours: 8.5
              unlocked_by: "mgr_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
              unlocked_by_name: "Manager Smith"
              unlocked_at: "2025-01-16T09:00:00Z"
              reason: "Employee requested correction"
              entries:
                - entry_id: "ent_12345678-1234-1234-1234-123456789abc"
                  entry_date: "2025-01-15"
                  work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  work_order_name: "Electrical Phase"
                  work_type_name: "Labor"
                  hours: 8.0
                  start_time: "08:00"
                  end_time: "16:00"
                  notes: "Main panel installation"
    
    TimesheetProcessedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [timesheet.processed]
            data:
              type: object
              required:
                - id
                - timesheet_id
                - employee_id
                - emp_number
                - employee_name
                - timesheet_date
                - total_hours
                - processed_by
                - processed_by_name
                - processed_at
              properties:
                id:
                  type: string
                  format: uuid
                  description: Timesheet ID (alias for timesheet_id)
                timesheet_id:
                  type: string
                  format: uuid
                  description: Unique identifier for the timesheet
                employee_id:
                  type: string
                  format: uuid
                  description: Employee UUID
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                  description: Full name of the employee
                timesheet_date:
                  type: string
                  format: date
                  description: Date of the daily timesheet
                total_hours:
                  type: number
                  format: float
                  description: Total hours in the timesheet
                processed_by:
                  type: string
                  format: uuid
                  description: User ID who processed the timesheet
                processed_by_name:
                  type: string
                  description: Name of manager who processed the timesheet
                processed_at:
                  type: string
                  format: date-time
                  description: Timestamp when timesheet was processed
                entries:
                  type: array
                  description: Full entry details
                  items:
                    type: object
                    properties:
                      entry_id:
                        type: string
                        format: uuid
                      entry_date:
                        type: string
                        format: date
                      work_order_id:
                        type: string
                        format: uuid
                        nullable: true
                      work_order_name:
                        type: string
                      work_type_name:
                        type: string
                      hours:
                        type: number
                      start_time:
                        type: string
                        nullable: true
                      end_time:
                        type: string
                        nullable: true
                      notes:
                        type: string
                        nullable: true
          example:
            event_id: "evt_timesheet_processed_001"
            event_type: "timesheet.processed"
            timestamp: "2025-01-16T16:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timesheet_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_12345678-1234-1234-1234-123456789abc"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              timesheet_date: "2025-01-15"
              total_hours: 8.5
              processed_by: "mgr_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
              processed_by_name: "Manager Smith"
              processed_at: "2025-01-16T16:00:00Z"
              entries:
                - entry_id: "ent_12345678-1234-1234-1234-123456789abc"
                  entry_date: "2025-01-15"
                  work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  work_order_name: "Electrical Phase"
                  work_type_name: "Labor"
                  hours: 8.0
                  start_time: "08:00"
                  end_time: "16:00"
                  notes: "Main panel installation"
    
    TimesheetRevertedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [timesheet.reverted]
            data:
              type: object
              required:
                - id
                - timesheet_id
                - employee_id
                - emp_number
                - employee_name
                - timesheet_date
                - reverted_by
                - reverted_by_name
                - reverted_at
              properties:
                id:
                  type: string
                  format: uuid
                  description: Timesheet ID (alias for timesheet_id)
                timesheet_id:
                  type: string
                  format: uuid
                  description: Unique identifier for the timesheet
                employee_id:
                  type: string
                  format: uuid
                  description: Employee UUID
                emp_number:
                  type: string
                  description: Employee number
                employee_name:
                  type: string
                  description: Full name of the employee
                timesheet_date:
                  type: string
                  format: date
                  description: Date of the daily timesheet
                total_hours:
                  type: number
                  description: Total hours in the timesheet
                reverted_by:
                  type: string
                  format: uuid
                  description: User ID who reverted the timesheet
                reverted_by_name:
                  type: string
                  description: Name of manager who reverted the timesheet
                reverted_at:
                  type: string
                  format: date-time
                  description: Timestamp when timesheet was reverted to draft
                entries:
                  type: array
                  description: Entry details at time of revert
                  items:
                    type: object
                    properties:
                      entry_id:
                        type: string
                        format: uuid
                      entry_date:
                        type: string
                        format: date
                      work_order_id:
                        type: string
                        format: uuid
                        nullable: true
                      work_order_name:
                        type: string
                      work_type_name:
                        type: string
                      hours:
                        type: number
                      start_time:
                        type: string
                        nullable: true
                      end_time:
                        type: string
                        nullable: true
                      notes:
                        type: string
                        nullable: true
          example:
            event_id: "evt_timesheet_reverted_001"
            event_type: "timesheet.reverted"
            timestamp: "2025-01-16T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              timesheet_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              employee_id: "emp_12345678-1234-1234-1234-123456789abc"
              emp_number: "EMP-001"
              employee_name: "John Smith"
              timesheet_date: "2025-01-15"
              total_hours: 8.5
              reverted_by: "mgr_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
              reverted_by_name: "Manager Smith"
              reverted_at: "2025-01-16T10:00:00Z"
              entries:
                - entry_id: "ent_12345678-1234-1234-1234-123456789abc"
                  entry_date: "2025-01-15"
                  work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  work_order_name: "Electrical Phase"
                  work_type_name: "Labor"
                  hours: 8.0
                  start_time: "08:00"
                  end_time: "16:00"
                  notes: "Main panel installation"
    
    # ==========================================
    # APP LIFECYCLE EVENT SCHEMAS
    # ==========================================
    
    AppInstalledPayload:
      type: object
      description: |
        Payload delivered synchronously during app installation. Delivered via OAuth token response
        or as a synchronous POST callback for catalog installations.
        
        **Note:** This is NOT a webhook - it's delivered synchronously during the installation handshake.
      required:
        - event
        - timestamp
        - data
      properties:
        event:
          type: string
          enum: ["app.installed"]
          description: Always "app.installed" for this payload
        timestamp:
          type: string
          format: date-time
          description: When the installation occurred
        data:
          type: object
          required:
            - install_id
            - company
            - credentials
            - granted_scopes
            - installed_by
          properties:
            install_id:
              type: string
              format: uuid
              description: Unique identifier for this app installation
            company:
              type: object
              description: The company that installed the app
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
                industry:
                  type: string
                timezone:
                  type: string
                employee_count:
                  type: integer
                modules_enabled:
                  type: object
                address:
                  type: object
                contact_info:
                  type: object
                project_status_mode:
                  type: string
                  enum: [simple, advanced]
                  description: Project workflow mode - 'simple' (active/closed) or 'advanced' (multi-stage pipeline including lead, estimating, pending_approval, approved, active, complete, closed)
            credentials:
              type: object
              description: API credentials for making authenticated requests
              properties:
                api_key:
                  type: string
                  description: Long-lived API key for REST API calls. Revoked when app is uninstalled.
                session_signing_secret:
                  type: string
                  description: Secret for signing JWT tokens for embedded app sessions. Rotated on reinstall.
            granted_scopes:
              type: array
              items:
                type: string
              description: List of permission scopes granted during installation (e.g., "work_orders:read", "employees:write")
            installed_by:
              type: object
              description: The user who installed the app
              properties:
                user_id:
                  type: string
                  format: uuid
                name:
                  type: string
                email:
                  type: string
                  format: email
      example:
        event: "app.installed"
        timestamp: "2026-01-13T12:00:00.000Z"
        data:
          install_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
          company:
            id: "123e4567-e89b-12d3-a456-426614174000"
            name: "Acme Corporation"
            industry: "construction"
            timezone: "America/New_York"
            employee_count: 50
            project_status_mode: "simple"
          credentials:
            api_key: "wsk_live_abc123xyz..."
            session_signing_secret: "secret_for_jwt_signing"
          granted_scopes:
            - "work_orders:read"
            - "work_orders:write"
            - "employees:read"
          installed_by:
            user_id: "user_456e7890-a1b2-c3d4-e5f6-789012345678"
            name: "John Smith"
            email: "john@acme.com"

    # ==========================================
    # ASSIGNMENT EVENTS
    # ==========================================
    
    AssignmentCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [assignment.created]
            data:
              type: object
              required:
                - assignment
              properties:
                assignment:
                  type: object
                  required:
                    - id
                    - company_id
                    - work_order_id
                    - assigned_start_date
                    - assigned_end_date
                    - created_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                    employee_id:
                      type: string
                      format: uuid
                      nullable: true
                    temp_employee_id:
                      type: string
                      format: uuid
                      nullable: true
                    work_order_id:
                      type: string
                      format: uuid
                    assigned_start_date:
                      type: string
                      format: date
                    assigned_end_date:
                      type: string
                      format: date
                    employee:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        emp_number:
                          type: string
                    work_order:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        phase_name:
                          type: string
                        work_order_number:
                          type: string
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_assignment_created_001"
            event_type: "assignment.created"
            timestamp: "2025-01-15T10:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              assignment:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
                work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                assigned_start_date: "2025-01-15"
                assigned_end_date: "2025-01-20"
                employee:
                  id: "emp_123e4567-e89b-12d3-a456-426614174001"
                  name: "John Smith"
                  emp_number: "EMP-001"
                work_order:
                  id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  phase_name: "Electrical Phase"
                  work_order_number: "WO-2025-001"
                created_at: "2025-01-15T10:30:00Z"
                updated_at: "2025-01-15T10:30:00Z"

    AssignmentUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [assignment.updated]
            data:
              type: object
              required:
                - assignment
              properties:
                assignment:
                  type: object
                  required:
                    - id
                    - company_id
                    - work_order_id
                    - assigned_start_date
                    - assigned_end_date
                    - updated_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                    employee_id:
                      type: string
                      format: uuid
                      nullable: true
                    temp_employee_id:
                      type: string
                      format: uuid
                      nullable: true
                    work_order_id:
                      type: string
                      format: uuid
                    assigned_start_date:
                      type: string
                      format: date
                    assigned_end_date:
                      type: string
                      format: date
                    employee:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        emp_number:
                          type: string
                    work_order:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        phase_name:
                          type: string
                        work_order_number:
                          type: string
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_assignment_updated_001"
            event_type: "assignment.updated"
            timestamp: "2025-01-15T14:30:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              assignment:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
                work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                assigned_start_date: "2025-01-15"
                assigned_end_date: "2025-01-25"
                updated_at: "2025-01-15T14:30:00Z"

    AssignmentDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [assignment.deleted]
            data:
              type: object
              required:
                - assignment
              properties:
                assignment:
                  type: object
                  required:
                    - id
                    - deleted_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    employee_id:
                      type: string
                      format: uuid
                      nullable: true
                    work_order_id:
                      type: string
                      format: uuid
                    deleted_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_assignment_deleted_001"
            event_type: "assignment.deleted"
            timestamp: "2025-01-15T16:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              assignment:
                id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
                work_order_id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                deleted_at: "2025-01-15T16:00:00Z"

    # ==========================================
    # SCHEDULE BATCH EVENTS
    # ==========================================
    
    ScheduleBatchCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [schedule.batch_created]
            data:
              $ref: '#/components/schemas/ScheduleBatchPayload'
          example:
            event_id: "evt_schedule_batch_001"
            event_type: "schedule.batch_created"
            timestamp: "2025-01-15T08:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              schedules:
                employee:
                  id: "emp_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "John Smith"
                  emp_number: "EMP-001"
                  is_site_lead: true
                work_order:
                  id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  phase_name: "Foundation Installation"
                  work_order_number: "WO-2025-001"
                assignment_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                schedules:
                  - id: "sch_001"
                    work_date: "2025-01-20"
                    start_time: "08:00"
                    end_time: "17:00"
                    is_active: true
                    is_override: false
                  - id: "sch_002"
                    work_date: "2025-01-21"
                    start_time: "08:00"
                    end_time: "17:00"
                    is_active: true
                    is_override: false
                changes:
                  days_added: ["2025-01-20", "2025-01-21"]
                  days_removed: []
                  days_modified: []

    ScheduleBatchUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [schedule.batch_updated]
            data:
              $ref: '#/components/schemas/ScheduleBatchPayload'
          example:
            event_id: "evt_schedule_batch_002"
            event_type: "schedule.batch_updated"
            timestamp: "2025-01-15T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              schedules:
                employee:
                  id: "emp_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: "John Smith"
                  emp_number: "EMP-001"
                  is_site_lead: false
                work_order:
                  id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  phase_name: "Foundation Installation"
                  work_order_number: "WO-2025-001"
                assignment_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                schedules:
                  - id: "sch_001"
                    work_date: "2025-01-20"
                    start_time: "07:00"
                    end_time: "16:00"
                    is_active: true
                    is_override: true
                  - id: "sch_003"
                    work_date: "2025-01-22"
                    start_time: "08:00"
                    end_time: "17:00"
                    is_active: true
                    is_override: false
                changes:
                  days_added: ["2025-01-22"]
                  days_removed: ["2025-01-21"]
                  days_modified:
                    - work_date: "2025-01-20"
                      previous:
                        start_time: "08:00"
                        end_time: "17:00"
                      current:
                        start_time: "07:00"
                        end_time: "16:00"

    ScheduleBatchDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [schedule.batch_deleted]
            data:
              $ref: '#/components/schemas/ScheduleBatchPayload'
          example:
            event_id: "evt_schedule_batch_003"
            event_type: "schedule.batch_deleted"
            timestamp: "2025-01-15T16:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              schedules:
                employee:
                  id: "emp_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  name: ""
                  emp_number: null
                  is_site_lead: false
                work_order:
                  id: "wo_987fcdeb-51a2-43f7-8c9d-0e1f2a3b4c5d"
                  phase_name: ""
                  work_order_number: null
                assignment_id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                schedules: []
                changes:
                  days_added: []
                  days_removed: ["2025-01-20", "2025-01-21", "2025-01-22"]
                  days_modified: []

    ScheduleBatchPayload:
      type: object
      required:
        - schedules
      properties:
        schedules:
          type: object
          required:
            - work_order
            - assignment_id
            - schedules
          properties:
            employee:
              type: object
              nullable: true
              description: The employee assigned to this work order (null for temp employees)
              properties:
                id:
                  type: string
                  format: uuid
                name:
                  type: string
                emp_number:
                  type: string
                  nullable: true
                is_site_lead:
                  type: boolean
                  description: True if this employee is the designated site lead for this specific work order
            work_order:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                phase_name:
                  type: string
                work_order_number:
                  type: string
                  nullable: true
            assignment_id:
              type: string
              format: uuid
            schedules:
              type: array
              description: All current active schedule days for this assignment
              items:
                type: object
                properties:
                  id:
                    type: string
                    format: uuid
                  work_date:
                    type: string
                    format: date
                  start_time:
                    type: string
                    nullable: true
                    description: Time in HH:MM format
                  end_time:
                    type: string
                    nullable: true
                    description: Time in HH:MM format
                  is_active:
                    type: boolean
                  is_override:
                    type: boolean
                    description: True if this day has custom times different from defaults
            changes:
              type: object
              description: Delta showing what changed in this batch operation
              properties:
                days_added:
                  type: array
                  items:
                    type: string
                    format: date
                  description: Dates that were added in this operation
                days_removed:
                  type: array
                  items:
                    type: string
                    format: date
                  description: Dates that were removed in this operation
                days_modified:
                  type: array
                  items:
                    type: object
                    properties:
                      work_date:
                        type: string
                        format: date
                      previous:
                        type: object
                        properties:
                          start_time:
                            type: string
                          end_time:
                            type: string
                      current:
                        type: object
                        properties:
                          start_time:
                            type: string
                          end_time:
                            type: string
                  description: Dates where times were modified

    # ==========================================
    # CUSTOM BOOKING EVENTS
    # ==========================================
    
    CustomBookingCreatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [custom_booking.created]
            data:
              type: object
              required:
                - custom_booking
              properties:
                custom_booking:
                  type: object
                  required:
                    - id
                    - company_id
                    - employee_id
                    - booking_date
                    - name
                    - start_time
                    - end_time
                    - created_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                    employee_id:
                      type: string
                      format: uuid
                    booking_date:
                      type: string
                      format: date
                    name:
                      type: string
                      description: Name/title of the booking
                    description:
                      type: string
                      nullable: true
                    start_time:
                      type: string
                      description: Time in HH:MM format
                    end_time:
                      type: string
                      description: Time in HH:MM format
                    work_type_id:
                      type: string
                      format: uuid
                      nullable: true
                    is_active:
                      type: boolean
                    is_availability:
                      type: boolean
                      description: Whether this booking represents an availability note rather than a time-bound booking
                    external_id:
                      type: string
                      nullable: true
                      description: External system reference ID
                    badge_text:
                      type: string
                      nullable: true
                    badge_url:
                      type: string
                      nullable: true
                    employee:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        emp_number:
                          type: string
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_custom_booking_created_001"
            event_type: "custom_booking.created"
            timestamp: "2025-01-15T08:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              custom_booking:
                id: "cb_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
                booking_date: "2025-01-15"
                name: "Training Session"
                description: "Annual safety training"
                start_time: "09:00"
                end_time: "12:00"
                is_active: true
                is_availability: false
                employee:
                  id: "emp_123e4567-e89b-12d3-a456-426614174001"
                  name: "John Smith"
                  emp_number: "EMP-001"
                created_at: "2025-01-15T08:00:00Z"
                updated_at: "2025-01-15T08:00:00Z"

    CustomBookingUpdatedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [custom_booking.updated]
            data:
              type: object
              required:
                - custom_booking
              properties:
                custom_booking:
                  type: object
                  required:
                    - id
                    - company_id
                    - employee_id
                    - booking_date
                    - name
                    - updated_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    company_id:
                      type: string
                      format: uuid
                    employee_id:
                      type: string
                      format: uuid
                    booking_date:
                      type: string
                      format: date
                    name:
                      type: string
                      description: Name/title of the booking
                    description:
                      type: string
                      nullable: true
                    start_time:
                      type: string
                      description: Time in HH:MM format
                    end_time:
                      type: string
                      description: Time in HH:MM format
                    work_type_id:
                      type: string
                      format: uuid
                      nullable: true
                    is_active:
                      type: boolean
                    is_availability:
                      type: boolean
                      description: Whether this booking represents an availability note rather than a time-bound booking
                    external_id:
                      type: string
                      nullable: true
                      description: External system reference ID
                    badge_text:
                      type: string
                      nullable: true
                    badge_url:
                      type: string
                      nullable: true
                    employee:
                      type: object
                      nullable: true
                      properties:
                        id:
                          type: string
                          format: uuid
                        name:
                          type: string
                        emp_number:
                          type: string
                    created_at:
                      type: string
                      format: date-time
                    updated_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_custom_booking_updated_001"
            event_type: "custom_booking.updated"
            timestamp: "2025-01-15T10:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              custom_booking:
                id: "cb_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                company_id: "123e4567-e89b-12d3-a456-426614174000"
                employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
                booking_date: "2025-01-15"
                name: "Training Session (Extended)"
                description: "Annual safety training - extended session"
                start_time: "09:00"
                end_time: "14:00"
                is_active: true
                is_availability: false
                updated_at: "2025-01-15T10:00:00Z"

    CustomBookingDeletedEvent:
      allOf:
        - $ref: '#/components/schemas/OutboundWebhookEvent'
        - type: object
          properties:
            event_type:
              type: string
              enum: [custom_booking.deleted]
            data:
              type: object
              required:
                - custom_booking
              properties:
                custom_booking:
                  type: object
                  required:
                    - id
                    - deleted_at
                  properties:
                    id:
                      type: string
                      format: uuid
                    employee_id:
                      type: string
                      format: uuid
                    booking_date:
                      type: string
                      format: date
                    deleted_at:
                      type: string
                      format: date-time
          example:
            event_id: "evt_custom_booking_deleted_001"
            event_type: "custom_booking.deleted"
            timestamp: "2025-01-15T16:00:00Z"
            company_id: "123e4567-e89b-12d3-a456-426614174000"
            data:
              custom_booking:
                id: "cb_a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                employee_id: "emp_123e4567-e89b-12d3-a456-426614174001"
                booking_date: "2025-01-15"
                deleted_at: "2025-01-15T16:00:00Z"

    # ==========================================
    # DISCRIMINATED UNION FOR ALL OUTBOUND EVENTS
    # ==========================================
    AllOutboundWebhookEvents:
      description: |
        Discriminated union of all outbound webhook event types.
        
        Use the `event_type` field to determine which specific event schema applies to the payload.
        This allows external systems to properly parse and handle different webhook events.
      oneOf:
        # Work Order Events
        - $ref: '#/components/schemas/WorkOrderCreatedEvent'
        - $ref: '#/components/schemas/WorkOrderUpdatedEvent'
        - $ref: '#/components/schemas/WorkOrderDeletedEvent'
        - $ref: '#/components/schemas/WorkOrderStatusChangedEvent'
        - $ref: '#/components/schemas/WorkOrderProgressUpdatedEvent'
        # Project Events
        - $ref: '#/components/schemas/ProjectCreatedEvent'
        - $ref: '#/components/schemas/ProjectUpdatedEvent'
        - $ref: '#/components/schemas/ProjectDeletedEvent'
        - $ref: '#/components/schemas/ProjectStatusChangedEvent'
        - $ref: '#/components/schemas/ProjectClosedEvent'
        # Employee Events
        - $ref: '#/components/schemas/EmployeeCreatedEvent'
        - $ref: '#/components/schemas/EmployeeUpdatedEvent'
        - $ref: '#/components/schemas/EmployeeDeletedEvent'
        # Assignment Events
        - $ref: '#/components/schemas/AssignmentCreatedEvent'
        - $ref: '#/components/schemas/AssignmentUpdatedEvent'
        - $ref: '#/components/schemas/AssignmentDeletedEvent'
        # Schedule Batch Events
        - $ref: '#/components/schemas/ScheduleBatchCreatedEvent'
        - $ref: '#/components/schemas/ScheduleBatchUpdatedEvent'
        - $ref: '#/components/schemas/ScheduleBatchDeletedEvent'
        # Custom Booking Events
        - $ref: '#/components/schemas/CustomBookingCreatedEvent'
        - $ref: '#/components/schemas/CustomBookingUpdatedEvent'
        - $ref: '#/components/schemas/CustomBookingDeletedEvent'
        # Time Off Events
        - $ref: '#/components/schemas/TimeOffCreatedEvent'
        - $ref: '#/components/schemas/TimeOffUpdatedEvent'
        - $ref: '#/components/schemas/TimeOffDeletedEvent'
        # Vendor Events
        - $ref: '#/components/schemas/VendorCreatedEvent'
        - $ref: '#/components/schemas/VendorUpdatedEvent'
        - $ref: '#/components/schemas/VendorDeletedEvent'
        # Vendor Contact Events
        - $ref: '#/components/schemas/VendorContactCreatedEvent'
        - $ref: '#/components/schemas/VendorContactUpdatedEvent'
        - $ref: '#/components/schemas/VendorContactDeletedEvent'
        # Vendor Location Events
        - $ref: '#/components/schemas/VendorLocationCreatedEvent'
        - $ref: '#/components/schemas/VendorLocationUpdatedEvent'
        - $ref: '#/components/schemas/VendorLocationDeletedEvent'
        # Timesheet Events
        - $ref: '#/components/schemas/TimesheetCreatedEvent'
        - $ref: '#/components/schemas/TimesheetSubmittedEvent'
        - $ref: '#/components/schemas/TimesheetUpdatedEvent'
        - $ref: '#/components/schemas/TimesheetLockedEvent'
        - $ref: '#/components/schemas/TimesheetUnlockedEvent'
        - $ref: '#/components/schemas/TimesheetProcessedEvent'
        - $ref: '#/components/schemas/TimesheetRevertedEvent'
        # Report Events
        - $ref: '#/components/schemas/DailyReportSubmittedEvent'
        - $ref: '#/components/schemas/CompletionReportSubmittedEvent'
        - $ref: '#/components/schemas/CompletionReportApprovedEvent'
        - $ref: '#/components/schemas/ReportUpdatedEvent'
        # Customer Events
        - $ref: '#/components/schemas/CustomerCreatedEvent'
        - $ref: '#/components/schemas/CustomerUpdatedEvent'
        - $ref: '#/components/schemas/CustomerDeletedEvent'
        # Location Events
        - $ref: '#/components/schemas/LocationCreatedEvent'
        - $ref: '#/components/schemas/LocationUpdatedEvent'
        - $ref: '#/components/schemas/LocationDeletedEvent'
        # Contact Events
        - $ref: '#/components/schemas/ContactCreatedEvent'
        - $ref: '#/components/schemas/ContactUpdatedEvent'
        - $ref: '#/components/schemas/ContactDeletedEvent'
        # Purchase Order Events
        - $ref: '#/components/schemas/POCreatedEvent'
        - $ref: '#/components/schemas/POUpdatedEvent'
        - $ref: '#/components/schemas/POSubmittedEvent'
        - $ref: '#/components/schemas/POApprovedEvent'
        - $ref: '#/components/schemas/PORejectedEvent'
        - $ref: '#/components/schemas/POReceivedEvent'
        - $ref: '#/components/schemas/POBilledEvent'
        - $ref: '#/components/schemas/POPartiallyBilledEvent'
        - $ref: '#/components/schemas/POUnbilledEvent'
        - $ref: '#/components/schemas/POAttachmentAddedEvent'
        - $ref: '#/components/schemas/POAttachmentRemovedEvent'
        - $ref: '#/components/schemas/POPaymentRecordedEvent'
        - $ref: '#/components/schemas/POPaymentDeletedEvent'
        - $ref: '#/components/schemas/POPaidEvent'
        # Invoice Events
        - $ref: '#/components/schemas/InvoiceCreatedEvent'
        - $ref: '#/components/schemas/InvoiceUpdatedEvent'
        - $ref: '#/components/schemas/InvoiceSentEvent'
        - $ref: '#/components/schemas/InvoicePaidEvent'
        - $ref: '#/components/schemas/InvoiceVoidedEvent'
        - $ref: '#/components/schemas/InvoicePaymentRecordedEvent'
        - $ref: '#/components/schemas/InvoicePaymentDeletedEvent'
        # Product Events
        - $ref: '#/components/schemas/ProductCreatedEvent'
        - $ref: '#/components/schemas/ProductUpdatedEvent'
        - $ref: '#/components/schemas/ProductDeletedEvent'
        # Usage Record Events
        - $ref: '#/components/schemas/UsageRecordCreatedEvent'
        - $ref: '#/components/schemas/UsageRecordUpdatedEvent'
        - $ref: '#/components/schemas/UsageRecordDeletedEvent'
        # Equipment Events
        - $ref: '#/components/schemas/EquipmentStatusChangedEvent'
        - $ref: '#/components/schemas/EquipmentCheckedOutEvent'
        - $ref: '#/components/schemas/EquipmentCheckedInEvent'
        - $ref: '#/components/schemas/EquipmentServiceRecordSubmittedEvent'
        # Vendor Credit Events
        - $ref: '#/components/schemas/VendorCreditCreatedEvent'
        - $ref: '#/components/schemas/VendorCreditUpdatedEvent'
        - $ref: '#/components/schemas/VendorCreditAppliedEvent'
        # Settings Events
        - $ref: '#/components/schemas/SettingsUpdatedEvent'
        # Task Events
        - $ref: '#/components/schemas/TaskCreatedEvent'
        - $ref: '#/components/schemas/TaskCompletedEvent'
        - $ref: '#/components/schemas/TaskReassignedEvent'
      discriminator:
        propertyName: event_type
        mapping:
          work_order.created: '#/components/schemas/WorkOrderCreatedEvent'
          work_order.updated: '#/components/schemas/WorkOrderUpdatedEvent'
          work_order.deleted: '#/components/schemas/WorkOrderDeletedEvent'
          work_order.status_changed: '#/components/schemas/WorkOrderStatusChangedEvent'
          work_order.progress_updated: '#/components/schemas/WorkOrderProgressUpdatedEvent'
          
          project.created: '#/components/schemas/ProjectCreatedEvent'
          project.updated: '#/components/schemas/ProjectUpdatedEvent'
          project.deleted: '#/components/schemas/ProjectDeletedEvent'
          project.status_changed: '#/components/schemas/ProjectStatusChangedEvent'
          project.closed: '#/components/schemas/ProjectClosedEvent'
          
          employee.created: '#/components/schemas/EmployeeCreatedEvent'
          employee.updated: '#/components/schemas/EmployeeUpdatedEvent'
          employee.deleted: '#/components/schemas/EmployeeDeletedEvent'
          
          assignment.created: '#/components/schemas/AssignmentCreatedEvent'
          assignment.updated: '#/components/schemas/AssignmentUpdatedEvent'
          assignment.deleted: '#/components/schemas/AssignmentDeletedEvent'
          
          schedule.batch_created: '#/components/schemas/ScheduleBatchCreatedEvent'
          schedule.batch_updated: '#/components/schemas/ScheduleBatchUpdatedEvent'
          schedule.batch_deleted: '#/components/schemas/ScheduleBatchDeletedEvent'
          
          custom_booking.created: '#/components/schemas/CustomBookingCreatedEvent'
          custom_booking.updated: '#/components/schemas/CustomBookingUpdatedEvent'
          custom_booking.deleted: '#/components/schemas/CustomBookingDeletedEvent'
          
          time_off.created: '#/components/schemas/TimeOffCreatedEvent'
          time_off.updated: '#/components/schemas/TimeOffUpdatedEvent'
          time_off.deleted: '#/components/schemas/TimeOffDeletedEvent'
          
          vendor.created: '#/components/schemas/VendorCreatedEvent'
          vendor.updated: '#/components/schemas/VendorUpdatedEvent'
          vendor.deleted: '#/components/schemas/VendorDeletedEvent'
          vendor.contact.created: '#/components/schemas/VendorContactCreatedEvent'
          vendor.contact.updated: '#/components/schemas/VendorContactUpdatedEvent'
          vendor.contact.deleted: '#/components/schemas/VendorContactDeletedEvent'
          vendor.location.created: '#/components/schemas/VendorLocationCreatedEvent'
          vendor.location.updated: '#/components/schemas/VendorLocationUpdatedEvent'
          vendor.location.deleted: '#/components/schemas/VendorLocationDeletedEvent'
          
          timesheet.created: '#/components/schemas/TimesheetCreatedEvent'
          timesheet.submitted: '#/components/schemas/TimesheetSubmittedEvent'
          timesheet.updated: '#/components/schemas/TimesheetUpdatedEvent'
          timesheet.locked: '#/components/schemas/TimesheetLockedEvent'
          timesheet.unlocked: '#/components/schemas/TimesheetUnlockedEvent'
          timesheet.processed: '#/components/schemas/TimesheetProcessedEvent'
          timesheet.reverted: '#/components/schemas/TimesheetRevertedEvent'
          
          report.daily_submitted: '#/components/schemas/DailyReportSubmittedEvent'
          report.completion_submitted: '#/components/schemas/CompletionReportSubmittedEvent'
          report.completion_approved: '#/components/schemas/CompletionReportApprovedEvent'
          report.updated: '#/components/schemas/ReportUpdatedEvent'
          
          customer.created: '#/components/schemas/CustomerCreatedEvent'
          customer.updated: '#/components/schemas/CustomerUpdatedEvent'
          customer.deleted: '#/components/schemas/CustomerDeletedEvent'
          
          location.created: '#/components/schemas/LocationCreatedEvent'
          location.updated: '#/components/schemas/LocationUpdatedEvent'
          location.deleted: '#/components/schemas/LocationDeletedEvent'
          
          contact.created: '#/components/schemas/ContactCreatedEvent'
          contact.updated: '#/components/schemas/ContactUpdatedEvent'
          contact.deleted: '#/components/schemas/ContactDeletedEvent'
          
          po.created: '#/components/schemas/POCreatedEvent'
          po.updated: '#/components/schemas/POUpdatedEvent'
          po.submitted: '#/components/schemas/POSubmittedEvent'
          po.approved: '#/components/schemas/POApprovedEvent'
          po.rejected: '#/components/schemas/PORejectedEvent'
          po.received: '#/components/schemas/POReceivedEvent'
          po.billed: '#/components/schemas/POBilledEvent'
          po.partially_billed: '#/components/schemas/POPartiallyBilledEvent'
          po.unbilled: '#/components/schemas/POUnbilledEvent'
          po.attachment.added: '#/components/schemas/POAttachmentAddedEvent'
          po.attachment.removed: '#/components/schemas/POAttachmentRemovedEvent'
          po.payment_recorded: '#/components/schemas/POPaymentRecordedEvent'
          po.payment_deleted: '#/components/schemas/POPaymentDeletedEvent'
          po.paid: '#/components/schemas/POPaidEvent'
          
          invoice.created: '#/components/schemas/InvoiceCreatedEvent'
          invoice.updated: '#/components/schemas/InvoiceUpdatedEvent'
          invoice.sent: '#/components/schemas/InvoiceSentEvent'
          invoice.paid: '#/components/schemas/InvoicePaidEvent'
          invoice.voided: '#/components/schemas/InvoiceVoidedEvent'
          invoice.deleted: '#/components/schemas/InvoiceDeletedEvent'
          invoice.payment_recorded: '#/components/schemas/InvoicePaymentRecordedEvent'
          invoice.payment_deleted: '#/components/schemas/InvoicePaymentDeletedEvent'
          
          product.created: '#/components/schemas/ProductCreatedEvent'
          product.updated: '#/components/schemas/ProductUpdatedEvent'
          product.deleted: '#/components/schemas/ProductDeletedEvent'
          
          usage_record.created: '#/components/schemas/UsageRecordCreatedEvent'
          usage_record.updated: '#/components/schemas/UsageRecordUpdatedEvent'
          usage_record.deleted: '#/components/schemas/UsageRecordDeletedEvent'
          
          equipment.status_changed: '#/components/schemas/EquipmentStatusChangedEvent'
          equipment.checked_out: '#/components/schemas/EquipmentCheckedOutEvent'
          equipment.checked_in: '#/components/schemas/EquipmentCheckedInEvent'
          equipment.service_record_submitted: '#/components/schemas/EquipmentServiceRecordSubmittedEvent'
          
          vendor_credit.created: '#/components/schemas/VendorCreditCreatedEvent'
          vendor_credit.updated: '#/components/schemas/VendorCreditUpdatedEvent'
          vendor_credit.applied: '#/components/schemas/VendorCreditAppliedEvent'
          
          settings.updated: '#/components/schemas/SettingsUpdatedEvent'

          task.created: '#/components/schemas/TaskCreatedEvent'
          task.completed: '#/components/schemas/TaskCompletedEvent'
          task.reassigned: '#/components/schemas/TaskReassignedEvent'

    # ==========================================================================
    # Settings Webhook Event Schemas
    # ==========================================================================

    SettingsUpdatedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [settings.updated]
        data:
          type: object
          properties:
            settings_area:
              type: string
              enum: [company, finance, timesheet, modules]
              description: Which settings section was changed
            changed_fields:
              type: array
              items:
                type: string
              description: List of field names that were modified
            timestamp:
              type: string
              format: date-time
              description: When the settings change occurred
          required: [settings_area, changed_fields, timestamp]

    # ==========================================================================
    # Task Webhook Event Schemas
    # ==========================================================================

    TaskPayload:
      type: object
      description: |
        Canonical task object emitted in `data.task` for every `task.*` webhook
        event. Mirrors the columns of the `tasks` table.
      required:
        - id
        - company_id
        - assignee_employee_id
        - title
        - priority
        - status
        - source_type
        - created_at
        - updated_at
      properties:
        id: { type: string, format: uuid }
        company_id: { type: string, format: uuid }
        assignee_employee_id: { type: string, format: uuid }
        created_by_employee_id: { type: string, format: uuid, nullable: true }
        assigned_by_employee_id: { type: string, format: uuid, nullable: true }
        title: { type: string }
        description: { type: string, nullable: true }
        priority:
          type: string
          enum: [low, normal, high, urgent]
        status:
          type: string
          enum: [open, completed, dismissed, expired]
        due_at: { type: string, format: date-time, nullable: true }
        reminder_at: { type: string, format: date-time, nullable: true }
        source_type:
          type: string
          enum: [user, internal, app]
          description: Origin of the task. `app` indicates it was created via the public Tasks API by an installed app.
        source_app_install_id: { type: string, format: uuid, nullable: true }
        source_feature:
          type: string
          nullable: true
          description: Internal feature key (e.g. `po_needs_approval`) when `source_type = "internal"`.
        project_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        related_entity_type: { type: string, nullable: true }
        related_entity_id: { type: string, nullable: true }
        action_type:
          type: string
          nullable: true
          enum: [deep_link, external_url, app_modal, none, null]
        action_label: { type: string, nullable: true }
        action_target: { type: string, nullable: true }
        action_context:
          type: object
          additionalProperties: true
          nullable: true
        requires_confirmation: { type: boolean }
        auto_complete_on_action: { type: boolean }
        completion_callback_url: { type: string, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }
        completed_by_employee_id: { type: string, format: uuid, nullable: true }
        dismissed_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    TaskCreatedEvent:
      type: object
      required: [event_type, data]
      description: |
        Fired when a task row is inserted, regardless of source (`user`,
        `internal`, or `app`). See echo-storm protection: subscriptions linked
        to the same API key that created the task are skipped.
      properties:
        event_type:
          type: string
          enum: [task.created]
        event_id: { type: string, format: uuid }
        timestamp: { type: string, format: date-time }
        company_id: { type: string, format: uuid }
        data:
          type: object
          required: [task]
          properties:
            task:
              $ref: '#/components/schemas/TaskPayload'

    TaskCompletedEvent:
      type: object
      required: [event_type, data]
      description: |
        Fired when a task transitions to `status = "completed"`. Other status
        changes (e.g. `dismissed`, `expired`) do not fire this event.
      properties:
        event_type:
          type: string
          enum: [task.completed]
        event_id: { type: string, format: uuid }
        timestamp: { type: string, format: date-time }
        company_id: { type: string, format: uuid }
        data:
          type: object
          required: [task]
          properties:
            task:
              allOf:
                - $ref: '#/components/schemas/TaskPayload'
                - type: object
                  required: [completed_at, completed_by_employee_id]
                  properties:
                    status:
                      type: string
                      enum: [completed]

    TaskReassignedEvent:
      type: object
      required: [event_type, data]
      description: |
        Fired when `assignee_employee_id` actually changes value. PATCHing the
        same employee back in does not fire this event. The new assignee is
        already applied in `data.task`; there is no `previous_assignee_employee_id`
        field — read it from your own mirror or from a preceding event.
      properties:
        event_type:
          type: string
          enum: [task.reassigned]
        event_id: { type: string, format: uuid }
        timestamp: { type: string, format: date-time }
        company_id: { type: string, format: uuid }
        data:
          type: object
          required: [task]
          properties:
            task:
              $ref: '#/components/schemas/TaskPayload'


    # ==========================================================================
    # Product Webhook Event Schemas
    # ==========================================================================

    ProductCreatedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [product.created]
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            type:
              type: string
              enum: [material, equipment, labor, service]
            sku:
              type: string
              nullable: true
            base_cost:
              type: number
            unit_of_measure:
              type: string
            is_active:
              type: boolean
            is_billable:
              type: boolean
            preferred_vendor_id:
              type: string
              format: uuid
              nullable: true
            burden_pct:
              type: number
              nullable: true
              description: "Burden percentage on top of base_cost (labor only)"
            is_role_default:
              type: boolean
              description: "When true, serves as default rate for all employees in the linked role"
            role_id:
              type: string
              format: uuid
              nullable: true
              description: "Role this product is linked to (labor only)"
            employee_id:
              type: string
              format: uuid
              nullable: true
              description: "Employee this labor rate is assigned to"

    ProductUpdatedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [product.updated]
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            type:
              type: string
            sku:
              type: string
              nullable: true
            base_cost:
              type: number
            unit_of_measure:
              type: string
            is_active:
              type: boolean
            is_billable:
              type: boolean
            preferred_vendor_id:
              type: string
              format: uuid
              nullable: true
            burden_pct:
              type: number
              nullable: true
              description: "Burden percentage on top of base_cost (labor only)"
            is_role_default:
              type: boolean
              description: "When true, serves as default rate for all employees in the linked role"
            role_id:
              type: string
              format: uuid
              nullable: true
              description: "Role this product is linked to (labor only)"
            employee_id:
              type: string
              format: uuid
              nullable: true
              description: "Employee this labor rate is assigned to"

    ProductDeletedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [product.deleted]
        data:
          type: object
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string

    UsageRecordWebhookPayload:
      type: object
      description: |
        Full usage record payload with product join, matching `fetchUsageRecordForWebhook`
        output. Identical shape to `GET /v1/usage-records/:id` responses — both channels
        route through `_shared/usageRecordService.ts`.

        **ID stability:** A usage_record `id` is stable for the lifetime of the row.
        Daily-report edits delete-and-reinsert the materials block, which fires fresh
        `usage_record.created` events with new IDs. Consumers must key off the webhook
        `id` per event and must not cache prior IDs across report edits.
      properties:
        payload_version:
          type: string
          description: Outbound payload schema version (stamped by webhook emitter). Currently `1.1.0`.
          example: "1.1.0"
        id:
          type: string
          format: uuid
        company_id:
          type: string
          format: uuid
        work_order_id:
          type: string
          format: uuid
        product_id:
          type: string
          format: uuid
          nullable: true
        product:
          type: object
          nullable: true
          description: Expanded product object from products table join
          properties:
            id:
              type: string
              format: uuid
            name:
              type: string
            sku:
              type: string
              nullable: true
            type:
              type: string
              enum: [material, equipment, labor, service]
            unit_of_measure:
              type: string
              nullable: true
        quantity:
          type: number
        unit_cost_snapshot:
          type: number
          nullable: true
          description: Frozen base cost at creation (per base unit, post UoM pre-divide)
        resale_price_snapshot:
          type: number
          nullable: true
          description: Frozen resale price at creation (per base unit, post UoM pre-divide)
        display_unit:
          type: string
          nullable: true
          description: Original quoted UoM before pre-divide (e.g. 'box of 12')
        display_unit_price:
          type: number
          nullable: true
          description: Original quoted resale price per display_unit
        divisor_applied:
          type: number
          nullable: true
          description: UoM divisor applied during pre-divide (1 if none)
        product_name_snapshot:
          type: string
          nullable: true
          description: Snapshot of product name at creation time
        po_line_item_id:
          type: string
          format: uuid
          nullable: true
        invoice_id:
          type: string
          format: uuid
          nullable: true
          description: Direct FK to the invoice this record was billed onto. `null` while unbilled.
        cost_code:
          type: string
          nullable: true
        status:
          type: string
          enum: [pending, invoiced, written-off]
        source:
          type: string
          enum: [field_worker, office, api]
        notes:
          type: string
          nullable: true
        metadata:
          type: object
        recorded_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        linked_invoice:
          allOf:
            - $ref: '#/components/schemas/LinkedInvoiceRef'
          nullable: true
          description: Most recent invoice that billed this usage record (since payload_version 1.1.0). `null` when not billed.



    UsageRecordCreatedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [usage_record.created]
        data:
          type: object
          properties:
            usage_record:
              $ref: '#/components/schemas/UsageRecordWebhookPayload'

    UsageRecordUpdatedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [usage_record.updated]
        data:
          type: object
          properties:
            usage_record:
              $ref: '#/components/schemas/UsageRecordWebhookPayload'

    UsageRecordDeletedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [usage_record.deleted]
        data:
          type: object
          properties:
            usage_record:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                deleted:
                  type: boolean
                  enum: [true]

    # ==========================================================================
    # Equipment Webhook Event Schemas
    # ==========================================================================

    EquipmentStatusChangedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [equipment.status_changed]
        data:
          type: object
          properties:
            equipment_asset:
              type: object
              properties:
                asset_id:
                  type: string
                  format: uuid
                old_status:
                  type: string
                  enum: [active, locked]
                new_status:
                  type: string
                  enum: [active, locked]
                asset_tag:
                  type: string
                  nullable: true
                name:
                  type: string
                reason:
                  type: string
                  nullable: true
                service_record_id:
                  type: string
                  format: uuid
                  nullable: true

    EquipmentCheckedOutEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [equipment.checked_out]
        data:
          type: object
          properties:
            equipment_checkout:
              type: object
              properties:
                checkout_id:
                  type: string
                  format: uuid
                asset_id:
                  type: string
                  format: uuid
                asset_tag:
                  type: string
                  nullable: true
                asset_name:
                  type: string
                work_order_id:
                  type: string
                  format: uuid
                checked_out_by:
                  type: string
                  format: uuid
                  nullable: true

    EquipmentCheckedInEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [equipment.checked_in]
        data:
          type: object
          properties:
            equipment_checkout:
              type: object
              properties:
                checkout_id:
                  type: string
                  format: uuid
                asset_id:
                  type: string
                  format: uuid
                asset_tag:
                  type: string
                  nullable: true
                asset_name:
                  type: string
                work_order_id:
                  type: string
                  format: uuid
                usage_record_id:
                  type: string
                  format: uuid
                  nullable: true
                auto_billed:
                  type: boolean

    EquipmentServiceRecordSubmittedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [equipment.service_record_submitted]
        data:
          type: object
          properties:
            equipment_service_record:
              type: object
              properties:
                service_record_id:
                  type: string
                  format: uuid
                asset_id:
                  type: string
                  format: uuid
                asset_tag:
                  type: string
                  nullable: true
                asset_name:
                  type: string
                record_type:
                  type: string
                  enum: [safety, maintenance, repair]
                result:
                  type: string
                  enum: [pass, fail, needs_repair]
                auto_locked:
                  type: boolean

    # ==========================================================================
    # Vendor Credit Webhook Event Schemas
    # ==========================================================================

    VendorCreditCreatedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [vendor_credit.created]
        data:
          type: object
          properties:
            vendor_credit:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                credit_number:
                  type: string
                vendor_id:
                  type: string
                  format: uuid
                purchase_order_id:
                  type: string
                  format: uuid
                  nullable: true
                project_id:
                  type: string
                  format: uuid
                  nullable: true
                work_order_id:
                  type: string
                  format: uuid
                  nullable: true
                date:
                  type: string
                  format: date
                subtotal:
                  type: number
                tax_amount:
                  type: number
                total_amount:
                  type: number
                status:
                  type: string
                  enum: [draft, open]
                reference:
                  type: string
                  nullable: true
                memo:
                  type: string
                  nullable: true
                sync_status:
                  type: string
                  enum: [unsynced, pending, synced, error]
                sync_message:
                  type: string
                  nullable: true
                sync_url:
                  type: string
                  format: uri
                  nullable: true
                linked_invoice:
                  allOf:
                    - $ref: '#/components/schemas/LinkedInvoiceRef'
                  nullable: true
                  description: Most recent invoice that billed this credit (since payload_version 1.1.0). `null` when not billed.

    VendorCreditUpdatedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [vendor_credit.updated]
        data:
          type: object
          properties:
            vendor_credit:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                credit_number:
                  type: string
                total_amount:
                  type: number
                status:
                  type: string
                  enum: [draft, open, void]
                previous_status:
                  type: string
                  nullable: true
                linked_invoice:
                  allOf:
                    - $ref: '#/components/schemas/LinkedInvoiceRef'
                  nullable: true
                  description: Most recent invoice that billed this credit (since payload_version 1.1.0). `null` when not billed.

    VendorCreditAppliedEvent:
      type: object
      required: [event_type, data]
      properties:
        event_type:
          type: string
          enum: [vendor_credit.applied]
        data:
          type: object
          properties:
            vendor_credit:
              type: object
              properties:
                id:
                  type: string
                  format: uuid
                credit_number:
                  type: string
                total_amount:
                  type: number
                status:
                  type: string
                  enum: [applied]
                previous_status:
                  type: string

    # ==========================================
    # TIMESHEETS REST SCHEMAS
    # ==========================================

    TimesheetResource:
      type: object
      properties:
        id: { type: string, format: uuid }
        company_id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        timesheet_date: { type: string, format: date }
        status:
          type: string
          enum: [draft, submitted, locked, processed]
        total_hours: { type: number, format: float }
        notes: { type: string, nullable: true }
        submitted_at: { type: string, format: date-time, nullable: true }
        submitted_by: { type: string, format: uuid, nullable: true }
        locked_at: { type: string, format: date-time, nullable: true }
        locked_by: { type: string, format: uuid, nullable: true }
        processed_at: { type: string, format: date-time, nullable: true }
        processed_by: { type: string, format: uuid, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        entries:
          type: array
          items: { $ref: '#/components/schemas/TimesheetEntryResource' }
        employee:
          type: object
          nullable: true
          properties:
            id: { type: string, format: uuid }
            first_name: { type: string }
            last_name: { type: string }
            email: { type: string }

    TimesheetEntryResource:
      type: object
      properties:
        id: { type: string, format: uuid }
        timesheet_id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        entry_date: { type: string, format: date }
        hours: { type: number, format: float }
        work_type_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        start_time: { type: string, nullable: true, description: "HH:MM:SS" }
        end_time: { type: string, nullable: true, description: "HH:MM:SS" }
        notes: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        work_type:
          type: object
          nullable: true
        work_order:
          type: object
          nullable: true
        linked_invoice:
          type: object
          nullable: true
          description: Echo of the invoice that consumed this entry, if any.

    TimesheetCreateRequest:
      type: object
      required: [timesheet_date]
      description: |
        Provide **either** `employee_id` (UUID) or `employee_emp_number` (human-friendly).
        If both are provided they must resolve to the same employee.
      properties:
        employee_id: { type: string, format: uuid, description: "Employee UUID (use this OR employee_emp_number)" }
        employee_emp_number: { type: string, maxLength: 50, description: "Employee number lookup, e.g. 'EMP-1042' (use this OR employee_id)" }
        timesheet_date: { type: string, format: date }
        status:
          type: string
          enum: [draft, submitted]
          default: submitted
        notes: { type: string, maxLength: 2000 }
        entries:
          type: array
          items: { $ref: '#/components/schemas/TimesheetEntryCreateRequest' }

    TimesheetUpdateRequest:
      type: object
      description: |
        Strict schema — `status`, `total_hours`, and audit fields are rejected
        with 400. Use the lifecycle endpoints for state changes.
        `employee_emp_number` is accepted as an alternative to `employee_id`.
      properties:
        employee_id: { type: string, format: uuid }
        employee_emp_number: { type: string, maxLength: 50, description: "Employee number lookup (alternative to employee_id)" }
        timesheet_date: { type: string, format: date }
        notes: { type: string, maxLength: 2000, nullable: true }
        entries:
          type: array
          items: { $ref: '#/components/schemas/TimesheetEntryCreateRequest' }


    TimesheetEntryCreateRequest:
      type: object
      required: [hours]
      properties:
        timesheet_id: { type: string, format: uuid, description: "Required for /v1/timesheet-entries POST; optional when nested in TimesheetCreateRequest." }
        entry_date: { type: string, format: date }
        hours: { type: number, format: float, minimum: 0 }
        work_type_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        start_time: { type: string, nullable: true }
        end_time: { type: string, nullable: true }
        notes: { type: string, nullable: true }

    TimesheetEntryUpdateRequest:
      type: object
      properties:
        hours: { type: number, format: float, minimum: 0 }
        work_type_id: { type: string, format: uuid, nullable: true }
        work_order_id: { type: string, format: uuid, nullable: true }
        project_id: { type: string, format: uuid, nullable: true }
        start_time: { type: string, nullable: true }
        end_time: { type: string, nullable: true }
        notes: { type: string, nullable: true }

    TimesheetBatchIdsRequest:
      type: object
      required: [timesheet_ids]
      properties:
        timesheet_ids:
          type: array
          minItems: 1
          maxItems: 500
          items: { type: string, format: uuid }

    TimesheetBatchLockResponse:
      type: object
      properties:
        locked_count: { type: integer }
        skipped_count: { type: integer }
        skipped_ids:
          type: array
          items: { type: string, format: uuid }

    TimesheetBatchProcessResponse:
      type: object
      properties:
        processed_count: { type: integer }
        skipped_count: { type: integer }
        skipped_ids:
          type: array
          items: { type: string, format: uuid }

    TimeOffResource:
      type: object
      properties:
        id: { type: string, format: uuid }
        company_id: { type: string, format: uuid }
        employee_id: { type: string, format: uuid }
        emp_number: { type: string, nullable: true }
        employee_name: { type: string, nullable: true }
        time_off_type:
          type: string
          enum: [vacation, sick_leave, personal, unpaid, training, other]
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        start_time: { type: string, nullable: true, description: "HH:MM for partial day" }
        end_time: { type: string, nullable: true, description: "HH:MM for partial day" }
        status:
          type: string
          enum: [approved, pending, denied]
        notes: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    TimeOffCreateRequest:
      type: object
      required: [emp_number, start_date, end_date]
      properties:
        emp_number:
          type: string
          minLength: 1
          maxLength: 50
          description: Employee number (the employee is resolved within the caller's company).
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        time_off_type:
          type: string
          enum: [vacation, sick_leave, personal, unpaid, training, other]
          default: vacation
        start_time:
          type: string
          nullable: true
          description: "HH:MM — partial day start"
        end_time:
          type: string
          nullable: true
          description: "HH:MM — partial day end"
        status:
          type: string
          enum: [approved, pending, denied]
          default: approved
        notes: { type: string, maxLength: 5000, nullable: true }

    TimeOffUpdateRequest:
      type: object
      description: |
        Partial update — only provided fields are changed. Send `null` to
        clear a nullable field. Unknown keys are rejected with 400.
      properties:
        time_off_type:
          type: string
          enum: [vacation, sick_leave, personal, unpaid, training, other]
        start_date: { type: string, format: date }
        end_date: { type: string, format: date }
        start_time: { type: string, nullable: true }
        end_time: { type: string, nullable: true }
        status:
          type: string
          enum: [approved, pending, denied]
        notes: { type: string, maxLength: 5000, nullable: true }

  responses:
    BadRequest:
      description: Validation error (e.g. unknown field, invalid type, missing required field).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Forbidden:
      description: API key lacks the required scope.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    NotFound:
      description: Resource not found or not in caller's company.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    Conflict:
      description: Invalid state transition (e.g. lock on draft, mutate entry on processed parent).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
