# Customer Portal — API Documentation

The JSON API behind the self-care customer portal. The web portal (`/customer-portal`) runs on it, and a mobile app can use the same endpoints with the same login.

---

## Basics

| | |
|---|---|
| **Base URL** | `https://your-domain/api/customer-portal` |
| **Format** | JSON in, JSON out. `application/json` or form fields are both accepted. |
| **Auth** | `Authorization: Bearer <token>` — the token comes from [Login](#login) |
| **Available** | Only while the global setting **`new-customer-portal`** is enabled. Otherwise every endpoint answers `404`. |

Every response is JSON, even if the request has no `Accept` header.

### Response shape

```json
{ "status": "success", "data": { ... } }
```

```json
{ "status": "error", "message": "Human-readable reason" }
```

### HTTP status codes

| Code | Meaning | What the app should do |
|---|---|---|
| `200` / `201` | Success | Use `data` |
| `401` | Token missing, expired, revoked, or the customer is no longer allowed in | Drop the token, show the login screen |
| `403` | Action not allowed (e.g. replies closed, payment blocked) | Show `message` |
| `404` | Not found — or the portal is switched off | Show `message` |
| `422` | Wrong input or wrong credentials | Show `message`; field errors are in `errors` |
| `429` | Too many requests (login: 10 per minute) | Wait and retry |
| `500` | Server error | Show a generic error |

Validation error (`422`):

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "password": ["The password field is required."]
  }
}
```

### Common value formats

- **Money** is a number in Taka (`1260`, `1260.5`). The `৳` sign is added by the app.
- **Dates** are `Y-m-d H:i:s` (`2026-09-24 18:53:56`) unless noted, in Asia/Dhaka time.
- **Traffic** values are `{ "value": "561.03", "unit": "GB" }` (or `MB`).

---

## Quick Reference

| Method | Endpoint | Auth | Purpose |
|---|---|---|---|
| GET | `/config` | — | Company branding, contacts, login mode |
| POST | `/auth/login` | — | Log in, get a token |
| GET | `/auth/me` | ✅ | Customer summary |
| POST | `/auth/refresh` | ✅ | Replace the token with a fresh one |
| POST | `/auth/logout` | ✅ | Revoke the token |
| GET | `/dashboard` | ✅ | Data usage and ticket statistics |
| GET | `/connection` | ✅ | Live connection status |
| POST | `/account/reactivate` | ✅ | Reactivate a deactivated line |
| GET | `/packages` | ✅ | Current package and change options |
| POST | `/packages/change` | ✅ | Change package |
| GET | `/billing` | ✅ | Amount to pay and payment methods |
| POST | `/billing/checkout` | ✅ | Start an online payment |
| GET | `/payments` | ✅ | Payment history (paginated) |
| GET | `/payments/{id}` | ✅ | One payment receipt |
| GET | `/tickets` | ✅ | Support ticket list |
| GET | `/tickets/types` | ✅ | Complaint types for a new ticket |
| POST | `/tickets` | ✅ | Create a ticket |
| GET | `/tickets/{id}` | ✅ | Ticket with its conversation |
| POST | `/tickets/{id}/reply` | ✅ | Reply on a ticket |
| GET | `/news` | ✅ | News / announcements |
| GET | `/news/{id}` | ✅ | One news item with related items |

---

## Config

### `GET /config`

Public. Call it on app start: the login screen needs `login_mode`, and branding/contact details are shown throughout.

```json
{
  "status": "success",
  "data": {
    "company": {
      "name": "Circle Network",
      "logo": "https://your-domain/storage/company/logo.png",
      "phone": "16237",
      "billing_phone": "09612345678",
      "email": "support@circlenetworkbd.net",
      "address": "Unity Trade Center, Savar"
    },
    "login_mode": "default",
    "links": {
      "forgot_password": null,
      "new_connection": "https://your-domain/client-register/create",
      "quick_payment": "https://your-domain/clientPaymentSearch"
    }
  }
}
```

`login_mode` decides what the login screen asks for:

| `login_mode` | Global setting | First field (`cid`) | Second field (`password`) |
|---|---|---|---|
| `default` | neither of the two below | **User ID** | **Password** |
| `cid_mobile` | `customer-login-by-cid-mobileNumber` | **Customer ID (CID)** — digits only | **Mobile number** on record |
| `customer_code` | `login_customer_code_and_password` | **Customer code** | **Password** |

If both settings are on, `cid_mobile` wins. `forgot_password` is always `null` for now (there is no reset flow) — tell the customer to call `company.phone`.

---

## Authentication

### Login

#### `POST /auth/login`

| Field | Type | Required | Description |
|---|---|---|---|
| `cid` | string | ✅ | User ID, CID or customer code — see `login_mode` |
| `password` | string | ✅ | Password, or the mobile number in `cid_mobile` mode |
| `remember` | boolean | — | `true`: token lasts **30 days**. Otherwise **1 day**. Apps should send `true`. |

In `cid_mobile` mode the number may be typed with `+88`, spaces or dashes — `+880 1711-000000`, `8801711000000` and `01711000000` all match.

```bash
curl -X POST "https://your-domain/api/customer-portal/auth/login" \
  -H "Content-Type: application/json" \
  -d '{"cid":"user1168","password":"secret","remember":true}'
