# Campaign, Paper BO, Paper Wallet & Paper Order API

**Base URL:** `{{BASE_URL}}/api`  
**Timezone:** Campaign / Paper BO window fields (`camp_reg_start`, `camp_reg_end`, `camp_start`, `camp_end`) are **Asia/Dhaka wall clocks**. The API keeps the frontend’s date/time digits as-is (strips `Z` / offsets, does not convert). Date-only input defaults to `00:00:00` (starts) or `23:59:59` (ends). Responses return naive `Y-m-d H:i:s` (not UTC `Z`) so clients must not re-apply +6. Server “now” stamps still use Asia/Dhaka via `AppDateTime::now()`.

**Replace in examples:**
- `{{BASE_URL}}` â†’ e.g. `http://127.0.0.1:8000` or `http://localhost/fintraBackend/fi_adm/public`
- `{{JWT_TOKEN}}` â†’ token from `auth/login` (production)
- `{{BACK_USER_ID}}` â†’ `fintra_back_users.user_id` (open-route testing for campaign create/update)
- `{{CAMP_ID}}` â†’ campaign primary key (`camp_id`)
- `{{CAMP_CODE}}` â†’ campaign unique code (`camp_code`)
- `{{PRIZE_ID}}` â†’ prize primary key (`prize_id`)
- `{{CLIENT_ID}}` â†’ client primary key (`client_user.user_id`)
- `{{PAPER_BO_ID}}` â†’ paper_bo primary key (`id`)
- `{{SECURITY_CODE}}` â†’ e.g. `IFIC`, `GP`
- `{{ORDER_NO}}` â†’ paper order number returned after placement

---

## Auth & routing

| Environment | Middleware | Notes |
|-------------|------------|--------|
| **Testing (current)** | None (open routes) | Campaign, paper BO, paper wallet, and paper order routes are registered under `//open routes for testing` in `routes/api.php` â€” no JWT required. |
| **Production** | `jwt.auth`, `check.token`, `dynamic.permission` | Protected route block is commented in `routes/api.php`; re-enable when going live. Register route names in your role/API permission table. |

**Open-route campaign create:** send `camp_create_by` (back-office `user_id`). **Update** does not use `camp_last_update_by` in the body â€” open-route updates need only `campains` / `prizes`; optional JWT or `camp_create_by` records who updated.

**Paper BO create / check:** no JWT required on open routes.

---

## Route index

### Campaign

| Action | Method | Route name | URL |
|--------|--------|------------|-----|
| Index (list â€” summary fields) | `GET` | `campaign-index` | `/api/campaign/campaign-index` |
| Detail (admin edit) | `GET` | `campaign-info` | `/api/campaign/campaign-info/{id}` |
| List by status (admin) | `GET\|POST` | `campaign-list-by-status` | `/api/campaign/campaign-list-by-status` |
| Client list by status | `GET\|POST` | `client-campaign-list-by-status` | `/api/campaign/client-campaign-list-by-status` |
| Create | `POST` | `campaign-store` | `/api/campaign/campaign-store` |
| Update | `POST` | `campaign-update/{id}` | `/api/campaign/campaign-update/{id}` |
| Scoreboard | `GET\|POST` | `campaign-scoreboard` | `/api/campaign/scoreboard` |
| Finalize winners (manual) | `POST` | `campaign-finalize-winners` | `/api/campaign/finalize-winners` |
| End and archive | `POST` | `campaign-end-and-archive` | `/api/campaign/end-and-archive` |
| Move archive to running | `POST` | `campaign-move-to-running` | `/api/campaign/move-to-running` |

### Paper BO

| Action | Method | Route name | URL |
|--------|--------|------------|-----|
| Create campaign paper BO | `POST` | `paper-bo-campaign-store` | `/api/paper-bo/campaign-store` |
| Update campaign paper BO name | `POST` | `paper-bo-update-name` | `/api/paper-bo/update-name` |
| Create normal paper BO | `POST` | `paper-bo-store` | `/api/paper-bo/store` |
| Check normal paper BO | `GET\|POST` | `check-paper-bo` | `/api/paper-bo/check-paper-bo` |
| Check campaign paper BO | `GET\|POST` | `check-campaign-paper-bo` | `/api/paper-bo/check-campaign-paper-bo` |
| Sellable shares | `GET\|POST` | `paper-bo-sellable-shares` | `/api/paper-bo/sellable-shares` |
| Purchasing power | `GET\|POST` | `paper-bo-purchasing-power` | `/api/paper-bo/purchasing-power` |

### Paper Wallet

| Action | Method | Route name | URL |
|--------|--------|------------|-----|
| Entry type catalog (DR/CR) | `GET` | `paper-wallet-index` | `/api/paper-wallet/paper-wallet-index` |
| Paper BO ledger | `GET\|POST` | `paper-bo-ledger` | `/api/paper-wallet/paper-bo-ledger` |
| Schedule wallet log (immature) | `GET\|POST` | `paper-wallet-schedule-wallet-log` | `/api/paper-wallet/schedule-wallet-log` |

### Paper Deposit (Transaction History)

| Action | Method | Route name | URL |
|--------|--------|------------|-----|
| List deposits (incl. opening) | `GET\|POST` | `paper-deposit-index` | `/api/paper-deposit/index` |
| Top-up deposit | `POST` | `paper-deposit-store` | `/api/paper-deposit/store` |

### Paper Order Management

> **Legacy v1 removed.** See [`paper_order_v1_legacy.md`](paper_order_v1_legacy.md) for archived routes
> (`market-depth`, `place-order`, `modify-order`, `cancel-order`, `order-status`, `portfolio`).
> Live trading APIs are documented in [`paperOrderMS.md`](paperOrderMS.md).

| Action | Method | Route name | URL |
|--------|--------|------------|-----|
| Order log (v2, paginated) | `GET\|POST` | `paper-order-order-log` | `/api/paper-order/order-log` |

---

## Enums

| Field | Allowed values |
|-------|----------------|
| `camp_type` | `trading_game`, `fair`, `marketing`, `Campaign` |
| `camp_visibility` | `user`, `bo`, `private` |
| `camp_scoring_method` | `return_pct`, `total_pnl`, `ul_pnl`, `rl_pnl` |
| `camp_status` | `running`, `archived` |
| `wallet` | `available`, `locked` (default `locked`) |
| `instant_cash` | `available`, `locked` (default `available`) |
| `prize_type` | `cash`, `product`, `certificate`, `bonus_capital`, `commission_discount`, `badge` |
| `prize_status` | `active`, `inactive` |
| `paper_bo.type` | `campaign`, `normal` |
| `paper_bo.status` | `ACTIVE`, `INACTIVE`, `HOLD` |
| `paper_bo.gender` | `M`, `F`, `O` |
| `paper_bo.disqualified` | `yes`, `no` |
| `paper_wallet.txn_type` | `buy`, `sell`, `commission`, `charge`, `deposit`, `bonus`, `penalty`, `refund`, `adjustment`, `dividend` |
| `paper_wallet.dr_cr` | `DR`, `CR` |
| `paper_wallet.status` | `pending`, `completed`, `reversed`, `failed` |
| `paper_wallet.is_reversal` | `yes`, `no` |
| `paper_order_log.side` | `B`, `S` |
| `paper_order_log.order_type` | `market`, `spot`, `parking`, `limit` |
| `paper_order_log.status` | `pending`, `executed`, `rejected`, `cancelled` |
| `mkt_security_code.category` | `A`, `B`, `Z`, `S` (other/missing categories default to T+1) |

