# 11 — API & Integration (with Sample Responses)

**Domain:** API & Integration  
**Purpose:** API-contract-first parallelization. Frontend SOFT-starts from these locks.  
**Envelope source of truth:** `App\Http\Traits\ApiResponseTrait`

> **Important:** Paths below are **proposed contracts** for Phase 1 (to be locked in `PMS-API-*` tasks).  
> They follow existing Project module style: `/api/v1/project/...` + `Authorization: Bearer` + `X-Company-Id`.  
> Implementors may adjust path names slightly when coding, but **must keep the JSON shapes** unless the team re-locks the contract.

---

## PMS-API-0 — Root envelope (sample)

### Required Headers (All Requests)

```http
Authorization: Bearer <token>
X-Company-Id: <company_id>
Accept: application/json
Content-Type: application/json
```

### Business Rules & Permissions
- **Permissions:** Use the `project.*` prefix for all permissions (e.g., `project.projects-view`, `project.projects-manage`).
- **Standard HTTP Codes:** `200` OK, `201` Created, `401` Unauthenticated, `403` Forbidden / Missing Permission, `404` Not Found (also used for IDOR protection), `422` Unprocessable Entity, `409` State Conflict.

### Success (single resource)

```http
HTTP/1.1 200 OK
Content-Type: application/json
```

```json
{
  "success": true,
  "message": "Operation successful",
  "data": {}
}
```

### Success (created)

```http
HTTP/1.1 201 Created
```

```json
{
  "success": true,
  "message": "Created successfully",
  "data": {
    "id": 1
  }
}
```

### Success (paginated list)

```json
{
  "success": true,
  "message": "Operation successful",
  "data": [],
  "meta": {
    "current_page": 1,
    "per_page": 15,
    "total": 0,
    "last_page": 1
  },
  "links": {
    "first": "https://api.example/api/v1/project/...?page=1",
    "last": "https://api.example/api/v1/project/...?page=1",
    "prev": null,
    "next": null
  }
}
```

### Validation error

```http
HTTP/1.1 422 Unprocessable Entity
```

```json
{
  "success": false,
  "message": "Validation failed.",
  "errors": {
    "name": ["The name field is required."]
  }
}
```

### Unauthorized / Forbidden (401 / 403)

```json
{
  "success": false,
  "message": "Unauthenticated.",
  "errors": {}
}
```

```json
{
  "success": false,
  "message": "This action is unauthorized.",
  "errors": {}
}
```

### Not Found / IDOR (404)

```http
HTTP/1.1 404 Not Found
```

```json
{
  "success": false,
  "message": "Resource not found.",
  "errors": {}
}
```

### State Conflict (409)

```http
HTTP/1.1 409 Conflict
```

```json
{
  "success": false,
  "message": "Cannot update resource due to state conflict (e.g., already completed).",
  "errors": {}
}
```

---

## Domain contract index

| Task ID | Domain | Owner | Unlocks FE |
| --- | --- | --- | --- |
| PMS-API-1.1 | Project Types | Sharif | 1.1-FE |
| PMS-API-1.2 | Service Master | Sharif | 1.2-FE |
| PMS-API-1.3 | Divisions/Roles | Sharif | 1.3-FE |
| PMS-API-1.4 | Commission Rules Shell | Sharif | 1.4-FE |
| PMS-API-2.1 | Projects | Ashraful | 2.1-FE |
| PMS-API-2.2 | Project Service Links | Ashraful | 2.2-FE |
| PMS-API-2.3 | Contracts | Ashraful | 2.3-FE |
| PMS-API-2.5 | Commission Share | Ashraful | 2.5-FE |
| PMS-API-3.1 | Teams & Membership | Munna | 3.1-FE |
| PMS-API-3.3 | Assignment & Act-As | Munna | 3.3-FE |

---

## PMS-API-1.1 — Project Types

**Base:** `/api/v1/project/types`  
**Permission:** `project.types-view|create|update|delete`

### POST create — request

```json
{
  "name": "Retainer Project",
  "code": "RETAINER",
  "description": "Monthly retainer engagement",
  "status": "active"
}
```

### POST create — response `201`

```json
{
  "success": true,
  "message": "Project type created successfully",
  "data": {
    "id": 1,
    "company_id": 1,
    "name": "Retainer Project",
    "code": "RETAINER",
    "description": "Monthly retainer engagement",
    "status": "active",
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-14T10:00:00+00:00"
  }
}
```

### GET index — response `200` (paginated `data[]` same object shape)

### GET active — response `200` (dropdown)

```json
{
  "success": true,
  "message": "Active project types fetched successfully",
  "data": [
    { "id": 1, "name": "Retainer Project", "code": "RETAINER" }
  ]
}
```

---

## PMS-API-1.2 — Service Master

**Base:** `/api/v1/project/pms_services`  
**Permissions:**  
- **View:** `project.pms-services-view` (or `project.projects-manage` / `project.projects-view`)
- **Create:** `project.pms-services-create` (or `project.projects-manage`)
- **Update:** `project.pms-services-update` (or `project.projects-manage`)
- **Delete:** `project.pms-services-delete` (or `project.projects-manage`)

### Common Request Headers
All API calls require:
- `Authorization: Bearer <token>`
- `X-Company-Id: <company_id>`
- `Accept: application/json`
- `Content-Type: application/json` (for POST/PUT)

> [!NOTE]
> `code` ব্যবহারকারী বা ক্লায়েন্ট থেকে রিকোয়েস্ট পেলোডে পাঠানো যাবে না। ব্যাকএন্ডে স্বয়ংক্রিয়ভাবে সার্ভিসের নামের স্লাগ এবং আইডি প্যাড করে ইউনিক কোড জেনারেট হয়ে স্টোর হবে এবং রেসপন্সে রিটার্ন করবে (e.g. `DIGITAL-MARKETING-0010`)।

### Standard Response Envelope
```json
{
  "success": true,
  "message": "Services retrieved successfully.",
  "data": { ... }
}
```

---

### 1. GET list (Paginated & Filtered)

`GET /api/v1/project/pms_services`

**Query Parameters:**
- `page` (integer, optional): Page number (e.g. `1`)
- `per_page` (integer, optional): Items per page (default: `15`)
- `status` (string, optional): Filter by `active` or `inactive`
- `search` (string, optional): Search by service name

**Response `200`:**
```json
{
  "success": true,
  "message": "Services retrieved successfully.",
  "data": [
    {
      "id": 10,
      "company_id": 1,
      "name": "Digital Marketing",
      "code": "DIGITAL-MARKETING-0010",
      "description": "Custom digital marketing and SEO services",
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "from": 1,
    "last_page": 1,
    "per_page": 15,
    "to": 1,
    "total": 1
  }
}
```

### 2. GET active (Dropdown List)

`GET /api/v1/project/pms_services/active`

**Response `200`:**
```json
{
  "success": true,
  "message": "Active services retrieved successfully.",
  "data": [
    {
      "id": 9,
      "company_id": 1,
      "name": "SEO",
      "code": "SEO-0009",
      "description": "Search engine optimization",
      "status": "active",
      "created_at": "2026-09-14T09:00:00.000000Z",
      "updated_at": "2026-09-14T09:00:00.000000Z"
    },
    {
      "id": 10,
      "company_id": 1,
      "name": "Digital Marketing",
      "code": "DIGITAL-MARKETING-0010",
      "description": "Custom digital marketing and SEO services",
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    }
  ]
}
```

### 3. GET show

`GET /api/v1/project/pms_services/{id}`

**Response `200`:**
```json
{
  "success": true,
  "message": "Service retrieved successfully.",
  "data": {
    "id": 10,
    "company_id": 1,
    "name": "Digital Marketing",
    "code": "DIGITAL-MARKETING-0010",
    "description": "Custom digital marketing and SEO services",
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:00:00.000000Z"
  }
}
```

### 4. POST create

`POST /api/v1/project/pms_services`

**Request Body:**
```json
{
  "name": "Digital Marketing",
  "description": "Custom digital marketing and SEO services",
  "status": "active"
}
```

**Response `201`:**
```json
{
  "success": true,
  "message": "Service created successfully.",
  "data": {
    "id": 10,
    "company_id": 1,
    "name": "Digital Marketing",
    "code": "DIGITAL-MARKETING-0010",
    "description": "Custom digital marketing and SEO services",
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:00:00.000000Z"
  }
}
```