```

✅ `200`

```json
{
  "status": "success",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
  "token_type": "bearer",
  "expires_in": 2592000,
  "data": { "...": "same as GET /auth/me" }
}
```

❌ `422` — `{"status":"error","message":"Invalid user ID or password."}`
(the wording follows the mode: *customer ID or mobile number*, *customer code or password*). Customers pending approval, and customers of a POP without bill generation, are refused the same way.

Store `token` securely (Keychain / Keystore) and send it on every other request:

```
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...
```

### `GET /auth/me`

The customer's summary. Refresh it after a payment, package change or reactivation.

```json
{
  "status": "success",
  "data": {
    "id": 57344,
    "cid": "57344",
    "name": "MD Tushar",
    "initials": "MT",
    "username": "cbb04@tushar",
    "customer_code": null,
    "status": "active",
    "is_active": true,
    "contact_no": "01711000000",
    "email": "tushar@example.com",
    "address": "Savar",
    "package": {
      "id": 3,
      "name": "CIRCLE-GOVT-1260TK",
      "price": 1260,
      "speed": "30 Mbps",
      "upload_speed": "30 Mbps",
      "data": "Unlimited"
    },
    "balance": 0,
    "due": 1260,
    "expire_date": "20-Sep-2026",
    "expire_date_iso": "2026-09-20",
    "days_left": -10,
    "billing_type": "monthly",
    "can_reactivate": false
  }
}
```

| Field | Notes |
|---|---|
| `status` | `active`, `inactive`, `deactive`, `expired`, `closed` … use `is_active` for a simple on/off |
| `balance` | Advance balance (never negative) |
| `due` | Amount owed (never negative) |
| `days_left` | Days until expiry; negative = already expired |
| `billing_type` | `monthly` or `day_to_day` |
| `can_reactivate` | `true` → show a **Reactivate** button ([see below](#post-accountreactivate)) |
| `package.speed` / `upload_speed` | `null` when not set on the package |

### `POST /auth/refresh`

Returns a new token with the same lifetime (same body as login). The old token stops working immediately. Useful to keep an app signed in: refresh when the token is within a few days of expiry.

### `POST /auth/logout`

Revokes the token. `{"status":"success","message":"Logged out"}`

---

## Dashboard

### `GET /dashboard`

```json
{
  "status": "success",
  "data": {
    "usage": {
      "enabled": true,
      "download": { "value": "561.03", "unit": "GB" },
      "upload":   { "value": "52.93",  "unit": "GB" },
      "total":    { "value": "613.96", "unit": "GB" }
    },
    "tokens": {
      "total": 46,
      "pending": 1,
      "monthly":   { "used": 128,  "avg": "32 / week",    "labels": ["Week 1","Week 2","Week 3","Week 4"], "data": [28,34,31,35] },
      "quarterly": { "used": 310,  "avg": "103 / month",  "labels": ["Month 1","Month 2","Month 3"],        "data": [95,102,113] },
      "yearly":    { "used": 1180, "avg": "295 / quarter","labels": ["Q1","Q2","Q3","Q4"],                  "data": [260,310,340,270] }
    }
  }
}
```

- `usage` — hide the data-usage card when `enabled` is `false` (global setting `radius_customer_bandwidth_limit`).
- `tokens` — support tickets: this month by week, this quarter by month, this year by quarter.

### `GET /connection`

Live status of the line. It asks the router, so it can take a few seconds — load it separately, not with the rest of the screen.

```json
{
  "status": "success",
  "data": {
    "show_traffic": true,
    "type": "pppoe",
    "online": true,
    "ip_address": "10.20.30.40",
    "mac": "AA:BB:CC:DD:EE:FF",
    "vendor": "TP-LINK TECHNOLOGIES CO.,LTD.",
    "connected_since": "30/Sep/2026 09:12:05 AM",
    "uptime": "4h18m2s",
    "last_logout": "29/Sep/2026 11:40:10 PM",
    "session_download": { "value": "3.42", "unit": "GB" },
    "session_upload":   { "value": "410.10", "unit": "MB" }
  }
}
```

- `type` is `pppoe` or `static`. For a static IP line `online`, session times and traffic are `null`.
- `show_traffic` mirrors the global setting `show-traffice` — hide the traffic figures when `false`.
- Any field the router could not give is `null`. `last_logout` can be a sentence such as `"User not connected Yet."`.

### `POST /account/reactivate`

For a customer deactivated for more than a month past expiry, when the global setting `customerPortalReactive` is on — the same **Reactive** button as the old portal. Call it only when `/auth/me` says `can_reactivate: true`.

It switches the line back on and raises the bills owed, so after success send the customer to **Bill pay**.

✅ `200` — `{"status":"success","message":"Re Active is successfull","data":{ ...same as /auth/me... }}`
❌ `422` — `{"status":"error","message":"Client re-active is not open"}`

---

## Packages

### `GET /packages`

```json
{
  "status": "success",
  "data": {
    "current": { "id": 3, "name": "CIRCLE-GOVT-1260TK", "price": 1260, "speed": "30 Mbps", "upload_speed": "30 Mbps", "data": "Unlimited" },
    "change_enabled": true,
    "options": [
      { "id": 4, "name": "CIRCLE-PREMIUM-2000TK", "price": 2000, "speed": "50 Mbps", "data": "Unlimited", "is_upgrade": true, "cost": 740 }
    ]
  }
}
```

- `change_enabled` — global setting `package_change_from_customer_portal`. When `false`, `options` is empty; hide package change.
- `options` — packages the customer may move to (only higher ones when `client_can_change_only_higher_package` is on).
- `cost` — charged now for the rest of this cycle. `0` for a downgrade.

### `POST /packages/change`

| Field | Type | Required |
|---|---|---|
| `package_id` | integer | ✅ an `id` from `options` |

The `cost` is taken from the customer's **advance balance**; the change is refused if the balance is lower.

✅ `200` — `{"status":"success","message":"Package changed successfully.","data":{ ...same as /auth/me... }}`
❌ `400` — `Insufficient balance to change package.` · `403` — package change is off · `422` — package not available

> ⚠️ Package changes cannot be refunded — ask the customer to confirm first.

---

## Billing & Payments

### `GET /billing`

```json
{
  "status": "success",
  "data": {
    "amount": 1260,
    "editable": true,
    "blocked": false,
    "external_url": null,
    "methods": [
      { "id": "bkash", "name": "bKash", "type": "mobile", "logo": "https://.../bkash.png" },
      { "id": "nagad", "name": "Nagad", "type": "mobile", "logo": "https://.../nagad.png" }
    ]
  }
}
```

| Field | Notes |
|---|---|
| `amount` | Suggested amount (due, or the package price when nothing is due) |
| `editable` | `false` → the customer cannot change the amount; the server uses `amount` anyway |
| `blocked` | `true` → payment is not allowed (deactivated with no due); show a "contact support" message |
| `external_url` | When set, payments are taken on another site: open this URL instead of the methods |
| `methods[].id` | `bkash`, `bkash_checkout`, `nagad`, `upay`, `ssl`, `uddokta`, `eps`, `surjopay`, `paystation` — only the ones switched on |
| `methods[].type` | `mobile` (mobile banking) or `card` (card & wallet) |

### `POST /billing/checkout`

| Field | Type | Required | Description |
|---|---|---|---|
| `method` | string | ✅ | A `methods[].id` from `/billing` |
| `amount` | number | ✅ | 1 – 1,000,000. Ignored when `editable` is `false`. |

```json
{
  "status": "success",
  "data": {
    "url": "https://your-domain/customer-portal/pay/Xk29fQ...",
    "return_url": "https://your-domain/customer-portal",
    "amount": 1260,
    "method": "bKash"
  }
}
```

❌ `403` payment blocked · `422` method not available / nothing to pay

#### Paying from a mobile app

The gateways run as web pages, so the payment itself happens in a browser view:

1. Call `POST /billing/checkout`.
2. Open `data.url` in a **WebView** (or an in-app browser tab). It is single-use and valid for **10 minutes** — do not cache it.
3. The customer pays on the gateway's page.
4. When the gateway is done, the WebView is sent to `data.return_url` with the outcome in the query string:
   - `https://your-domain/customer-portal?payment=success&message=Payment%20Successful#/dashboard`
   - `https://your-domain/customer-portal?payment=failed&message=Transaction%20is%20Canceled#/billing`
   - or without `payment` when the gateway gave no message — treat that as "finished, check the balance".