**Date rules:**
- Registration: `camp_reg_start` â‰¤ `camp_reg_end` â‰¤ `camp_end`
- Campaign (trading): `camp_start` â‰¤ `camp_end`
- Registration may open before trading starts (`camp_reg_start` may be before `camp_start`).

**Account opening wallet entries (automatic on paper BO create):**
1. `DEPOSIT` (CR) on `paper_wallet` — full `initial_deposit`
2. Matching `paper_deposit` row (same amount) — so Transaction History (`GET|POST /api/paper-deposit/index`) shows the opening deposit
3. `BO_ACCOUNT_OPENING_FEE` (DR) — **150** (from entry catalog rate)

Net `cash_balance` = `initial_deposit − 150`. Creation fails with **422** if `initial_deposit < 150`. Top-ups via `/api/paper-deposit/store` also write both ledger and `paper_deposit`. Index backfills a missing opening `paper_deposit` once for older BOs that only had the wallet credit.

**Uniqueness rules:**
- One client â†’ **one** normal paper BO (`type=normal`)
- One client â†’ **one** enrollment per campaign (`type=campaign` + `campaign_id`)

---

## 0. Login (get JWT â€” production)

| Item | Value |
|------|--------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/auth/login` |

### cURL

```bash
curl --location '{{BASE_URL}}/api/auth/login' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "email": "admin@example.com",
  "password": "your_password"
}'
```

Copy `access_token` / `token` from the response into `{{JWT_TOKEN}}`.

---

# A. Campaign APIs

## 1. Index â€“ Campaign list (summary)

Paginated admin list (`camp_id` desc). **Summary fields only** â€” use [campaign-info](#1b-campaign-info--detail) for full data, `pass_key`, prizes, and activity log.

| Item | Value |
|------|--------|
| **Method** | `GET` |
| **URL** | `{{BASE_URL}}/api/campaign/campaign-index` |
| **Params** | `page` (optional, default `1`), `per_page` (optional, default `25`, max `100`) |

### List row fields

`camp_id`, `camp_code`, `camp_title`, `camp_type`, `camp_visibility`, `camp_status`, `camp_reg_start`, `camp_reg_end`, `camp_start`, `camp_end`, `camp_scoring_method`, `camp_user_limit`, `camp_user_enroll`, `prize_count`

### cURL

```bash
curl --location '{{BASE_URL}}/api/campaign/campaign-index?page=1&per_page=25' \
--header 'Accept: application/json'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Campaign list fetched successfully",
  "data": {
    "campaigns": [
      {
        "camp_id": 2,
        "camp_code": "TG2026",
        "camp_title": "Trading Game 2026",
        "camp_type": "trading_game",
        "camp_visibility": "private",
        "camp_status": "running",
        "camp_reg_start": "2026-07-15 00:00:00",
        "camp_reg_end": "2026-08-20 23:59:59",
        "camp_start": "2026-08-01 00:00:00",
        "camp_end": "2026-09-30 23:59:59",
        "camp_scoring_method": "ul_pnl",
        "camp_user_limit": 150,
        "camp_user_enroll": 0,
        "prize_count": 6
      }
    ],
    "pagination": {
      "total": 48,
      "per_page": 25,
      "current_page": 1,
      "last_page": 2,
      "from": 1,
      "to": 25
    }
  }
}
```

---

## 1b. Campaign info â€“ Detail

Full campaign for admin view/edit. Includes **`pass_key`**, all configuration fields, **prizes**, and full **`log`** from `paper_bo_activity_log`.

| Item | Value |
|------|--------|
| **Method** | `GET` |
| **URL** | `{{BASE_URL}}/api/campaign/campaign-info/{{CAMP_ID}}` |

### cURL

```bash
curl --location '{{BASE_URL}}/api/campaign/campaign-info/{{CAMP_ID}}' \
--header 'Accept: application/json'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Campaign fetched successfully",
  "data": {
    "campains": {
      "camp_id": 2,
      "camp_code": "TG2026",
      "camp_title": "Trading Game 2026 (Updated)",
      "pass_key": "PAPER01",
      "camp_trade_value_limit": "60000.00",
      "prizes": [],
      "log": []
    }
  }
}
```

> `pass_key` is returned **only** on campaign-info (and create/update responses), not on campaign-index.

---

## 2. List by status â€“ Current / Upcoming / Old (admin)

Groups campaigns using `camp_start` and `camp_end`.  
Requires `client_id`; each campaign includes an `enrollment` object when the client is enrolled.

| Key | Rule |
|-----|------|
| `current_campain` | `camp_status = running` and `camp_start` â‰¤ now â‰¤ `camp_end` |
| `upcomeing_campain` | `camp_status = running` and `camp_start` > now |
| `old_campain` | `camp_status = running` and `camp_end` < now |
| `archived_campain` | `camp_status = archived` (admin end+archive) |

| Item | Value |
|------|--------|
| **Method** | `GET` or `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/campaign-list-by-status` |
| **Params** | `client_id` (required) |

### Enrollment object (when enrolled)

| Field | Value |
|-------|--------|
| `paper_bo_id` | Enrolled paper BO id |
| `type` | `campaign` |
| `rank` | Serial place `1..N` within the campaign (see [section 4](#4-campaign-scoreboard)); `null` if disqualified |
| `score` | Numeric score per `camp_scoring_method` (see section 4) |
| `portfolio` | Object: `cash_balance`, `invested_value`, `total_equity`, `realized_pnl`, `unrealized_pnl`, `total_return_pct` |

When not enrolled, `enrollment` is `null`. `rank`/`score`/`portfolio` reflect the paper BO's last-refreshed aggregates. For **active running campaigns**, todays-mkt LTP ingest also mark-to-markets participant holdings so scoreboard return % stays current without every client polling portfolio. Calling this endpoint also lazily finalizes campaign winners for any enrolled campaign whose `camp_end` has passed (see [section 5](#5-finalize-campaign-winners)).

### cURL (GET)

```bash
curl --location '{{BASE_URL}}/api/campaign/campaign-list-by-status?client_id={{CLIENT_ID}}' \
--header 'Accept: application/json'
```

### cURL (POST)

```bash
curl --location '{{BASE_URL}}/api/campaign/campaign-list-by-status' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "client_id": {{CLIENT_ID}}
}'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Campaign list fetched successfully",
  "data": {
    "current_campain": [
      {
        "camp_id": 1,
        "camp_code": "TG2026",
        "camp_title": "Trading Game 2026",
        "enrollment": {
          "paper_bo_id": 2,
          "type": "campaign",
          "rank": 3,
          "score": -0.1523,
          "portfolio": {
            "cash_balance": "99247.29",
            "invested_value": "602.71",
            "total_equity": "99847.75",
            "realized_pnl": "0.00",
            "unrealized_pnl": "-2.25",
            "total_return_pct": "-0.1523"
          }
        },
        "prizes": []
      }
    ],
    "upcomeing_campain": [
      {
        "camp_id": 2,
        "camp_code": "TG2027",
        "camp_title": "Upcoming Game",
        "enrollment": null,
        "prizes": []
      }
    ],
    "old_campain": [],
    "archived_campain": []
  }
}
```

---

## 3. Client campaign list by status (client page)

Returns all campaigns grouped as active / upcoming / passed.  
If the client has a campaign `paper_bo`, attaches participation balances.

| Item | Value |
|------|--------|
| **Method** | `GET` or `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/client-campaign-list-by-status` |
| **Params** | `client_id` (required) |

| Key | Rule |
|-----|------|
| `active_campain` | `camp_start` â‰¤ now â‰¤ `camp_end` |
| `upcomeing_campain` | `camp_start` > now |
| `passed_campain` | `camp_end` < now |

### cURL (GET)

```bash
curl --location '{{BASE_URL}}/api/campaign/client-campaign-list-by-status?client_id={{CLIENT_ID}}' \
--header 'Accept: application/json'
```

### cURL (POST)

```bash
curl --location '{{BASE_URL}}/api/campaign/client-campaign-list-by-status' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "client_id": {{CLIENT_ID}}
}'
```

### Success response (200) â€“ participated campaign includes paper_bo fields

```json
{
  "status": true,
  "message": "Client campaign list fetched successfully",
  "data": {
    "active_campain": [
      {
        "camp_id": 1,
        "camp_code": "TG2026",
        "camp_title": "Trading Game 2026",
        "is_participated": true,
        "paper_bo_id": 12,
        "cash_balance": "99850.00",
        "total_equity": "99850.00",
        "realized_pnl": "0.00",
        "unrealized_pnl": "0.00",
        "total_return_pct": "0.0000",
        "prizes": []
      }
    ],
    "upcomeing_campain": [
      {
        "camp_id": 2,
        "camp_code": "TG2027",
        "camp_title": "Upcoming Game",
        "is_participated": false,
        "prizes": []
      }
    ],
    "passed_campain": []
  }
}
```

---

## 4. Campaign scoreboard

Ranks every `type=campaign` paper BO enrolled in a campaign by the campaign's `camp_scoring_method`. Uses **serial ranking** — every eligible participant gets a unique place `1..N` by score (highest first). Equal scores are broken by `paper_bo_id` ascending so ranks never tie. Disqualified paper BOs (`disqualified = 'yes'`) are listed with `rank: null` at the end, excluded from ranking and prize eligibility.

Calling this endpoint also lazily finalizes winners (writes `campaign_winners` rows) if `camp_end` has already passed and winners have not been assigned yet â€” see section 5.

**Scoring method â†’ paper BO field(s):**

| `camp_scoring_method` | Score formula |
|------------------------|----------------|
| `return_pct` | `((cash + purchaseable_immature + non_purchaseable_immature + holdings MTM) − initial_deposit) / initial_deposit × 100` |
| `total_pnl` | `realized_pnl + unrealized_pnl` |
| `ul_pnl` | `unrealized_pnl` |
| `rl_pnl` | `realized_pnl` |

> Scores use each paper BO's cached aggregates (cash + immature + holdings MTM). For active campaigns (`camp_status=running`, within `camp_start`–`camp_end`), LTP ingest refresh keeps MTM / return % up to date for scoreboard. Portfolio and execute-queue still apply for on-demand refresh and fills. Legacy v1 poll-driven auto-execution was removed — see [`paper_order_v1_legacy.md`](paper_order_v1_legacy.md).

| Item | Value |
|------|-------|
| **Method** | `GET` or `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/scoreboard` |
| **Params** | `camp_id` (required) |

### cURL

```bash
curl --location '{{BASE_URL}}/api/campaign/scoreboard?camp_id={{CAMP_ID}}' \
--header 'Accept: application/json'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Scoreboard fetched successfully",
  "data": {
    "camp_id": 1,
    "camp_title": "Active Trading Game Campaign 2026",
    "camp_scoring_method": "return_pct",
    "campaign_ended": false,
    "winners_finalized": false,
    "total_participants": 3,
    "scoreboard": [
      {
        "paper_bo_id": 2,
        "client_id": 52,
        "client_name": "MOHAMMAD JAHANGIR ALAM",
        "disqualified": false,
        "score": 0,
        "cash_balance": "99850.00",
        "invested_value": "0.00",
        "total_equity": "99850.00",
        "realized_pnl": "0.00",
        "unrealized_pnl": "0.00",
        "total_return_pct": "0.0000",
        "rank": 1
      },
      {
        "paper_bo_id": 4,
        "client_id": 623,
        "client_name": "RELIANCE INTERNATIONAL",
        "disqualified": false,
        "score": 0,
        "cash_balance": "99850.00",
        "invested_value": "0.00",
        "total_equity": "99850.00",
        "realized_pnl": "0.00",
        "unrealized_pnl": "0.00",
        "total_return_pct": "0.0000",
        "rank": 2
      },
      {
        "paper_bo_id": 1,
        "client_id": 527,
        "client_name": "SHAFIUL MAHMUD PARTHO",
        "disqualified": false,
        "score": -0.1523,
        "cash_balance": "99347.75",
        "invested_value": "502.25",
        "total_equity": "99847.75",
        "realized_pnl": "0.00",
        "unrealized_pnl": "-2.25",
        "total_return_pct": "-0.1523",
        "rank": 3
      }
    ]
  }
}
```

---

## 5. Finalize campaign winners

Manually (re)runs winner assignment for a campaign whose `camp_end` has passed. Normally you don't need to call this â€” the scoreboard endpoint and `campaign-list-by-status` both trigger it automatically once a campaign ends. Use this endpoint to force a recompute (`force: true`) or to confirm/inspect the final `campaign_winners` rows with participant and prize details.

**Assignment rule:** for each eligible (non-disqualified) participant with a rank, find the active `campaign_prizes` row whose `prize_rank_from`–`prize_rank_to` range contains that rank, and insert a `campaign_winners` row (`disbursement_status = pending`). Ranks are unique serial places, so each prize tier matches at most one participant per exact rank (use a range like `1–3` to cover 1st–3rd). Idempotent by default — if winners already exist for the campaign, it reports that and returns the existing rows instead of duplicating them.

| Item | Value |
|------|-------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/finalize-winners` |
| **Body** | `camp_id` (required), `force` (optional boolean â€” deletes existing winners for the campaign and recomputes) |

