{"openapi":"3.0.0","paths":{"/auth/otp/send":{"post":{"operationId":"AuthController_sendOtp","summary":"Send OTP code","description":"Sends a 6-digit one-time password to the given email address.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","example":"user@company.com"}}}}}},"responses":{"200":{"description":"OTP sent successfully"},"400":{"description":"Invalid email"}},"tags":["auth"]}},"/auth/otp/verify":{"post":{"operationId":"AuthController_verifyOtp","summary":"Verify OTP code","description":"Verifies the 6-digit OTP and returns a session token on success.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","code"],"properties":{"email":{"type":"string","example":"user@company.com"},"code":{"type":"string","example":"123456"}}}}}},"responses":{"200":{"description":"Returns `{ sessionToken }` — use as `Bearer` token for subsequent requests"},"401":{"description":"Invalid or expired OTP"}},"tags":["auth"]}},"/auth/workspace/integrations":{"get":{"operationId":"AuthController_getIntegrations","summary":"Get CRM integration status","description":"Returns which CRM integrations (Attio, HubSpot) are connected for the authenticated workspace.","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"Integration connection status"},"401":{"description":"Unauthorized"}},"tags":["auth"],"security":[{"bearer":[]}]}},"/auth/workspace/disconnect-attio":{"post":{"operationId":"AuthController_disconnectAttio","summary":"Disconnect Attio CRM","description":"Removes Attio OAuth credentials from the workspace. Does not delete deals or reps already synced.","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"Attio disconnected"},"401":{"description":"Unauthorized"}},"tags":["auth"],"security":[{"bearer":[]}]}},"/auth/workspace/disconnect-hubspot":{"post":{"operationId":"AuthController_disconnectHubspot","summary":"Disconnect HubSpot CRM","description":"Removes HubSpot OAuth credentials from the workspace.","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"HubSpot disconnected"},"401":{"description":"Unauthorized"}},"tags":["auth"],"security":[{"bearer":[]}]}},"/auth/workspace/disconnect-salesforce":{"post":{"operationId":"AuthController_disconnectSalesforce","summary":"Disconnect Salesforce CRM","description":"Removes Salesforce OAuth credentials from the workspace.","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"Salesforce disconnected"},"401":{"description":"Unauthorized"}},"tags":["auth"],"security":[{"bearer":[]}]}},"/auth/status":{"get":{"operationId":"AuthController_getStatus","summary":"Check auth status","description":"Returns whether the provided token is valid and which workspace it belongs to.","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"Auth status and workspace info"},"401":{"description":"Invalid token"}},"tags":["auth"],"security":[{"bearer":[]}]}},"/auth/me":{"get":{"operationId":"AuthController_getMe","summary":"Get current user","description":"Returns the authenticated user's profile: repId, email, name, role, and workspace details.","parameters":[{"name":"authorization","required":true,"in":"header","schema":{"type":"string"}}],"responses":{"200":{"description":"User profile"},"401":{"description":"Unauthorized"}},"tags":["auth"],"security":[{"bearer":[]}]}},"/api/fields":{"get":{"operationId":"FieldMappingController_getAttributes","summary":"List CRM field attributes","description":"Returns all available deal fields from the connected CRM (Attio or HubSpot) that can be used in field mapping and plan conditions.","parameters":[],"responses":{"200":{"description":"Array of CRM field definitions with slug, label, and type"},"401":{"description":"Unauthorized"}},"tags":["fields"],"security":[{"bearer":[]}]}},"/api/fields/attributes":{"get":{"operationId":"FieldMappingController_getAttributesAlias","summary":"List CRM field attributes (deprecated alias)","deprecated":true,"description":"Use `GET /api/fields`.","parameters":[],"responses":{"200":{"description":""}},"tags":["fields"],"security":[{"bearer":[]}]}},"/api/fields/attributes/{slug}/options":{"get":{"operationId":"FieldMappingController_getFieldOptions","summary":"Get field option values","description":"Returns the available option values for a select/status CRM field — used to populate condition dropdowns.","parameters":[{"name":"slug","required":true,"in":"path","description":"CRM field slug (e.g. `deal_stage`, `deal_type`)","schema":{"type":"string"}}],"responses":{"200":{"description":"Array of `{ label, value }` option pairs"},"401":{"description":"Unauthorized"},"404":{"description":"Field not found or not a select type"}},"tags":["fields"],"security":[{"bearer":[]}]}},"/api/fields/auto-match":{"post":{"operationId":"FieldMappingController_autoMatch","summary":"Auto-match CRM fields","description":"Inspects the connected CRM's deal schema and automatically suggests field mappings based on name/type heuristics.","parameters":[],"responses":{"201":{"description":"Suggested field mapping — call `POST /api/fields/save` to persist it"},"401":{"description":"Unauthorized"}},"tags":["fields"],"security":[{"bearer":[]}]}},"/api/fields/save":{"post":{"operationId":"FieldMappingController_saveMapping","summary":"Save field mapping","description":"Persists the CRM-to-engine field mapping for the workspace. The mapping tells the engine which CRM field slug maps to each standard field (dealAmount, dealStage, closeDate, etc.) and custom measure slugs used in plan rules.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["mapping"],"properties":{"mapping":{"type":"object","properties":{"dealAmount":{"type":"string","example":"monetary_value","description":"CRM field slug for deal revenue"},"dealStage":{"type":"string","example":"stage","description":"CRM field slug for deal stage"},"dealOwner":{"type":"string","example":"owner","description":"CRM field slug for deal owner"},"closeDate":{"type":"string","example":"close_date"},"dealName":{"type":"string","example":"name"},"contractYears":{"type":"string","example":"contract_years"},"isNewLogo":{"type":"string","example":"is_new_logo"},"measures":{"type":"object","additionalProperties":{"type":"string"},"description":"Custom measure slug → CRM field slug mappings"}}}}}}}},"responses":{"201":{"description":"Field mapping saved to workspace"},"401":{"description":"Unauthorized"}},"tags":["fields"],"security":[{"bearer":[]}]}},"/api/fields/current":{"get":{"operationId":"FieldMappingController_getCurrentMapping","summary":"Get current field mapping","description":"Returns the field mapping currently saved on the workspace.","parameters":[],"responses":{"200":{"description":"Current field mapping object"},"401":{"description":"Unauthorized"}},"tags":["fields"],"security":[{"bearer":[]}]}},"/api/commissions":{"get":{"operationId":"CommissionsController_getDashboard","summary":"Get commission dashboard","description":"Returns commission data depending on query params:\n- **No params** — team overview (manager+ only)\n- **`repId`** — full rep dashboard: earnings, attainment, closed deals, open pipeline\n- **`dealId`** — single deal breakdown with calculation trace","parameters":[{"name":"repId","required":false,"in":"query","description":"Rep ID — returns rep-level dashboard","schema":{"type":"string"}},{"name":"dealId","required":false,"in":"query","description":"Deal ID — returns per-deal commission breakdown with trace","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Period filter (e.g. `2026-04` for monthly, `2026-Q2` for quarterly)","schema":{"type":"string"}}],"responses":{"200":{"description":"Commission dashboard data"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — team view requires manager or admin role"}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/commissions/dashboard":{"get":{"operationId":"CommissionsController_getDashboardAlias","summary":"Get commission dashboard (alias for GET /api/commissions)","description":"Friction #2: alias kept for callers who guess `/dashboard`. Identical to GET /api/commissions.","parameters":[{"name":"repId","required":false,"in":"query","schema":{"type":"string"}},{"name":"dealId","required":false,"in":"query","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/commissions/periods":{"get":{"operationId":"CommissionsController_getPeriods","summary":"Get available periods","description":"Returns distinct periods that have commission events. Omit repId for all workspace periods.","parameters":[{"name":"repId","required":false,"in":"query","description":"Optional rep ID to filter periods for","schema":{"type":"string"}}],"responses":{"200":{"description":"Array of period strings (e.g. ['2026-04', '2026-Q2'])"}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/commissions/recalculate":{"post":{"operationId":"CommissionsController_recalculate","summary":"Recalculate commissions","description":"Triggers a recalculation for the specified scope. Provide one of:\n- `dealId` — recalculate a single deal\n- `repId` — recalculate all deals for a rep\n- `planId` — recalculate all deals under a plan\n- none — recalculate the entire workspace (admin only)\n\nRequires **admin** role.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"repId":{"type":"string","description":"Recalculate all deals for this rep"},"planId":{"type":"string","description":"Recalculate all deals under this plan"},"dealId":{"type":"string","description":"Recalculate a single deal"}}}}}},"responses":{"200":{"description":"Recalculation finished. Response shape: `{ triggered, count?, skipped?, reason?, scope }`. `count` is the number of deals fed into the engine, not the number of commission events written. To see actual events created, query `GET /api/commissions?dealId=<id>` per deal or check the commission_events table."},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/commissions/sync":{"post":{"operationId":"CommissionsController_sync","summary":"Sync deals from CRM","description":"Pulls the latest deals from the connected CRM and upserts deal snapshots. **Side effect**: also re-syncs the CRM rep list before fetching deals — this is the recommended path for HubSpot setup until `POST /api/reps/sync` is fixed (COM-148 / B1). Optionally triggers recalculation after sync.\n\nResponse: `{ synced, totalDeals, triggered, message }`. `triggered` counts deals fed into the engine, **not** commission events written.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"recalculate":{"type":"boolean","description":"If true, recalculates commissions for all synced deals after import","default":false},"reconcile":{"type":"boolean","description":"If true, treats any locally-known deal NOT in the CRM sync result as deleted and reverses its earned events. Required because HubSpot's `deal.deletion` webhooks are not guaranteed and pre-existing OAuth installs lack the subscription until re-install. Defaults to false — only set this when you've pulled the full deal set (not a filtered subset).","default":false}}}}}},"responses":{"200":{"description":"Sync results — deal counts, recalculation summary, and reconciliation count"},"401":{"description":"Unauthorized"}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/commissions/simulate":{"post":{"operationId":"CommissionsController_simulate","summary":"Simulate commission calculation (dry-run)","description":"Dry-runs the commission engine against deal snapshots without writing to the ledger. Useful for previewing payouts before committing changes to a plan. To persist commission events, use POST /api/commissions/recalculate.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"planConfig":{"type":"object","description":"Plan config to evaluate. Omit when passing planId (server loads the plan's config automatically)."},"planId":{"type":"string","description":"Plan identity ID. When provided the server loads the plan's active config and quota context — no need to pass planConfig."},"period":{"type":"string","example":"2026-Q2","description":"Period label (e.g. '2026-Q2', '2026-04') or 'all' to include every deal regardless of close date."}},"examples":{"byPlanId":{"summary":"Simulate using an existing saved plan (recommended)","value":{"planId":"<plan-uuid>","period":"2026-Q2"}},"byConfig":{"summary":"Simulate with an inline plan config","value":{"planConfig":{"name":"Test","rules":[{"name":"Commission","measure":"closed_won_revenue","executionPhase":"per_deal","tierBy":"attainment","tierMode":"marginal","tiers":[{"tierIndex":0,"minThreshold":0,"rate":0.08}]}]},"period":"2026-Q2"}}}}}}},"responses":{"200":{"description":"Simulation result with per-rule breakdown and trace"},"401":{"description":"Unauthorized"}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/commissions/cleanup":{"post":{"operationId":"CommissionsController_cleanup","summary":"Delete orphaned commission events","description":"Removes commission events whose plan_rule no longer exists (from deleted plans). Admin only.","parameters":[],"responses":{"200":{"description":""}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/commissions/reset":{"post":{"operationId":"CommissionsController_reset","summary":"Reset workspace commission data","description":"Resets commission data with a configurable scope. Admin only. Used for E2E testing and clean-slate replays. COM-181: default scope is `events` — assignments and the deal cache survive, so you don't need to re-assign + re-sync after every reset.","parameters":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"scope":{"type":"string","enum":["events","snapshots","all"],"default":"events","description":"What to clear. `events` (default) = commission_events + quota.current_value only. `snapshots` = events + deal_snapshots + deals (CRM cache). `all` = everything including assignment rows AND quota rows (COM-186: actually deletes quota rows, not just resets current_value). Response contains an integer count for each cleared category, including `quotas`."}}}}}},"responses":{"200":{"description":"Reset counts"}},"tags":["commissions"],"security":[{"bearer":[]}]}},"/api/deals":{"get":{"operationId":"DealsController_list","summary":"List deal snapshots","description":"Returns deals known to CompCode (latest snapshot per deal). Lets doc-only consumers discover deal IDs to drill into via `GET /api/commissions?dealId=...` without first having to query the CRM. Filter by rep, stage, or recency.","parameters":[{"name":"repId","required":false,"in":"query","description":"Filter to deals owned by this rep","schema":{"type":"string"}},{"name":"stage","required":false,"in":"query","description":"Filter by deal stage (exact match, e.g. `closedwon`)","schema":{"type":"string"}},{"name":"since","required":false,"in":"query","description":"ISO timestamp — only deals whose latest snapshot is at or after this time","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","description":"Max rows (default 100, max 500)","schema":{"type":"string"}},{"name":"offset","required":false,"in":"query","description":"Pagination offset (default 0)","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ deals: [...], count }` — one row per deal_id, latest snapshot"},"401":{"description":"Unauthorized"}},"tags":["deals"],"security":[{"bearer":[]}]}},"/api/deals/{dealId}":{"delete":{"operationId":"DealsController_reverse","summary":"Reverse a deal's commission events (manual reconciliation)","description":"Friction #20: when the CRM never delivered a `deal.deletion` webhook (or the OAuth install predates the subscription), an admin can use this to reverse every earned event on a deal. Calls the same reversal path used by the deletion webhook — every active earned row becomes `status=reversed`, quota.current_value is decremented, and the change is captured in `commission_event_history`. The deal snapshot is preserved for audit; supply `?hard=true` to also delete the snapshot row.","parameters":[{"name":"dealId","required":true,"in":"path","description":"CRM deal ID to reverse","schema":{"type":"string"}},{"name":"hard","required":false,"in":"query","description":"Also delete the deal snapshot (default false)","schema":{"type":"string"}}],"responses":{"200":{"description":"`{ reversed, statementLocked, softDeleted, hardDeleted }`. `softDeleted` is true whenever the snapshot moved to `deleted_at` (default behavior); `hardDeleted` is true only when `?hard=true` physically removed the row."},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["deals"],"security":[{"bearer":[]}]}},"/api/plans":{"get":{"operationId":"PlansController_listPlans","summary":"List plans","description":"Returns one row per plan identity by default, preferring the active version when multiple exist. To inspect every historical version, pass `?versions=all`. Use `GET /api/plans/:planId/history` for a focused version-history view of a single plan.","parameters":[{"name":"status","required":false,"in":"query","description":"Filter by plan status","schema":{"enum":["draft","active","archived","deleted"],"type":"string"}},{"name":"versions","required":false,"in":"query","description":"Pass `all` to return every plan version row instead of deduplicating by identity","schema":{"enum":["all"],"type":"string"}}],"responses":{"200":{"description":"Array of plans (one row per identity by default, or every version when `versions=all`)"},"401":{"description":"Unauthorized"}},"tags":["plans"],"security":[{"bearer":[]}]},"post":{"operationId":"PlansController_createPlan","summary":"Create a plan","description":"Creates a new plan with a versioned configuration. The `config` object follows the CompCode plan schema (rules, tiers, conditions). Requires **admin** role.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","config"],"properties":{"name":{"type":"string","example":"AE Commission Plan 2026"},"effectiveStart":{"type":"string","format":"date","example":"2026-01-01","description":"Plan start date (YYYY-MM-DD)"},"effectiveEnd":{"type":"string","format":"date","example":"2026-12-31","description":"Plan end date (YYYY-MM-DD)"},"fieldMapping":{"type":"object","description":"Optional — override CRM field slugs for this workspace. E.g. `{ 'closed_won_revenue': 'hs_deal_amount' }`"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","example":"EUR","description":"Optional ISO 4217 — overrides workspace default. Omit to inherit."},"config":{"type":"object","description":"Plan configuration. All rules share the same effective period as the plan version.","properties":{"dealConditions":{"type":"array","description":"Optional — plan-level conditions. ALL must match for ANY commission to fire on a deal.","items":{"oneOf":[{"type":"object","required":["field","fieldType","operator"],"description":"Leaf condition — evaluated directly against a deal field.","properties":{"field":{"type":"string","description":"CRM field slug (from GET /api/fields/attributes)"},"fieldType":{"type":"string","enum":["text","number","currency","date","select","status","checkbox","timestamp"]},"operator":{"type":"string","enum":["equals","not_equals","contains","not_contains","in","not_in","gt","gte","lt","lte","between","before","after","in_period","in_last_n_days","is_set","is_not_set","is_true","is_false"]},"value":{"description":"Comparison value (omit for is_set/is_not_set/is_true/is_false operators)"}}},{"type":"object","required":["operator","rules"],"description":"Condition group — combine leaf conditions with AND or OR logic.","properties":{"operator":{"type":"string","enum":["and","or"],"description":"'and' = all rules must pass. 'or' = at least one rule must pass."},"rules":{"type":"array","items":{"type":"object"},"minItems":1,"description":"Leaf conditions inside this group."}}}]}},"rules":{"type":"array","description":"One or more payout rules. Rules are evaluated independently (or in dependency order for cascade rules).","items":{"type":"object","required":["name","measure","tiers"],"properties":{"name":{"type":"string","description":"Unique rule name — used as the measure alias in quota API"},"measure":{"type":"string","description":"CRM field slug to aggregate for attainment (e.g. 'closed_won_revenue', 'hs_mrr'). Get valid slugs from GET /api/fields/attributes."},"executionPhase":{"type":"string","enum":["per_deal","cascade","periodic"],"default":"per_deal","description":"'per_deal' = runs on each deal. 'cascade' = manager override, input is sum of downstream rules. 'periodic' = time-based (not yet auto-scheduled)."},"attainmentPeriod":{"type":"string","enum":["monthly","quarterly","annual"],"default":"quarterly","description":"Quota period. Quota targets must use matching period format (monthly→YYYY-MM, quarterly→YYYY-QN, annual→YYYY)."},"tierBy":{"type":"string","enum":["attainment","value"],"default":"attainment","description":"'attainment' = tier based on quota % (1.0 = 100%). 'value' = tier based on raw deal field value. Requires tierValueField when 'value'."},"tierMode":{"type":"string","enum":["full_rate","marginal"],"default":"full_rate","description":"'full_rate' = each deal pays at the highest tier its cumulative-attainment-at-close qualifies for (non-retroactive: prior deals are NOT re-rated when a later deal crosses a higher tier). 'marginal' = deal revenue split across tier bands proportionally, like tax brackets. Only valid with tierBy='attainment'."},"tierValueField":{"type":"string","description":"Required when tierBy='value'. CRM field slug whose value determines which tier applies."},"payoutBase":{"type":"string","description":"What the tier rate multiplies against. 'variable_target' = rep OTE. '1' = flat $ payout (use with flatAmount tiers). Any field slug = payout from that CRM field. Defaults to measure field."},"dependsOn":{"type":"string","description":"For cascade rules — name of the upstream rule whose output is this rule's input. Use '_total' to depend on sum of all per_deal rules."},"cap":{"type":"number","description":"Flat-dollar PERIOD cap on payout for this rule (COM-196). Total commission across the period cannot exceed this amount; engine clamps each event against remaining headroom. Use for SPIFFs, per-meeting bonuses, MBO programs (`cap: 5000` = max $5,000 per period regardless of event count). Use `capMultiplier` for variable-target × multiplier semantics."},"floor":{"type":"number","deprecated":true,"description":"DEPRECATED — flat-dollar floor on payout per fire (only applied when a tier matches). Use `floorAttainment` to set an attainment gate."},"capMultiplier":{"type":"number","description":"Max payout as a multiple of variableTarget (e.g. 2.0 = no more than 2× the rep's variable target per fire)."},"floorAttainment":{"type":"number","description":"Attainment ratio gate (0–1+). Below this, payout is 0. E.g. 0.5 = no payout until rep hits 50% quota."},"quotaGroup":{"type":"string","description":"Optional — named quota pool. Rules sharing the same quotaGroup value share one combined quota target and compute attainment from the group total. E.g. 'total_revenue' for MRR+ARR+upgrade rules sharing a $50K pool."},"quotaMeasure":{"type":"string","description":"Optional — CRM field slug used for attainment tracking instead of measure. Use when payout and quota tracking use different fields, e.g. measure='hs_arr' (pay on ARR) with quotaMeasure='hs_mrr' (track monthly MRR quota). When both quotaMeasure and quotaGroup are set, the group attainment sums quotaMeasure values across all rules."},"conditions":{"type":"array","description":"Optional rule-level conditions. Top-level items are AND-ed. Use a group object with operator='or' for OR logic.","items":{"oneOf":[{"type":"object","required":["field","fieldType","operator"],"properties":{"field":{"type":"string"},"fieldType":{"type":"string","enum":["text","number","currency","date","select","status","checkbox","timestamp"]},"operator":{"type":"string","enum":["equals","not_equals","contains","not_contains","in","not_in","gt","gte","lt","lte","between","before","after","in_period","in_last_n_days","is_set","is_not_set","is_true","is_false"]},"value":{"description":"Comparison value (omit for is_set/is_not_set/is_true/is_false)"}}},{"type":"object","required":["operator","rules"],"properties":{"operator":{"type":"string","enum":["and","or"]},"rules":{"type":"array","items":{"type":"object"},"minItems":1}}}]}},"tiers":{"type":"array","description":"Tier definitions. At least one tier required. For a flat rate plan use a single tier with minThreshold:0.","items":{"type":"object","required":["tierIndex","minThreshold"],"properties":{"tierIndex":{"type":"integer","description":"Sort order (0-based)"},"name":{"type":"string","description":"Display name (e.g. 'Base', 'Accelerator', 'Stretch')"},"minThreshold":{"type":"number","description":"For tierBy='attainment': ratio (0.0–2.0+, where 1.0=100% quota). For tierBy='value': raw field value."},"rate":{"type":"number","description":"Commission rate as decimal (e.g. 0.08 = 8%). Mutually exclusive with flatAmount."},"flatAmount":{"type":"number","description":"Fixed $ payout when tier matches. Mutually exclusive with rate."},"repId":{"type":"string","format":"uuid","description":"Optional — per-rep override. Null/omit for all reps."}}}}}}}},"examples":{"flat-rate-plan":{"summary":"Flat 8% commission on closed revenue","value":{"rules":[{"name":"Base Commission","measure":"closed_won_revenue","executionPhase":"per_deal","attainmentPeriod":"monthly","tierBy":"attainment","tierMode":"full_rate","tiers":[{"tierIndex":0,"name":"Base","minThreshold":0,"rate":0.08}]}]}},"tiered-accelerator":{"summary":"Tiered accelerator: 8% base → 10% at 80% quota → 12% at 100% → 15% at 120%","value":{"dealConditions":[{"field":"deal_type","fieldType":"select","operator":"equals","value":"new_business"}],"rules":[{"name":"MRR Commission","measure":"closed_won_mrr","executionPhase":"per_deal","attainmentPeriod":"monthly","tierBy":"attainment","tierMode":"full_rate","payoutBase":"closed_won_mrr","tiers":[{"tierIndex":0,"name":"Ramp","minThreshold":0,"rate":0.08},{"tierIndex":1,"name":"Base","minThreshold":0.8,"rate":0.1},{"tierIndex":2,"name":"On Target","minThreshold":1,"rate":0.12},{"tierIndex":3,"name":"Accelerator","minThreshold":1.2,"rate":0.15}]},{"name":"Renewal Bonus","measure":"renewal_revenue","executionPhase":"per_deal","attainmentPeriod":"quarterly","tierBy":"attainment","tierMode":"marginal","conditions":[{"field":"deal_type","fieldType":"select","operator":"equals","value":"renewal"}],"tiers":[{"tierIndex":0,"name":"Base","minThreshold":0,"rate":0.05},{"tierIndex":1,"name":"Stretch","minThreshold":1,"rate":0.08}]}]}}}}}}}}},"responses":{"201":{"description":"Plan created — returns the new plan version with generated ID"},"400":{"description":"Invalid plan configuration"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/templates":{"get":{"operationId":"PlansController_listTemplates","summary":"List plan templates","description":"Returns the canonical starter plan shapes (flat commission, marginal AE, retroactive AE, AE+manager cascade, MBO bonus, SDR meeting bonus, multi-measure, new-hire ramp). Templates are checked into the repo, not stored per-workspace. Each `planConfig` is a real V3 plan body — POST it at `/api/plans` after filling in `name` and `effectiveStart`.","parameters":[],"responses":{"200":{"description":"Catalog of plan templates"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/templates/{templateId}":{"get":{"operationId":"PlansController_getTemplate","summary":"Get a plan template","description":"Returns a single template by its slug.","parameters":[{"name":"templateId","required":true,"in":"path","description":"Template slug, e.g. `marginal-tier-ae`","schema":{"type":"string"}}],"responses":{"200":{"description":"Plan template"},"404":{"description":"Template not found"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/{planId}":{"get":{"operationId":"PlansController_getPlan","summary":"Get a plan","description":"Returns a single plan version including all rules and tiers.","parameters":[{"name":"planId","required":true,"in":"path","description":"Plan version ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Plan version with rules and tiers"},"401":{"description":"Unauthorized"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]},"delete":{"operationId":"PlansController_deletePlan","summary":"Delete a plan","description":"Hard-deletes the plan identity and all of its versions, rules, and tiers. Accepts either a plan identity ID or a plan version ID — version IDs are resolved to their parent identity first. Prefer archiving (`PATCH /:planRef/status` with `\"deleted\"` or `\"archived\"`) to preserve history. Requires **admin** role.","parameters":[{"name":"planId","required":true,"in":"path","description":"Plan identity ID (preferred) or plan version ID — both resolve to the parent identity","schema":{"type":"string"}}],"responses":{"200":{"description":"Plan deleted"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/{planId}/history":{"get":{"operationId":"PlansController_getPlanHistory","summary":"Get plan version history","description":"Returns all versions of a plan in reverse chronological order.","parameters":[{"name":"planId","required":true,"in":"path","description":"Plan version ID (any version of the plan)","schema":{"type":"string"}}],"responses":{"200":{"description":"Array of plan versions ordered newest first"},"401":{"description":"Unauthorized"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/{planRef}":{"patch":{"operationId":"PlansController_updatePlan","summary":"Update a plan","description":"Updates a plan's configuration or effective dates. Creates a new plan version — the previous version is preserved in history. Accepts EITHER the plan identity ID (`planId` in GET responses) or the plan-version ID (`id` in GET responses); both resolve to the active version. Requires **admin** role.","parameters":[{"name":"planRef","required":true,"in":"path","description":"Plan identity ID or plan-version ID (either resolves to the active version)","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"config":{"type":"object","description":"New plan configuration (replaces current)"},"effectiveStart":{"type":"string","format":"date"},"effectiveEnd":{"type":"string","format":"date"},"currency":{"type":"string","nullable":true,"pattern":"^[A-Z]{3}$","example":"EUR","description":"Optional ISO 4217 override. Send `null` to clear the override and inherit from the workspace."},"status":{"type":"string","enum":["draft","active","archived","deleted"],"description":"Set the plan's lifecycle status. When provided alone, no new version is created — the existing active version is transitioned in place. When provided with `config` / `effectiveStart` / `effectiveEnd`, a new version is created AND the new version is set to this status. (COM-179: prior behavior created a new active version even when status:archived was sent, leaving plans stuck in active.)"}}}}}},"responses":{"200":{"description":"New plan version created (config/dates update) or status updated in place (status-only update)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/{planRef}/status":{"patch":{"operationId":"PlansController_updatePlanStatus","summary":"Archive or reactivate a plan","description":"Sets the plan's status. Allowed values: draft, active, archived, deleted. Archived plans retain their history and commission events. Requires **admin** role.","parameters":[{"name":"planRef","required":true,"in":"path","description":"Plan identity ID (preferred) or plan version ID — both resolve to the active version. Renamed from `:planId` for consistency with `PATCH /:planRef`.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePlanStatusDto"}}}},"responses":{"200":{"description":"Status updated"},"400":{"description":"Invalid status value"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/{planVersionId}/overrides":{"get":{"operationId":"PlansController_listRepOverrides","summary":"List per-rep rate overrides","description":"Returns all per-rep tier rate overrides for a plan version, grouped by rep ID.","parameters":[{"name":"planVersionId","required":true,"in":"path","description":"Plan version ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Overrides grouped by repId"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/plans/{planVersionId}/overrides/{repId}":{"put":{"operationId":"PlansController_setRepOverrides","summary":"Set per-rep rate overrides","description":"Sets or replaces per-rep tier rate overrides for a specific rep on a plan version. Pass the rules and tiers with the new `rate` or `flatAmount` for the rep — `minThreshold` is copied from the global tier at the same `tierIndex`. Requires **admin** role.","parameters":[{"name":"planVersionId","required":true,"in":"path","description":"Plan version ID","schema":{"type":"string"}},{"name":"repId","required":true,"in":"path","description":"Rep ID to override","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rules"],"properties":{"rules":{"type":"array","items":{"type":"object","required":["ruleId","tiers"],"properties":{"ruleId":{"type":"string","format":"uuid","description":"Plan rule ID"},"tiers":{"type":"array","items":{"type":"object","required":["tierIndex"],"properties":{"tierIndex":{"type":"integer","description":"Tier index (0-based, must match a global tier)"},"rate":{"type":"number","description":"Override commission rate (decimal, e.g. 0.09 = 9%)"},"flatAmount":{"type":"number","description":"Override flat payout amount ($)"}}}}}}}}}}}},"responses":{"200":{"description":"Overrides set"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]},"delete":{"operationId":"PlansController_deleteRepOverrides","summary":"Delete per-rep rate overrides","description":"Removes all per-rep tier rate overrides for a specific rep from a plan version, reverting them to global tier rates. Requires **admin** role.","parameters":[{"name":"planVersionId","required":true,"in":"path","description":"Plan version ID","schema":{"type":"string"}},{"name":"repId","required":true,"in":"path","description":"Rep ID whose overrides to remove","schema":{"type":"string"}}],"responses":{"200":{"description":"Overrides removed"},"404":{"description":"Plan not found"}},"tags":["plans"],"security":[{"bearer":[]}]}},"/api/assignments":{"get":{"operationId":"AssignmentsController_getAssignments","summary":"List assignments","description":"Returns plan-to-rep assignments. Without filters returns the full roster. Use `repId` or `planId` to narrow results.","parameters":[{"name":"repId","required":false,"in":"query","description":"Filter by rep","schema":{"type":"string"}},{"name":"planId","required":false,"in":"query","description":"Filter by plan version","schema":{"type":"string"}}],"responses":{"200":{"description":"Array of assignments with rep, plan, and effective date info"},"401":{"description":"Unauthorized"}},"tags":["assignments"],"security":[{"bearer":[]}]},"post":{"operationId":"AssignmentsController_createAssignment","summary":"Assign reps to a plan","description":"Creates one assignment per rep in `repIds`, linking them to `planId` with optional effective date bounds. Requires **admin** role.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["repIds","planId"],"properties":{"repIds":{"type":"array","items":{"type":"string"},"description":"List of rep IDs to assign"},"planId":{"type":"string","description":"Plan identity ID — use `planId` from GET /api/plans response (NOT the version `id`)"},"effectiveStart":{"type":"string","format":"date","example":"2026-01-01"},"effectiveEnd":{"type":"string","format":"date","example":"2026-12-31","description":"Omit for open-ended assignment"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","example":"EUR","description":"Optional ISO 4217 — overrides the plan/workspace default for this rep only. Omit to inherit."}}}}}},"responses":{"201":{"description":"Assignments created"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"}},"tags":["assignments"],"security":[{"bearer":[]}]}},"/api/assignments/{assignmentId}":{"patch":{"operationId":"AssignmentsController_updateAssignment","summary":"Update assignment dates","description":"Updates the effective start and/or end date of an existing assignment. Requires **admin** role.","parameters":[{"name":"assignmentId","required":true,"in":"path","description":"Assignment ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"effectiveStart":{"type":"string","format":"date"},"effectiveEnd":{"type":"string","format":"date","nullable":true,"description":"Pass null to remove end date"},"currency":{"type":"string","nullable":true,"pattern":"^[A-Z]{3}$","example":"EUR","description":"Optional ISO 4217 override. Send `null` to clear and inherit from plan → workspace."}}}}}},"responses":{"200":{"description":"Assignment updated"},"401":{"description":"Unauthorized"},"404":{"description":"Assignment not found"}},"tags":["assignments"],"security":[{"bearer":[]}]},"delete":{"operationId":"AssignmentsController_removeAssignment","summary":"Remove an assignment","description":"Unlinks a rep from a plan. Does not delete historical commission events. Requires **admin** role.","parameters":[{"name":"assignmentId","required":true,"in":"path","description":"Assignment ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Assignment removed"},"401":{"description":"Unauthorized"},"404":{"description":"Assignment not found"}},"tags":["assignments"],"security":[{"bearer":[]}]}},"/api/quotas":{"get":{"operationId":"QuotasController_getQuotas","summary":"List quotas","description":"Returns quota targets. Without filters returns all quotas for the workspace (grid view). Filter by `repId` to get quotas for a specific rep.","parameters":[{"name":"repId","required":false,"in":"query","description":"Filter by rep ID","schema":{"type":"string"}},{"name":"planId","required":false,"in":"query","description":"Filter by plan ID (identity)","schema":{"type":"string"}}],"responses":{"200":{"description":"Array of quota records with rep, plan, period, and target values"},"401":{"description":"Unauthorized"}},"tags":["quotas"],"security":[{"bearer":[]}]},"post":{"operationId":"QuotasController_setQuota","summary":"Set quota targets","description":"Sets a quota target for one or more reps for a given period. Creates or upserts — running this again for the same rep+period+plan overwrites the previous value. Requires **admin** role.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["repIds","target"],"properties":{"repIds":{"type":"array","items":{"type":"string"},"description":"Reps to set quota for"},"period":{"type":"string","example":"2026-04","description":"Period in `YYYY-MM` (monthly), `YYYY-QN` (quarterly), or `YYYY` (annual) format. Use `periods[]` to set multiple periods at once."},"periods":{"type":"array","items":{"type":"string"},"example":["2026-01","2026-02","2026-03"],"description":"Optional — set the same quota for multiple periods at once. Overrides `period` when provided."},"target":{"type":"number","example":150000,"description":"Quota target amount (same currency as deal revenue)"},"planId":{"type":"string","description":"Optional — scope quota to a specific plan"},"planRuleId":{"type":"string","description":"Optional — scope quota to a specific plan rule. Use rule `id` from `GET /api/plans/:id` rules[] response."},"quotaGroup":{"type":"string","example":"total_revenue","description":"Optional — named quota pool shared across multiple rules. When set, planRuleId must be omitted. All rules with matching quotaGroup share this combined target for attainment."},"measure":{"type":"string","description":"Optional — rule name alias for planRuleId (e.g. 'Monthly MRR Commission')"},"variableTarget":{"type":"number","description":"Optional variable compensation target (OTE component)"}}}}}},"responses":{"201":{"description":"Quotas set"},"400":{"description":"Missing required fields"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["quotas"],"security":[{"bearer":[]}]}},"/api/quotas/{quotaId}":{"delete":{"operationId":"QuotasController_deleteQuota","summary":"Delete a quota","description":"Removes a quota record. The rep will show 0% attainment for that period until a new quota is set. Requires **admin** role.","parameters":[{"name":"quotaId","required":true,"in":"path","description":"Quota ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Quota deleted"},"401":{"description":"Unauthorized"},"404":{"description":"Quota not found"}},"tags":["quotas"],"security":[{"bearer":[]}]}},"/api/statements/export.csv":{"get":{"operationId":"StatementsController_exportPayrollCsv","summary":"Export payroll as CSV","description":"Returns a CSV file with one row per rep: name, email, commissions, bonuses, adjustments, net payout, and statement status. Requires manager or admin role.","parameters":[{"name":"period","required":false,"in":"query","description":"Period (e.g. `2026-Q2`). Defaults to current quarter.","schema":{"type":"string"}}],"responses":{"200":{"description":"CSV download"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — manager or admin role required"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements":{"get":{"operationId":"StatementsController_getStatement","summary":"Get statement","description":"Returns commission statement data.\n- **No `repId`** — batch team view (manager+ only)\n- **`repId`** — full statement for a specific rep including line items and totals","parameters":[{"name":"repId","required":false,"in":"query","description":"Rep ID — omit for team batch view","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Period (e.g. `2026-04`). Defaults to current month.","schema":{"type":"string"}}],"responses":{"200":{"description":"Statement with line items, totals, and approval status"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — team view requires manager or admin role"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/export":{"get":{"operationId":"StatementsController_exportStatement","summary":"Export statement for payroll (D10)","description":"Returns the rep's statement line items in a payroll-ready format. Default `format=csv` for spreadsheet import; pass `format=json` for the full structured shape.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Period (defaults to current month)","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","description":"Output format (default `csv`)","schema":{"enum":["csv","json"],"type":"string"}}],"responses":{"200":{"description":"CSV (text/csv) or JSON payload"},"401":{"description":"Unauthorized"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/status":{"get":{"operationId":"StatementsController_getStatus","summary":"Get statement approval status","description":"Returns the current approval status (`draft` or `approved`) for a rep's statement in the given period.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Period (defaults to current month)","schema":{"type":"string"}}],"responses":{"200":{"description":"Statement status object"},"401":{"description":"Unauthorized"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/generate":{"post":{"operationId":"StatementsController_generateAll","summary":"Generate statements for many reps in one call (COM-188)","description":"Workspace-level batch generate. Snapshots commission events for every rep listed in `repIds` (or every active rep when `repIds` is omitted) for the given period. Already-approved statements are left intact and surfaced in `skippedReps`. Useful at quarter-end close — replaces the 50-call per-rep loop. Requires **manager** or **admin** role.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"string","example":"2026-Q2","description":"Period to snapshot (defaults to current quarter)"},"repIds":{"type":"array","items":{"type":"string"},"description":"Optional explicit rep set. Omit to target every active rep in the workspace. Each entry can be an internal rep ID, CRM member ID, or email."}}}}}},"responses":{"200":{"description":"{ period, generated, skipped, errors, statements: [{repId, version, statementId, netPayout}], skippedReps: [{repId, reason}], errorReps: [{repId, error}] }"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — manager or admin role required"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/generate":{"post":{"operationId":"StatementsController_generate","summary":"Generate statement snapshot","description":"Snapshots all commission events for the rep+period into a versioned statement record. Each call creates a new version — the statement is not locked until `approve` is called. Requires **manager** or **admin** role.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"period":{"type":"string","example":"2026-04","description":"Period to snapshot (defaults to current month)"}}}}}},"responses":{"200":{"description":"Generated statement with line items and totals"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — manager or admin role required"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/approve":{"post":{"operationId":"StatementsController_approve","summary":"Approve or revert statement","description":"Sets statement status to `approved` (locks it — prevents further recalculation) or reverts to `draft`. Requires **manager** or **admin** role.\n\n**COM-189 — body shape is `{period, status}`.** There is NO `{approve: true}` shorthand; that returns 400 `VALIDATION_FAILED` with `period and status required`. Use `status: \"approved\"` to lock, `status: \"draft\"` to unlock.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["period","status"],"properties":{"period":{"type":"string","example":"2026-04"},"status":{"type":"string","enum":["approved","draft"],"description":"`approved` locks the statement; `draft` unlocks it. NOT `approve: true`."}},"example":{"period":"2026-Q2","status":"approved"}}}}},"responses":{"200":{"description":"Statement status updated"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — manager or admin role required"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/adjust":{"post":{"operationId":"StatementsController_adjust","summary":"Add manual adjustment","description":"Adds a manual commission adjustment to a rep's statement. Blocked if the statement is already `approved`. Requires **admin** role.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["amount","reason","period"],"properties":{"dealId":{"type":"string","description":"Optional deal to attach this adjustment to"},"amount":{"type":"number","example":500,"description":"Adjustment amount (negative for clawbacks)"},"reason":{"type":"string","example":"Spiff bonus for new logo"},"period":{"type":"string","example":"2026-04"},"idempotencyKey":{"type":"string","description":"Client-supplied UUID for deduplicating retries"}}}}}},"responses":{"200":{"description":"Adjustment applied"},"400":{"description":"Statement is approved — cannot adjust"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/adjust/{adjustmentId}":{"patch":{"operationId":"StatementsController_updateAdjustment","summary":"Edit a manual adjustment","description":"Updates the amount and/or reason on an existing adjustment / true-up / clawback row. Blocked when the statement is approved (revert to draft first). Requires **admin** role.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}},{"name":"adjustmentId","required":true,"in":"path","description":"commission_events.id of the adjustment to edit","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"amount":{"type":"number","description":"New adjustment amount (negative for clawbacks)"},"reason":{"type":"string","description":"New free-text reason"}}}}}},"responses":{"200":{"description":"Adjustment updated"},"400":{"description":"Adjustment not found or no fields to update"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"},"409":{"description":"Statement is approved — cannot edit"}},"tags":["statements"],"security":[{"bearer":[]}]},"delete":{"operationId":"StatementsController_deleteAdjustment","summary":"Delete a manual adjustment","description":"Hard-deletes an adjustment / true-up / clawback row. Only adjustment-style events can be deleted; engine-written commission_earned events are never reachable here. Blocked when the statement is approved (revert to draft first). Requires **admin** role.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}},{"name":"adjustmentId","required":true,"in":"path","description":"commission_events.id of the adjustment to delete","schema":{"type":"string"}}],"responses":{"200":{"description":"Adjustment deleted (or already absent)"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"},"409":{"description":"Statement is approved — cannot delete"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/true-up":{"post":{"operationId":"StatementsController_trueUp","summary":"Generate statement true-up","description":"Computes the delta between the approved statement total and the current live commission ledger, then inserts a `true_up` event for the difference. Returns `requires_approval` when the delta exceeds $500 — pass `force: true` to bypass. Requires **manager** or **admin** role. Statement must already be approved.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["period"],"properties":{"period":{"type":"string","example":"2026-04"},"force":{"type":"boolean","description":"Bypass the $500 approval threshold (admin only)"}}}}}},"responses":{"200":{"description":"True-up result: action, delta, and optional approvalThreshold"},"400":{"description":"Period missing"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — manager or admin role required"},"409":{"description":"Statement is not approved"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/comments":{"get":{"operationId":"StatementsController_getComments","summary":"List statement comments","description":"Returns all comments on a rep's statement for the given period.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}},{"name":"period","required":false,"in":"query","description":"Period (defaults to current month)","schema":{"type":"string"}}],"responses":{"200":{"description":"Array of comments"},"401":{"description":"Unauthorized"}},"tags":["statements"],"security":[{"bearer":[]}]},"post":{"operationId":"StatementsController_addComment","summary":"Add a comment","description":"Adds a comment to a rep's statement. Optionally attach it to a specific deal.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["period","comment"],"properties":{"period":{"type":"string","example":"2026-04"},"dealId":{"type":"string","description":"Optional — attach comment to a specific deal"},"comment":{"type":"string","example":"Confirmed with finance — clawback waived."}}}}}},"responses":{"201":{"description":"Comment added"},"401":{"description":"Unauthorized"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/statements/{repId}/comments/{commentId}":{"delete":{"operationId":"StatementsController_deleteComment","summary":"Delete a comment","description":"Removes a comment from a statement. Requires ownership or manager role.","parameters":[{"name":"commentId","required":true,"in":"path","description":"Comment ID","schema":{"type":"string"}},{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{}}],"responses":{"200":{"description":"Comment deleted"},"401":{"description":"Unauthorized"},"404":{"description":"Comment not found"}},"tags":["statements"],"security":[{"bearer":[]}]}},"/api/reps":{"get":{"operationId":"RepsController_getReps","summary":"List reps","description":"Returns all reps in the workspace, including their role, active status, CRM member ID, and avatar.","parameters":[],"responses":{"200":{"description":"Array of rep records"},"401":{"description":"Unauthorized"}},"tags":["reps"],"security":[{"bearer":[]}]}},"/api/reps/{repId}":{"get":{"operationId":"RepsController_getRepById","summary":"Get a rep with CRM linkage","description":"Returns a single rep plus the data a doc-only consumer needs to verify CRM wiring: `linkage.provider`, `linkage.crmId`, `linkage.syncedAt`, and `linkage.lastDealOwnedAt` (timestamp of the most recent deal_snapshot owned by this rep, or null if none). Use this to confirm a HubSpot/Attio owner is correctly mapped before debugging missing commissions.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Rep with linkage info"},"400":{"description":"Rep not found in this workspace"},"401":{"description":"Unauthorized"}},"tags":["reps"],"security":[{"bearer":[]}]},"patch":{"operationId":"RepsController_updateRep","summary":"Update rep role or status","description":"Updates a rep's role (`admin`, `manager`, `rep`) or active status. Requires **admin** role.","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["admin","manager","rep"],"description":"New role"},"active":{"type":"boolean","description":"Set false to deactivate — deactivated reps are excluded from commission calculations"},"managerRepId":{"type":"string","nullable":true,"description":"ID of the manager this rep reports to (null to unset)"}}}}}},"responses":{"200":{"description":"Rep updated"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"},"404":{"description":"Rep not found"}},"tags":["reps"],"security":[{"bearer":[]}]}},"/api/reps/sync":{"post":{"operationId":"RepsController_syncReps","summary":"Sync reps from CRM","description":"Fetches all members from the connected CRM and upserts them into the workspace rep list. Use this before assigning reps to plans during initial setup. Requires **admin** role.","parameters":[],"responses":{"200":{"description":"{ synced: true, count: number, reps: [...] }"},"400":{"description":"No CRM configured for this workspace"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["reps"],"security":[{"bearer":[]}]}},"/api/reps/bulk-invite":{"post":{"operationId":"RepsController_bulkInvite","summary":"Invite multiple reps at once","description":"Sends an invite email to each rep in `repIds`. Failures (no email, Resend down) are returned per-rep in `failed[]` so the caller can show partial-success state. Requires **admin** role.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["repIds"],"properties":{"repIds":{"type":"array","items":{"type":"string"}}}}}}},"responses":{"200":{"description":"{ invited: string[], failed: [{ repId, reason }], inviteCount: number }"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["reps"],"security":[{"bearer":[]}]}},"/api/reps/{repId}/invite":{"post":{"operationId":"RepsController_inviteRep","summary":"Invite a rep to CompCode","description":"Sends an invite email to the rep's address, activates them, and marks invite_status as 'invited'. On first successful login the status flips to 'accepted' automatically. Requires **admin** role and a rep with an email address (CRM-synced reps without an email cannot be invited).","parameters":[{"name":"repId","required":true,"in":"path","description":"Rep ID","schema":{"type":"string"}}],"responses":{"200":{"description":"{ invited: true, email: string }"},"400":{"description":"Rep not found or has no email"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["reps"],"security":[{"bearer":[]}]}},"/api/workspace":{"get":{"operationId":"WorkspaceController_getWorkspace","summary":"Get workspace info","description":"Returns workspace metadata: name, CRM connection status, field mapping, and Stripe subscription tier. COM-187: `plans[]` lists only ACTIVE plans by default — pass `?includeDeleted=true` to also include archived (`status=\"deleted\"`) plans, which the dashboard's Plans Manager needs.","parameters":[{"name":"includeDeleted","required":false,"in":"query","description":"When true, include archived plans (`status=\"deleted\"`) in `plans[]`. Defaults to false.","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Workspace info object"},"401":{"description":"Unauthorized"}},"tags":["workspace"],"security":[{"bearer":[]}]},"patch":{"operationId":"WorkspaceController_updateWorkspace","summary":"Update workspace name or default currency","description":"Renames the workspace and/or sets the default ISO 4217 currency that flows down to plans and assignments. Requires **admin** role.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","example":"ScraperAPI"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","example":"USD","description":"ISO 4217 default for new plans"}}}}}},"responses":{"200":{"description":"{ updated: true, name?: string, currency?: string }"},"400":{"description":"Invalid name or currency"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["workspace"],"security":[{"bearer":[]}]}},"/api/workspace/key":{"get":{"operationId":"WorkspaceController_getApiKey","summary":"Get masked API key","description":"Returns the masked workspace API key (`ws_****...****`). Requires **admin** role.","parameters":[],"responses":{"200":{"description":"{ masked: string | null, isSet: boolean }"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["workspace"],"security":[{"bearer":[]}]}},"/api/workspace/key/regenerate":{"post":{"operationId":"WorkspaceController_regenerateApiKey","summary":"Regenerate API key","description":"Generates a new workspace API key, invalidating the previous one. Returns the masked new key. Requires **admin** role.","parameters":[],"responses":{"200":{"description":"{ masked: string }"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["workspace"],"security":[{"bearer":[]}]}},"/api/workspace/audit-log":{"get":{"operationId":"WorkspaceController_getAuditLog","summary":"Get audit log","description":"Returns a paginated list of admin actions (plan changes, user updates, approvals). Requires **admin** role.","parameters":[{"name":"limit","required":false,"in":"query","description":"Max records to return (default 50)","schema":{"type":"number"}},{"name":"offset","required":false,"in":"query","description":"Records to skip for pagination (default 0)","schema":{"type":"number"}}],"responses":{"200":{"description":"Paginated audit log entries"},"401":{"description":"Unauthorized"},"403":{"description":"Forbidden — admin role required"}},"tags":["workspace"],"security":[{"bearer":[]}]}},"/api/audit-log":{"get":{"operationId":"WorkspaceController_getAuditLogAlias","summary":"Get audit log (deprecated alias)","deprecated":true,"description":"Use `/api/workspace/audit-log`.","parameters":[{"name":"limit","required":true,"in":"query","schema":{"type":"string"}},{"name":"offset","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["workspace"],"security":[{"bearer":[]}]}},"/api/configurator/save":{"post":{"operationId":"ConfiguratorController_save","summary":"Save generated plan","description":"Persists a plan configuration (typically from `POST /api/configurator/generate`) to the database. Optionally saves a field mapping alongside the plan.","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["planConfig"],"properties":{"planConfig":{"type":"object","description":"Plan configuration to save (must pass Zod schema validation)"},"fieldMapping":{"type":"object","description":"Optional CRM field mapping to save with the plan"}}}}}},"responses":{"200":{"description":"Plan saved — returns the created plan version ID"},"400":{"description":"Plan configuration failed validation"},"401":{"description":"Unauthorized"}},"tags":["configurator"],"security":[{"bearer":[]}]}},"/api/configurator/plans":{"get":{"operationId":"ConfiguratorController_listPlans","summary":"List plans (configurator view)","description":"Returns all active plans formatted for the configurator UI — includes rule summaries for quick display.","parameters":[],"responses":{"200":{"description":"Array of plans with condensed rule summaries"},"401":{"description":"Unauthorized"}},"tags":["configurator"],"security":[{"bearer":[]}]}},"/api/configurator/plans/{planId}/status":{"patch":{"operationId":"ConfiguratorController_updatePlanStatus","summary":"Update plan status (configurator)","description":"Archives or reactivates a plan from the configurator interface. Equivalent to `PATCH /api/plans/:planId/status`.","parameters":[{"name":"planId","required":true,"in":"path","description":"Plan version ID","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["active","deleted"]}}}}}},"responses":{"200":{"description":"Status updated"},"401":{"description":"Unauthorized"}},"tags":["configurator"],"security":[{"bearer":[]}]}},"/api/configurator/plans/{planId}":{"get":{"operationId":"ConfiguratorController_getPlan","summary":"Get plan (configurator view)","description":"Returns a single plan formatted for the configurator editor, including the full rule and tier structure.","parameters":[{"name":"planId","required":true,"in":"path","description":"Plan version ID","schema":{"type":"string"}}],"responses":{"200":{"description":"Plan with full rule/tier detail"},"401":{"description":"Unauthorized"},"404":{"description":"Plan not found"}},"tags":["configurator"],"security":[{"bearer":[]}]}}},"info":{"title":"CompCode API","description":"## Commission plans as code\n\nThe first commission platform where plans are created, modified, and versioned entirely through an API.\n\n### Authentication\nAll `/api/*` endpoints require a `Bearer` token in the `Authorization` header.\nObtain a token via `POST /auth/otp/verify` (email OTP) or use your workspace API key directly.\n\n### Tag groups\n| Tag | Description |\n|-----|-------------|\n| `plans` | Create, version, and manage commission plan configurations |\n| `commissions` | Dashboard, recalculation, sync, and simulation |\n| `assignments` | Assign reps to plans with effective date ranges |\n| `quotas` | Set per-rep quota targets |\n| `statements` | Versioned commission snapshots with approval workflow |\n| `reps` | Rep registry — list and update role/active status |\n| `workspace` | Workspace info and audit log |\n| `configurator` | AI-powered plan generation and save |\n| `chat` | Streaming AI plan architect chat |\n| `fields` | CRM field mapping configuration |\n| `auth` | OAuth flows, OTP login, session management |","version":"3.0","contact":{}},"tags":[{"name":"plans","description":"Commission plan CRUD — create, version, archive, and delete plans"},{"name":"commissions","description":"Dashboard data, recalculation, CRM sync, and dry-run simulation"},{"name":"assignments","description":"Link reps to plans with optional effective date ranges"},{"name":"quotas","description":"Set and manage per-rep quota targets"},{"name":"statements","description":"Versioned commission snapshots — generate, approve, adjust, and comment"},{"name":"reps","description":"Rep registry — list reps and update role or active status"},{"name":"workspace","description":"Workspace metadata and admin audit log"},{"name":"configurator","description":"AI plan generation from natural language and save to DB"},{"name":"chat","description":"Streaming SSE chat with the AI plan architect"},{"name":"fields","description":"CRM field attribute discovery and field-mapping configuration"},{"name":"auth","description":"OAuth flows (Attio, Google, HubSpot), email OTP, and session management"}],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"Paste your session token or API key"}},"schemas":{"UpdatePlanStatusDto":{"type":"object","properties":{"status":{"type":"string","enum":["draft","active","archived","deleted"],"example":"deleted"}},"required":["status"]}}}}