5. Watch the WebView's URL; as soon as it starts with `return_url`, **close it**, read `payment` / `message`, and show the result.
6. Call `GET /auth/me` (new balance / due / expiry) and `GET /payments`.

The message is only for display — the real result is the updated balance and payment history.

### `GET /payments?page=1`

10 per page, newest first.

```json
{
  "status": "success",
  "data": [
    { "id": 90211, "amount": 1260, "discount": 0, "method": "bkash", "receipt_number": "MR-000912",
      "description": "Bill payment via bKash", "date": "2026-09-02 11:20:04" }
  ],
  "meta": { "current_page": 1, "last_page": 3, "total": 25 }
}
```

### `GET /payments/{id}`

A receipt, with the same details as the old portal's invoice.

```json
{
  "status": "success",
  "data": {
    "id": 90211,
    "receipt_number": "MR-000912",
    "amount": 1260,
    "discount": 0,
    "method": "bkash",
    "transaction_id": "BK7A2X9QPL",
    "description": "Bill payment via bKash",
    "date": "2026-09-02 11:20:04",
    "customer": { "cid": "57344", "name": "MD Tushar", "username": "cbb04@tushar", "package": "CIRCLE-GOVT-1260TK" },
    "company": { "name": "Circle Network", "address": "Unity Trade Center, Savar", "logo": "https://.../logo.png", "terms": "..." }
  }
}
```