### cURL

```bash
curl --location '{{BASE_URL}}/api/campaign/finalize-winners' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "camp_id": {{CAMP_ID}}
}'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Winners finalized successfully",
  "data": {
    "camp_id": 1,
    "winners": [
      {
        "winner_id": 1,
        "camp_id": 1,
        "participant_id": 52,
        "prize_id": 1,
        "final_rank": 1,
        "final_equity": "99850.00",
        "final_return_pct": "0.0000",
        "disbursement_status": "pending",
        "disbursed_at": null,
        "disbursed_by": null,
        "remarks": null,
        "created_at": "2026-08-17T00:00:05.000000Z",
        "participant": {
          "user_id": 52,
          "display_name": "MOHAMMAD JAHANGIR ALAM",
          "email": "md.jahangiralam1993@gmail.com"
        },
        "prize": {
          "prize_id": 1,
          "prize_title": "1st Prize Cash",
          "prize_type": "cash",
          "prize_value": "50000.00"
        }
      }
    ]
  }
}
```

### Business errors (422)

- `Campaign has not ended yet.` â€” `camp_end` is still in the future.

### Other outcomes (200, `status: true`)

- `"Winners were already finalized for this campaign."` â€” idempotent no-op; returns the existing winners.
- `"No eligible participants/prizes to assign winners."` â€” campaign ended but had no eligible (non-disqualified, ranked) participants, or no `active` prizes configured.