### 5. PUT update

`PUT /api/v1/project/pms_services/{id}`

**Request Body:**
```json
{
  "name": "Digital Marketing & Growth",
  "description": "Updated description",
  "status": "active"
}
```

**Response `200`:**
```json
{
  "success": true,
  "message": "Service updated successfully.",
  "data": {
    "id": 10,
    "company_id": 1,
    "name": "Digital Marketing & Growth",
    "code": "DIGITAL-MARKETING-0010",
    "description": "Updated description",
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:30:00.000000Z"
  }
}
```

### 6. DELETE destroy

`DELETE /api/v1/project/pms_services/{id}`

**Response `200`:**
```json
{
  "success": true,
  "message": "Service deleted successfully.",
  "data": null
}
```

### 7. Errors & Validation

#### Validation Error (`422 Unprocessable Entity`)
```json
{
  "message": "The name has already been taken.",
  "errors": {
    "name": [
      "The name has already been taken."
    ]
  }
}
```

#### Forbidden Error (`403 Forbidden`)
```json
{
  "success": false,
  "message": "User does not have the right permissions."
}
```

#### Not Found Error (`404 Not Found`)
```json
{
  "success": false,
  "message": "Service not found."
}
```

---

## PMS-API-1.3 — Team Divisions & Roles

**Domain:** 04. Team Management / Configuration  
**Task ID:** PMS-API-1.3  
**Assignee:** Sharif · Lane C  
**Depends on:** PMS-API-0  
**Unlocks FE:** PMS-1.3-FE  
**Related Tables / Schema:** `team_divisions`, `designations` (reuse), `pms_contract_role_maps`  

### Headers (Required on all requests)

```http
Authorization: Bearer <token>
X-Company-Id: <company_id>
Accept: application/json
Content-Type: application/json
```

### Permissions

| Resource | Operation | Required Permissions (any of) |
| --- | --- | --- |
| `team-divisions` | List / Show | `project.team-divisions-view`, `project.assignments-view`, `project.projects-manage`, `project.projects-view` |
| `team-divisions` | Create / Update / Delete | `project.team-divisions-manage`, `project.assignments-manage`, `project.projects-manage` |
| `contract-roles` | List / Show | `project.contract-roles-view`, `project.assignments-view`, `project.projects-manage`, `project.projects-view` |
| `contract-roles` | Create / Update / Delete | `project.contract-roles-manage`, `project.assignments-manage`, `project.projects-manage` |

---

### Endpoints Overview

- `GET /api/v1/project/team-divisions` — List team divisions (unpaginated list / dropdown reference; paginated if `page` or `per_page` query params are passed)
- `GET /api/v1/project/team-divisions/{id}` — Show single team division
- `POST /api/v1/project/team-divisions` — Create team division
- `PUT /api/v1/project/team-divisions/{id}` — Update team division
- `DELETE /api/v1/project/team-divisions/{id}` — Delete team division
- `GET /api/v1/project/contract-roles` — List contract roles (unpaginated list / role reference; paginated if `page` or `per_page` query params are passed)
- `GET /api/v1/project/contract-roles/{id}` — Show single contract role mapping
- `POST /api/v1/project/contract-roles` — Map designation to division as contract role
- `PUT /api/v1/project/contract-roles/{id}` — Update contract role mapping
- `DELETE /api/v1/project/contract-roles/{id}` — Delete contract role mapping

---

### 1. Team Divisions API

#### GET `/api/v1/project/team-divisions` — List Divisions

**Query Parameters:**
- `status` *(optional, string)*: Filter by `active` or `inactive`.
- `code` *(optional, string)*: Filter by division code (`BD`, `PD`).
- `search` *(optional, string)*: Search by name or code.
- `page` *(optional, integer)*: If provided, triggers paginated response.
- `per_page` *(optional, integer, default: 15)*: Items per page when paginated.

**Response `200 OK` (Default List / Dropdown):**

```json
{
  "success": true,
  "message": "Team divisions fetched successfully",
  "data": [
    {
      "id": 1,
      "company_id": 1,
      "code": "BD",
      "name": "Business Development",
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    },
    {
      "id": 2,
      "company_id": 1,
      "code": "PD",
      "name": "Product Development",
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    }
  ]
}
```

**Response `200 OK` (Paginated when `?page=1&per_page=15`):**

```json
{
  "success": true,
  "message": "Team divisions fetched successfully",
  "data": [
    {
      "id": 1,
      "company_id": 1,
      "code": "BD",
      "name": "Business Development",
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    },
    {
      "id": 2,
      "company_id": 1,
      "code": "PD",
      "name": "Product Development",
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 15,
    "total": 2,
    "last_page": 1
  },
  "links": {
    "first": "https://api.example/api/v1/project/team-divisions?page=1",
    "last": "https://api.example/api/v1/project/team-divisions?page=1",
    "prev": null,
    "next": null
  }
}
```

#### GET `/api/v1/project/team-divisions/{id}` — Show Division

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Team division retrieved successfully.",
  "data": {
    "id": 1,
    "company_id": 1,
    "code": "BD",
    "name": "Business Development",
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:00:00.000000Z"
  }
}
```

#### POST `/api/v1/project/team-divisions` — Create Division

**Request Body:**

```json
{
  "code": "BD",
  "name": "Business Development",
  "status": "active"
}
```

| Field | Type | Validation Rules | Description |
| --- | --- | --- | --- |
| `code` | string | `required`, `in:BD,PD`, `unique:team_divisions,code,company_id` | Division code enum |
| `name` | string | `required`, `string`, `max:100` | Division display name |
| `status` | string | `sometimes`, `in:active,inactive` | Defaults to `active` |

**Response `201 Created`:**

```json
{
  "success": true,
  "message": "Team division created successfully.",
  "data": {
    "id": 1,
    "company_id": 1,
    "code": "BD",
    "name": "Business Development",
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:00:00.000000Z"
  }
}
```

#### PUT `/api/v1/project/team-divisions/{id}` — Update Division

**Request Body:**

```json
{
  "name": "Business Development & Partnerships",
  "status": "active"
}
```

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Team division updated successfully.",
  "data": {
    "id": 1,
    "company_id": 1,
    "code": "BD",
    "name": "Business Development & Partnerships",
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:15:00.000000Z"
  }
}
```

#### DELETE `/api/v1/project/team-divisions/{id}` — Delete Division

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Team division deleted successfully.",
  "data": null
}
```

---

### 2. Contract Roles API

Contract roles bridge company designations (`designations` table) with PMS divisions (`BD`/`PD`) via `pms_contract_role_maps`. PMS does NOT reinvent designations.

#### GET `/api/v1/project/contract-roles` — List Contract Roles

**Query Parameters:**
- `status` *(optional, string)*: Filter by `active` or `inactive`.
- `division_code` *(optional, string)*: Filter by `BD` or `PD`.
- `is_act_as_allowed` *(optional, boolean)*: Filter by whether act-as delegation is permitted.
- `search` *(optional, string)*: Search by role name or label.
- `page` *(optional, integer)*: Triggers paginated response if specified.
- `per_page` *(optional, integer, default: 15)*: Items per page.

**Response `200 OK` (Default List / Reference):**

```json
{
  "success": true,
  "message": "Contract roles fetched successfully",
  "data": [
    {
      "id": 1,
      "company_id": 1,
      "designation_id": 101,
      "name": "Sales Person",
      "label": null,
      "division_code": "BD",
      "is_act_as_allowed": true,
      "sort_order": 1,
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    },
    {
      "id": 2,
      "company_id": 1,
      "designation_id": 102,
      "name": "Closer",
      "label": null,
      "division_code": "BD",
      "is_act_as_allowed": true,
      "sort_order": 2,
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    },
    {
      "id": 3,
      "company_id": 1,
      "designation_id": 201,
      "name": "PM",
      "label": "Member of Project",
      "division_code": "PD",
      "is_act_as_allowed": true,
      "sort_order": 1,
      "status": "active",
      "created_at": "2026-09-14T10:00:00.000000Z",
      "updated_at": "2026-09-14T10:00:00.000000Z"
    }
  ]
}
```

> **CRITICAL BUSINESS RULE:**  
> **PM** in `name`/`label` means **Member of Project** (delivery contributor / execution team member), **NOT Project Manager**. All docs, frontend dropdown helper text, tooltips, and badges must present `PM` as `Member of Project`.

#### GET `/api/v1/project/contract-roles/{id}` — Show Contract Role

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Contract role retrieved successfully.",
  "data": {
    "id": 3,
    "company_id": 1,
    "designation_id": 201,
    "name": "PM",
    "label": "Member of Project",
    "division_code": "PD",
    "is_act_as_allowed": true,
    "sort_order": 1,
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:00:00.000000Z"
  }
}
```