❌ `404` — not found, or not this customer's payment.

---

## Support Tickets

### `GET /tickets`

```json
{
  "status": "success",
  "data": [
    {
      "id": 5321,
      "number": "1005321",
      "status": "pending",
      "title": "Slow speed",
      "category": "Internet",
      "description": "Speed drops every evening",
      "date": "2026-09-24 18:53:56",
      "assigned_to": "Rahim",
      "notes_count": 2,
      "can_reply": true
    }
  ]
}
```

- `id` is used in URLs (`/tickets/{id}`); show `number` to the customer.
- `status`: `pending` (open) or `completed` (closed).
- `assigned_to`: `null` → show "Call centre".
- `description` is `""` while the global setting `hide-token-details-in-customer-portal` is on.

### `GET /tickets/types`

Complaint types for a new ticket.

```json
{
  "status": "success",
  "data": [
    { "value": 7, "category_id": 2, "label": "Slow speed", "description": "Internet" }
  ]
}
```

### `POST /tickets`

| Field | Type | Required | Description |
|---|---|---|---|
| `type` | integer | ✅ | A `value` from `/tickets/types` |
| `description` | string | ✅ | What happened, max 500 characters |

✅ `201` — `{"status":"success","message":"Ticket created","data":{ ...one ticket as in the list... }}`