---

## 6. End and archive campaign

Immediately ends a campaign, moves it to `archived` status, and finalizes winners. If `camp_end` or `camp_reg_end` is still in the future, both are shortened to **now** so registration and trading stop immediately. The campaign then appears only in `archived_campain` on the admin list â€” not in `current_campain` / `upcomeing_campain` / `old_campain`. Optional **`remarks`** are stored on the **`archived`** row in `paper_bo_activity_log` (`payload.remarks` / `summary`), not appended to `camp_discription`.

| Item | Value |
|------|-------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/end-and-archive` |
| **Body** | `camp_id` (required), optional `remarks`; optional `camp_create_by` or JWT to record acting user (any back-office user â€” not required to be the creator) |

### cURL

```bash
curl --location '{{BASE_URL}}/api/campaign/end-and-archive' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "camp_id": {{CAMP_ID}},
  "remarks": "Closed early by admin"
}'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Campaign ended and archived successfully.",
  "data": {
    "campains": {
      "camp_id": 1,
      "camp_status": "archived",
      "camp_archived_at": "2026-07-21 16:00:00",
      "camp_end": "2026-07-21 16:00:00"
    },
    "winners_finalized": true,
    "winners": []
  }
}
```

### Business errors (422)

- `Campaign is already archived.`

---

## 7. Move archive to running

Restores an archived campaign to `running` status so it re-enters the normal date-based buckets (`current_campain`, etc.). Optionally supply new `camp_end` / `camp_reg_end` dates; if `camp_end` is still in the past and you omit a new date, the API extends `camp_end` by **30 days** from now.

| Item | Value |
|------|-------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/move-to-running` |
| **Body** | `camp_id` (required), optional `camp_end`, `camp_reg_end` (`Y-m-d H:i:s`); optional `camp_create_by` or JWT for audit (any back-office user) |

### cURL

```bash
curl --location '{{BASE_URL}}/api/campaign/move-to-running' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "camp_id": {{CAMP_ID}},
  "camp_end": "2026-09-30 23:59:59",
  "camp_reg_end": "2026-09-15 23:59:59"
}'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Campaign moved back to running successfully.",
  "data": {
    "campains": {
      "camp_id": 1,
      "camp_status": "running",
      "camp_archived_at": null,
      "camp_end": "2026-09-30 23:59:59"
    }
  }
}
```

### Business errors (422)

- `Only archived campaigns can be moved back to running.`

---

## 8. Store â€“ Create campaign with prizes

| Item | Value |
|------|--------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/campaign-store` |
| **Body** | JSON: `{ "camp_create_by": ..., "campains": {...}, "prizes": [...] }` |

> Payload key is `campains` (as used by the API). `campaigns` is also accepted.

### cURL (open route â€” include `camp_create_by`)

```bash
curl --location '{{BASE_URL}}/api/campaign/campaign-store' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "camp_create_by": {{BACK_USER_ID}},
  "campains": {
    "camp_code": "TG2026",
    "camp_title": "Trading Game 2026",
    "camp_type": "trading_game",
    "camp_visibility": "user",
    "pass_key": "PAPER01",
    "camp_reg_start": "2026-07-15 00:00:00",
    "camp_reg_end": "2026-08-15 23:59:59",
    "camp_start": "2026-08-01 00:00:00",
    "camp_end": "2026-09-30 23:59:59",
    "camp_scoring_method": "return_pct",
    "camp_commission_pct": 1.500,
    "camp_user_limit": 100,
    "camp_initial_deposit": 100000.00,
    "camp_deposit_limit": 500000.00,
    "camp_discription": "Paper trading competition for retail clients.",
    "camp_trade_value_limit": 50000.00,
    "camp_trade_limit": 100,
    "camp_daily_trade_limit": 10,
    "camp_sector_limit_list": ["Bank", "Pharma"],
    "camp_catagoty_list": ["A", "B"],
    "camp_security_whitelist": ["GP", "SQURPHARMA"],
    "camp_security_blacklist": [],
    "wallet": "locked",
    "instant_cash": "available"
  },
  "prizes": [
    {
      "prize_rank_from": 1,
      "prize_rank_to": 1,
      "prize_type": "cash",
      "prize_title": "1st Prize",
      "prize_value": 50000.00,
      "prize_description": "Cash award for rank 1",
      "prize_image_path": null,
      "prize_status": "active"
    },
    {
      "prize_rank_from": 2,
      "prize_rank_to": 3,
      "prize_type": "certificate",
      "prize_title": "Runner-up Certificate",
      "prize_value": null,
      "prize_description": "Certificate for ranks 2â€“3",
      "prize_status": "active"
    }
  ]
}'
```

### Success response (201)

```json
{
  "status": true,
  "message": "Campaign created successfully",
  "data": {
    "campains": {
      "camp_id": 2,
      "camp_code": "TG2026",
      "camp_status": "running",
      "prize_count": 2
    }
  }
}
```

> Full campaign detail (including `pass_key`, prizes, and `log`) is on **`GET /api/campaign/campaign-info/{id}`**. Update responses still return the full detail shape.

### Validation error (422)

```json
{
  "status": false,
  "message": "Validation error",
  "errors": {}
}
```

### Unauthenticated (401) â€” open route without `camp_create_by`

```json
{
  "status": false,
  "message": "Unauthenticated. For open testing, send camp_create_by (fintra_back_users.user_id)."
}
```

---

## 9. Update â€“ Update campaign (and optionally prizes)

| Item | Value |
|------|--------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/campaign/campaign-update/{{CAMP_ID}}` |
| **Body** | JSON: `{ "campains": {...}, "prizes": [...] }` â€” optional `camp_create_by` or JWT to set update audit |

