# CATMS API Contract (for Web & Mobile clients)

Base URL (local dev): `http://localhost:5000/api`

Full endpoint list, roles and permissions are documented in [`backend/README.md`](backend/README.md).
This file documents the exact JSON shapes returned by the API, since Sequelize serializes
fields in camelCase.

CATMS is a **multi-tenant subscription app**: CA firms ("organizations") register, a platform
`super_admin` approves them and sets a license (seat) count, and each organization's own
`admin` then manages their firm's employees/clients/services/tasks within that seat limit.
Everything below except the `super_admin`-only organization-management endpoints is scoped
to the caller's own organization automatically (via their JWT) - clients never send an
`organizationId` themselves.

## Roles
`super_admin` (platform admin, manages organizations, no organization of their own),
`admin` (organization admin / firm partner), `ca` (Chartered Accountant), `article`,
`accountant`, `cost_accountant`.

## Auth

`POST /auth/login`
```json
// request
{ "email": "admin@catms.local", "password": "Admin@123" }
// response 200
{
  "token": "eyJhbGciOi...",
  "user": {
    "id": 1, "name": "Firm Administrator", "email": "admin@catms.local",
    "phone": null, "role": "admin", "designation": "Partner", "organizationId": 1,
    "isActive": true, "createdAt": "2026-01-01T00:00:00.000Z"
  }
}
```
`organizationId` is `null` for `super_admin` users. Send the token on every subsequent
request: `Authorization: Bearer <token>`.

Login (and every subsequent request, re-checked live) is rejected with **403** if the user's
organization is not `approved`:
```json
{ "message": "Your organization's subscription request is still pending approval. We'll notify you once it's reviewed." }
```
(similar messages for `rejected` and `suspended`). Not applicable to `super_admin`.

## Organizations / subscriptions

`POST /organizations/register` (public, no auth) - a new firm signs up:
```json
// request
{
  "organizationName": "Acme & Co Chartered Accountants",
  "contactName": "Rahul Verma", "contactEmail": "rahul@acmeca.test", "contactPhone": "9998887777",
  "address": "...", "gstin": "...", "pan": "...",
  "requestedLicenseCount": 5,
  "password": "Acme@123"
}
// response 201 - no token; the admin user cannot log in until approved
{
  "message": "Subscription request submitted. You will be able to sign in once your organization is approved.",
  "organization": { "id": 8, "name": "Acme & Co Chartered Accountants", "status": "pending", "requestedLicenseCount": 5, "licenseCount": null, ... }
}
```

`POST /organizations/onboard` (`super_admin` only) - platform admin creates an organization that is
**immediately approved**, with an org admin user and a subscription purchase child record
(`subscriptionCount` minimum **2**):
```json
// request
{
  "organizationName": "Acme & Co Chartered Accountants",
  "contactName": "Rahul Verma", "contactEmail": "rahul@acmeca.test", "contactPhone": "9998887777",
  "address": "...", "gstin": "...", "pan": "...", "notes": "...", "planName": "Professional",
  "subscriptionCount": 5,
  "adminName": "Rahul Verma", "adminEmail": "rahul@acmeca.test", "adminPassword": "Acme@123", "adminPhone": "9998887777"
}
// response 201
{
  "message": "Organization onboarded successfully.",
  "organization": {
    "id": 9, "name": "Acme & Co Chartered Accountants", "status": "approved",
    "licenseCount": 5, "usedSeats": 1, "availableSeats": 4,
    "subscriptions": [{ "id": 1, "quantity": 5, "status": "active", "planName": "Professional", ... }]
  },
  "admin": { "id": 12, "name": "Rahul Verma", "email": "rahul@acmeca.test", "role": "admin", ... }
}
```

### Additional license requests

Org admins can request more seats when the license limit is reached. Super admin approves/rejects.

- `POST /organizations/me/license-requests` (org `admin`) `{ quantity, reason? }`
  - Rejected with 409 if a pending request already exists for the organization.
- `GET /organizations/me/license-requests?status=` (org `admin`) - own org's requests
- `GET /organizations/license-requests?status=` (`super_admin`) - all orgs' requests
- `PATCH /organizations/license-requests/:requestId/approve` (`super_admin`) `{ approvedQuantity? }`
  - Increases `licenseCount`, creates an `organization_subscriptions` child record, marks request approved
- `PATCH /organizations/license-requests/:requestId/reject` (`super_admin`) `{ reason? }`

Creating an employee when seats are exhausted returns:
```json
{
  "code": "LICENSE_LIMIT_REACHED",
  "message": "License limit reached (2/2 seats used). Request additional licenses for approval.",
  "usedSeats": 2, "licenseCount": 2, "availableSeats": 0
}
```

`GET /organizations/me` (any organization member) - own org + license usage:
```json
{ "organization": { "id": 1, "name": "Demo CA Firm", "status": "approved", "planName": "Trial", "licenseCount": 10, "usedSeats": 3, "availableSeats": 7, "subscriptions": [...], ... } }
```