### `GET /tickets/{id}`

A ticket with its conversation. Only messages meant for the customer are included; internal staff notes never are.

```json
{
  "status": "success",
  "data": {
    "id": 5321,
    "number": "1005321",
    "status": "pending",
    "title": "Slow speed",
    "category": "Internet",
    "description": "Speed drops every evening",
    "date": "2026-09-24 18:53:56",
    "assigned_to": "Rahim",
    "notes_count": 2,
    "can_reply": true,
    "notes": [
      { "id": 88, "from": "support",  "author": "Rahim", "message": "We are checking your line.", "date": "2026-09-24 19:10:02" },
      { "id": 91, "from": "customer", "author": null,    "message": "Still slow today.",          "date": "2026-09-25 20:01:44" }
    ]
  }
}
```

Show `from: "customer"` messages on the right as the customer's own.

### `POST /tickets/{id}/reply`

| Field | Type | Required |
|---|---|---|
| `message` | string | ✅ max 2000 characters |

Only when `can_reply` is `true` (ticket still open and `hide-token-details-in-customer-portal` off).

✅ `201` — `{"status":"success","message":"Reply sent successfully","data":{ "id": 92, "from": "customer", "author": null, "message": "...", "date": "..." }}`
❌ `403` replies closed · `404` not this customer's ticket

---

## News

Announcements written by staff under **Settings → Customer Portal News**.

### `GET /news`

```json
{
  "status": "success",
  "data": [
    {
      "id": 1,
      "important": true,
      "date": "19 Sep 2026",
      "icon": "megaphone",
      "image": "https://your-domain/storage/customer-portal-news/lan-cache.jpg",
      "en": { "title": "LAN Cache is Now Active", "summary": "Enjoy faster access..." },
      "bn": { "title": "LAN Cache এখন চালু", "summary": "..." }
    }
  ]
}
```

- Show `en` or `bn` by the app's language. `bn` falls back to the English text when no Bangla was written.
- `image` can be `null`. `icon` is a [Lucide](https://lucide.dev/icons) icon name: `megaphone`, `newspaper`, `map-pin`, `headphones`, `wifi`, `zap`, `gift`, `wrench`, `shield`, `bell-ring`.

### `GET /news/{id}`

Same fields plus `body` in `en` / `bn`, and up to 3 `related` items:

```json
{
  "status": "success",
  "data": { "id": 1, "...": "...", "en": { "title": "...", "summary": "...", "body": "Full text..." }, "bn": { "...": "..." } },
  "related": [ { "id": 2, "...": "..." } ]
}
```

---

## Integration Checklist

- [ ] On start: `GET /config` → build the login screen from `login_mode`.
- [ ] Login with `remember: true`; keep the token in secure storage.
- [ ] Send `Authorization: Bearer <token>` on every request.
- [ ] On any `401`: delete the token and go to the login screen.
- [ ] Home: `GET /auth/me` + `GET /dashboard`; load `GET /connection` separately.
- [ ] Show **Reactivate** only when `can_reactivate` is `true`.
- [ ] Payments: `POST /billing/checkout` → open `url` in a WebView → close it when the URL starts with `return_url` → refresh `/auth/me`.
- [ ] Hide package change when `change_enabled` is `false`, the data card when `usage.enabled` is `false`, and replies when `can_reply` is `false`.
- [ ] Use HTTPS only.