**Prize sync rules (when `prizes` is sent):**
- Include `prize_id` → update that prize
- Omit `prize_id` → create a new prize
- Existing prizes missing from the array → deleted (or set `inactive` if already linked to winners)
- Omit `prizes` entirely → leave existing prizes unchanged

**Paper BO trade-limit sync:** when the update payload includes any of `camp_trade_limit`, `camp_daily_trade_limit`, `camp_trade_value_limit`, or `camp_commission_pct`, those values are pushed to **all** enrolled `paper_bo` rows for the campaign (`PaperBo::syncTradeLimitsFromCampaign`). Count/daily remaining are recomputed as `max(0, newCap − used)` so past trades still count; per-trade `trade_value_limit_remaining` is set equal to the new cap. `daily_trade_value_limit*` is not on the campaign and is left unchanged. Response includes `data.paper_bo_limits_synced` (row count).

### cURL – update campaign + sync prizes

```bash
curl --location '{{BASE_URL}}/api/campaign/campaign-update/{{CAMP_ID}}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "campains": {
    "camp_code": "TG2026",
    "camp_title": "Trading Game 2026 (Updated)",
    "camp_type": "trading_game",
    "camp_visibility": "private",
    "pass_key": "PAPER01",
    "camp_reg_start": "2026-07-15 00:00:00",
    "camp_reg_end": "2026-08-20 23:59:59",
    "camp_start": "2026-08-01 00:00:00",
    "camp_end": "2026-09-30 23:59:59",
    "camp_scoring_method": "ul_pnl",
    "camp_commission_pct": 2.000,
    "camp_user_limit": 150,
    "camp_initial_deposit": 100000.00,
    "camp_deposit_limit": 500000.00,
    "camp_discription": "Updated description",
    "camp_trade_value_limit": 60000.00,
    "camp_trade_limit": 120,
    "camp_daily_trade_limit": 12,
    "camp_sector_limit_list": ["Bank"],
    "camp_catagoty_list": ["A"],
    "camp_security_whitelist": ["GP"],
    "camp_security_blacklist": ["XYZ"]
  },
  "prizes": [
    {
      "prize_id": 1,
      "prize_rank_from": 1,
      "prize_rank_to": 1,
      "prize_type": "cash",
      "prize_title": "1st Prize (Updated)",
      "prize_value": 75000.00,
      "prize_description": "Updated cash award",
      "prize_status": "active"
    },
    {
      "prize_id": 2,
      "prize_rank_from": 4,
      "prize_rank_to": 10,
      "prize_type": "badge",
      "prize_title": "Top 10 Badge",
      "prize_value": null,
      "prize_description": "Participation badge for ranks 4â€“10",
      "prize_status": "active"
    }
  ]
}'
```

### cURL â€“ update campaign fields only (no prize changes)

```bash
curl --location '{{BASE_URL}}/api/campaign/campaign-update/{{CAMP_ID}}' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "campains": {
    "camp_title": "Trading Game 2026 (Title Only)",
    "camp_user_limit": 200
  }
}'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Campaign updated successfully",
  "data": {
    "campains": {
      "camp_id": 2,
      "camp_code": "TG2026",
      "log": [],
      "prizes": []
    }
  }
}
```

> Create/update responses use the same campaign shape as `campaign-index` (audit in `log` only; prizes nested once under `campains`).

### Not found (404)

```json
{
  "status": false,
  "message": "Campaign not found"
}
```

---

# B. Paper BO APIs

## 10. Create campaign paper BO (enroll in campaign)

Creates `paper_bo` with `type=campaign`.  
Inherits from campaign: `initial_deposit`, `total_trade_limit`, `daily_trade_limit`, `trade_value_limit`, `commission_pct`.  
`daily_trade_value_limit` uses default `100000000`.  
Increments `camp_user_enroll`.

**On create, two `paper_wallet` ledger rows are written automatically:**
1. `DEPOSIT` (CR) â€” campaign `camp_initial_deposit`
2. `BO_ACCOUNT_OPENING_FEE` (DR) â€” 150

Net `cash_balance` = `total_equity` = `initial_deposit âˆ’ 150`.

If `client_user.display_name`, `email`, or `mobile` is null/empty, they are filled from request `name`, `email`, `phone`. `display_name` also backfills when it is still the `"User"` placeholder set at signup (not just when null/empty) — the request `name` is rejected outright (422) if it is literally `"User"` (case-insensitive), so the placeholder can never be re-written back onto `display_name`.

| Item | Value |
|------|--------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/paper-bo/campaign-store` |

### Body

| Field | Required | Notes |
|-------|----------|--------|
| `client_id` | yes | `client_user.user_id` |
| `campaign_code` | yes | `campaigns.camp_code` (unique) |
| `name` | yes | max 150 |
| `email` | no | max 100 |
| `phone` | yes | max 15 |
| `pass_key` | conditional | Required when campaign `camp_visibility` is `private` |

### cURL

```bash
curl --location '{{BASE_URL}}/api/paper-bo/campaign-store' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "client_id": {{CLIENT_ID}},
  "campaign_code": "{{CAMP_CODE}}",
  "name": "John Doe",
  "email": "john@example.com",
  "phone": "01700000000"
}'
```

Private campaign example (include `pass_key`):

```bash
curl --location '{{BASE_URL}}/api/paper-bo/campaign-store' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "client_id": {{CLIENT_ID}},
  "campaign_code": "{{CAMP_CODE}}",
  "pass_key": "secret123",
  "name": "John Doe",
  "email": "john@example.com",
  "phone": "01700000000"
}'
```

### Success response (201)

```json
{
  "status": true,
  "message": "Campaign paper BO created successfully",
  "data": {
    "account_status": true,
    "name": "John Doe",
    "cash_balance": "99850.00",
    "commission_pct": "1.500",
    "total_equity": "99850.00",
    "realized_pnl": "0.00",
    "unrealized_pnl": "0.00",
    "total_return_pct": "0.0000"
  }
}
```

### Business error examples (422)

- Registration has not opened yet.
- Campaign registration period has ended.
- Campaign user limit reached.
- Client is already enrolled in this campaign.
- `pass_key` missing for a private campaign.
- Invalid pass key for this campaign.
- Initial deposit is insufficient to cover BO account opening fee.

---

## 10.1 Update campaign paper BO name

Updates `paper_bo.name` for a **campaign** paper BO only.  
If `client_user.display_name` is null/blank or still the `"User"` placeholder, it is also set to the new name (scoreboard/winners read `display_name`). A real existing `display_name` is never overwritten.

| Item | Value |
|------|--------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/paper-bo/update-name` |