#### POST `/api/v1/project/contract-roles` — Map Designation to Division

**Request Body:**

```json
{
  "designation_id": 201,
  "division_code": "PD",
  "label_override": "Member of Project",
  "sort_order": 1,
  "is_act_as_allowed": true,
  "status": "active"
}
```

| Field | Type | Validation Rules | Description |
| --- | --- | --- | --- |
| `designation_id` | integer | `required`, `exists:designations,id,company_id`, `unique:pms_contract_role_maps,designation_id,company_id,division_code` | Designation belonging to tenant company |
| `division_code` | string | `required`, `in:BD,PD` | Team division code |
| `label_override` | string\|null | `nullable`, `string`, `max:100` | PMS-specific display label (e.g., "Member of Project") |
| `sort_order` | integer | `sometimes`, `integer`, `min:0` | Display ordering (default `0`) |
| `is_act_as_allowed` | boolean | `sometimes`, `boolean` | Allows acting as this role in assignments (default `true`) |
| `status` | string | `sometimes`, `in:active,inactive` | Role mapping status (default `active`) |

**Response `201 Created`:**

```json
{
  "success": true,
  "message": "Contract role mapped successfully.",
  "data": {
    "id": 3,
    "company_id": 1,
    "designation_id": 201,
    "name": "PM",
    "label": "Member of Project",
    "division_code": "PD",
    "is_act_as_allowed": true,
    "sort_order": 1,
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:00:00.000000Z"
  }
}
```

#### PUT `/api/v1/project/contract-roles/{id}` — Update Contract Role Mapping

**Request Body:**

```json
{
  "label_override": "Member of Project",
  "is_act_as_allowed": true,
  "sort_order": 1,
  "status": "active"
}
```

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Contract role updated successfully.",
  "data": {
    "id": 3,
    "company_id": 1,
    "designation_id": 201,
    "name": "PM",
    "label": "Member of Project",
    "division_code": "PD",
    "is_act_as_allowed": true,
    "sort_order": 1,
    "status": "active",
    "created_at": "2026-09-14T10:00:00.000000Z",
    "updated_at": "2026-09-14T10:20:00.000000Z"
  }
}
```

#### DELETE `/api/v1/project/contract-roles/{id}` — Delete Contract Role Mapping

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Contract role deleted successfully.",
  "data": null
}
```

---

### 3. Business Rules & Seeding Contract

1. **PM Means Member of Project:**
   - In all PMS Phase 1 documentation, UI labels, tooltips, select options, and API examples, `PM` = **Member of Project** (delivery contributor).
   - It does **not** stand for Project Manager.
2. **No Separate ActAs Master:**
   - Act-As permissions and assignment capabilities are governed directly by the `is_act_as_allowed` boolean on each contract role mapping (`pms_contract_role_maps.is_act_as_allowed`).
   - There is no separate `act_as_masters` or delegate table.
3. **Designations Reuse:**
   - Standard HR designations (`designations` table) are reused. PMS links designations to divisions via `pms_contract_role_maps`.
   - `designation_id` must belong to the active company context (`X-Company-Id`).
4. **Default Seeds (`TeamDivisionAndContractRoleSeeder`):**
   - **Divisions:**
     - `BD` (`Business Development`, `status: active`)
     - `PD` (`Product Development`, `status: active`)
   - **Contract Roles (seeded automatically for active companies):**
     - BD: `Sales Person` (`sort_order: 1`, `is_act_as_allowed: true`)
     - BD: `Closer` (`sort_order: 2`, `is_act_as_allowed: true`)
     - BD: `Manager` (`sort_order: 3`, `is_act_as_allowed: true`)
     - BD: `Regional Manager` (`sort_order: 4`, `is_act_as_allowed: true`)
     - PD: `PM` (`label_override: "Member of Project"`, `sort_order: 1`, `is_act_as_allowed: true`)

---

### 4. Error Responses

#### Validation Error (`422 Unprocessable Entity`)

```json
{
  "success": false,
  "message": "Validation failed.",
  "errors": {
    "code": [
      "The selected code is invalid."
    ],
    "designation_id": [
      "This designation is already mapped to the specified division."
    ]
  }
}
```

#### Unauthorized (`401 Unauthorized`)

```json
{
  "success": false,
  "message": "Unauthenticated.",
  "errors": {}
}
```

#### Forbidden (`403 Forbidden`)

```json
{
  "success": false,
  "message": "This action is unauthorized.",
  "errors": {}
}
```

#### Not Found (`404 Not Found`)

```json
{
  "success": false,
  "message": "Team division not found.",
  "errors": {}
}
```

---

## PMS-API-1.4 — Commission Rules Shell

**Domain:** 08. Commission Integration / Configuration  
**Task ID:** PMS-API-1.4  
**Task Serial:** PMS-API-1.4 — Commission Rules Shell API Contract · Spike  
**Assignee:** Sharif · Lane C  
**Depends on:** PMS-API-0 (Root Envelope)  
**Unlocks FE:** PMS-1.4-FE  
**Related Tables / Schema:** `commission_rules`  

---

### Headers (Required on all requests)

```http
Authorization: Bearer <token>
X-Company-Id: <company_id>
Accept: application/json
Content-Type: application/json
```

---

### Permissions

| Resource | Operation | Required Permissions (any of) |
| --- | --- | --- |
| `commission-rules` | List / Show | `project.commission-rules-view`, `project.commission-rules-manage`, `project.projects-manage`, `project.projects-view` |
| `commission-rules` | Create / Update / Delete | `project.commission-rules-manage`, `project.projects-manage` |

---

### Business Rules & Critical Architectural Constraints

1. **Shell Only — Explicitly Forbid Generate / Calculate in Phase 1:**
   - There is **NO** `POST /api/v1/project/commission-rules/calculate` or `POST /api/v1/project/commission-rules/generate` endpoint.
   - **Rule ≠ Earning Invariant:** Commission rules are static rule configurations/policy templates (`commission_rules` table). They do not compute or distribute earnings directly.
   - Commission calculation and earning generation are handled downstream in Phase 2 / earning batch processing (`commission_earning_records`).
2. **Mutual Exclusivity (Percentage vs. Fixed Amount):**
   - Exactly one of `percentage` or `fixed_amount` must be non-null and set.
   - Both cannot be null (validation error `422`).
   - Both cannot be set simultaneously (validation error `422`).
   - `percentage`: numeric between `0` and `100` (stored as `decimal(8,4)`, returned as formatted string e.g. `"10.0000"`).
   - `fixed_amount`: numeric between `0` and `9999999999999.99` (stored as `decimal(15,2)`, returned as formatted string e.g. `"500.00"`).
3. **Calculation Base & Commission Type Enums:**
   - `commission_type`: `one_time`, `recurring`, `monthly`, `contract_based`, `milestone_based`. (Dropdown options can be referenced via read-only `/api/v1/project/commission-types`).
   - `calculation_base`: `contract_value`, `team_pool`, `bd_pool`, `pd_pool`.
   - `team_division`: `BD`, `PD`, or `null`.
   - `status`: `active`, `inactive` (default: `active`).
4. **Effective Dates:**
   - `effective_to` must be greater than or equal to `effective_from` (`after_or_equal:effective_from`).
5. **Multi-Tenancy & Foreign Key Integrity:**
   - `company_id` is automatically scoped from tenant context (`X-Company-Id`). Clients cannot specify or alter `company_id`.
   - `role_designation_id` (optional) must reference a valid designation belonging to the same tenant company. Referencing another company's designation triggers `422 Unprocessable Entity`.
   - IDOR attempts to access or modify rules belonging to another company yield `404 Not Found`.

---

### Endpoints Overview

