{"openapi":"3.0.0","info":{"title":"Backend API","version":"0.1.0","description":"API documentation for this service. Use GET /api-docs.json for the full spec as copy-pasteable JSON. All schemas include example values."},"servers":[{"url":"/api"}],"tags":[{"name":"Auth","description":"Authentication — register, login, logout, password reset, and current user"},{"name":"Users","description":"User management — CRUD, search, and role assignment"},{"name":"Roles","description":"RBAC roles and permissions"},{"name":"Health","description":"Service liveness and version"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}},"schemas":{"ApiSuccessResponse":{"type":"object","properties":{"success":{"type":"boolean","example":true},"statusCode":{"type":"integer","example":200},"data":{"type":"object","description":"Response payload"},"message":{"type":"string","example":"Data has been successfully retrieved."}},"example":{"success":true,"statusCode":200,"data":{},"message":"Data has been successfully retrieved."}},"ApiSuccessResponseWithPagination":{"type":"object","properties":{"success":{"type":"boolean","example":true},"statusCode":{"type":"integer","example":200},"data":{"type":"array","items":{"type":"object"}},"message":{"type":"string"},"pagination":{"description":"Present only when the client passes `page` or `page_size` query params; omitted when no pagination is requested (all results returned).","type":"object","properties":{"total":{"type":"integer","example":100},"pageIndex":{"type":"integer","example":1},"pageSize":{"type":"integer","example":20}}}},"example":{"success":true,"statusCode":200,"data":[],"message":"Data has been successfully retrieved.","pagination":{"total":100,"pageIndex":1,"pageSize":20}}},"ApiErrorResponse":{"type":"object","properties":{"success":{"type":"boolean","example":false},"statusCode":{"type":"integer","example":401},"data":{"type":"object","nullable":true},"message":{"type":"string"}},"example":{"success":false,"statusCode":401,"data":null,"message":"Invalid or expired authentication token. Please log in again."}}}},"paths":{"/v1/work-calendars":{"get":{"tags":["Work calendars"],"summary":"List working-time calendars with their non-working weekdays","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Calendars","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Work calendars"],"summary":"Create a calendar and its non-working weekdays","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name"],"properties":{"code":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"is_default":{"type":"boolean","description":"Companies with no explicit calendar use this one"},"week_days":{"type":"array","description":"Weekdays that are NOT worked. Omitting a weekday means it is worked. `nth_weeks` empty means every week; [2,4] means the 2nd and 4th occurrence only, which is how alternate Saturdays are expressed.\n","items":{"type":"object","properties":{"day_of_week":{"type":"integer","minimum":0,"maximum":6},"nth_weeks":{"type":"array","items":{"type":"integer","minimum":1,"maximum":5}}}}}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"Code already exists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/work-calendars/mine":{"get":{"tags":["Work calendars"],"summary":"The caller's own working calendar and its holidays for a year","description":"Self-service — requires a valid login but no grant. Resolves from the caller's own employment record, so it can only ever return the calendar that applies to them: never another company's, and never the admin list.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"year","schema":{"type":"integer","example":2026}}],"responses":{"200":{"description":"The caller's calendar and holidays","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"The caller has no employment record","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/work-calendars/{id}":{"get":{"tags":["Work calendars"],"summary":"Get one calendar","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Calendar","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Work calendars"],"summary":"Update a calendar. `week_days`, when sent, REPLACES the whole set.","description":"Saved as one transaction. `is_default: true` moves the default here; sending `is_default: false` for the current default is refused — move it instead.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":120},"description":{"type":"string","nullable":true,"maxLength":500},"is_default":{"type":"boolean"},"week_days":{"type":"array","description":"Weekly offs. Fewer than 7 entries — at least one day must be worked.","items":{"type":"object","required":["day_of_week"],"properties":{"day_of_week":{"type":"integer","minimum":0,"maximum":6,"description":"0 = Sunday"},"nth_weeks":{"type":"array","description":"Which occurrences in the month are off (1-5). Empty = every week.","items":{"type":"integer","minimum":1,"maximum":5}}}}}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Invalid week days","or switching the default off (WORK_CALENDAR_DEFAULT_REQUIRED)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Work calendars"],"summary":"Soft-delete a calendar (refused while default or in use)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"Still assigned to companies","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/work-calendars/assign-company":{"post":{"tags":["Work calendars"],"summary":"Point a company at a calendar, or back at the default with a null calendar_id","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["company_id","calendar_id"],"properties":{"company_id":{"type":"string","pattern":"^\\d+$"},"calendar_id":{"type":"string","nullable":true,"pattern":"^\\d+$","description":"null = follow the default calendar"}}}}}},"responses":{"200":{"description":"Assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/holidays":{"get":{"tags":["Work calendars"],"summary":"List holidays on a calendar, optionally for one year","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"calendar_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"year","schema":{"type":"integer"}}],"responses":{"200":{"description":"Holidays","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Work calendars"],"summary":"Add a holiday to a calendar","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["calendar_id","date","name"],"properties":{"calendar_id":{"type":"string"},"date":{"type":"string","example":"2026-01-26"},"name":{"type":"string"},"is_optional":{"type":"boolean","description":"A restricted/floating holiday. The date stays a WORKING day for everyone; an employee opts in by raising a restricted-holiday leave request for it, which their annual quota caps.\n"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"Date already a holiday","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/holidays/range":{"post":{"tags":["Work calendars"],"summary":"Add a shutdown or festival block as one range","description":"Expands the range into one holiday row per date — the storage shape the resolver and the per-date uniqueness index both rely on. Dates that are already holidays are skipped rather than failing the batch, and the response reports what was created and what was skipped, with a reason for each.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["calendar_id","from_date","to_date","name"],"properties":{"calendar_id":{"type":"string"},"from_date":{"type":"string","example":"2026-11-08"},"to_date":{"type":"string","example":"2026-11-12"},"name":{"type":"string","example":"Diwali shutdown"},"is_optional":{"type":"boolean"},"skip_non_working":{"type":"boolean","description":"Skip dates that are already a weekly off"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Inverted or over-long range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/holidays/{id}":{"put":{"tags":["Work calendars"],"summary":"Rename a holiday or change whether it is restricted","description":"The date cannot be changed — remove the holiday and add the correct one.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":200},"is_optional":{"type":"boolean","description":"true = restricted: a working day an employee opts into as leave"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Work calendars"],"summary":"Remove a holiday","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/webhooks/{channelType}/{code}":{"post":{"tags":["Webhooks"],"summary":"Generic inbound webhook receiver — dispatches to the registered connector adapter for signature verification","parameters":[{"in":"path","name":"channelType","required":true,"schema":{"type":"string"}},{"in":"path","name":"code","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Accepted (queued for processing","or already-processed duplicate)":null},"401":{"description":"Rejected — unknown channel","type mismatch":null,"or signature verification failed (uniform response":null,"see class doc)":null}}}},"/v1/warranty-entries":{"get":{"tags":["Warranty Registry"],"summary":"Warranty registry — every delivered watch and the date its cover ends (FR-06-016)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Entries","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/warranty-entries/summary":{"get":{"tags":["Warranty Registry"],"summary":"KPI strip — active, expiring in 30 days, claims awaiting decision, claim rate","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/warranty-entries/claims":{"get":{"tags":["Warranty Registry"],"summary":"Warranty claims — tickets raised against a registered warranty, with their decision","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Claims","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/warranty-entries/{id}":{"get":{"tags":["Warranty Registry"],"summary":"A warranty entry with its claims history","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/warranty-entries/{id}/serial":{"put":{"tags":["Warranty Registry"],"summary":"Record the delivered unit's serial number (links the serial trace when registered)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/warranty-entries/sync-delivered":{"post":{"tags":["Warranty Registry"],"summary":"Register warranty entries for delivered orders that have none yet (idempotent)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Sync result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/public/support/tickets":{"post":{"tags":["Public Support"],"summary":"Public support form — no login. Creates a ticket and returns only its reference (FR-06-001/002)","responses":{"201":{"description":"Ticket reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"429":{"description":"Too many submissions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/public/support/options":{"get":{"tags":["Public Support"],"summary":"Complaint categories the public form offers","responses":{"200":{"description":"Options","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets":{"get":{"tags":["Support Tickets"],"summary":"Ticket queue, sorted by breach risk then age","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Tickets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Support Tickets"],"summary":"Log a ticket for an off-system contact (RSK-14)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/counts":{"get":{"tags":["Support Tickets"],"summary":"Queue tab counts (all, mine, unassigned, past target, awaiting decision, resolved)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/resolution-targets":{"get":{"tags":["Support Tickets"],"summary":"Resolution target per priority (FR-06-018)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Targets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"put":{"tags":["Support Tickets"],"summary":"Set resolution targets (Support Manager)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Targets","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/assignable-users":{"get":{"tags":["Support Tickets"],"summary":"Active people who can own tickets (or, with for=service_jobs, work service jobs)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"People","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/attachments/{attachmentId}":{"get":{"tags":["Support Tickets"],"summary":"Download a supporting file the security scan has cleared (PRD §9.4)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The file","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"Not scanned yet or blocked by the scan","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/support-tickets/options":{"get":{"tags":["Support Tickets"],"summary":"Active ticket categories, priorities and resolution types (FR-06-007)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Options","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}":{"get":{"tags":["Support Tickets"],"summary":"Ticket with warranty eligibility, links, timeline, replacement and service jobs","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Ticket","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Support Tickets"],"summary":"Update category, priority or subject","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/link-order":{"post":{"tags":["Support Tickets"],"summary":"Link the order behind an unverified ticket (FR-06-003)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/assign":{"post":{"tags":["Support Tickets"],"summary":"Assign or reassign the owner, history retained (FR-06-008)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Assigned","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/status":{"post":{"tags":["Support Tickets"],"summary":"Manual status change — request info, customer replied, close, reopen (FR-06-009)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/messages":{"post":{"tags":["Support Tickets"],"summary":"Add a customer message or internal note to the timeline (FR-06-015)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Added","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/warranty-decision":{"post":{"tags":["Support Tickets"],"summary":"Approve or reject the warranty claim; outside eligibility needs reason + ceiling (FR-06-004/006)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Decided","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Above ceiling or no approve grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/support-tickets/{id}/replacement":{"post":{"tags":["Support Tickets"],"summary":"Approve a replacement — reserves stock through a sales order, or holds awaiting stock and raises a requirement (FR-06-011/012)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Replacement recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/check-stock":{"post":{"tags":["Support Tickets"],"summary":"Retry reserving the replacement and resume the ticket if stock is now reserved","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Ticket","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/support-tickets/{id}/resolve":{"post":{"tags":["Support Tickets"],"summary":"Resolve as repair, replacement, service, refund recommendation or refusal (FR-06-010)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Resolved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/suppliers":{"get":{"tags":["Suppliers"],"summary":"List suppliers, scoped to the caller's company access (FR-07-001)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Matches code, name, city or supplies"},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive"]}},{"in":"query","name":"company_id","schema":{"type":"string"}},{"in":"query","name":"country","schema":{"type":"string"}}],"responses":{"200":{"description":"Supplier list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"post":{"tags":["Suppliers"],"summary":"Create a supplier — the code is server-assigned (FR-07-001)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","company_id"],"properties":{"name":{"type":"string","maxLength":200},"company_id":{"type":"string"},"country":{"type":"string"},"city":{"type":"string"},"contact_person":{"type":"string"},"contact_phone":{"type":"string"},"contact_email":{"type":"string","format":"email"},"address":{"type":"string"},"state":{"type":"string"},"postal_code":{"type":"string"},"tax_id":{"type":"string"},"payment_terms":{"type":"string"},"currency":{"type":"string","enum":["INR","USD","EUR","GBP","AED","CNY"]},"lead_time_days":{"type":"integer","minimum":0},"notes":{"type":"string","maxLength":2000},"supplies":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/suppliers/{id}":{"get":{"tags":["Suppliers"],"summary":"Fetch one supplier (FR-07-001)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Supplier","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Suppliers"],"summary":"Update a supplier — the code is immutable (FR-07-001)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/suppliers/{id}/deactivate":{"post":{"tags":["Suppliers"],"summary":"Deactivate a supplier — never deleted, its purchase history stays resolvable","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/suppliers/{id}/activate":{"post":{"tags":["Suppliers"],"summary":"Reactivate a previously deactivated supplier","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Activated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/suppliers/{id}/performance":{"get":{"tags":["Suppliers"],"summary":"Supplier performance — on-time delivery, short supply, rejection rate (FR-07-014)","description":"Computed on read from purchase-order and goods-receipt rows. Returns nulls and has_data=false when the supplier has no completed orders, so the caller can say \"no data yet\" rather than reporting a misleading 0%.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Performance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/service-jobs":{"get":{"tags":["Service Jobs"],"summary":"Service jobs, earliest promised first","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Jobs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Service Jobs"],"summary":"Raise a service job from a ticket (FR-06-013)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/service-jobs/counts":{"get":{"tags":["Service Jobs"],"summary":"Tab counts (all, open, overdue, under warranty, chargeable)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Counts","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/service-jobs/{id}":{"get":{"tags":["Service Jobs"],"summary":"A service job with its inspection, parts and stage timeline","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Job","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Service Jobs"],"summary":"Change technician or promised date","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/service-jobs/{id}/receive":{"post":{"tags":["Service Jobs"],"summary":"Receive the collected unit with its arrival inspection (FR-06-014)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Received","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/service-jobs/{id}/advance":{"post":{"tags":["Service Jobs"],"summary":"Move to diagnosed, repaired, returned or cancelled","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Advanced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/service-jobs/{id}/parts":{"post":{"tags":["Service Jobs"],"summary":"Record parts used — issued from stock as real movements","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Parts issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-returns":{"get":{"tags":["Sales Returns"],"summary":"List returns / RMA, scoped to the caller's company access","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Return list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Sales Returns"],"summary":"Record a return against a delivered order (FR-02-015)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error (e.g. order not delivered)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-returns/{id}":{"get":{"tags":["Sales Returns"],"summary":"Get a single return with lines","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Return","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found (or out of the caller's company scope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-returns/{id}/receive":{"post":{"tags":["Sales Returns"],"summary":"Log physical receipt of the returned goods (RAISED -> QC_HOLD)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Received","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-returns/{id}/inspect":{"post":{"tags":["Sales Returns"],"summary":"Record per-line inspection outcome — sellable credits on_hand, damaged never becomes sellable (FR-02-015)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Inspected and closed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-orders":{"get":{"tags":["Sales Orders"],"summary":"List the order book, scoped to the caller's company access, with exception-queue and search filters (FR-02-017/018)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"exceptions_only","schema":{"type":"boolean"},"description":"Actionable exception queue — short, actionable unmapped, unpaid prepaid and stalled orders. Excludes historical orders and orders the channel already shipped."},{"in":"query","name":"hold_reason","schema":{"type":"string","enum":["short_stock","unmapped_product","historical_product"]},"description":"Report filter — historical_product lists unmapped orders whose products the channel no longer lists."}],"responses":{"200":{"description":"Sales order list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Sales Orders"],"summary":"Manually create a sales order (source = \"manual\", FR-02-001)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-orders/export":{"get":{"tags":["Sales Orders"],"summary":"Export the same filtered order book as CSV (FR-02-018). Requires the export permission (FR-09-004); streamed, capped at 100,000 rows, audited.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"CSV file"},"400":{"description":"EXPORT_TOO_LARGE — narrow the filters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-orders/{id}":{"get":{"tags":["Sales Orders"],"summary":"Get a single sales order with lines and full timeline (FR-02-016)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Sales order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found (or out of the caller's company scope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Sales Orders"],"summary":"Permitted pre-dispatch edit — shipping address, line quantities; recorded on the timeline (FR-02-012)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-orders/{id}/confirm":{"post":{"tags":["Sales Orders"],"summary":"Check and reserve stock for every line, all-or-nothing; SHORT if any line lacks availability (FR-02-007/008/009)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Confirmed or short","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-orders/{id}/confirm-payment":{"post":{"tags":["Sales Orders"],"summary":"Irreversibly mark a prepaid or unclassified order as paid","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Payment confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Already confirmed","COD":null,"or order is packed/terminal":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-orders/{id}/cancel":{"post":{"tags":["Sales Orders"],"summary":"Cancel with a reason, releasing every reserved line immediately (FR-02-013)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-channels":{"get":{"tags":["SalesChannels"],"summary":"List connected sales channels, scoped to the caller's company access (FR-10-017, FR-10-005)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"type","schema":{"type":"string"}}],"responses":{"200":{"description":"Sales channel list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["SalesChannels"],"summary":"Register a new sales channel (config only — does not connect it). company_id must be within the caller's own company scope.","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"company_id outside the caller's scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-channels/{code}":{"get":{"tags":["SalesChannels"],"summary":"Get a single sales channel by code, scoped to the caller's company access. Credentials are never included in the response.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sales channel","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found (or out of the caller's company scope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["SalesChannels"],"summary":"Update sales channel config (sync frequency, allocation rule, safety buffer, active flag) — never credentials","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-channels/{code}/connect":{"post":{"tags":["SalesChannels"],"summary":"Connect (or reconnect) a channel — validates credentials via the registered adapter's testConnection before persisting them","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Connected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"502":{"description":"Connection test failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-channels/{code}/test-connection":{"post":{"tags":["SalesChannels"],"summary":"Re-test the currently stored credentials against the channel's API","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Test result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-channels/{code}/disconnect":{"post":{"tags":["SalesChannels"],"summary":"Disconnect a channel and clear its stored credentials","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Disconnected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-channels/{code}/resync":{"post":{"tags":["SalesChannels"],"summary":"Sync now — queue a sync run (products → orders → inventory) for this channel. Queued, not synchronous; the run's live progress and counts appear on /sync-events.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Sync queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found (or out of the caller's company scope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"Channel is disabled (SALES_CHANNEL_INACTIVE) or not connected (SALES_CHANNEL_NOT_CONNECTED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/sales-channels/{code}/sync-events":{"get":{"tags":["SalesChannels"],"summary":"The channel's sync ledger, newest first — sync runs (trigger_type \"poll\", with trigger manual/scheduled/webhook and a live `summary` of per-area counts) and the verified webhook inbox (trigger_type \"webhook\")","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Sync events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-channels/{code}/audit-events":{"get":{"tags":["SalesChannels"],"summary":"Recent connector audit events for this channel (connect/disconnect/sync/failure trail)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Audit events","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/roles":{"get":{"tags":["Roles"],"summary":"List roles with their grant matrix (SCR-10-02)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"List of roles","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Roles"],"summary":"Create a role with its grant matrix, approval ceilings and delegate (FR-10-003)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Role created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/roles/{id}":{"get":{"tags":["Roles"],"summary":"Get a single role with its full grant matrix","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Role","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Roles"],"summary":"Update a role's grants, approval ceilings, delegate and platform access","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"grants":{"type":"array","items":{"type":"object"}},"approval_ceilings":{"type":"object","additionalProperties":{"type":"number"}},"delegate_role_code":{"type":"string","nullable":true},"login_enabled":{"type":"boolean","description":"Whether holding this role permits signing in at all. Setting it false removes platform access from every holder on their next request, without touching their credentials or this role's grants — set it back to true to restore access. Refused for a protected administrator role, and refused when it would leave nobody able to sign in.\n"}}}}}},"responses":{"200":{"description":"Role updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Protected role guard","or sign-in removal refused on a protected role":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"Would leave no one able to sign in","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/roles/{id}/deactivate":{"post":{"tags":["Roles"],"summary":"Deactivate a non-protected role (FR-10-012)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Role deactivated"},"403":{"description":"Protected role guard"}}}},"/v1/roles/{id}/reactivate":{"post":{"tags":["Roles"],"summary":"Reactivate a previously deactivated role","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Role reactivated"},"404":{"description":"Not found"}}}},"/v1/reports":{"get":{"tags":["Reports"],"summary":"The report catalogue — only reports whose source data the caller may also read (FR-09-001)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"[{ code, category, title, description, filters { date, company, warehouse }, group_by_options, default_group_by, can_export }]","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/reports/{code}":{"get":{"tags":["Reports"],"summary":"One page of a report, scoped to the caller and filtered by date, company and warehouse (FR-09-002/003)","description":"Figures are read from this database only. Money totals are per currency, never summed across currencies. A filter outside the caller's scope returns no rows.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}},{"in":"query","name":"company_id","schema":{"type":"string"}},{"in":"query","name":"warehouse_id","schema":{"type":"string"}},{"in":"query","name":"group_by","schema":{"type":"string"}},{"in":"query","name":"sort","schema":{"type":"string"}},{"in":"query","name":"order","schema":{"type":"string","enum":["asc","desc"]}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","enum":[25,50,100]}}],"responses":{"200":{"description":"{ code, title, columns, rows, totals, mixed_currencies, window, group_by, filters, notes } + pagination","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}},"403":{"description":"REPORT_SOURCE_FORBIDDEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"REPORT_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/reports/{code}/export":{"get":{"tags":["Reports"],"summary":"The whole filtered report as CSV (FR-09-004) — requires the export permission; capped at 100,000 rows; audited","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"CSV file (UTF-8 with BOM)"},"400":{"description":"EXPORT_TOO_LARGE / RANGE_TOO_LARGE / INVALID_REPORT_PARAM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/reports/lot_traceability/rows/{lotId}/movements":{"get":{"tags":["Reports"],"summary":"Stock movements recorded against one lot (lot traceability drill-down)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"lotId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"[{ id, created_at, movement_type, direction, quantity, warehouse, reference_type, note, by_user }]","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/quality/inspections":{"get":{"tags":["Quality"],"summary":"All inspections — goods receipts, returns and service-job arrival checks, newest first","description":"A read-only register. Service-job rows (M6) carry a result but no piece counts or site, and are returned only to holders of service_job.read.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"kind","schema":{"type":"string","enum":["goods_receipt","sales_return","service_job"]}},{"in":"query","name":"result","schema":{"type":"string","enum":["pending","passed","partial","rejected","repairable","not_repairable","no_fault_found"]}},{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Code, source document or SKU"},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"Rows — each { id, kind, code, against, result, sku, item_count, checked, passed, rework, rejected, inspector_names, site, date }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/quality/inspections/summary":{"get":{"tags":["Quality"],"summary":"The All inspections KPI strip — awaiting inspection (48 h), corrective actions, overall rate","description":"`corrective_actions` is null for a caller without corrective_action.read. The rate names its formula (still to be confirmed, O-M5-7).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"{ awaiting_inspection { inspections, pieces, over_threshold, threshold_hours }, corrective_actions { open, critical, overdue } | null, rate { formula, value, checked, rejected } }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/quality/defect-rate":{"get":{"tags":["Quality"],"summary":"Defect rate by supplier, product, reason or month (FR-05-008)","description":"Derived on read from current inspection lines. The response names the formula used (`formula`) — the definition is still to be confirmed (O-M5-7). Raw counts are returned beside the rate.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"group_by","schema":{"type":"string","enum":["supplier","product","reason","month"],"default":"supplier"}},{"in":"query","name":"source_type","schema":{"type":"string","enum":["goods_receipt","sales_return"]}},{"in":"query","name":"from","schema":{"type":"string","format":"date"},"description":"By inspection date"},{"in":"query","name":"to","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"{ formula { code, label, definition }, group_by, rows[{ key, label, sub_label, checked, passed, rework, rejected, lots, defective_lots, rate, share_of_rejected }], totals }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/quality-defects":{"get":{"tags":["Quality"],"summary":"List defect catalogue entries","description":"Without page/page_size every matching entry is returned, in display order. `selectable=true` returns only what a new inspection may pick (active, not a system entry).","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"},"description":"Matches code or name"},{"in":"query","name":"severity","schema":{"type":"string"},"description":"defect-severity master code"},{"in":"query","name":"is_active","schema":{"type":"string","enum":["true","false"]}},{"in":"query","name":"selectable","schema":{"type":"string","enum":["true","false"]}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"Entries — each { id, code, name, severity, severity_label, standard_action, description, display_order, is_active, is_system, created_at, updated_at }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Quality"],"summary":"Add a defect catalogue entry","description":"The code is stored upper-case and can never be changed afterwards.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","name","severity"],"properties":{"code":{"type":"string","maxLength":50,"pattern":"^[A-Za-z0-9_]+$"},"name":{"type":"string","maxLength":200},"severity":{"type":"string","description":"Active defect-severity master code"},"standard_action":{"type":"string","maxLength":1000,"nullable":true},"description":{"type":"string","maxLength":1000,"nullable":true},"reason":{"type":"string","maxLength":2000,"description":"Recorded in the audit trail"}}}}}},"responses":{"201":{"description":"Entry created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED or QUALITY_DEFECT_SEVERITY_INVALID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"QUALITY_DEFECT_CODE_TAKEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/quality-defects/{id}":{"get":{"tags":["Quality"],"summary":"Get a defect catalogue entry","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Entry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"QUALITY_DEFECT_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Quality"],"summary":"Update a defect catalogue entry","description":"`code` is immutable and is refused if sent.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","severity"],"properties":{"name":{"type":"string","maxLength":200},"severity":{"type":"string"},"standard_action":{"type":"string","maxLength":1000,"nullable":true},"description":{"type":"string","maxLength":1000,"nullable":true},"reason":{"type":"string","maxLength":2000}}}}}},"responses":{"200":{"description":"Entry updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED or QUALITY_DEFECT_SEVERITY_INVALID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"QUALITY_DEFECT_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/quality-defects/{id}/status":{"put":{"tags":["Quality"],"summary":"Activate or deactivate a defect catalogue entry","description":"Never a delete — a deactivated entry still resolves on past inspections. System entries cannot be deactivated.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["is_active"],"properties":{"is_active":{"type":"boolean"},"reason":{"type":"string","maxLength":2000}}}}}},"responses":{"200":{"description":"Status set","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"QUALITY_DEFECT_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"QUALITY_DEFECT_SYSTEM","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/quality/audit-log":{"get":{"tags":["Quality"],"summary":"The quality audit trail — inspections, corrective actions and the defect catalogue, newest first","description":"Read-only. Each entry carries who, when, what changed (from → to) and why. Entries whose record is outside the caller's companies are not returned, and corrective-action entries need corrective_action.read.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"target_type","schema":{"type":"string","enum":["inspection","corrective_action","quality_defect"]}},{"in":"query","name":"target_id","schema":{"type":"string"},"description":"One record's history; needs target_type. An inspection's includes its lines."},{"in":"query","name":"actor_user_id","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string","format":"date"}},{"in":"query","name":"to","schema":{"type":"string","format":"date"}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Record code, action or reason"},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"Entries — each { id, created_at, action, target_type, target_id, target_code, inspection_id, actor_user_id, actor_name, actor_roles, changes[{field, from, to}], reason }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/quality/audit-log/actors":{"get":{"tags":["Quality"],"summary":"People who appear in the caller's quality audit trail — the actor filter's options","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"People — each { id, full_name }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-requisitions/stock-position":{"get":{"tags":["Purchase Requisitions"],"summary":"Available, reserved, incoming and suggested quantity for a variant at a warehouse (FR-07-004)","description":"Suggested quantity uses the Inventory module's own shortfall formula — reorder level minus (available + incoming) — so this screen and the Material Requirements screen cannot disagree. Incoming is already deducted.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"product_variant_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"warehouse_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Stock position","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-requisitions/convertible":{"get":{"tags":["Purchase Requisitions"],"summary":"Approved requisitions for one supplier that can be grouped into a single PO (FR-07-006)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"supplier_id","required":false,"schema":{"type":"string"},"description":"Omit to list every open requirement, whoever it names"}],"responses":{"200":{"description":"Convertible requisitions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-requisitions":{"get":{"tags":["Purchase Requisitions"],"summary":"List purchase requirements (FR-07-004)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Matches code","reason or SKU":null},{"in":"query","name":"status","schema":{"type":"string","enum":["PENDING_APPROVAL","APPROVED","REJECTED","CONVERTED_TO_PO"]}},{"in":"query","name":"triggered_by","schema":{"type":"string","enum":["MANUAL","REORDER_LEVEL","SALES_ORDER_SHORTFALL"]}},{"in":"query","name":"company_id","schema":{"type":"string"}},{"in":"query","name":"supplier_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Requisition list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Purchase Requisitions"],"summary":"Raise a purchase requirement — always by a person (FR-07-002/003/005)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["product_variant_id","warehouse_id","quantity"],"properties":{"product_variant_id":{"type":"string"},"warehouse_id":{"type":"string"},"quantity":{"type":"integer","minimum":1},"needed_by":{"type":"string","format":"date"},"reason":{"type":"string"},"candidate_supplier_id":{"type":"string"},"triggered_by":{"type":"string","enum":["MANUAL","REORDER_LEVEL","SALES_ORDER_SHORTFALL"]},"estimated_value":{"type":"number","minimum":0}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/purchase-requisitions/{id}":{"get":{"tags":["Purchase Requisitions"],"summary":"Fetch one purchase requirement","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Requisition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Purchase Requisitions"],"summary":"Correct a requirement that has not been decided on yet (FR-07-005)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["product_variant_id","warehouse_id","quantity"],"properties":{"product_variant_id":{"type":"string"},"warehouse_id":{"type":"string"},"quantity":{"type":"integer","minimum":1},"needed_by":{"type":"string","format":"date"},"reason":{"type":"string","maxLength":500},"candidate_supplier_id":{"type":"string"},"estimated_value":{"type":"number","minimum":0},"priority":{"type":"string","enum":["Low","Normal","High","Urgent"]}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Already approved or rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/purchase-requisitions/from-sales-order":{"post":{"tags":["Purchase Requisitions"],"summary":"Raise purchase requirements for a short sales order's lines (FR-07-003)","description":"One requirement per product and warehouse. A short line joins the requirement already awaiting approval for its position (its quantity grows by the line's shortfall); otherwise a new one is raised with triggered_by SALES_ORDER_SHORTFALL. Lines already covered by a live requirement, lines no longer short, and unmapped lines are skipped and reported. Nothing is approved or ordered (FR-07-005).\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sales_order_id"],"properties":{"sales_order_id":{"type":"string"},"sales_order_line_ids":{"type":"array","items":{"type":"string"},"description":"Omit to cover every short line"},"needed_by":{"type":"string","format":"date","description":"Defaults to the order's promised date when that is not in the past"},"reason":{"type":"string","maxLength":500},"priority":{"type":"string","enum":["Low","Normal","High","Urgent"]}}}}}},"responses":{"201":{"description":"Raised — data holds created, merged and skipped","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Order is not held for short stock","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"Order not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/purchase-requisitions/{id}/approve":{"post":{"tags":["Purchase Requisitions"],"summary":"Approve a requirement — the decision is recorded (FR-07-005)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-requisitions/{id}/reject":{"post":{"tags":["Purchase Requisitions"],"summary":"Reject a requirement with a recorded reason (FR-07-005)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rejection_reason"],"properties":{"rejection_reason":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-orders":{"get":{"tags":["Purchase Orders"],"summary":"List purchase orders (FR-07-007), optionally only overdue ones (FR-07-010)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Matches PO number or supplier name"},{"in":"query","name":"status","schema":{"type":"string","enum":["DRAFT","PENDING_APPROVAL","CONFIRMED","REJECTED","IN_TRANSIT","RECEIVED","CLOSED"]}},{"in":"query","name":"supplier_id","schema":{"type":"string"}},{"in":"query","name":"company_id","schema":{"type":"string"}},{"in":"query","name":"overdue","schema":{"type":"string","enum":["true","false"]},"description":"Past the expected date without full receipt"}],"responses":{"200":{"description":"Purchase order list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Purchase Orders"],"summary":"Create a draft purchase order, optionally grouping approved requisitions (FR-07-006/007)","description":"Every requisition in `requisition_ids` must be approved, unconverted and name this same supplier — one purchase order covers one supplier. They are marked converted in the same transaction that writes the order.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["supplier_id","currency","lines"],"properties":{"supplier_id":{"type":"string"},"currency":{"type":"string","enum":["INR","USD","EUR","GBP","AED","CNY"]},"incoterm":{"type":"string"},"order_date":{"type":"string","format":"date","description":"When the order was placed. Defaults to today."},"expected_date":{"type":"string","format":"date"},"requisition_ids":{"type":"array","items":{"type":"string"}},"lines":{"type":"array","minItems":1,"items":{"type":"object","required":["product_variant_id","quantity","unit_price","destination_warehouse_id"],"properties":{"product_variant_id":{"type":"string"},"quantity":{"type":"integer","minimum":1},"unit_price":{"type":"number","minimum":0},"destination_warehouse_id":{"type":"string"},"expected_date":{"type":"string","format":"date"}}}}}}}}},"responses":{"201":{"description":"Created as DRAFT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-orders/{id}":{"get":{"tags":["Purchase Orders"],"summary":"One purchase order with its lines and per-line quantity tracking (FR-07-009)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Purchase order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Purchase Orders"],"summary":"Edit a draft purchase order — drafts only","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","enum":["INR","USD","EUR","GBP","AED","CNY"]},"incoterm":{"type":"string"},"order_date":{"type":"string","format":"date","description":"Omit to keep the stored date."},"expected_date":{"type":"string","format":"date"},"lines":{"type":"array","minItems":1,"description":"Replaces every line. Omit to leave the lines untouched.","items":{"type":"object","required":["product_variant_id","quantity","unit_price","destination_warehouse_id"],"properties":{"product_variant_id":{"type":"string"},"quantity":{"type":"integer","minimum":1},"unit_price":{"type":"number","minimum":0},"destination_warehouse_id":{"type":"string"},"expected_date":{"type":"string","format":"date"}}}}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-orders/{id}/open-lines":{"get":{"tags":["Purchase Orders"],"summary":"Outstanding lines, for pre-filling a goods receipt (FR-07-012)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Open lines","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-orders/{id}/submit":{"post":{"tags":["Purchase Orders"],"summary":"Submit for the approval-ceiling check (FR-07-008)","description":"The order's value is summed from its stored lines and compared against the submitter's own resolved ceiling. Within it, the order is confirmed and the decision recorded; over it, the order waits for someone whose ceiling covers it.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Confirmed or awaiting approval","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/purchase-orders/{id}/approve":{"post":{"tags":["Purchase Orders"],"summary":"Approve an order awaiting a decision (FR-07-008)","description":"Refused if the caller's ceiling does not cover the total, or if they submitted it themselves.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Approved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Above the caller's ceiling","or their own submission":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/purchase-orders/{id}/reject":{"post":{"tags":["Purchase Orders"],"summary":"Reject an order awaiting a decision, with a recorded reason (FR-07-008)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Rejected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/products":{"get":{"tags":["Products"],"summary":"List the product catalogue, scoped to the caller's company access (FR-11-007)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Product list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Products"],"summary":"Create a product (optionally with its variants). internal_code/sku are ERP-assigned unless supplied explicitly (FR-11-007, Super Administrator).","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"company_id outside the caller's scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/products/{id}":{"get":{"tags":["Products"],"summary":"Get a single product by id, scoped to the caller's company access","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Product","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found (or out of the caller's company scope)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Products"],"summary":"Update product config fields — never internal_code (immutable)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/products/{id}/listing":{"get":{"tags":["Products"],"summary":"FR-04-017 — product + variants + per-channel mapping breakdown, including on-hand/available quantities per variant (M4 Inventory).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Listing view","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/payroll/runs/{id}/approve":{"post":{"tags":["Payroll"],"summary":"Approve and lock a DRAFT payroll run","description":"Refused (PAYROLL_DRAFT_STALE) if attendance, leave or salary data changed since the draft was calculated — recalculate first. When any line has unmarked working days (counted as absent), `confirm_unmarked: true` is required (PAYROLL_UNMARKED_ATTENDANCE otherwise); the confirmation is audited.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"confirm_unmarked":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Approved and locked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Already approved / stale draft / unmarked attendance not confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/payroll/runs/{id}/recalculate":{"post":{"tags":["Payroll"],"summary":"Rebuild a DRAFT run's lines from current attendance, leave and salary data","description":"Same run, same id. Refused once the run is approved.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Recalculated run","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Already approved (PAYROLL_RUN_ALREADY_APPROVED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/payroll/runs/{id}/attendance-exceptions":{"get":{"tags":["Payroll"],"summary":"Working days with no attendance, per employee, for a DRAFT run (live)","description":"A WEEKLY_OFF or HOLIDAY entry on a day the working calendar says is worked does not count as attendance — the date is listed, and also in `off_marked_dates`. `missing_ranges` groups the dates into stretches with no marked working day between them.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ unmarked_days, employees: [{ employee_id, employee_code, employee_name, missing_dates, off_marked_dates, missing_ranges: [{ from, to, days }] }] }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/org/companies":{"get":{"tags":["Organisation"],"summary":"Active companies (read-only reference list — for scope pickers only)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Companies","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/org/warehouses":{"get":{"tags":["Organisation"],"summary":"Active warehouses (read-only reference list — for scope pickers only)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Warehouses","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/master-list/categories":{"get":{"tags":["MasterList"],"summary":"Grouped reference master categories with value counts","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Category groups","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["MasterList"],"summary":"Create a master category","description":"The code is derived from the name (kebab-case, suffixed when taken) and never changes. New categories are not system categories, so they can later be deactivated or deleted.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","group_name"],"properties":{"name":{"type":"string","maxLength":120},"group_name":{"type":"string","maxLength":80},"used_in":{"type":"string","maxLength":200}}}}}},"responses":{"201":{"description":"Category created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Name taken (MASTER_CATEGORY_NAME_ALREADY_EXISTS)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/master-list/categories/{categoryCode}":{"put":{"tags":["MasterList"],"summary":"Rename or regroup a master category (system ones included)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","group_name"],"properties":{"name":{"type":"string","maxLength":120},"group_name":{"type":"string","maxLength":80},"used_in":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Category updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["MasterList"],"summary":"Delete an empty, admin-created master category (soft delete)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Category deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"System category","or it still has values (MASTER_CATEGORY_SYSTEM_LOCKED / MASTER_CATEGORY_HAS_VALUES)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/master-list/categories/{categoryCode}/deactivate":{"post":{"tags":["MasterList"],"summary":"Deactivate an admin-created master category","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Category deactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"System category (MASTER_CATEGORY_SYSTEM_LOCKED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/master-list/categories/{categoryCode}/reactivate":{"post":{"tags":["MasterList"],"summary":"Reactivate a master category","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Category reactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/master-list/{categoryCode}/values":{"get":{"tags":["MasterList"],"summary":"List values for a reference master category","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"Master values","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["MasterList"],"summary":"Create a value in a reference master category","description":"The code is optional — left blank it is derived from the label (snake_case, suffixed when taken). Either way it cannot be changed afterwards. The value is placed at the end of the master; display order is not accepted.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["label"],"properties":{"code":{"type":"string","maxLength":60,"pattern":"^[A-Za-z0-9][A-Za-z0-9_-]*$"},"label":{"type":"string","maxLength":120},"description":{"type":"string","maxLength":500},"is_active":{"type":"boolean"}}}}}},"responses":{"201":{"description":"Value created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/master-list/{categoryCode}/values/{id}":{"put":{"tags":["MasterList"],"summary":"Update a reference master value","description":"Label, description and active flag. The code is fixed (MASTER_VALUE_CODE_LOCKED); a system value cannot be set inactive (MASTER_VALUE_SYSTEM_LOCKED). A label must be unique within the master (MASTER_VALUE_LABEL_ALREADY_EXISTS).\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Value updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/master-list/{categoryCode}/values/{id}/deactivate":{"post":{"tags":["MasterList"],"summary":"Deactivate a reference master value","description":"Refused for a system value the app reads by code (MASTER_VALUE_SYSTEM_LOCKED).","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Value deactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/master-list/{categoryCode}/values/{id}/reactivate":{"post":{"tags":["MasterList"],"summary":"Reactivate a reference master value","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"categoryCode","required":true,"schema":{"type":"string"}},{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Value reactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/webhooks/logistics/{code}":{"post":{"tags":["Webhooks"],"summary":"Courier partner tracking callback (token-verified)","parameters":[{"in":"path","name":"code","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Accepted"},"401":{"description":"Rejected (uniform response)"}}}},"/v1/logistics-connections/{id}/sync":{"post":{"tags":["Logistics Connections"],"summary":"Start a read-only Shiprocket sync (\"Sync now\") — returns the RUNNING run","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"mode":{"type":"string","enum":["FULL","INCREMENTAL"]}}}}}},"responses":{"201":{"description":"Run started","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"A sync is already running","or sync is switched off":null}}}},"/v1/logistics-connections/{id}/sync-runs":{"get":{"tags":["Logistics Connections"],"summary":"Shiprocket sync history with statistics, newest first","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Runs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/logistics-connections/{id}/provider-channels":{"get":{"tags":["Logistics Connections"],"summary":"Stores integrated in Shiprocket (live read of GET /channels) for store linking","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Channels","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Shiprocket rejected the credentials"}}}},"/v1/logistics-connections/{id}/provider-shipments":{"get":{"tags":["Logistics Connections"],"summary":"Synced Shiprocket shipments with their match result (reconciliation)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"match_status","schema":{"type":"string","enum":["MATCHED","UNMATCHED","AMBIGUOUS"]}},{"in":"query","name":"search","schema":{"type":"string"}}],"responses":{"200":{"description":"Shipments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/shiprocket-tracking":{"get":{"tags":["Fulfilment"],"summary":"Shiprocket shipments matched to sales orders, with latest status (read-only)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Shipments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/sales-orders/{id}/shiprocket-tracking":{"get":{"tags":["Sales Orders"],"summary":"The order's Shiprocket shipments (ERP-booked and synced) with tracking timelines","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Tracking","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/logistics-connections":{"get":{"tags":["Logistics Connections"],"summary":"Courier partner connections in the caller's company scope (credentials never returned)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Connections","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Logistics Connections"],"summary":"Connect a courier partner for a company","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/logistics-connections/{id}":{"put":{"tags":["Logistics Connections"],"summary":"Update a connection — a blank password or webhook token keeps the stored value","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/logistics-connections/{id}/test":{"post":{"tags":["Logistics Connections"],"summary":"Authenticate with the partner without booking anything","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Tested","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/loans-advances/{id}":{"put":{"tags":["Loans & Advances"],"summary":"Edit a PENDING_APPROVAL loan or advance (requester only)","description":"The person who raised the request corrects it before anyone decides it. EMI is recomputed; the approver and reference number are unchanged. Switching between a loan and an advance is refused — withdraw and raise a new request instead.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind","amount","months"],"properties":{"kind":{"type":"string","enum":["SALARY_ADVANCE","PERSONAL_LOAN","TRAVEL_ADVANCE"]},"amount":{"type":"number","exclusiveMinimum":0},"months":{"type":"integer","minimum":1,"maximum":120},"reason":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the requester (LOAN_EDIT_NOT_ALLOWED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Already decided","or loan/advance switch (LOAN_ALREADY_DECIDED / LOAN_KIND_CHANGE_NOT_ALLOWED)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/loans-advances/{id}/withdraw":{"post":{"tags":["Loans & Advances"],"summary":"Withdraw a PENDING_APPROVAL loan or advance (requester only)","description":"The person who raised the request takes it back before anyone decides it. Status becomes withdrawn; nothing is recovered through payroll.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Withdrawn","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the requester (LOAN_WITHDRAW_NOT_ALLOWED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Already decided (LOAN_ALREADY_DECIDED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/leave/requests/preview":{"get":{"tags":["Leave"],"summary":"What a leave request would cost — working days, and the paid/unpaid split","description":"Server-side day count so the client never reimplements the working-day rule. Excludes weekly offs and holidays on the employee's company calendar and reports why each excluded date is off. `overlap` names a pending or approved request that shares a date with the range (creating would be refused with 409 LEAVE_OVERLAPS_EXISTING), or is null. Read-only — creates nothing.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"employee_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"leave_type_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"from_date","required":true,"schema":{"type":"string","format":"date"}},{"in":"query","name":"to_date","required":true,"schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Preview","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not your request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Invalid range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/leave/requests/{id}":{"put":{"tags":["Leave"],"summary":"Edit a PENDING leave request (requester only)","description":"The person who raised the request changes its type, dates or reason before anyone decides it. Re-priced like a new request; the days it held are released and the new ones held in the same transaction. Approver and request number are unchanged.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["leave_type_id","from_date","to_date"],"properties":{"leave_type_id":{"type":"string"},"from_date":{"type":"string","format":"date"},"to_date":{"type":"string","format":"date"},"reason":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the requester (LEAVE_EDIT_NOT_ALLOWED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"Overlaps another request (LEAVE_OVERLAPS_EXISTING)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Not pending","invalid range":null,"no working days":null,"or unpaid cap (LEAVE_REQUEST_NOT_PENDING / LEAVE_NO_WORKING_DAYS / LEAVE_UNPAID_CAP_EXCEEDED)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/leave/requests/{id}/withdraw":{"post":{"tags":["Leave"],"summary":"Withdraw a PENDING leave request","description":"The requester (or HR/Admin on their behalf) takes back a request nobody has decided yet. Status becomes cancelled, the days it held are released, and its dates are free for a new request.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"Withdrawn","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not the requester","HR or Admin (LEAVE_WITHDRAW_NOT_ALLOWED)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Not pending (LEAVE_REQUEST_NOT_PENDING)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/leave/requests/{id}/cancel":{"post":{"tags":["Leave"],"summary":"Cancel APPROVED leave (HR/Admin only)","description":"Returns the days it took to the balance and removes the LEAVE attendance rows approval wrote, so those days can be marked as what actually happened. Refused once payroll has been approved for any month it touches — correct the pay with a payroll adjustment instead. A draft run does not block (recalculate it).\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["cancellation_reason"],"properties":{"cancellation_reason":{"type":"string","minLength":1,"maxLength":500}}}}}},"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"403":{"description":"Not HR/Admin (LEAVE_CANCEL_NOT_ALLOWED)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Not approved","or payroll already run (LEAVE_REQUEST_NOT_APPROVED / LEAVE_CANCEL_PAYROLL_RUN)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued":{"post":{"tags":["Assets Issued"],"summary":"Issue an asset, or add one to store unassigned (FR-08-011)","description":"Omit `asset_tag` and the server allocates the next `AST-<yymm>-<NNN>`. Supply it only when the asset already carries a printed label; a supplied tag is trimmed, upper-cased and rejected with ASSET_TAG_TAKEN if it exists. Omit `employee_id` to add the asset to store; supplying one makes `issued_at` required.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["item","company_id"],"properties":{"asset_tag":{"type":"string","maxLength":30,"description":"Generated as AST-yymm-NNN when omitted"},"item":{"type":"string","maxLength":200,"description":"Name of an ACTIVE asset-list item (case-insensitive). The asset kind is taken from it."},"company_id":{"type":"string"},"employee_id":{"type":"string","nullable":true},"issued_at":{"type":"string","format":"date"},"value":{"type":"number","nullable":true},"condition":{"type":"string"},"return_due_at":{"type":"string","format":"date","nullable":true}}}}}},"responses":{"201":{"description":"Asset issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Validation failed — VALIDATION_FAILED, ASSET_ITEM_NOT_IN_CATALOGUE, ASSET_ITEM_INACTIVE, ASSET_ISSUED_AT_REQUIRED, ASSET_COMPANY_MISMATCH, ASSET_TAG_TAKEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/{id}":{"put":{"tags":["Assets Issued"],"summary":"Edit an asset's details","description":"Never changes the holder or the status. Changing `item` requires an active asset-list item; keeping the current item is always allowed. `kind` is re-derived from the item whenever it is in the asset list.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["item"],"properties":{"item":{"type":"string","maxLength":200},"value":{"type":"number","nullable":true},"condition":{"type":"string","maxLength":200},"return_due_at":{"type":"string","format":"date","nullable":true}}}}}},"responses":{"200":{"description":"Asset updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED, ASSET_ITEM_NOT_IN_CATALOGUE, ASSET_ITEM_INACTIVE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"ASSET_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/{id}/return":{"post":{"tags":["Assets Issued"],"summary":"Return an issued asset to store","description":"Clears the holder, issue date and return-due date. The asset becomes `in_store`, except one flagged `needs_repair`, which stays `needs_repair`.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"condition":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Returned to store","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"ASSET_NOT_ISSUED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/{id}/issue":{"post":{"tags":["Assets Issued"],"summary":"Issue an in-store asset to an employee","description":"The same asset record moves on — tag, item and value are kept. Allowed only while the asset is `in_store` with no holder; the employee must belong to the asset's company. Sets status `with_employee`.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["employee_id","issued_at"],"properties":{"employee_id":{"type":"string"},"issued_at":{"type":"string","format":"date","description":"Not in the future"},"return_due_at":{"type":"string","format":"date","nullable":true,"description":"Not before issued_at"}}}}}},"responses":{"200":{"description":"Asset issued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED, ASSET_COMPANY_MISMATCH","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"ASSET_NOT_FOUND, EMPLOYEE_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_NOT_IN_STORE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/{id}/status":{"post":{"tags":["Assets Issued"],"summary":"Flag or clear return-due / needs-repair","description":"Never changes the holder. Allowed moves — in_store → needs_repair; with_employee → return_due | needs_repair; return_due → with_employee; needs_repair → with_employee when someone holds it, otherwise in_store. Anything else is rejected with ASSET_STATUS_TRANSITION_INVALID, whose error details carry `from`, `to` and `allowed`.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["with_employee","in_store","return_due","needs_repair"]}}}}}},"responses":{"200":{"description":"Status changed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_STATUS_TRANSITION_INVALID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inventory/stock":{"get":{"tags":["Inventory"],"summary":"List stock positions (one row per SKU per warehouse), scoped to the caller's company/site access (FR-04-001/020)","description":"Returns one row per SKU per warehouse by default. Pass `group_by=variant` for one row per SKU with quantities summed across the warehouses the caller can see, each row carrying `warehouse_count` and a `warehouses[]` breakdown of the same per-warehouse rows. Grouped rows have no stock-position `id` — address a single position by the `id` inside `warehouses[]`. Stored data is unchanged either way (FR-04-001 keeps one StockLevel per SKU per warehouse); grouping is read-side only.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"group_by","schema":{"type":"string","enum":["variant"]},"description":"Collapse the per-warehouse rows into one row per SKU"},{"in":"query","name":"warehouse_id","schema":{"type":"string"},"description":"Limit to one warehouse (applies before grouping)"},{"in":"query","name":"below_reorder","schema":{"type":"boolean"},"description":"Only positions at or below their reorder level. With group_by=variant this compares the SKU total against the sum of its warehouses' levels."},{"in":"query","name":"search","schema":{"type":"string"},"description":"Match SKU","product name or warehouse name":null},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer"}}],"responses":{"200":{"description":"Stock list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Inventory"],"summary":"Receive stock for a SKU at a warehouse — creates the stock position on first use (FR-04-001/004)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Stock position","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/stock/{id}":{"get":{"tags":["Inventory"],"summary":"Get one stock position (FR-04-001/002/003)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Stock position","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/stock/{id}/movements":{"get":{"tags":["Inventory"],"summary":"Movement ledger for one stock position (FR-04-005)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Movement ledger","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Inventory"],"summary":"Record a stock movement — the only write path to a stock balance (FR-04-004/006/007/008/012/013)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Updated stock position","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation error (e.g. would drive available below zero","or missing required reason)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inventory/movements":{"get":{"tags":["Inventory"],"summary":"Global, cross-SKU movement ledger (FR-04-005 — prototype's \"Movements in and out\")","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Movement ledger","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/inventory/stock/{id}/reorder-config":{"put":{"tags":["Inventory"],"summary":"Update reorder level / buffer quantity (FR-04-010)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/transfers":{"get":{"tags":["Inventory"],"summary":"List warehouse-to-warehouse transfers (FR-04-009)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Transfer list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Inventory"],"summary":"Create a warehouse-to-warehouse transfer — in-transit until received (FR-04-009)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/transfers/{id}/receive":{"post":{"tags":["Inventory"],"summary":"Receive an in-transit transfer at its destination warehouse (FR-04-009)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Received","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/transfers/{id}/cancel":{"post":{"tags":["Inventory"],"summary":"Cancel an in-transit transfer, returning stock to the source warehouse (FR-04-009)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/serial-numbers":{"get":{"tags":["Inventory"],"summary":"List registered serial numbers (FR-04-014b)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Serial number list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Inventory"],"summary":"Register a serial number against a stock position (FR-04-014b)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/lots":{"get":{"tags":["Inventory"],"summary":"List component lots/batches (FR-04-014a)","description":"Each row carries goods_receipt_id, goods_receipt_line_id, purchase_order_id and supplier_id when a Goods Receipt registered the lot; all four are null for a lot registered by hand, which has only the free-text references.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Lot list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Inventory"],"summary":"Receive a new lot/batch — registers the lot and writes the matching receipt movement (FR-04-014a)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/lots/{id}/issue":{"post":{"tags":["Inventory"],"summary":"Issue quantity out of a lot (FR-04-014a)","description":"Only an AVAILABLE or PARTLY_ISSUED lot can be issued (422 INVENTORY_LOT_NOT_ISSUABLE otherwise).","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Lot is not issuable or quantity exceeds what is available","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inventory/lots/{id}/status":{"put":{"tags":["Inventory"],"summary":"Change a lot's status — e.g. quarantine or reject (FR-04-014a)","description":"Only for a lot registered by hand. Refused (422 INVENTORY_LOT_STATUS_SET_BY_RECEIPT) for any lot a goods receipt registered — its status comes from that line's QC decision, before and after it is made.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Lot status is set by its goods receipt QC decision","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inventory/skus/{productVariantId}/reorder-config":{"put":{"tags":["Inventory"],"summary":"Set the SKU's company-wide reorder level — the level that triggers a purchase requisition","description":"Distinct from `/stock/{id}/reorder-config`, which sets one warehouse's level and triggers a transfer request. This one is held per SKU and compared against total available across every warehouse in scope.\n","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Updated SKU reorder level","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/material-requirements":{"get":{"tags":["Inventory"],"summary":"The replenishment worklist (FR-04-016), in two halves.","description":"`need_type=purchase` (default) lists SKUs whose total available across every warehouse in scope, netted against `incoming` from open purchase orders, is at or below the SKU's `global_reorder_level` — these must be bought and feed FR-07-002's requisition. `need_type=transfer` lists warehouses below their own `reorder_level`, netted against what is already in transit to them, where the company as a whole is NOT short — these only need stock moving and feed a transfer request. A SKU short company-wide never appears in the transfer half: moving stock cannot fix it.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"need_type","schema":{"type":"string","enum":["purchase","transfer"]},"description":"Which shortfall to list"},{"in":"query","name":"warehouse_id","schema":{"type":"string"},"description":"Limit the transfer half to one warehouse (the purchase half is company-wide by definition)"}],"responses":{"200":{"description":"Material requirements","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/reconcile":{"post":{"tags":["Inventory"],"summary":"On-demand reconciliation of stored balances against the movement ledger (FR-04-019). Never auto-corrects — raises an exception for a human to resolve.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Reconciliation result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/exceptions":{"get":{"tags":["Inventory"],"summary":"Reconciliation and channel-drift exceptions awaiting resolution (FR-04-018/019)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"source","schema":{"type":"string","enum":["ledger","channel"]}},{"in":"query","name":"warehouse_id","schema":{"type":"string"}},{"in":"query","name":"include_resolved","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Exception list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/inventory/exceptions/{id}/resolve":{"post":{"tags":["Inventory"],"summary":"Mark an inventory exception resolved. Never changes a balance — the human corrects stock through the normal movement path first.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Resolved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/channel-access-scopes":{"get":{"tags":["Inventory"],"summary":"What the channel's stored credential is actually permitted to do, per connector capability. Read-only preflight.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"sales_channel_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Scope report","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/channel-locations":{"get":{"tags":["Inventory"],"summary":"Warehouse-to-channel-location mappings for one sales channel","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"sales_channel_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"post":{"tags":["Inventory"],"summary":"Map one channel stocking location onto a SKMEI warehouse","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Created mapping","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/channel-locations/available":{"get":{"tags":["Inventory"],"summary":"The channel's own live stocking locations, offered for mapping. Read-only channel call.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"sales_channel_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Channel location list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/channel-locations/{id}":{"put":{"tags":["Inventory"],"summary":"Re-point a channel-location mapping at a different warehouse, or make it the channel default","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated mapping","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}},"delete":{"tags":["Inventory"],"summary":"Remove a channel-location mapping (soft delete)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/channel-sync":{"post":{"tags":["Inventory"],"summary":"Pull a channel's own stock position and compare it to the ERP's (FR-04-018 inbound). `report` mode changes no balance.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Sync summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inventory/channel-publish":{"post":{"tags":["Inventory"],"summary":"Republish ERP-computed availability to the channel (FR-04-018 outbound). Returns the plan only unless `confirm` is true.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Publish plan or result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/inspections":{"get":{"tags":["Quality"],"summary":"List inspections — the inspector's queue (unfinished first, oldest first)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"},"description":"Inspection, GRN or RMA number"},{"in":"query","name":"status","schema":{"type":"string","enum":["PENDING","IN_PROGRESS","COMPLETED"]}},{"in":"query","name":"outcome","schema":{"type":"string","enum":["passed","partial","rejected"]}},{"in":"query","name":"source_type","schema":{"type":"string","enum":["goods_receipt","sales_return"]}},{"in":"query","name":"warehouse_id","schema":{"type":"string"}},{"in":"query","name":"goods_receipt_id","schema":{"type":"string"}},{"in":"query","name":"sales_return_id","schema":{"type":"string"}},{"in":"query","name":"from","schema":{"type":"string","format":"date-time"}},{"in":"query","name":"to","schema":{"type":"string","format":"date-time"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"Inspections with line totals","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/inspections/{id}":{"get":{"tags":["Quality"],"summary":"One inspection with its lines, defects, inspectors and photo metadata","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Inspection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"INSPECTION_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Quality"],"summary":"Save draft values on lines not yet submitted (no reconciliation until submit)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["lines"],"properties":{"note":{"type":"string","nullable":true},"lines":{"type":"array","items":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"passed_quantity":{"type":"integer"},"rework_quantity":{"type":"integer"},"rejected_quantity":{"type":"integer"},"quality_defect_id":{"type":"string","nullable":true},"note":{"type":"string","nullable":true},"quantity_presented":{"type":"integer","description":"Sales-return lines only"}}}}}}}}},"responses":{"200":{"description":"Saved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED or INSPECTION_LINE_NOT_DRAFT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inspections/{id}/lines/{lineId}/submit":{"post":{"tags":["Quality"],"summary":"Submit one line — reconciles, moves stock through the goods receipt or return, cannot be edited afterwards","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"lineId","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"The line's values. Omitted, the saved draft values are submitted.","content":{"application/json":{"schema":{"type":"object","properties":{"passed_quantity":{"type":"integer"},"rework_quantity":{"type":"integer"},"rejected_quantity":{"type":"integer"},"quality_defect_id":{"type":"string"},"note":{"type":"string"},"quantity_presented":{"type":"integer"}}}}}},"responses":{"200":{"description":"Submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"INSPECTION_LINE_NOT_RECONCILED (details carry the difference), INSPECTION_DEFECT_REQUIRED, INSPECTION_DEFECT_INVALID, INSPECTION_LINE_ALREADY_SUBMITTED, or a stock refusal","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inspections/{id}/submit":{"post":{"tags":["Quality"],"summary":"Submit every remaining line, all-or-nothing","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lines":{"type":"array","items":{"type":"object","required":["id","passed_quantity","rework_quantity","rejected_quantity"]}}}}}}},"responses":{"200":{"description":"Submitted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Any line's refusal refuses them all","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inspections/{id}/lines/{lineId}/correct":{"post":{"tags":["Quality"],"summary":"Correct a submitted line — a new line supersedes it and only the net difference moves stock","description":"Pieces cannot be moved back out of rejected (or, on a return, out of damaged). A reason is required and is recorded in the audit trail.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"lineId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["passed_quantity","rework_quantity","rejected_quantity","reason"],"properties":{"passed_quantity":{"type":"integer"},"rework_quantity":{"type":"integer"},"rejected_quantity":{"type":"integer"},"quality_defect_id":{"type":"string"},"note":{"type":"string"},"reason":{"type":"string"}}}}}},"responses":{"200":{"description":"Corrected","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"INSPECTION_CORRECTION_FROM_REJECTED, INSPECTION_CORRECTION_FROM_DAMAGED, INSPECTION_CORRECTION_UNCHANGED, or a stock refusal when the passed goods were already used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inspections/{id}/lines/{lineId}/attachments":{"post":{"tags":["Quality"],"summary":"Attach one photo (JPG or PNG, up to 10 MB, 5 per line) to a line not yet submitted","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"lineId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"201":{"description":"Attached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"INSPECTION_PHOTO_INVALID, INSPECTION_PHOTO_TOO_LARGE, INSPECTION_PHOTO_TOO_MANY, INSPECTION_LINE_NOT_DRAFT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inspections/{id}/lines/{lineId}/attachments/{attachmentId}":{"get":{"tags":["Quality"],"summary":"Download a line's photo (scope-checked, never cached)","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"lineId","required":true,"schema":{"type":"string"}},{"in":"path","name":"attachmentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The image bytes"},"404":{"description":"INSPECTION_PHOTO_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"delete":{"tags":["Quality"],"summary":"Remove a photo from a line not yet submitted","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"lineId","required":true,"schema":{"type":"string"}},{"in":"path","name":"attachmentId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Removed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"INSPECTION_LINE_NOT_DRAFT","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inbound-shipments":{"get":{"tags":["Inbound Shipments"],"summary":"List consignments in transit against purchase orders (FR-07-011)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Matches consignment code","BL/AWB":null,"bill of entry or PO number":null},{"in":"query","name":"status","schema":{"type":"string","enum":["SHIPPED","IN_TRANSIT","CUSTOMS_CLEARED","DELIVERED"]}},{"in":"query","name":"mode","schema":{"type":"string"}},{"in":"query","name":"purchase_order_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Shipment list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Inbound Shipments"],"summary":"Record a consignment against a confirmed purchase order (FR-07-011)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["purchase_order_id"],"properties":{"purchase_order_id":{"type":"string"},"mode":{"type":"string","enum":["Sea","Air","Road","Courier"]},"bl_awb_number":{"type":"string"},"port":{"type":"string"},"load_details":{"type":"string"},"etd":{"type":"string","format":"date"},"eta":{"type":"string","format":"date"},"bill_of_entry":{"type":"string"},"duty_amount":{"type":"number","minimum":0},"notes":{"type":"string"},"status":{"type":"string","enum":["SHIPPED","IN_TRANSIT","CUSTOMS_CLEARED","DELIVERED"]}}}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/inbound-shipments/{id}":{"get":{"tags":["Inbound Shipments"],"summary":"One consignment","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Shipment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Inbound Shipments"],"summary":"Update a consignment as it moves — customs, duty, arrival","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/goods-receipt":{"get":{"tags":["Goods Receipt"],"summary":"List goods receipts (FR-07-012)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Matches GRN code or PO number"},{"in":"query","name":"status","schema":{"type":"string","enum":["QC_PENDING","QC_DONE","POSTED_TO_STOCK","CLOSED_SHORT_CLAIMED"]}},{"in":"query","name":"purchase_order_id","schema":{"type":"string"}},{"in":"query","name":"warehouse_id","schema":{"type":"string"}}],"responses":{"200":{"description":"Receipt list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Goods Receipt"],"summary":"Record what arrived against a purchase order (FR-07-012/013)","description":"Received quantity is posted into Inventory's QC-hold bucket, not into sellable stock (R-7.7). The purchase order line's received total and the order's own status update in the same transaction. Each line is also registered in Lots & Batches as a QUARANTINED lot linked to the supplier, order and receipt; its QC decision later makes the lot AVAILABLE or REJECTED. A 409 GRN_LOT_DUPLICATE is returned when a typed lot number repeats across lines or is already registered for the company. One receipt is one warehouse: every line's order destination must be `warehouse_id`, or the receipt is refused with 422 GRN_LINE_WAREHOUSE_MISMATCH.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["purchase_order_id","warehouse_id","lines"],"properties":{"purchase_order_id":{"type":"string"},"warehouse_id":{"type":"string"},"notes":{"type":"string"},"lines":{"type":"array","minItems":1,"items":{"type":"object","required":["purchase_order_line_id","quantity_received"],"properties":{"purchase_order_line_id":{"type":"string"},"quantity_received":{"type":"integer","minimum":1},"damaged_quantity":{"type":"integer","minimum":0,"description":"At most quantity_received"},"remarks":{"type":"string","maxLength":500},"lot_no":{"type":"string","maxLength":60,"description":"Supplier batch number. Blank registers the lot as <GRN code>-<line position>"}}}}}}}}},"responses":{"201":{"description":"Created. Each line carries lot_no and lot_id","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"409":{"description":"Lot number already used","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"422":{"description":"Validation failed, or a line is ordered for a different warehouse (GRN_LINE_WAREHOUSE_MISMATCH)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/goods-receipt/{id}":{"get":{"tags":["Goods Receipt"],"summary":"One goods receipt with its lines, short/excess, QC state and each line's lot_no / lot_id","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Receipt","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/goods-receipt/{id}/lines/{lineId}/qc":{"post":{"tags":["Goods Receipt"],"summary":"Accept or reject a received line, releasing it from QC hold (FR-07-013)","description":"Accepting releases the held quantity into sellable stock; rejecting moves it to the rejected bucket. A decision cannot be reversed — a correction is a new receipt.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"lineId","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decision"],"properties":{"decision":{"type":"string","enum":["ACCEPTED","REJECTED"]},"reason":{"type":"string","description":"Required when rejecting"}}}}}},"responses":{"200":{"description":"Decision recorded","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/pick-queue":{"get":{"tags":["Fulfilment"],"summary":"Orders ready to pick (or blocked, with the reason), by priority then age (FR-03-001, FR-03-005)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Pick queue","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/fulfilment/pack-list":{"get":{"tags":["Fulfilment"],"summary":"One day's pack list — what still needs picking or packing, and what was packed or handed over that day (read-only, derived)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"date","schema":{"type":"string","format":"date"},"description":"Calendar day in the business timezone; defaults to today"},{"in":"query","name":"stage","schema":{"type":"string"},"description":"PICKING, TO_PACK, PACKED, HANDED_OVER — one or comma-separated"},{"in":"query","name":"warehouse_id","schema":{"type":"string"}},{"in":"query","name":"fulfilled_by","schema":{"type":"string","enum":["SHIPROCKET","MARKETPLACE"]}},{"in":"query","name":"search","schema":{"type":"string"},"description":"Order no., channel order no., customer, pick list or SKU"}],"responses":{"200":{"description":"{ date, summary, rows }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Invalid filter","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fulfilment/pick-lists":{"get":{"tags":["Fulfilment"],"summary":"Pick lists in scope","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Pick lists","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Fulfilment"],"summary":"Generate a pick list — the order moves to picking and its reservation becomes an allocation (FR-03-002/003)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Pick list created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"422":{"description":"Order not eligible (held","unpaid":null,"short":null,"already picked)":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fulfilment/pick-lists/{id}":{"get":{"tags":["Fulfilment"],"summary":"Pick list with lines, package and history","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Pick list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found or out of scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fulfilment/pick-lists/{id}/confirm":{"put":{"tags":["Fulfilment"],"summary":"Confirm picked quantity per line — any difference is flagged (FR-03-004)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Confirmed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/packages":{"get":{"tags":["Fulfilment"],"summary":"Packages in scope","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Packages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Fulfilment"],"summary":"Mark an order packed — the packer and time are recorded (FR-03-006)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Packed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/packages/{id}/handover-manual":{"post":{"tags":["Fulfilment"],"summary":"Record an own-store handover with courier and waybill typed in — only when no courier partner is connected (BRD D-02)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Dispatched","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/shipments":{"get":{"tags":["Fulfilment"],"summary":"Every parcel in scope, whoever carries it (SCR-03-02)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Shipments","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Fulfilment"],"summary":"Book a packed own-store parcel with the courier partner and receive courier, waybill and label (FR-03-007)","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Created — status AWAITING_PICKUP","or CREATION_FAILED with the reason":null,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/shipments/{id}":{"get":{"tags":["Fulfilment"],"summary":"Shipment with its carrier scans and history","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Shipment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/shipments/{id}/retry":{"post":{"tags":["Fulfilment"],"summary":"Retry a failed courier booking — an existing partner booking is adopted, never duplicated","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Retried","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/shipments/{id}/handover":{"post":{"tags":["Fulfilment"],"summary":"Mark a parcel handed over — the order is dispatched and physical stock reduced (FR-03-010)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Handed over","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/shipments/{id}/refresh-tracking":{"post":{"tags":["Fulfilment"],"summary":"Pull the latest scans from the courier partner now — never sets a status by hand (FR-03-009)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Refreshed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/fulfilment/shipments/{id}/label":{"get":{"tags":["Fulfilment"],"summary":"The courier label PDF (private copy, D-M3-13)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"PDF","content":{"application/pdf":{}}},"404":{"description":"No label yet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/fulfilment/exceptions":{"get":{"tags":["Fulfilment"],"summary":"Failed, uncollected, stalled and otherwise stuck fulfilment work, oldest first (FR-03-012, PRD §11.6)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Exceptions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/fulfilment/summary":{"get":{"tags":["Fulfilment"],"summary":"Fulfilment headline figures incl. MET-06 and MET-11","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/exceptions":{"get":{"tags":["Exceptions"],"summary":"Everything short, stuck, failed or overdue that the caller may act on (FR-09-005)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"type","schema":{"type":"string"},"description":"Comma-separated exception types"},{"in":"query","name":"severity","schema":{"type":"string","enum":["warning","danger"]}},{"in":"query","name":"company_id","schema":{"type":"string"}},{"in":"query","name":"warehouse_id","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":100}}],"responses":{"200":{"description":"[{ id, type, severity, source_module, title, detail, record_type, record_id, record_code, link, company_id, warehouse_id, since, age_hours, threshold_hours }]","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/exceptions/summary":{"get":{"tags":["Exceptions"],"summary":"Exception counts per type for the notification bell and the panel header (FR-09-005)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"{ total, danger, by_type[{ type, title, count, danger }], generated_at }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/dashboard/channel-summary":{"get":{"tags":["Dashboard"],"summary":"Channel KPI block — revenue, orders, AOV, customers, cancelled/refunded/pending-fulfilment counts, with optional previous-period comparison (FR-01-002)","description":"Reads the synchronized SKMEI database only. Money figures are stated per currency; the response names the primary currency and flags a mixed-currency window.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string","example":"2026-08-01"}},{"in":"query","name":"to","required":true,"schema":{"type":"string","example":"2026-08-31"}},{"in":"query","name":"channel_code","schema":{"type":"string"}},{"in":"query","name":"channel_type","schema":{"type":"string","default":"shopify"}},{"in":"query","name":"compare","schema":{"type":"boolean"}},{"in":"query","name":"order_status","schema":{"type":"string","enum":["unfulfilled","partially_fulfilled","fulfilled","cancelled","refunded","other"]}},{"in":"query","name":"product_variant_id","schema":{"type":"string"}}],"responses":{"200":{"description":"KPI summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"Invalid date range","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/dashboard/sales-trend":{"get":{"tags":["Dashboard"],"summary":"Revenue and order volume per day/week/month bucket, with optional previous-period series (FR-01-002, FR-01-005)","description":"Buckets are cut in the default user timezone. Empty buckets are returned as zero so the series has no gaps.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string"}},{"in":"query","name":"to","required":true,"schema":{"type":"string"}},{"in":"query","name":"granularity","schema":{"type":"string","enum":["day","week","month"],"default":"day"}},{"in":"query","name":"channel_code","schema":{"type":"string"}},{"in":"query","name":"compare","schema":{"type":"boolean"}}],"responses":{"200":{"description":"Trend series","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/dashboard/order-status-summary":{"get":{"tags":["Dashboard"],"summary":"Order counts and revenue by derived status bucket — unfulfilled, partially fulfilled, fulfilled, cancelled, refunded, other (FR-01-002)","description":"Buckets are derived from the channel's own reported financial/fulfilment status, held verbatim on the order. The mapping is documented in dashboard.service.ts.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string"}},{"in":"query","name":"to","required":true,"schema":{"type":"string"}},{"in":"query","name":"channel_code","schema":{"type":"string"}}],"responses":{"200":{"description":"Status breakdown","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/dashboard/product-performance":{"get":{"tags":["Dashboard"],"summary":"Top-selling products for the window plus the channel's mapped/active listing counts (FR-01-002, FR-01-004)","description":"Lines the channel sent that no ERP SKU is mapped to yet are returned with a null product_variant_id and the channel-reported title, so chart revenue reconciles with KPI revenue.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string"}},{"in":"query","name":"to","required":true,"schema":{"type":"string"}},{"in":"query","name":"channel_code","schema":{"type":"string"}}],"responses":{"200":{"description":"Product performance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/dashboard/inventory-summary":{"get":{"tags":["Dashboard"],"summary":"Stock position for the variants this channel lists — on-hand/available units and in-stock, low-stock, out-of-stock, unstocked item counts (FR-01-002)","description":"Point-in-time by nature — stock_levels holds current balances, so the date range does not apply. Availability uses the FR-04-003 formula, identical to the Inventory module.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string"}},{"in":"query","name":"to","required":true,"schema":{"type":"string"}},{"in":"query","name":"channel_code","schema":{"type":"string"}}],"responses":{"200":{"description":"Inventory summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/dashboard/customer-summary":{"get":{"tags":["Dashboard"],"summary":"Active, new and repeat customers for the window, plus new customers per bucket (FR-01-002)","description":"There is no customer master in this system — the synced order carries the identity inline, so the customer key is the order's lowercased email. Orders with no email are excluded from the counts and reported separately.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string"}},{"in":"query","name":"to","required":true,"schema":{"type":"string"}},{"in":"query","name":"granularity","schema":{"type":"string","enum":["day","week","month"]}},{"in":"query","name":"compare","schema":{"type":"boolean"}},{"in":"query","name":"channel_code","schema":{"type":"string"}}],"responses":{"200":{"description":"Customer summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/dashboard/sync-health":{"get":{"tags":["Dashboard"],"summary":"Per-channel sync health — connection status, last started/completed/failed sync, last successful sync, next scheduled sync, per-run record counters, inbox backlog and last error (FR-01-002)","description":"Read from sales_channels and connector_audit_events. Inactive channels are included so a disabled connector is visible rather than absent. Credentials are never returned.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"from","required":true,"schema":{"type":"string"}},{"in":"query","name":"to","required":true,"schema":{"type":"string"}},{"in":"query","name":"channel_code","schema":{"type":"string"}},{"in":"query","name":"channel_type","schema":{"type":"string","default":"shopify"}}],"responses":{"200":{"description":"Sync health rows","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/customers":{"get":{"tags":["Customers"],"summary":"List customers built from orders, scoped to the caller's companies (FR-06-017)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Customer list","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}}},"/v1/customers/summary":{"get":{"tags":["Customers"],"summary":"KPI strip — customers, repeat buyers, new this month, open issues, lifetime value","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Summary","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/customers/{id}":{"get":{"tags":["Customers"],"summary":"A customer with their orders, tickets, warranties and service jobs on one screen (FR-06-017)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Customer","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"Not found or out of scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/customers/backfill":{"post":{"tags":["Customers"],"summary":"Link every order that pre-dates the customer record to its customer (idempotent)","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Orders linked","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/custom-masters/{id}/values/{valueId}/reactivate":{"post":{"tags":["CustomMasters"],"summary":"Reactivate a custom master row value","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}},{"in":"path","name":"valueId","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Value reactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/custom-masters":{"get":{"tags":["CustomMasters"],"summary":"List custom master definitions (paginated + searchable)","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"page","schema":{"type":"integer","minimum":1}},{"in":"query","name":"page_size","schema":{"type":"integer","minimum":1,"maximum":200}},{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"category","schema":{"type":"string"}},{"in":"query","name":"is_active","schema":{"type":"string","enum":[true,false]}}],"responses":{"200":{"description":"Custom masters","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["CustomMasters"],"summary":"Create a custom master definition with fields","security":[{"bearerAuth":[]}],"responses":{"201":{"description":"Custom master created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/custom-masters/{id}":{"put":{"tags":["CustomMasters"],"summary":"Update a custom master definition","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Custom master updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/custom-masters/{id}/deactivate":{"post":{"tags":["CustomMasters"],"summary":"Deactivate a custom master definition","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Custom master deactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/corrective-actions":{"get":{"tags":["Quality"],"summary":"List corrective actions — open first, soonest due first","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"},"description":"Action, defect, inspection, GRN or RMA number"},{"in":"query","name":"status","schema":{"type":"string","enum":["OPEN","CLOSED"]}},{"in":"query","name":"severity","schema":{"type":"string"},"description":"defect-severity master code, read from the defect"},{"in":"query","name":"owner_user_id","schema":{"type":"string"}},{"in":"query","name":"overdue","schema":{"type":"string","enum":["true","false"]},"description":"Open actions past their due date"},{"in":"query","name":"inspection_line_id","schema":{"type":"string"}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"Corrective actions","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Quality"],"summary":"Raise a corrective action against a submitted inspection line's defect","description":"The defect and the rework pieces still held are taken from the line when raised (rejected pieces are final, so not counted). Only one open action per line.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["inspection_line_id"],"properties":{"inspection_line_id":{"type":"string"},"cause":{"type":"string","maxLength":2000,"nullable":true},"action_taken":{"type":"string","maxLength":2000,"nullable":true},"owner_user_id":{"type":"string","nullable":true},"due_date":{"type":"string","format":"date","nullable":true}}}}}},"responses":{"201":{"description":"Raised","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED, CORRECTIVE_ACTION_LINE_NOT_SUBMITTED, CORRECTIVE_ACTION_LINE_HAS_NO_DEFECT or CORRECTIVE_ACTION_OWNER_INVALID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"CORRECTIVE_ACTION_LINE_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"CORRECTIVE_ACTION_ALREADY_OPEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/corrective-actions/assignable-users":{"get":{"tags":["Quality"],"summary":"People who can own a corrective action (hold the M5 defects & corrective action view grant)","description":"Limited to people who can see the given company, or any company the caller can see.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"company_id","schema":{"type":"string"},"description":"The company of the action being assigned"}],"responses":{"200":{"description":"People — each { id, full_name, email }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/corrective-actions/{id}":{"get":{"tags":["Quality"],"summary":"One corrective action with its defect, inspection and source document","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Corrective action","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"CORRECTIVE_ACTION_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}},"put":{"tags":["Quality"],"summary":"Update an open corrective action","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"cause":{"type":"string","maxLength":2000,"nullable":true},"action_taken":{"type":"string","maxLength":2000,"nullable":true},"owner_user_id":{"type":"string","nullable":true},"due_date":{"type":"string","format":"date","nullable":true},"reason":{"type":"string","maxLength":2000,"description":"Recorded in the audit trail"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"CORRECTIVE_ACTION_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"CORRECTIVE_ACTION_CLOSED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/corrective-actions/{id}/status":{"put":{"tags":["Quality"],"summary":"Close a corrective action","description":"Needs a closing note and a recorded action taken (sent here if not recorded before). On a goods-receipt action with rework still held (`rework_to_settle` > 0), `fixed_quantity` and `failed_quantity` must add up to it — fixed pieces are released to available stock, failed ones rejected. Closed actions cannot be reopened or edited.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status","closing_note"],"properties":{"status":{"type":"string","enum":["CLOSED"]},"closing_note":{"type":"string","maxLength":2000},"action_taken":{"type":"string","maxLength":2000,"nullable":true},"fixed_quantity":{"type":"integer","minimum":0,"nullable":true,"description":"Rework pieces made good → available stock"},"failed_quantity":{"type":"integer","minimum":0,"nullable":true,"description":"Rework pieces not made good → rejected"}}}}}},"responses":{"200":{"description":"Closed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED, CORRECTIVE_ACTION_ACTION_REQUIRED, CORRECTIVE_ACTION_SPLIT_MISMATCH or CORRECTIVE_ACTION_NOTHING_TO_SETTLE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"CORRECTIVE_ACTION_CLOSED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/login":{"post":{"tags":["Auth"],"summary":"Authenticate a user and receive a JWT","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email"},"password":{"type":"string"}}}}}},"responses":{"200":{"description":"Login successful","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Invalid credentials","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/me":{"get":{"tags":["Auth"],"summary":"Get the currently authenticated user","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Current user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"401":{"description":"Unauthenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/auth/change-password":{"post":{"tags":["Auth"],"summary":"Set a new password","description":"`current_password` is REQUIRED for a voluntary change and OPTIONAL when the account is flagged `must_change_password` — on that path the caller has just authenticated with the temporary password and carries the JWT proving it, so it is not asked for again. Supplying a wrong one on the forced path is ignored; omitting it on a voluntary change returns `AUTH_CURRENT_PASSWORD_REQUIRED`.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["new_password"],"properties":{"current_password":{"type":"string","description":"Required unless the account must change its password."},"new_password":{"type":"string","minLength":8,"maxLength":200}}}}}},"responses":{"200":{"description":"Password updated"},"400":{"description":"AUTH_CURRENT_PASSWORD_REQUIRED or AUTH_CURRENT_PASSWORD_INVALID"}}}},"/v1/auth/me/profile":{"get":{"tags":["Auth"],"summary":"Get the signed-in user's employment + personal profile","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Profile returned"}}},"put":{"tags":["Auth"],"summary":"Update the signed-in user's personal profile fields","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Profile updated"}}}},"/v1/auth/sign-out":{"post":{"tags":["Auth"],"summary":"Invalidate the current JWT","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Logged out","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/attendance/calendar-month":{"get":{"tags":["Attendance"],"summary":"Company working-calendar classification for one month","description":"Overlay for the attendance month sheet — which dates are working, weekly off, holiday, or optional holiday. Uses the same work-calendar service as leave and payroll. Requires attendance.read (not holiday.read).\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"company_id","required":true,"schema":{"type":"string"}},{"in":"query","name":"month","required":true,"schema":{"type":"string","example":"2026-09"}}],"responses":{"200":{"description":"Month day classifications"}}}},"/v1/attendance/days/bulk":{"post":{"tags":["Attendance"],"summary":"Mark several dates Present or Absent for one employee","description":"Each date follows the single-day rules (employment window, approved leave, no manual LEAVE). Returns which dates were marked and which failed, and why.\n","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["employee_id","dates","status"],"properties":{"employee_id":{"type":"string"},"dates":{"type":"array","items":{"type":"string","format":"date"},"maxItems":62},"status":{"type":"string","enum":["present","absent"]},"remarks":{"type":"string","maxLength":500}}}}}},"responses":{"200":{"description":"{ marked: string[], failed: [{ date, code, message }] }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}}}}},"/v1/assets-issued/kinds":{"get":{"tags":["Assets Issued"],"summary":"List asset kinds","description":"Without page/page_size every matching kind is returned. Sorted by name.","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive"]}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"Kinds — each { id, name, status, active_item_count, item_count, created_at, updated_at }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Assets Issued"],"summary":"Add an asset kind","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":60,"description":"Unique, case-insensitive"}}}}}},"responses":{"201":{"description":"Kind created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_KIND_NAME_TAKEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/kinds/{id}":{"put":{"tags":["Assets Issued"],"summary":"Rename an asset kind","description":"Assets already recorded keep the kind label they were issued under.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string","maxLength":60}}}}}},"responses":{"200":{"description":"Kind updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"ASSET_KIND_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_KIND_NAME_TAKEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/kinds/{id}/deactivate":{"post":{"tags":["Assets Issued"],"summary":"Deactivate an asset kind","description":"Refused while active items are filed under it (error details carry `active_item_count`).","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Kind deactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"ASSET_KIND_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_KIND_IN_USE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/kinds/{id}/reactivate":{"post":{"tags":["Assets Issued"],"summary":"Reactivate an asset kind","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Kind reactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"ASSET_KIND_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/items":{"get":{"tags":["Assets Issued"],"summary":"List asset items","description":"Without page/page_size every matching item is returned, sorted by kind then name. `issued_count` / `total_count` count assets recorded under the item's name (held by someone / all), within the caller's company scope.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"query","name":"search","schema":{"type":"string"}},{"in":"query","name":"kind_id","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"type":"string","enum":["active","inactive"]}},{"in":"query","name":"page","schema":{"type":"integer"}},{"in":"query","name":"page_size","schema":{"type":"integer","maximum":200}}],"responses":{"200":{"description":"Items — each { id, name, kind_id, kind_name, kind_status, status, issued_count, total_count, created_at, updated_at }","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponseWithPagination"}}}}}},"post":{"tags":["Assets Issued"],"summary":"Add an asset item","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","kind_id"],"properties":{"name":{"type":"string","maxLength":200,"description":"Unique, case-insensitive"},"kind_id":{"type":"string","description":"An active kind"}}}}}},"responses":{"201":{"description":"Item created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED, ASSET_KIND_INACTIVE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"ASSET_KIND_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_ITEM_NAME_TAKEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/items/{id}":{"put":{"tags":["Assets Issued"],"summary":"Rename an asset item or move it to another kind","description":"Moving to another kind requires that kind to be active; keeping the current kind is always allowed. Assets already recorded keep their item and kind labels.\n","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","kind_id"],"properties":{"name":{"type":"string","maxLength":200},"kind_id":{"type":"string"}}}}}},"responses":{"200":{"description":"Item updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"400":{"description":"VALIDATION_FAILED, ASSET_KIND_INACTIVE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"404":{"description":"ASSET_ITEM_NOT_FOUND, ASSET_KIND_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_ITEM_NAME_TAKEN","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/items/{id}/deactivate":{"post":{"tags":["Assets Issued"],"summary":"Deactivate an asset item","description":"Allowed while assets of it are issued — it only stops new issues of the item.","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Item deactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"ASSET_ITEM_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}},"/v1/assets-issued/items/{id}/reactivate":{"post":{"tags":["Assets Issued"],"summary":"Reactivate an asset item","security":[{"bearerAuth":[]}],"parameters":[{"in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Item reactivated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiSuccessResponse"}}}},"404":{"description":"ASSET_ITEM_NOT_FOUND","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}},"409":{"description":"ASSET_ITEM_KIND_INACTIVE","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrorResponse"}}}}}}}}}