### Body

| Field | Required | Notes |
|-------|----------|--------|
| `paperbo_id` | yes | `paper_bo.id` of a `type=campaign` account |
| `name` | yes | Min **4** characters (more than 3), max 150; rejected if literally `"User"` (case-insensitive) |

### cURL

```bash
curl --location '{{BASE_URL}}/api/paper-bo/update-name' \
  --header 'Content-Type: application/json' \
  --header 'Accept: application/json' \
  --data '{
    "paperbo_id": {{paper_bo_id}},
    "name": "John Doe"
  }'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Paper BO name updated successfully.",
  "data": {
    "paperbo_id": 12,
    "name": "John Doe",
    "display_name_synced": true
  }
}
```

### Business / validation errors (422)

- `name` shorter than 4 characters, or `"User"`.
- Paper BO is not `type=campaign` → `Only campaign paper BO names can be updated.`

---

## 11. Create normal paper BO

Creates `paper_bo` with `type=normal` (`campaign_id` = null).  
Trade limits use paper_bo defaults.

**Same automatic wallet entries as campaign enroll:** `DEPOSIT` then `BO_ACCOUNT_OPENING_FEE` (150).  
Net balance = `initial_deposit âˆ’ 150`.

If `client_user.display_name`, `email`, or `mobile` is null/empty, they are filled from request `name`, `email`, `phone`.

| Item | Value |
|------|--------|
| **Method** | `POST` |
| **URL** | `{{BASE_URL}}/api/paper-bo/store` |

### Body

| Field | Required | Notes |
|-------|----------|--------|
| `client_id` | yes | `client_user.user_id` |
| `type` | yes | must be `normal` |
| `name` | yes | max 150 |
| `email` | no | max 100 |
| `phone` | yes | max 15 |
| `initial_deposit` | yes | â‰¥ 150 (must cover opening fee) |
| `commission_pct` | yes | 0â€“100 |

### Default limits applied

| Field | Default |
|-------|---------|
| `total_trade_limit` | `10000000000` |
| `daily_trade_limit` | `100000000` |
| `trade_value_limit` | `10000000000` |
| `daily_trade_value_limit` | `100000000` |

### cURL

```bash
curl --location '{{BASE_URL}}/api/paper-bo/store' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "client_id": {{CLIENT_ID}},
  "type": "normal",
  "name": "John Doe",
  "email": "john@example.com",
  "phone": "01700000000",
  "initial_deposit": 100000,
  "commission_pct": 0.45
}'
```

### Success response (201)

```json
{
  "status": true,
  "message": "Paper BO created successfully",
  "data": {
    "account_status": true,
    "name": "John Doe",
    "cash_balance": "99850.00",
    "commission_pct": "0.450",
    "total_equity": "99850.00",
    "realized_pnl": "0.00",
    "unrealized_pnl": "0.00",
    "total_return_pct": "0.0000"
  }
}
```

### Business error examples (422)

- Client already has a normal paper BO account.

---

## 12. Check normal paper BO

Checks whether client has a `paper_bo` with `type=normal`.

| Item | Value |
|------|--------|
| **Method** | `GET` or `POST` |
| **URL** | `{{BASE_URL}}/api/paper-bo/check-paper-bo` |
| **Params** | `client_id` (required) |

### cURL (GET)

```bash
curl --location '{{BASE_URL}}/api/paper-bo/check-paper-bo?client_id={{CLIENT_ID}}' \
--header 'Accept: application/json'
```

### cURL (POST)

```bash
curl --location '{{BASE_URL}}/api/paper-bo/check-paper-bo' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "client_id": {{CLIENT_ID}}
}'
```

### Found (200)

```json
{
  "account_status": true,
  "paper_bo_id": 12,
  "type": "normal",
  "name": "John Doe",
  "cash_balance": "99850.00",
  "wallet_balance": 99850,
  "purchaseable_immature_balance": 0,
  "non_purchaseable_immature_balance": 0,
  "bridge_loan_available_balance": 0,
  "commission_pct": "0.450",
  "total_equity": "99850.00",
  "realized_pnl": "0.00",
  "unrealized_pnl": "0.00",
  "total_return_pct": "0.0000"
}
```

### Not found (200)

Returns client profile hints from `client_user` (`display_name` â†’ `name`, `mobile` â†’ `phone`, `email`). Missing values are `"Not available"`.

```json
{
  "account_status": false,
  "name": "John Doe",
  "phone": "01700000000",
  "email": "Not available"
}
```

---

## 13. Check campaign paper BO

Checks whether client has a `paper_bo` with `type=campaign` for a given campaign.

| Item | Value |
|------|--------|
| **Method** | `GET` or `POST` |
| **URL** | `{{BASE_URL}}/api/paper-bo/check-campaign-paper-bo` |
| **Params** | `client_id`, `campaign_id` (required) |

### cURL (GET)

```bash
curl --location '{{BASE_URL}}/api/paper-bo/check-campaign-paper-bo?client_id={{CLIENT_ID}}&campaign_id={{CAMP_ID}}' \
--header 'Accept: application/json'
```

### cURL (POST)

```bash
curl --location '{{BASE_URL}}/api/paper-bo/check-campaign-paper-bo' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "client_id": {{CLIENT_ID}},
  "campaign_id": {{CAMP_ID}}
}'
```

### Found (200)