| Method | URI | Description |
| --- | --- | --- |
| `GET` | `/api/v1/project/commission-rules` | List commission rules (unpaginated list / dropdown reference; paginated when `page` or `per_page` query params passed) |
| `GET` | `/api/v1/project/commission-rules/{id}` | Show single commission rule |
| `POST` | `/api/v1/project/commission-rules` | Create commission rule |
| `PUT` | `/api/v1/project/commission-rules/{id}` | Update commission rule |
| `DELETE` | `/api/v1/project/commission-rules/{id}` | Delete commission rule |
| `POST` | `/api/v1/project/commission-rules/calculate` | **FORBIDDEN / NOT IMPLEMENTED** in Phase 1 (Yields `404 Not Found`) |

---

### 1. GET `/api/v1/project/commission-rules` — List Rules

Fetches commission rules for the authenticated tenant company. Supports filtering and unpaginated or paginated results.

#### Query Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | No | Filter by status (`active`, `inactive`) |
| `commission_type` | string | No | Filter by type (`one_time`, `recurring`, `monthly`, `contract_based`, `milestone_based`) |
| `calculation_base` | string | No | Filter by calculation base (`contract_value`, `team_pool`, `bd_pool`, `pd_pool`) |
| `team_division` | string | No | Filter by division (`BD`, `PD`) |
| `search` | string | No | Fuzzy search across rule name |
| `page` | integer | No | Triggers paginated response if provided |
| `per_page` | integer | No | Items per page (default: `15`) |

#### Response `200 OK` (Default Unpaginated / Reference List)

```json
{
  "success": true,
  "message": "Commission rules fetched successfully",
  "data": [
    {
      "id": 5,
      "company_id": 1,
      "name": "BD Sales One-Time 10% of BD Pool",
      "commission_type": "one_time",
      "calculation_base": "bd_pool",
      "percentage": "10.0000",
      "fixed_amount": null,
      "frequency": "quarterly",
      "duration_value": 6,
      "duration_type": "months",
      "team_division": "BD",
      "role_designation_id": 101,
      "status": "active",
      "effective_from": "2026-01-01",
      "effective_to": "2026-12-31",
      "created_by": 1,
      "updated_by": null,
      "created_at": "2026-09-14T10:00:00+00:00",
      "updated_at": "2026-09-14T10:00:00+00:00"
    },
    {
      "id": 6,
      "company_id": 1,
      "name": "PD Milestone Fixed 500",
      "commission_type": "milestone_based",
      "calculation_base": "contract_value",
      "percentage": null,
      "fixed_amount": "500.00",
      "frequency": null,
      "duration_value": null,
      "duration_type": null,
      "team_division": "PD",
      "role_designation_id": null,
      "status": "active",
      "effective_from": "2026-02-01",
      "effective_to": null,
      "created_by": 1,
      "updated_by": null,
      "created_at": "2026-09-14T10:15:00+00:00",
      "updated_at": "2026-09-14T10:15:00+00:00"
    }
  ]
}
```

#### Response `200 OK` (Paginated when `?page=1&per_page=15`)

```json
{
  "success": true,
  "message": "Commission rules fetched successfully",
  "data": [
    {
      "id": 5,
      "company_id": 1,
      "name": "BD Sales One-Time 10% of BD Pool",
      "commission_type": "one_time",
      "calculation_base": "bd_pool",
      "percentage": "10.0000",
      "fixed_amount": null,
      "frequency": "quarterly",
      "duration_value": 6,
      "duration_type": "months",
      "team_division": "BD",
      "role_designation_id": 101,
      "status": "active",
      "effective_from": "2026-01-01",
      "effective_to": "2026-12-31",
      "created_by": 1,
      "updated_by": null,
      "created_at": "2026-09-14T10:00:00+00:00",
      "updated_at": "2026-09-14T10:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 15,
    "total": 1,
    "last_page": 1
  },
  "links": {
    "first": "https://api.example/api/v1/project/commission-rules?page=1",
    "last": "https://api.example/api/v1/project/commission-rules?page=1",
    "prev": null,
    "next": null
  }
}
```

---

### 2. GET `/api/v1/project/commission-rules/{id}` — Show Single Rule

Retrieves full details of a specific commission rule, including linked role designation details if available.

#### Response `200 OK`

```json
{
  "success": true,
  "message": "Commission rule retrieved successfully.",
  "data": {
    "id": 5,
    "company_id": 1,
    "name": "BD Sales One-Time 10% of BD Pool",
    "commission_type": "one_time",
    "calculation_base": "bd_pool",
    "percentage": "10.0000",
    "fixed_amount": null,
    "frequency": "quarterly",
    "duration_value": 6,
    "duration_type": "months",
    "team_division": "BD",
    "role_designation_id": 101,
    "role_designation": {
      "id": 101,
      "title": "Business Development Executive"
    },
    "status": "active",
    "effective_from": "2026-01-01",
    "effective_to": "2026-12-31",
    "created_by": 1,
    "updated_by": null,
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-14T10:00:00+00:00"
  }
}
```

---

### 3. POST `/api/v1/project/commission-rules` — Create Rule

Creates a new commission rule. Exactly one of `percentage` or `fixed_amount` must be supplied.

#### Request Validation Rules

| Field | Type | Rules | Description |
| --- | --- | --- | --- |
| `name` | string | `required`, `string`, `max:150` | Descriptive name of rule |
| `commission_type` | string | `required`, `in:one_time,recurring,monthly,contract_based,milestone_based` | Lifecycle / schedule type |
| `calculation_base` | string | `required`, `in:contract_value,team_pool,bd_pool,pd_pool` | Pool / base value |
| `percentage` | numeric | `nullable`, `min:0`, `max:100` | Percentage value (mutually exclusive with `fixed_amount`) |
| `fixed_amount` | numeric | `nullable`, `min:0`, `max:9999999999999.99` | Fixed currency amount (mutually exclusive with `percentage`) |
| `frequency` | string | `nullable`, `string`, `max:30` | Optional frequency (e.g. `quarterly`, `monthly`) |
| `duration_value` | integer | `nullable`, `integer`, `min:1` | Optional duration number |
| `duration_type` | string | `nullable`, `string`, `max:30` | Optional duration unit (e.g. `months`, `days`) |
| `team_division` | string | `nullable`, `in:BD,PD` | Target division code |
| `role_designation_id` | integer | `nullable`, `exists:designations,id` (same company) | Designation reference FK |
| `status` | string | `sometimes`, `in:active,inactive` | Defaults to `active` |
| `effective_from` | date | `nullable`, `date` (`YYYY-MM-DD`) | Effective start date |
| `effective_to` | date | `nullable`, `date`, `after_or_equal:effective_from` | Effective end date |

#### Sample A: Percentage-Based Rule

##### POST Request

```json
{
  "name": "BD Sales One-Time 10% of BD Pool",
  "commission_type": "one_time",
  "calculation_base": "bd_pool",
  "percentage": 10.0,
  "fixed_amount": null,
  "frequency": "quarterly",
  "duration_value": 6,
  "duration_type": "months",
  "team_division": "BD",
  "role_designation_id": 101,
  "status": "active",
  "effective_from": "2026-01-01",
  "effective_to": "2026-12-31"
}
```

##### Response `201 Created`

```json
{
  "success": true,
  "message": "Commission rule created successfully.",
  "data": {
    "id": 5,
    "company_id": 1,
    "name": "BD Sales One-Time 10% of BD Pool",
    "commission_type": "one_time",
    "calculation_base": "bd_pool",
    "percentage": "10.0000",
    "fixed_amount": null,
    "frequency": "quarterly",
    "duration_value": 6,
    "duration_type": "months",
    "team_division": "BD",
    "role_designation_id": 101,
    "status": "active",
    "effective_from": "2026-01-01",
    "effective_to": "2026-12-31",
    "created_by": 1,
    "updated_by": null,
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-14T10:00:00+00:00"
  }
}
```

#### Sample B: Fixed-Amount-Based Rule

##### POST Request

```json
{
  "name": "PD Milestone Fixed 500",
  "commission_type": "milestone_based",
  "calculation_base": "contract_value",
  "percentage": null,
  "fixed_amount": 500.0,
  "frequency": null,
  "duration_value": null,
  "duration_type": null,
  "team_division": "PD",
  "role_designation_id": null,
  "status": "active",
  "effective_from": "2026-02-01",
  "effective_to": null
}
```

##### Response `201 Created`