`super_admin`-only management:
- `GET /organizations?status=pending|approved|rejected|suspended` - list all organizations (includes `subscriptions`, `usedSeats`)
- `GET /organizations/:id` - single org, includes `usedSeats`, `subscriptions`, and primary `admin`
- `POST /organizations/onboard` - see above
- `PATCH /organizations/:id/approve` `{ licenseCount, planName }`
- `PATCH /organizations/:id/reject` `{ reason }`
- `PATCH /organizations/:id/suspend`
- `PATCH /organizations/:id/reactivate`
- `PUT /organizations/:id/license` `{ licenseCount, planName }` - adjust later (rejects if below seats currently in use; increases add a subscription purchase child record for the delta)
- `GET /organizations/:id/members` - list organization users (includes `privilege`: `admin` | `standard`)
- `PATCH /organizations/:id/members/:userId/privilege` `{ privilege: "admin"|"standard", role? }`
  - `admin` sets role to organization admin
  - `standard` demotes to a non-admin role (`ca` | `article` | `accountant` | `cost_accountant`; default `ca`)
  - Cannot demote the last active organization admin

Organization object shape:
```json
{
  "id": 1, "name": "Demo CA Firm", "contactName": "Firm Administrator", "contactEmail": "admin@catms.local",
  "contactPhone": null, "address": null, "gstin": null, "pan": null,
  "status": "approved", "planName": "Trial",
  "requestedLicenseCount": 10, "licenseCount": 10, "rejectionReason": null, "notes": null,
  "approvedBy": 1, "approvedAt": "...", "createdAt": "...", "updatedAt": "...",
  "subscriptions": [
    { "id": 1, "organizationId": 1, "quantity": 10, "planName": "Trial", "status": "active", "purchasedBy": 1, "purchasedAt": "..." }
  ]
}
```

## Employee (User)
```json
{
  "id": 3, "name": "Priya Sharma", "email": "priya@firm.com", "phone": "9999999999",
  "role": "ca", "designation": "Senior CA", "organizationId": 1, "isActive": true, "createdAt": "..."
}
```
`POST /employees` is rejected with 400 if the organization has no free license seats:
```json
{ "message": "License limit reached (10/10 seats used). Contact support to add more licenses." }
```

## Client
```json
{
  "id": 5, "name": "Ramesh Traders", "companyName": "Ramesh Traders Pvt Ltd",
  "email": "contact@rameshtraders.com", "phone": "9876543210",
  "address": "12 MG Road, Chennai", "gstin": "33ABCDE1234F1Z5", "pan": "ABCDE1234F",
  "notes": null, "status": "active", "organizationId": 1, "createdBy": 1,
  "createdAt": "...", "updatedAt": "...",
  "services": [{ "id": 1, "name": "GST Filing", "description": "...", "isActive": true }]
}
```

## Service
```json
{ "id": 1, "name": "GST Filing", "description": "Periodic GST return filing", "organizationId": 1, "isActive": true }
```
Services are scoped per organization (each org has its own catalog).

## Task

A task has execution **assignees** (one or more employees who do the work) and, optionally,
a sequential approval workflow after execution: **Execution → Review → Approval → Invoicing →
Completed**. Each of Review/Approval/Invoicing is skipped automatically if no one was
designated for it (`reviewerId`/`approverId`/`invoiceRaiserId` left blank).

```json
{
  "id": 42, "title": "File Q1 GST Return", "description": "GSTR-3B for Apr-Jun",
  "organizationId": 1, "clientId": 5, "serviceId": 1, "assignedBy": 3, "createdBy": 3,
  "reviewerId": 4, "approverId": 3, "invoiceRaiserId": 6,
  "reviewedBy": null, "reviewedAt": null, "approvedBy": null, "approvedAt": null,
  "invoicedBy": null, "invoicedAt": null, "invoiceNumber": null,
  "priority": "high", "status": "in_progress",
  "startDate": "2026-07-01", "dueDate": "2026-07-20", "completedAt": null,
  "createdAt": "...", "updatedAt": "...",
  "client": { "id": 5, "name": "Ramesh Traders", "companyName": "Ramesh Traders Pvt Ltd" },
  "service": { "id": 1, "name": "GST Filing" },
  "assignees": [
    { "id": 7, "name": "Anil Kumar", "role": "accountant" },
    { "id": 9, "name": "Divya Rao", "role": "article" }
  ],
  "assigner": { "id": 3, "name": "Priya Sharma", "role": "ca" },
  "creator": { "id": 3, "name": "Priya Sharma", "role": "ca" },
  "reviewer": { "id": 4, "name": "Karthik Iyer", "role": "ca" },
  "approver": { "id": 3, "name": "Priya Sharma", "role": "ca" },
  "invoiceRaiser": { "id": 6, "name": "Meena Pillai", "role": "accountant" },
  "reviewedByUser": null, "approvedByUser": null, "invoicedByUser": null,
  "comments": [
    {
      "id": 100, "taskId": 42, "userId": 7, "comment": "Awaiting client documents",
      "createdAt": "...", "author": { "id": 7, "name": "Anil Kumar", "role": "accountant" }
    }
  ],
  "history": [
    {
      "id": 200, "taskId": 42, "changedBy": 3, "oldStatus": null, "newStatus": "pending",
      "createdAt": "...", "changer": { "id": 3, "name": "Priya Sharma", "role": "ca" }
    }
  ]
}
```
`comments` and `history` are only included on `GET /tasks/:id`, not on the list endpoint.