Same shape as [check normal paper BO](#8-check-normal-paper-bo) when enrolled (`type` will be `campaign`).

```json
{
  "account_status": true,
  "paper_bo_id": 12,
  "type": "campaign",
  "name": "John Doe",
  "cash_balance": "99850.00",
  "commission_pct": "1.500",
  "total_equity": "99850.00",
  "realized_pnl": "0.00",
  "unrealized_pnl": "0.00",
  "total_return_pct": "0.0000"
}
```

### Not found (200)

Same shape as [check normal paper BO â€” not found](#8-check-normal-paper-bo).

---

# C. Paper Wallet APIs

## 14. Entry type catalog (DR/CR reference)

Static list of debit/credit entry types for UI labels and rates.  
Some entries include a `code` used in `paper_wallet.reference_no` when ledger rows are created.

| Item | Value |
|------|--------|
| **Method** | `GET` |
| **URL** | `{{BASE_URL}}/api/paper-wallet/paper-wallet-index` |

### cURL

```bash
curl --location '{{BASE_URL}}/api/paper-wallet/paper-wallet-index' \
--header 'Accept: application/json'
```

### Success response (200)

```json
{
  "status": true,
  "message": "Paper wallet entry types fetched successfully",
  "data": {
    "DR": [
      {
        "title": "Commission",
        "details": "Brokerage fee charged on trade value, rate set per house",
        "rate": "N/A"
      },
      {
        "code": "BO_ACCOUNT_OPENING_FEE",
        "title": "BO account opening fee",
        "details": "One-time charge at account creation, CDBL + broker portion",
        "rate": "150"
      }
    ],
    "CR": [
      {
        "code": "DEPOSIT",
        "title": "Deposit",
        "details": "Add capital, initial or top-up",
        "rate": "N/A"
      }
    ]
  }
}
```

**Coded entries (used in ledger `reference_no`):**

| Code | Side | Rate | Used when |
|------|------|------|-----------|
| `DEPOSIT` | CR | N/A | Initial/top-up deposit |
| `BO_ACCOUNT_OPENING_FEE` | DR | 150 | Paper BO account creation |
| `BO_ACCOUNT_RENEWAL_FEE` | DR | 150 | Annual renewal (future) |
| `MARGIN_LOAN_INTEREST` | DR | N/A | Margin interest (future) |
| `BOND_TRANSACTION_FEE` | DR | 50 | Bond trades (future) |

---

## 15. Paper BO ledger (all transactions)

Returns `paper_wallet` rows for a paper BO account, oldest first (`txn_date`, then `id`).  
Optional date filter on `txn_date` (inclusive, Asia/Dhaka day boundaries). Paginated â€” default **20** rows per page, max **200**.

| Item | Value |
|------|--------|
| **Method** | `GET` or `POST` |
| **URL** | `{{BASE_URL}}/api/paper-wallet/paper-bo-ledger` |

### Params

| Field | Required | Notes |
|-------|----------|--------|
| `paperbo_id` | yes | `paper_bo.id` |
| `from_date` | no | `Y-m-d`; required together with `to_date` |
| `to_date` | no | `Y-m-d`; must be â‰¥ `from_date` |
| `page` | no | Default `1` |
| `per_page` | no | Default `20`, max `200` |

Omit both dates to return the full ledger history (paginated). The `summary` block (`transaction_count`, `total_dr`, `total_cr`) is aggregated over the **entire filtered result set**, not just the current page.

### cURL (GET â€” page 1, default 20 rows)

```bash
curl --location '{{BASE_URL}}/api/paper-wallet/paper-bo-ledger?paperbo_id={{PAPER_BO_ID}}&page=1&per_page=20' \
--header 'Accept: application/json'
```

### cURL (POST â€” date range + pagination)

```bash
curl --location '{{BASE_URL}}/api/paper-wallet/paper-bo-ledger' \
--header 'Accept: application/json' \
--header 'Content-Type: application/json' \
--data '{
  "paperbo_id": {{PAPER_BO_ID}},
  "from_date": "2026-07-01",
  "to_date": "2026-07-16",
  "page": 1,
  "per_page": 50
}'
```

### Success response (200)

```json
{
  "status": true,
  "message": "2 transaction(s) fetched.",
  "data": {
    "paperbo_id": 1,
    "client_id": 10,
    "type": "campaign",
    "campaign_id": 3,
    "cash_balance": "99850.00",
    "filter": {
      "from_date": "2026-07-01",
      "to_date": "2026-07-16"
    },
    "summary": {
      "transaction_count": 2,
      "total_dr": 150,
      "total_cr": 100000
    },
    "meta": {
      "total": 2,
      "page": 1,
      "per_page": 20,
      "last_page": 1
    },
    "transactions": [
      {
        "txn_type": "deposit",
        "dr_cr": "CR",
        "amount": "100000.00",
        "balance_before": "0.00",
        "balance_after": "100000.00",
        "description": "Deposit",
        "security_code": null,
        "quantity": null,
        "price": null,
        "status": "completed",
        "is_reversal": "no",
        "reversed_by_id": null,
        "txn_date": "2026-07-16T15:39:12.000000Z",
        "value_date": "2026-07-16T00:00:00.000000Z"
      },
      {
        "txn_type": "charge",
        "dr_cr": "DR",
        "amount": "150.00",
        "balance_before": "100000.00",
        "balance_after": "99850.00",
        "description": "BO account opening fee",
        "security_code": null,
        "quantity": null,
        "price": null,
        "status": "completed",
        "is_reversal": "no",
        "reversed_by_id": null,
        "txn_date": "2026-07-16T15:39:12.000000Z",
        "value_date": "2026-07-16T00:00:00.000000Z"
      }
    ]
  }
}
```

### Validation error (422)

```json
{
  "status": false,
  "message": "Validation error",
  "errors": {
    "paperbo_id": ["The paperbo id field is required."]
  }
}
```

---

# D. Paper Order Management APIs

> **Removed (2026-08-06).** Legacy v1 place/modify/cancel/status/depth/portfolio lived in
> `PaperOrderManagementController` and has been deleted. Full archived endpoint docs +
> auto-execution business logic: [`paper_order_v1_legacy.md`](paper_order_v1_legacy.md).
>
> Current trading APIs: [`paperOrderMS.md`](paperOrderMS.md) (v2 place, execute-queue, modify/cancel, depth-v2, order-detail / order-log).

---

## Postman quick setup

1. Create environment variables:
   - `BASE_URL` = `http://127.0.0.1:8000`
   - `JWT_TOKEN` = *(production only)*
   - `BACK_USER_ID` = `1`
   - `CAMP_ID` = `1`
   - `CAMP_CODE` = `CAMP2026`
   - `PRIZE_ID` = `1`
   - `CLIENT_ID` = `1`
   - `PAPER_BO_ID` = `1`
   - `SECURITY_CODE` = `IFIC`
   - `ORDER_NO` = `ORD-2026-000001`
2. Import each cURL via **Import â†’ Raw text** (or paste into a request).
3. For protected routes (production), set header:
   - `Authorization: Bearer {{JWT_TOKEN}}`
   - `Accept: application/json`
4. For POST bodies, use **Body â†’ raw â†’ JSON**.

---

## Field reference

### `campains` object

| Field | Type | Required (create) | Notes |
|-------|------|-------------------|--------|
| `camp_code` | string(32) | yes | Unique |
| `camp_title` | string(150) | yes | |
| `camp_type` | enum | yes | See enums above |
| `camp_visibility` | enum | yes | `user`, `bo`, `private` |
| `pass_key` | string(16) | no | Required for private campaign enroll; returned on campaign-info / create / update only |
| `camp_reg_start` | datetime | yes | Registration opens |
| `camp_reg_end` | datetime | yes | Registration closes; â‰¤ `camp_end` |
| `camp_start` | datetime | yes | Trading / campaign activity window start |
| `camp_end` | datetime | yes | Trading / campaign activity window end; â‰¥ `camp_start` |
| `camp_status` | enum | no | `running` (default) or `archived`; use end-and-archive / move-to-running endpoints |
| `camp_archived_at` | datetime | no | Set when archived; cleared when restored to running |
| `camp_scoring_method` | enum | yes | See enums above |
| `camp_commission_pct` | decimal(5,3) | yes | 0â€“100 |
| `camp_user_limit` | int | yes | â‰¥ 1 |
| `camp_user_enroll` | int | no | Defaults to 0 on create |
| `camp_initial_deposit` | decimal(15,2) | yes | Must be â‰¥ 150 for paper BO enroll; must not exceed `camp_deposit_limit` when set |
| `camp_deposit_limit` | decimal(15,2) | no | Max deposit cap per participant; `null` = no limit |
| `camp_discription` | text | no | Spelling as in DB |
| `camp_trade_value_limit` | decimal(15,2) | no | Default `10000000000` â€” max value per trade |
| `camp_trade_limit` | int | no | Default `10000` â€” max trades per user |
| `camp_daily_trade_limit` | int | no | Default `1000` â€” max trades per user per day |
| `camp_sector_limit_list` | json/array | no | Default `["all"]` â€” all sectors allowed |
| `camp_catagoty_list` | json/array | no | Default `["all"]` â€” all categories; spelling as in DB |
| `camp_security_whitelist` | json/array | no | Default `["all"]` â€” all securities allowed |
| `camp_security_blacklist` | json/array | no | Default `[]` â€” none blocked |
| `wallet` | enum | no | Paper wallet UI/access: `available` or `locked`; default **`locked`** |
| `instant_cash` | enum | no | Instant cash UI/access: `available` or `locked`; default **`available`** |

### `prizes[]` object

| Field | Type | Required | Notes |
|-------|------|----------|--------|
| `prize_id` | int | update only | Present = update existing |
| `prize_rank_from` | int | yes | â‰¥ 1 |
| `prize_rank_to` | int | yes | â‰¥ `prize_rank_from` |
| `prize_type` | enum | yes | See enums above |
| `prize_title` | string(150) | yes | |
| `prize_value` | decimal(12,2) | no | Cash/bonus value |
| `prize_description` | text | no | |
| `prize_image_path` | string(255) | no | |
| `prize_status` | enum | no | Default `active` |

### `paper_bo` defaults (when not inherited from campaign)

| Field | Default |
|-------|---------|
| `initial_deposit` | `100000` |
| `total_trade_limit` | `10000000000` |
| `daily_trade_limit` | `100000000` |
| `trade_value_limit` | `10000000000` |
| `daily_trade_value_limit` | `100000000` |
| `commission_pct` | `0.45` |
| `invested_value` | `0` |
| `realized_pnl` | `0` |
| `unrealized_pnl` | `0` |
| `total_return_pct` | `0` |
| `cash_balance` | Set after opening wallet entries: `initial_deposit âˆ’ 150` |
| `total_equity` | Same as `cash_balance` on create |

### `paper_wallet` row (ledger)

| Field | Type | Notes |
|-------|------|--------|
| `paperbo_id` | bigint FK | â†’ `paper_bo.id` |
| `campaign_id` | bigint FK, nullable | Denormalized; null for normal BO |
| `txn_type` | enum | See enums above; includes `immature_credit`, `immature_consumed`, `immature_matured` for v2 immature cash flow |
| `dr_cr` | enum | `DR` or `CR` |
| `amount` | decimal(15,2) | Always positive |
| `balance_before` | decimal(15,2) | `cash_balance` before entry |
| `balance_after` | decimal(15,2) | `cash_balance` after entry |
| `reference_no` | string(50) | Entry code e.g. `DEPOSIT`, `BO_ACCOUNT_OPENING_FEE` |
| `description` | string(255) | Human title from catalog |
| `remarks` | text | Details from catalog |
| `security_code` | string(64) | For buy/sell/dividend |
| `quantity` | bigint | Buy/sell only |
| `price` | decimal(12,4) | Buy/sell only |
| `related_trade_id` | bigint | Future FK to `game_trades` |
| `status` | enum | Default `completed` for opening entries |
| `is_reversal` | enum | `yes` / `no` |
| `reversed_by_id` | bigint | Self-FK when reversed |
| `txn_date` | datetime | Transaction timestamp |
| `value_date` | date | Value/settlement date |
| `created_by` | int | `client_user.user_id`; null for system |

### `paper_order_log` execution fields

| Field | Type | Notes |
|-------|------|-------|
| `order_no` | string(32), unique | Generated as `ORD-{YEAR}-{ID}` |
| `paperbo_id` | bigint FK | Paper BO account |
| `campaign_id` | bigint FK, nullable | Null for normal BO |
| `security_code` | string(64) | Copied from security master |
| `security_category` | string(4) | Category snapshot at order time |
| `settlement_date` | date | Calculated from category/order type |
| `side` | enum | `B` / `S` |
| `order_type` | enum | `market`, `spot`, `parking`, `limit` |
| `requested_qty` | bigint | Requested quantity |
| `requested_price` | decimal(12,4), nullable | Required for limit/parking |
| `executed_qty` | bigint, nullable | Filled after execution |
| `executed_price` | decimal(12,4), nullable | Filled after execution |
| `gross_value` | decimal(15,2), nullable | Quantity x execution price |
| `commission_amount` | decimal(12,2), nullable | Brokerage commission |
| `net_value` | decimal(15,2), nullable | Buy: gross + commission; sell: gross - commission |
| `status` | enum | `pending`, `executed`, `rejected`, `cancelled` |
| `rejection_reason` | string(255), nullable | Execution-time rejection reason, or the cancellation reason when `status = cancelled` |

> Per-fill details and wallet/holding FKs live on `paper_exe_log` (v2). Legacy v1 also used a `scheduled_execution_at` column and related_holding/wallet FKs on the order header — see [`paper_order_v1_legacy.md`](paper_order_v1_legacy.md).

### Security category field

Migration `2026_07_18_000001_add_paper_order_execution_support.php` adds
`mkt_security_code.category`. Existing records default to `A`; update each security
with its actual exchange category before using settlement rules in production.

### `campaign_winners` row

| Field | Type | Notes |
|-------|------|-------|
| `winner_id` | bigint PK | |
| `camp_id` | bigint FK | â†’ `campaigns.camp_id` |
| `participant_id` | int FK | â†’ `client_user.user_id` (paper BO's `client_id`) |
| `prize_id` | bigint FK | â†’ `campaign_prizes.prize_id`; matched by rank range |
| `final_rank` | int | Serial place at the time winners were finalized |
| `final_equity` | decimal(15,2) | Paper BO `total_equity` snapshot |
| `final_return_pct` | decimal(8,4) | Paper BO `total_return_pct` snapshot |
| `disbursement_status` | enum | `pending`, `processing`, `disbursed`, `cancelled`; always created as `pending` |
| `disbursed_at` / `disbursed_by` | nullable | Set when a back-office user processes disbursement (not automated here) |

Written by `CampaignController::assignWinnersIfDue()`, triggered lazily from `campaign/scoreboard`, `campaign/campaign-list-by-status`, or explicitly via `campaign/finalize-winners`. Only campaigns with `camp_end` in the past, at least one eligible ranked participant, and at least one `active` prize get winners assigned; a campaign with no matching prize tier for a given rank simply has no winner row for that participant.