```json
{
  "success": true,
  "message": "Commission rule created successfully.",
  "data": {
    "id": 6,
    "company_id": 1,
    "name": "PD Milestone Fixed 500",
    "commission_type": "milestone_based",
    "calculation_base": "contract_value",
    "percentage": null,
    "fixed_amount": "500.00",
    "frequency": null,
    "duration_value": null,
    "duration_type": null,
    "team_division": "PD",
    "role_designation_id": null,
    "status": "active",
    "effective_from": "2026-02-01",
    "effective_to": null,
    "created_by": 1,
    "updated_by": null,
    "created_at": "2026-09-14T10:15:00+00:00",
    "updated_at": "2026-09-14T10:15:00+00:00"
  }
}
```

---

### 4. PUT `/api/v1/project/commission-rules/{id}` — Update Rule

Updates an existing commission rule. Supports partial updates. When changing calculation method, explicitly clear the alternate field (e.g. set `percentage: null` when supplying `fixed_amount`).

#### PUT Request

```json
{
  "name": "BD Sales One-Time Updated Fixed Rule",
  "percentage": null,
  "fixed_amount": 250.0,
  "status": "inactive"
}
```

#### Response `200 OK`

```json
{
  "success": true,
  "message": "Commission rule updated successfully.",
  "data": {
    "id": 5,
    "company_id": 1,
    "name": "BD Sales One-Time Updated Fixed Rule",
    "commission_type": "one_time",
    "calculation_base": "bd_pool",
    "percentage": null,
    "fixed_amount": "250.00",
    "frequency": "quarterly",
    "duration_value": 6,
    "duration_type": "months",
    "team_division": "BD",
    "role_designation_id": 101,
    "status": "inactive",
    "effective_from": "2026-01-01",
    "effective_to": "2026-12-31",
    "created_by": 1,
    "updated_by": 1,
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-15T08:30:00+00:00"
  }
}
```

---

### 5. DELETE `/api/v1/project/commission-rules/{id}` — Delete Rule

Deletes a commission rule belonging to the tenant company.

#### Response `200 OK`

```json
{
  "success": true,
  "message": "Commission rule deleted successfully.",
  "data": null
}
```

---

### 6. Forbidden / Out-of-Scope Endpoints (Phase 1 Freeze)

> [!CAUTION]
> **No Generate / Calculate Endpoints in Phase 1:**
> Any call to `/api/v1/project/commission-rules/calculate` or `/api/v1/project/commission-rules/generate` will be rejected (`404 Not Found` or `405 Method Not Allowed`).
> FE client implementations must **not** render or trigger calculation requests against commission rule shell endpoints.

---

### 7. Common Error Responses

#### 400 Bad Request (Missing Company Context)

```json
{
  "success": false,
  "message": "Tenant company context is required.",
  "errors": {}
}
```

#### 401 Unauthorized

```json
{
  "success": false,
  "message": "Unauthenticated.",
  "errors": {}
}
```

#### 403 Forbidden

```json
{
  "success": false,
  "message": "This action is unauthorized.",
  "errors": {}
}
```

#### 404 Not Found (Record Non-Existent or IDOR Mismatch)

```json
{
  "success": false,
  "message": "Commission rule not found.",
  "errors": {}
}
```

#### 422 Unprocessable Entity (Both Percentage & Fixed Amount Set)

```json
{
  "message": "Percentage and fixed_amount cannot both be set. (and 1 more error)",
  "errors": {
    "percentage": [
      "Percentage and fixed_amount cannot both be set."
    ],
    "fixed_amount": [
      "Percentage and fixed_amount cannot both be set."
    ]
  }
}
```

#### 422 Unprocessable Entity (Neither Percentage nor Fixed Amount Set)

```json
{
  "message": "Exactly one of percentage or fixed_amount must be set. (and 1 more error)",
  "errors": {
    "percentage": [
      "Exactly one of percentage or fixed_amount must be set."
    ],
    "fixed_amount": [
      "Exactly one of percentage or fixed_amount must be set."
    ]
  }
}
```

#### 422 Unprocessable Entity (Invalid Dates / `effective_to` < `effective_from`)

```json
{
  "message": "The effective to date must be greater than or equal to the effective from date.",
  "errors": {
    "effective_to": [
      "The effective to date must be greater than or equal to the effective from date."
    ]
  }
}
```

#### 422 Unprocessable Entity (Cross-Company Role Designation Reference)

```json
{
  "message": "The selected role designation id is invalid.",
  "errors": {
    "role_designation_id": [
      "The selected role designation id is invalid."
    ]
  }
}
```

---

## PMS-API-2.1 — Projects

**Base:** `/api/v1/project/projects`  
**Permission:** `project.projects-*`

### POST create — request

```json
{
  "client_id": 55,
  "name": "ABC Digital Growth",
  "project_type_id": 1,
  "project_category_id": 3,
  "description": "Digital growth engagement",
  "start_date": "2026-01-01",
  "expected_end_date": "2026-12-31",
  "status": "draft",
  "owner_user_id": 12
}
```

### POST create — response `201`

```json
{
  "success": true,
  "message": "Project created successfully",
  "data": {
    "id": 100,
    "company_id": 1,
    "client_id": 55,
    "client": { "id": 55, "name": "ABC Corporation" },
    "name": "ABC Digital Growth",
    "project_type_id": 1,
    "project_type": { "id": 1, "name": "Retainer Project" },
    "project_category_id": 3,
    "project_category": { "id": 3, "name": "Digital Marketing" },
    "description": "Digital growth engagement",
    "start_date": "2026-01-01",
    "expected_end_date": "2026-12-31",
    "status": "draft",
    "owner_user_id": 12,
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-14T10:00:00+00:00"
  }
}
```

### GET index query

`?status=active&client_id=55&search=ABC&page=1`

---

## PMS-API-2.2 — Project Service Links

**Base:** `/api/v1/project/projects/{projectId}/services`

### POST attach — request

```json
{
  "service_id": 10,
  "status": "active"
}
```

### POST attach — response `201`

```json
{
  "success": true,
  "message": "Service attached to project successfully",
  "data": {
    "id": 501,
    "project_id": 100,
    "service_id": 10,
    "service": { "id": 10, "name": "Digital Marketing", "code": "DM" },
    "status": "active",
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-14T10:00:00+00:00"
  }
}
```

### Duplicate attach — response `422`

```json
{
  "success": false,
  "message": "Validation failed.",
  "errors": {
    "service_id": ["This service is already attached to the project."]
  }
}
```

---

## PMS-API-2.3 — Contracts

**Base:** `/api/v1/project/contracts`

### POST create — request

```json
{
  "contract_number": "DM-2026-001",
  "project_id": 100,
  "project_service_link_id": 501,
  "start_date": "2026-02-01",
  "end_date": "2026-04-30",
  "contract_value": 100.0,
  "currency": "USD",
  "allocated_hours": 40,
  "billing_type": "retainer",
  "payment_terms": "Net 15",
  "renewal_type": "manual",
  "status": "draft",
  "notes": null
}
```

### POST create — response `201`

```json
{
  "success": true,
  "message": "Contract created successfully",
  "data": {
    "id": 900,
    "company_id": 1,
    "contract_number": "DM-2026-001",
    "project_id": 100,
    "project_service_link_id": 501,
    "client_id": 55,
    "client": { "id": 55, "name": "ABC Corporation" },
    "service": { "id": 10, "name": "Digital Marketing" },
    "start_date": "2026-02-01",
    "end_date": "2026-04-30",
    "contract_value": "100.00",
    "currency": "USD",
    "allocated_hours": "40.00",
    "billing_type": "retainer",
    "payment_terms": "Net 15",
    "renewal_type": "manual",
    "status": "draft",
    "notes": null,
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-14T10:00:00+00:00"
  }
}
```

### Status transition (optional dedicated endpoint)

`POST /api/v1/project/contracts/{id}/status`

```json
{ "status": "active", "note": "Approved by commercial" }
```

---

## PMS-API-2.5 — Commission Share (Team / BD / PD)

**Base:** `/api/v1/project/contracts/{contractId}/commission-shares`

### PUT/POST set — request

```json
{
  "team_share_percent": 30.0,
  "bd_share_percent": 40.0,
  "pd_share_percent": 60.0,
  "effective_from": "2026-02-01"
}
```

### Response `200` / `201` (server computes pools from contract_value)