**`status` values (in workflow order):** `pending`, `in_progress`, `on_hold`, `review`,
`pending_approval`, `pending_invoice`, `completed`, `cancelled`.
**`priority` values:** `low`, `medium`, `high`, `urgent`.

### Creating a task with (optionally) a full workflow
```json
// POST /tasks
{
  "title": "File Q1 GST Return", "description": "...",
  "clientId": 5, "serviceId": 1,
  "assigneeIds": [7, 9],
  "reviewerId": 4, "approverId": 3, "invoiceRaiserId": 6,
  "priority": "high", "startDate": "2026-07-01", "dueDate": "2026-07-20"
}
```
`assigneeIds` is required (non-empty array). `reviewerId`/`approverId`/`invoiceRaiserId` are
each optional (omit or send `null` to skip that stage entirely for this task) - all must be
active employees in the same organization if provided. The same 3 fields can be changed later
via `PUT /tasks/:id`.

### Advancing / rejecting a task's stage
`PATCH /tasks/:id/status` - **who can call this depends on the task's current stage**:

| Current status | Who may act | 
|---|---|
| `pending` / `in_progress` / `on_hold` | any of the task's `assignees` |
| `review` | the task's `reviewerId` |
| `pending_approval` | the task's `approverId` |
| `pending_invoice` | the task's `invoiceRaiserId` |

`admin` always has full override (any transition, including reopening a closed task).
Holding the `ca` role does **not** by itself grant access to a stage - only `admin` overrides.

- To advance: `{ "status": "completed" }` - means "I'm done with my part". The server
  automatically routes the task to whichever stage comes next (skipping any stage nobody was
  designated for), stamping `reviewedBy`/`reviewedAt`, `approvedBy`/`approvedAt`, or
  `invoicedBy`/`invoicedAt` as appropriate. When the invoice raiser completes their stage,
  you may also pass `"invoiceNumber": "INV-2026-001"`.
- To send back for rework: `{ "status": "in_progress", "comment": "..." }` from `review` /
  `pending_approval` / `pending_invoice` - **`comment` is required** and is automatically
  posted as a follow-up note (e.g. "Sent back at review stage: ...").
- `{ "status": "cancelled" }` - `admin`/`ca` only, from any stage.
- `{ "status": "pending" }` / `{ "status": "on_hold" }` - only during the execution stage.

### Reassigning execution assignees
`PATCH /tasks/:id/reassign` body: `{ assigneeIds: [7, 9] }` - replaces the *entire* set of
execution assignees (not additive). Does not affect reviewer/approver/invoice raiser.

### Filtering
`GET /tasks?assignedTo=<userId>` filters to tasks where that employee is one of the execution
assignees (not reviewer/approver/invoice raiser).

## Dashboard stats — `GET /dashboard/stats`
```json
{
  "totalTasks": 34,
  "byStatus": {
    "pending": 5, "in_progress": 10, "on_hold": 2,
    "review": 3, "pending_approval": 2, "pending_invoice": 1,
    "completed": 9, "cancelled": 2
  },
  "overdue": 4,
  "totalClients": 20,
  "activeClients": 18,
  "totalEmployees": 9,
  "licenseCount": 10,
  "availableSeats": 1,
  "openTasksByEmployee": [
    { "employee": { "id": 7, "name": "Anil Kumar", "role": "accountant" }, "openTasks": 6 }
  ]
}
```
`totalClients`, `activeClients`, `totalEmployees`, `licenseCount`, `availableSeats` and
`openTasksByEmployee` are only present for `admin`/`ca` users. For non-privileged users,
`byStatus`/`totalTasks`/`overdue` cover tasks where they're an assignee, reviewer, approver,
or invoice raiser.

## Error shape
All errors are `{ "message": "..." }` with an appropriate HTTP status code (400/401/403/404/409/500).

## Branding requirements (both Web and Mobile)
- App name: **CATMS - CA Task Management System**
- Header: company logo (placeholder acceptable for now), top-level navigation menu, the
  logged-in user's name, and a logout icon/button.
- Footer: copyright message for **Surren Technologies Pvt. Ltd**, e.g.
  `© {year} Surren Technologies Pvt. Ltd. All rights reserved.`