```json
{
  "success": true,
  "message": "Commission share saved successfully",
  "data": {
    "id": 70,
    "contract_id": 900,
    "calculation_base": "contract_value",
    "contract_value": "100.00",
    "team_share_percent": "30.0000",
    "bd_share_percent": "40.0000",
    "pd_share_percent": "60.0000",
    "team_pool_amount": "30.00",
    "bd_pool_amount": "12.00",
    "pd_pool_amount": "18.00",
    "currency": "USD",
    "effective_from": "2026-02-01",
    "effective_to": null
  }
}
```

### BD+PD ≠ 100 — response `422`

```json
{
  "success": false,
  "message": "Validation failed.",
  "errors": {
    "bd_share_percent": ["BD share percent plus PD share percent must equal 100."]
  }
}
```

---

## PMS-API-3.1 — Service Teams & Permanent Membership

**Bases:**  
- `/api/v1/project/service-teams`  
- `/api/v1/project/service-teams/{teamId}/members`  
- `/api/v1/project/service-team-members` (global list)

### Common Request Headers
All API endpoints require the following headers:
- `Authorization: Bearer <token>`
- `X-Company-Id: <company_uuid>`
- `Accept: application/json`
- `Content-Type: application/json` (for POST/PUT)

### Permission Middleware
- **View:** `permission:project.teams-view|project.projects-manage|project.projects-view`
- **Manage:** `permission:project.teams-manage|project.projects-manage`

### Standard Response Envelope
All successful responses adhere to the standard envelope:
```json
{
  "success": true,
  "message": "Human readable action message.",
  "data": { ... },
  "meta": { ... } // included when paginated
}
```

---

### Service Teams Endpoints

#### 1. GET `/api/v1/project/service-teams` (List Teams)
**Query Parameters:**
- `page` (integer, optional): Page number (e.g. `1`)
- `per_page` (integer, optional): Items per page (default: `15`)
- `service_id` (integer, optional): Filter by `pms_services.id`
- `status` (string, optional): Filter by `active` or `inactive`
- `search` (string, optional): Search by team name

**Response `200`:**
```json
{
  "success": true,
  "message": "Service teams retrieved successfully.",
  "data": [
    {
      "id": 20,
      "company_id": 1,
      "service_id": 10,
      "service": {
        "id": 10,
        "name": "Digital Marketing",
        "code": "DIGITAL-MARKETING-0010",
        "status": "active"
      },
      "name": "Digital Marketing Team",
      "status": "active",
      "created_at": "2026-09-15T10:00:00.000000Z",
      "updated_at": "2026-09-15T10:00:00.000000Z"
    }
  ]
}
```

#### 2. POST `/api/v1/project/service-teams` (Create Team)
**Request Body:**
```json
{
  "service_id": 10,
  "name": "Digital Marketing Team",
  "status": "active"
}
```

**Response `201`:**
```json
{
  "success": true,
  "message": "Service team created successfully.",
  "data": {
    "id": 20,
    "company_id": 1,
    "service_id": 10,
    "service": {
      "id": 10,
      "name": "Digital Marketing",
      "code": "DIGITAL-MARKETING-0010",
      "status": "active"
    },
    "name": "Digital Marketing Team",
    "status": "active",
    "created_at": "2026-09-15T10:00:00.000000Z",
    "updated_at": "2026-09-15T10:00:00.000000Z"
  }
}
```

#### 3. GET `/api/v1/project/service-teams/{id}` (Show Team)
**Response `200`:**
```json
{
  "success": true,
  "message": "Service team retrieved successfully.",
  "data": {
    "id": 20,
    "company_id": 1,
    "service_id": 10,
    "service": {
      "id": 10,
      "name": "Digital Marketing"
    },
    "name": "Digital Marketing Team",
    "status": "active",
    "created_at": "2026-09-15T10:00:00.000000Z",
    "updated_at": "2026-09-15T10:00:00.000000Z"
  }
}
```

#### 4. PUT `/api/v1/project/service-teams/{id}` (Update Team)
**Request Body:**
```json
{
  "name": "Digital Marketing Growth Team",
  "status": "active"
}
```

**Response `200`:**
```json
{
  "success": true,
  "message": "Service team updated successfully.",
  "data": {
    "id": 20,
    "company_id": 1,
    "service_id": 10,
    "service": {
      "id": 10,
      "name": "Digital Marketing"
    },
    "name": "Digital Marketing Growth Team",
    "status": "active",
    "created_at": "2026-09-15T10:00:00.000000Z",
    "updated_at": "2026-09-15T10:15:00.000000Z"
  }
}
```

#### 5. DELETE `/api/v1/project/service-teams/{id}` (Delete Team)
> Blocked if team has active members in `service_team_members` or assignments in `contract_team_assignments` (returns `422`).

**Response `200`:**
```json
{
  "success": true,
  "message": "Service team deleted successfully.",
  "data": null
}
```

---

### Service Team Members Endpoints (Permanent Membership)

#### 1. GET `/api/v1/project/service-teams/{teamId}/members` (List Team Members)
**Query Parameters:**
- `page` (integer, optional): Page number
- `per_page` (integer, optional): Items per page (default: `15`)
- `division` (string, optional): Filter by `BD` or `PD`
- `status` (string, optional): Filter by `active` or `inactive`
- `employee_id` (integer, optional): Filter by `employee_personal_infos.id`
- `search` (string, optional): Search by employee first name, last name, or employee number

**Response `200`:**
```json
{
  "success": true,
  "message": "Team members retrieved successfully.",
  "data": [
    {
      "id": 300,
      "company_id": 1,
      "team_id": 20,
      "team": {
        "id": 20,
        "name": "Digital Marketing Team"
      },
      "employee_id": 77,
      "employee": {
        "id": 77,
        "employee_no": "EMP-001",
        "name": "Rahim Uddin",
        "first_name": "Rahim",
        "last_name": "Uddin"
      },
      "division": "BD",
      "status": "active",
      "joined_at": "2026-01-15",
      "left_at": null,
      "created_at": "2026-09-15T10:00:00.000000Z",
      "updated_at": "2026-09-15T10:00:00.000000Z"
    }
  ]
}
```

#### 2. POST `/api/v1/project/service-teams/{teamId}/members` (Add Member)
**Request Body:**
```json
{
  "employee_id": 77,
  "division": "BD",
  "status": "active",
  "joined_at": "2026-01-15"
}
```

**Response `201`:**
```json
{
  "success": true,
  "message": "Team member added successfully.",
  "data": {
    "id": 300,
    "company_id": 1,
    "team_id": 20,
    "team": {
      "id": 20,
      "name": "Digital Marketing Team"
    },
    "employee_id": 77,
    "employee": {
      "id": 77,
      "employee_no": "EMP-001",
      "name": "Rahim Uddin",
      "first_name": "Rahim",
      "last_name": "Uddin"
    },
    "division": "BD",
    "status": "active",
    "joined_at": "2026-01-15",
    "left_at": null,
    "created_at": "2026-09-15T10:00:00.000000Z",
    "updated_at": "2026-09-15T10:00:00.000000Z"
  }
}
```

#### 3. GET `/api/v1/project/service-teams/{teamId}/members/{id}` (Show Member)
**Response `200`:**
```json
{
  "success": true,
  "message": "Team member retrieved successfully.",
  "data": {
    "id": 300,
    "company_id": 1,
    "team_id": 20,
    "employee_id": 77,
    "employee": {
      "id": 77,
      "name": "Rahim Uddin"
    },
    "division": "BD",
    "status": "active",
    "joined_at": "2026-01-15",
    "left_at": null,
    "created_at": "2026-09-15T10:00:00.000000Z",
    "updated_at": "2026-09-15T10:00:00.000000Z"
  }
}
```

#### 4. PUT `/api/v1/project/service-teams/{teamId}/members/{id}` (Update Member)
**Request Body:**
```json
{
  "division": "PD",
  "status": "inactive",
  "left_at": "2026-03-01"
}
```

**Response `200`:**
```json
{
  "success": true,
  "message": "Team member updated successfully.",
  "data": {
    "id": 300,
    "company_id": 1,
    "team_id": 20,
    "employee_id": 77,
    "division": "PD",
    "status": "inactive",
    "joined_at": "2026-01-15",
    "left_at": "2026-03-01",
    "created_at": "2026-09-15T10:00:00.000000Z",
    "updated_at": "2026-09-15T10:30:00.000000Z"
  }
}
```

#### 5. DELETE `/api/v1/project/service-teams/{teamId}/members/{id}` (Remove Member)
**Response `200`:**
```json
{
  "success": true,
  "message": "Team member removed successfully.",
  "data": null
}
```

---

### Critical Distinction & Business Rules
1. **Permanent Membership ≠ Contract Assignment:**
   - Permanent membership in `service_team_members` tracks an employee's permanent home team (e.g. SEO Team, Web Dev Team).
   - Permanent membership has **no** `designation_id` and **no** `is_act_as`.
   - Contract roles and Act-As assignments are strictly managed under contract assignment endpoints (`/api/v1/project/contracts/{contractId}/members`) in table `contract_members`.
2. **Team ≠ Contract Team:**
   - A Service Team is a permanent company unit under a service.
   - An employee can be a permanent member of a service team without having any contract assignments.
3. **Unique Active Membership:**
   - Only one `active` membership is permitted per `team_id` + `employee_id`. Adding another active membership for the same employee in the same team returns HTTP `422`.
4. **Tenant Isolation:**
   - Both `team_id` and `employee_id` must belong to the tenant company specified in `X-Company-Id`. Cross-company access is blocked (returns `403` or `422`).
5. **Date Integrity:**
   - `left_at` must be null or greater than or equal to `joined_at` (`left_at >= joined_at`).
6. **Division:**
   - `division` must be strictly `BD` (Business Development) or `PD` (Production / Delivery).

---

### Error Responses

#### 401 Unauthenticated
```json
{
  "message": "Unauthenticated."
}
```

#### 403 Forbidden
```json
{
  "success": false,
  "message": "This action is unauthorized."
}
```

#### 422 Unprocessable Entity (Validation Errors)
```json
{
  "message": "Employee already has an active membership in this team.",
  "errors": {
    "employee_id": [
      "Employee already has an active membership in this team."
    ]
  }
}
```

---

## PMS-API-3.3 — Contract Assignment & Act-As

**Domain:** 06. Contract Role & Act-As  
**Task ID:** PMS-API-3.3  
**Assignee:** Munna · Lane B  
**Depends on:** PMS-API-2.3, PMS-API-3.1  
**Unlocks FE:** PMS-3.3-FE  
**Related Tables / Schema:** `contract_team_assignments`, `contract_members`

### Common Request Headers
All API calls require:
- `Authorization: Bearer <token>`
- `X-Company-Id: <company_id>`
- `Accept: application/json`
- `Content-Type: application/json` (for POST/PUT)

### Permission Middleware
| Resource / Operation | Route Pattern | Required Permission (any of) |
| --- | --- | --- |
| Contract Members (List / Show) | `GET /api/v1/project/contracts/{contractId}/members[/{id}]` | `project.assignments-view`, `project.assignments-manage`, `project.projects-manage`, `project.projects-view` |
| Contract Members (Assign / Update / Unassign) | `POST|PUT|DELETE /api/v1/project/contracts/{contractId}/members[/{id}]` | `project.assignments-manage`, `project.projects-manage` |
| Contract Team Assignments (List) | `GET /api/v1/project/contracts/{contractId}/teams` | `project.assignments-view`, `project.assignments-manage`, `project.projects-manage`, `project.projects-view` |

### Base Paths
- Members: `/api/v1/project/contracts/{contractId}/members`
- Teams: `/api/v1/project/contracts/{contractId}/teams`

---

### 1. GET `/api/v1/project/contracts/{contractId}/members` — List Contract Members

Returns list or paginated contract members. Each member record returns their assigned role on the contract, `is_act_as` flag, and their `permanent_team` (SEO, Web Dev, etc.) resolved dynamically from `service_team_members` so FE can display permanent vs contract role clearly.

**Query Parameters:**
- `division` *(optional, string)*: Filter by `BD` or `PD`.
- `status` *(optional, string)*: Filter by `active` or `inactive`.
- `is_act_as` *(optional, boolean)*: Filter by Act-As delegation flag (`1`/`0` or `true`/`false`).
- `employee_id` *(optional, integer)*: Filter by employee ID.
- `designation_id` *(optional, integer)*: Filter by designation ID.
- `team_id` *(optional, integer)*: Filter by associated service team ID.
- `search` *(optional, string)*: Search by employee name or employee number.
- `page` *(optional, integer)*: Triggers paginated response.
- `per_page` *(optional, integer, default: 15)*: Items per page when paginated.

**Response `200 OK` (Paginated):**

```json
{
  "success": true,
  "message": "Contract members retrieved successfully.",
  "data": [
    {
      "id": 800,
      "contract_id": 900,
      "assignment_id": 50,
      "employee_id": 77,
      "employee": {
        "id": 77,
        "name": "Rahim",
        "employee_no": "EMP-0077",
        "status": "active"
      },
      "division": "BD",
      "designation_id": 101,
      "designation": {
        "id": 101,
        "name": "Sales Person"
      },
      "is_act_as": true,
      "permanent_team": {
        "id": 15,
        "name": "SEO Team",
        "service": {
          "id": 9,
          "name": "SEO"
        }
      },
      "status": "active",
      "assigned_at": "2026-02-01T10:00:00+00:00",
      "unassigned_at": null,
      "created_at": "2026-02-01T10:00:00+00:00",
      "updated_at": "2026-02-01T10:00:00+00:00"
    }
  ],
  "meta": {
    "current_page": 1,
    "per_page": 15,
    "total": 1,
    "last_page": 1
  }
}
```

---

### 2. POST `/api/v1/project/contracts/{contractId}/members` — Assign Member & Act-As

Assigns an employee to a contract in a specific division (`BD` or `PD`) with an assigned role (`designation_id`). If the role differs from their permanent role or if assigned across services, `is_act_as: true` records the delegated role without altering their permanent HR designation or permanent team membership. Also auto-provisions or links the underlying `contract_team_assignments` record when `team_id` is supplied.

**Request Body:**

```json
{
  "employee_id": 77,
  "division": "BD",
  "designation_id": 101,
  "is_act_as": true,
  "team_id": 20
}
```

| Field | Type | Validation Rules | Description |
| --- | --- | --- | --- |
| `employee_id` | integer | `required`, `integer`, `exists:employee_personal_infos,id` | Tenant employee |
| `division` | string | `required`, `string`, `in:BD,PD` | Team division |
| `designation_id` | integer | `required`, `integer`, `exists:designations,id` | Contract role designation |
| `is_act_as` | boolean | `sometimes`, `boolean` | Defaults to `false` |
| `team_id` | integer\|null | `nullable`, `integer`, `exists:service_teams,id` | Team for team assignment link |
| `status` | string | `sometimes`, `in:active,inactive` | Defaults to `active` |
| `assigned_at` | string\|null | `nullable`, `date` | Optional assignment timestamp |

**Response `201 Created`:**

```json
{
  "success": true,
  "message": "Contract member assigned successfully.",
  "data": {
    "id": 800,
    "contract_id": 900,
    "assignment_id": 50,
    "employee_id": 77,
    "employee": {
      "id": 77,
      "name": "Rahim",
      "employee_no": "EMP-0077",
      "status": "active"
    },
    "division": "BD",
    "designation_id": 101,
    "designation": {
      "id": 101,
      "name": "Sales Person"
    },
    "is_act_as": true,
    "permanent_team": {
      "id": 15,
      "name": "SEO Team",
      "service": {
        "id": 9,
        "name": "SEO"
      }
    },
    "status": "active",
    "assigned_at": "2026-02-01T10:00:00+00:00",
    "unassigned_at": null,
    "created_at": "2026-02-01T10:00:00+00:00",
    "updated_at": "2026-02-01T10:00:00+00:00"
  }
}
```

#### Business Meaning of Rahim Sample:
Rahim’s **permanent** team remains SEO (`permanent_team.name = "SEO Team"`). On the Digital Marketing contract he is **Act As** Sales Person (`designation.name = "Sales Person"`, `is_act_as = true`). Official HR designation and permanent team membership are completely unchanged.

---

### 3. GET `/api/v1/project/contracts/{contractId}/members/{id}` — Show Contract Member

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Contract member retrieved successfully.",
  "data": {
    "id": 800,
    "contract_id": 900,
    "assignment_id": 50,
    "employee_id": 77,
    "employee": {
      "id": 77,
      "name": "Rahim",
      "employee_no": "EMP-0077",
      "status": "active"
    },
    "division": "BD",
    "designation_id": 101,
    "designation": {
      "id": 101,
      "name": "Sales Person"
    },
    "is_act_as": true,
    "permanent_team": {
      "id": 15,
      "name": "SEO Team",
      "service": {
        "id": 9,
        "name": "SEO"
      }
    },
    "status": "active",
    "assigned_at": "2026-02-01T10:00:00+00:00",
    "unassigned_at": null,
    "created_at": "2026-02-01T10:00:00+00:00",
    "updated_at": "2026-02-01T10:00:00+00:00"
  }
}
```

---

### 4. PUT `/api/v1/project/contracts/{contractId}/members/{id}` — Update Contract Member

Allows updating the member's contract designation, division, Act-As status, team link, or active status.

**Request Body:**

```json
{
  "division": "BD",
  "designation_id": 101,
  "is_act_as": true,
  "team_id": 20,
  "status": "active"
}
```

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Contract member updated successfully.",
  "data": {
    "id": 800,
    "contract_id": 900,
    "assignment_id": 50,
    "employee_id": 77,
    "employee": {
      "id": 77,
      "name": "Rahim",
      "employee_no": "EMP-0077",
      "status": "active"
    },
    "division": "BD",
    "designation_id": 101,
    "designation": {
      "id": 101,
      "name": "Sales Person"
    },
    "is_act_as": true,
    "permanent_team": {
      "id": 15,
      "name": "SEO Team",
      "service": {
        "id": 9,
        "name": "SEO"
      }
    },
    "status": "active",
    "assigned_at": "2026-02-01T10:00:00+00:00",
    "unassigned_at": null,
    "created_at": "2026-02-01T10:00:00+00:00",
    "updated_at": "2026-02-01T10:30:00+00:00"
  }
}
```

---

### 5. DELETE `/api/v1/project/contracts/{contractId}/members/{id}` — Unassign Member

Softly unassigns the member: sets `status = 'inactive'`, records `unassigned_at = now()`, and appends an `unassigned` event to `contract_member_histories`.

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Contract member unassigned successfully.",
  "data": null
}
```

---

### 6. GET `/api/v1/project/contracts/{contractId}/teams` — List Contract Teams

Lists team assignments for the contract with active member counts.

**Query Parameters:**
- `division` *(optional, string)*: Filter by `BD` or `PD`.
- `status` *(optional, string)*: Filter by `active` or `inactive`.
- `team_id` *(optional, integer)*: Filter by service team ID.
- `page` *(optional, integer)*: Triggers paginated response.
- `per_page` *(optional, integer, default: 15)*: Items per page when paginated.

**Response `200 OK`:**

```json
{
  "success": true,
  "message": "Contract team assignments retrieved successfully.",
  "data": [
    {
      "id": 50,
      "company_id": 1,
      "contract_id": 900,
      "team_id": 20,
      "team": {
        "id": 20,
        "name": "Digital Marketing Team",
        "service": {
          "id": 10,
          "name": "Digital Marketing",
          "code": "DIGITAL-MARKETING-0010"
        }
      },
      "division": "BD",
      "status": "active",
      "active_members_count": 1,
      "created_at": "2026-02-01T10:00:00+00:00",
      "updated_at": "2026-02-01T10:00:00+00:00"
    }
  ]
}
```

---

### 7. Errors & Business Rules

#### Business Rules (BR-06, BR-07, BR-08):
1. **Act-As Isolation:** Assigning an employee with `is_act_as = true` only records contract responsibility; it does NOT alter `service_team_members` or the employee's official HR designation.
2. **Duplicate Active Assignment in Same Division:** An employee cannot have more than one active assignment in the **same division** (`BD` or `PD`) on the same contract. Attempting to assign again returns `422 Unprocessable Entity`.
3. **Multi-Contract Assignment:** An employee can be assigned to multiple distinct contracts concurrently.
4. **Inactive Employee:** An inactive employee (`status != 'active'`) cannot be assigned to a contract.
5. **Cancelled Contract:** A cancelled contract cannot receive new member assignments.
6. **PM Role:** In accordance with PMS rules, `PM` designation/role represents **Member of Project** (delivery contributor), not Project Manager.

#### Validation Error (`422 Unprocessable Entity`):
```json
{
  "success": false,
  "message": "Employee already has an active assignment in this division for this contract.",
  "errors": {
    "employee_id": [
      "Employee already has an active assignment in this division for this contract."
    ]
  }
}
```

#### Not Found (`404 Not Found`):
```json
{
  "success": false,
  "message": "Contract member not found."
}
```

#### Forbidden (`403 Forbidden`):
```json
{
  "success": false,
  "message": "User does not have the right permissions."
}
```

---

## Integration

| Task | Owner | Notes |
| --- | --- | --- |
| PMS-1.5-BE Client integration | Sharif | Reuse Client module dropdown/API |
| PMS-5.2-BE Payment cleared hook | Ashraful | Event/interface only in Phase 1 |

### Payment cleared event payload (sample)

```json
{
  "company_id": 1,
  "project_id": 100,
  "contract_id": 900,
  "payment_reference": "PAY-2026-0001",
  "payment_date": "2026-03-01",
  "payment_status": "cleared",
  "payment_amount": "100.00",
  "applicable_net_base": "93.00"
}
```

Phase 1 listener may no-op / log only.

---

## Reports API (lightweight samples)

Same envelope. Example employee assignment report row:

```json
{
  "success": true,
  "message": "Assignment report fetched successfully",
  "data": [
    {
      "employee_id": 77,
      "employee_name": "Rahim",
      "permanent_memberships": [
        { "team": "SEO Team", "division": "PD", "service": "SEO" }
      ],
      "contract_assignments": [
        {
          "contract_number": "DM-2026-001",
          "division": "BD",
          "role": "Sales Person",
          "is_act_as": true
        }
      ]
    }
  ]
}
```

Owners of `PMS-5.3`…`PMS-5.8` expand full report contracts when starting those cards.

---

## PMS-API-2.3 — Contracts API

**Base:** `/api/v1/project/contracts`  
**Permissions:**  
- **View:** `project.contracts-view` (or `project.projects-manage` / `project.projects-view`)
- **Create/Update/Delete:** `project.contracts-manage` (or `project.projects-manage`)

### Common Request Headers
All API calls require:
- `Authorization: Bearer <token>`
- `X-Company-Id: <company_id>`
- `Accept: application/json`
- `Content-Type: application/json` (for POST/PUT)

### POST create — request

```json
{
  "contract_number": "DM-2026-001",
  "project_id": 100,
  "project_service_link_id": 501,
  "start_date": "2026-02-01",
  "end_date": "2026-04-30",
  "contract_value": 100.0,
  "currency": "USD",
  "allocated_hours": 40,
  "billing_type": "retainer",
  "payment_terms": "Net 15",
  "renewal_type": "manual",
  "status": "draft",
  "notes": null
}
```

### POST create — response `201`

```json
{
  "success": true,
  "message": "Contract created successfully",
  "data": {
    "id": 900,
    "company_id": 1,
    "contract_number": "DM-2026-001",
    "project_id": 100,
    "project_service_link_id": 501,
    "client_id": 55,
    "client": { "id": 55, "name": "ABC Corporation" },
    "service": { "id": 10, "name": "Digital Marketing" },
    "start_date": "2026-02-01",
    "end_date": "2026-04-30",
    "contract_value": "100.00",
    "currency": "USD",
    "allocated_hours": "40.00",
    "billing_type": "retainer",
    "payment_terms": "Net 15",
    "renewal_type": "manual",
    "status": "draft",
    "notes": null,
    "created_at": "2026-09-14T10:00:00+00:00",
    "updated_at": "2026-09-14T10:00:00+00:00"
  }
}
```

### POST update status (Status Transition)

`POST /api/v1/project/contracts/{id}/status`

```json
{ 
  "status": "active", 
  "note": "Approved by commercial" 
}
```

---

## How FE uses these samples

1. **Preferred:** paste-ready FE / `PMS-API-*` cards in `PMS_PHASE1_TASK_CARDS.md` already embed request/response — আলাদা ফাইল খোলার দরকার নেই।  
2. This file (`11-api.md`) = backup / WBS index copy।  
3. Copy `data` shape into TypeScript types / mocks; BE merge হলে same shape।  
4. Field change লাগলে `PMS-API-*` re-lock + Bablu sync।
