# Ready Broker Backend

NestJS + MongoDB (Mongoose) API. Modules: auth, admin, staff, chat, call, notification, uploads, email-template, languages, pages, settings, backup, common (helpers).

## Response rules (mandatory)

Every API response must use exactly one of these two `CommonService` methods (`src/common/common.service.ts`). Do not return raw objects, and do not use `ResponseHelper` or any other response shape.

1. Normal (single object, list without pagination, or no data):

```ts
return this.common.successResponse(message, data);
// { status: 'success', message, data }
```

2. Paginated data:

```ts
return this.common.paginatedResponse(message, data, total, page, limit);
// { status: 'success', message, data, pagination: { total, page, limit, totalPages, hasNextPage, hasPrevPage } }
```

Errors are thrown as `HttpException` with a translated message (see below), never returned as a custom body.

## Translation rules (mandatory)

Every message we send to a client, in a response, an error, a notification or an email, must be translated into the user's language. Never hardcode English text in `successResponse`, `paginatedResponse`, `HttpException` or a notification.

### API responses and errors

- Keys live in `i18n/api-response/en.json` and `i18n/api-response/hi.json`. Add every new key to **both** files.
- Keys are `UPPER_SNAKE_CASE`, e.g. `STAFF_CREATED`. Parameters use `{name}`.
- Translate with `TranslationService`:

```ts
const lang = this.common.getUserLanguage(req);
return this.common.successResponse(
    this.translationService.translate('STAFF_CREATED', lang),
    data,
);

throw new HttpException(
    { message: this.translationService.translate('USER_NOT_FOUND', lang) },
    HttpStatus.NOT_FOUND,
);
```

- `getUserLanguage` resolves `req.lang`, then the passed language, then `req.user_data.language`, then `'en'`. Unknown languages and missing keys fall back to English, then to the key itself. A key showing up in a response means it is missing from the JSON files.

### Notifications

- Store **keys** (not text) in the notification `title` and `message` fields. Keys live in `i18n/notification/en.json` and `i18n/notification/hi.json`; add to both.
- Translate when reading or sending, using `translateKey(lang, key, I18nType.NOTIFICATION)` or `processNotificationsWithTranslation(notifications, lang)` from `CommonService`. Push notifications must be translated using the recipient's language, not the sender's.

### Emails

- Templates are stored in the `email-templates` collection and seeded from `email-templates.json` (each has `en` and `hi`). Send through `EmailService.sendEmailFromDBTemplate` or its helpers, passing the recipient's language.
- Brand name comes from `APP_NAME` (`Ready Broker`) through the `{{appName}}` variable. Do not hardcode it.

## Roles

`UserType` (`src/user/schema/users.schema.ts`): `SUPER_ADMIN`, `POLICY_STAFF`, `ADMIN_STAFF`, `AGENT`. `POLICY_STAFF` and `ADMIN_STAFF` are the two staff types (`STAFF_ROLES`).

- `SUPER_ADMIN`, `POLICY_STAFF` and `ADMIN_STAFF` log in through the admin panel (`PANEL_ROLES`, `POST /admin/auth/login`). Access is role-based only (there is no per-module permission matrix).
- `AGENT` logs in through a separate flow (not built yet). Do not accept `AGENT` on the admin login.
- Restrict routes with `@UseGuards(AuthGuard, RolesGuard)` and `@Roles(...)`.

## Admin auth routes

`POST /admin/auth/login`, `POST /admin/auth/forgot-password` (emails a single-use link to `ADMIN_PANEL_URL/reset-password?token=...`), `POST /admin/auth/reset-password` (`token` + `password`), `PATCH /admin/auth/change-password`, `POST /admin/auth/logout`, `GET|PATCH /admin/profile`. `POST /admin/auth/accept-invite` (`token` + `password`) sets a new staff member's first password.

## Staff (in-house) rules

- Routes `/admin/staff`: `POST` add, `GET` list (`user_type`, `status` ACTIVE/INACTIVE/INVITED, `search`), `GET :id` (with activity log), `PATCH :id` (edit, change role, `is_active`), `POST :id/resend-invite`, `DELETE :id`. Open to `SUPER_ADMIN` and `ADMIN_STAFF`.
- Only `SUPER_ADMIN` can add, change the role of, deactivate or remove an `ADMIN_STAFF` (Sub-Admin); nobody can change or remove themselves.
- Adding staff emails an invite link (`ADMIN_PANEL_URL/accept-invite?token=...`, valid 48 h). Invited staff cannot log in until they accept. Deactivating or changing a role ends their sessions.
- The audit trail (staff and agents) lives in the `activity-logs` collection, not on the user. Write with `ActivityService.log(subjectId, action, actor, meta)` or `logMany(...)`, read with `timeline(subjectId, translate)` (all from `CommonModule`). Each `action` needs an i18n key `ACTIVITY_<action>`; `meta.role` and `meta.reason` fill `{role}` and `{reason}`. Logging is best effort and never fails the action.
- `AdminSeeder.migrateUserActivity()` moves any old embedded `users.activity` arrays into that collection at startup (idempotent).
- Catalog reads (categories, insurers) are open to all panel roles; writes are `SUPER_ADMIN` and `ADMIN_STAFF`.

## Admin panel spec (source: `../readybroker-admin.html`, do not edit it)

The HTML is the UI prototype the API is built against. Build modules one at a time when asked; match its fields, filters and rules. Currency is INR, dates show as `DD MMM YYYY`.

**Role names in the UI:** Master Admin = `SUPER_ADMIN`; Policy Staff = `POLICY_STAFF`; Sub-Admin = `ADMIN_STAFF`; Agent / POSP = `AGENT` (uses a separate mobile app, not this panel). Login screen is for admin and staff only.

**Screens and what each needs from the API**

| Screen | Data and actions |
|---|---|
| Auth | login, forgot password (link valid 30 min), profile (name, email, mobile), change password (current, new, confirm). Built. |
| Dashboard | GWP this month and % vs last month, policies booked, active agents and pending onboarding, lead conversion %, monthly premium trend (FY), product mix by category, top performers (agents and staff by premium this quarter). |
| Reports | agent-wise, product-wise, insurer-wise, commission (earned/paid/payable), renewal (30/60/90 days), lead conversion. Each filterable by month or quarter and exportable. |
| Agents and Staff | Agents tab: list with search and KYC status filter (Pending KYC / Approved / Rejected), invite agent (name, mobile, email, region, products to assign, sends KYC onboarding link), detail with profile, region, assigned products, KYC (IRDAI number, training hours, PAN, Aadhaar, training certificate, per-document Verified status and view), approve or reject, deactivate, activity log, commission tab, policies tab. Staff tab: Policy Staff and Sub-Admins, add staff (name, mobile, email, role; invite link to set password), detail with role change, deactivate, activity log, leads handled, policies issued. |
| Customers | one record per customer: name, mobile, email, address; list shows active policies, total premium, last activity; detail shows policies (with insured family members), claims, leads and enquiries. |
| Policies | manual offline entry: policy number, assign to agent or staff, customer, product and insurer (only products that person is authorised to sell), gross premium, frequency (one-time, monthly, quarterly, annually), net premium excl. GST and agent commission auto-calculated, issue and expiry date, schedule PDF upload. List filters: product, insurer, assignee, issued period, search. Shows days left to expiry. |
| Leads pipeline | kanban stages New, In Discussion, Quotation Shared, Converted, Lost (drag to move). Capture lead: customer name, mobile, product category, assignee, estimated premium, notes. Lead detail: last call, next follow-up, notes history with author, convert to policy, quote comparison. |
| Quote comparison | per lead: product category, 2 to 5 insurer quotes (insurer, gross premium, coverage or IDV, add-ons), AI summary (recommended, lowest premium), WhatsApp text and PDF export. |
| Renewals | policies expiring in 30, 60 or 90 days, premium at risk, urgent count (under 30 days), retention %, dispatch renewal notice. |
| Claims | log claim (policy, estimated amount, incident date, notes), status Filed, In Process, Settled, Rejected; settled amount when settled; insurer claim reference (optional); status timeline with note and author. |
| Commission and payout | agents only (staff are salaried). Per policy: gross premium, rate, commission, Pending or Paid with date, mark paid, filter by agent, search, CSV export. Totals: pool, disbursed, pending. |
| Products and commission | product name, category, insurer, GST rate %, agent commission as percentage of net premium or flat amount per policy, status, edit. Filter by category. |
| Settings | Categories and Insurer Directory. Built. |
| Topbar | notification bell, profile menu. |

**Rules to keep**
- Net premium = gross premium / (1 + GST rate). Agent commission comes from the product rule: percentage of net premium or a flat amount.
- Agents can only sell products assigned to them; policies and leads pick from that list.
- Deactivating a category or insurer hides it from new products only; existing data is unchanged.
- Sub-Admins have full portal access except adding or removing other Sub-Admins, which only Master Admin can do.
- Regions are free text ("Mumbai, Maharashtra") and can be added on the fly.
- A lead is created as soon as a customer enquires, before any policy exists.

**Build status:** done: auth and profile, staff (Policy Staff and Sub-Admin), categories, insurers. Staff detail's leads-handled and policies-issued tabs wait on the leads and policies modules. Agents: invite, OTP login, KYC submission, review loop and notifications done. Customers and leads (capture, board, notes, follow-up, assignment) done. Products and policies (with lead conversion) done. Not started: quote comparison, renewals, claims, commission, reports, dashboard, notifications feed.

## Agents (POSP) rules

- Agents are `users` with `user_type: AGENT` and flat agent fields on the user (no nested object): `agent_no`, `agent_code` (`AG-8041`), `agent_status`, `region_id`, `category_ids`, `invited_by`, `invited_at`, `accepted_at`, `irdai_number`, `training_hours`, `kyc_documents`, `kyc_submitted_at`, `kyc_reviewed_by`, `kyc_reviewed_at`, `kyc_reject_reason`. Email and mobile are both required at invite and unique among agents (older agents may have no email). `users.email` is unique only when set (partial index; `AdminSeeder` runs `syncIndexes()` at startup to migrate older databases).
- Flow: **Invited -> OTP login -> document submission -> Under review -> Approved -> dashboard**; if rejected: **Rejected (with note) -> resubmit -> Under review -> ...** until approved. Stored `agent_status`: `INVITED`, `ACCEPTED` (after first OTP login), `SUBMITTED` (under review), `APPROVED`, `REJECTED`. Document status: `PENDING`, `APPROVED`, `REJECTED`.
- Admin routes `/admin/agents` (`SUPER_ADMIN`, `ADMIN_STAFF`): `POST` invite, `GET` list (filters `status` (`PENDING` covers invited, accepted and submitted), `region_id`, `category_id`, `search`), `GET summary`, `GET :id`, `PATCH :id`, `POST :id/approve`, `POST :id/reject` (mandatory `reason`), `POST :id/reopen` (rejected -> back to review), `POST :id/resend-invite`. Approve and reject only work on `SUBMITTED`.
- Approve or reject: sets the documents to `APPROVED` or `REJECTED`, stores an in-app notification, pushes to the agent's devices (FCM tokens saved at login, text in the agent's language), and emails the agent if they have an email (`agent-approved`, `agent-rejected` templates). Use `NotificationService.notifyUser()` for any push; it never throws.
- Invite: **email only, no SMS invite.** Email is required at invite. The link is `AGENT_APP_URL/login?phone=<number>` (template `invite-broker`); it carries no secret, the agent proves identity with the OTP. If the email fails the agent is still saved and the response has `email_sent: false`; use `POST :id/resend-invite` (needs an email on file).
- The agent module is one `AgentController` and one `AgentService` serving two audiences with different URL prefixes. Every route has a role guard: `/admin/agents/*` is `SUPER_ADMIN` and `ADMIN_STAFF` only (`AdminOnly()`), `/agent/*` is `AGENT` only (`AgentOnly()`), `/agent/dashboard` also needs `AgentApprovedGuard` (`ApprovedAgentOnly()`), and only `send-otp` and `verify-otp` are public. Add new agent routes with one of those decorators.
- Agent app routes: `POST /agent/auth/send-otp` (404 if the number was not invited), `POST /agent/auth/verify-otp` (returns `token` plus the agent view), `POST /agent/auth/logout`, `GET /agent/me`, `POST /agent/kyc` (first submission and every resubmission; needs all four documents, valid PAN, IRDAI number, training hours; optional name and email), `GET /agent/dashboard` (placeholder behind `AgentApprovedGuard`, 403 until approved).
- The agent view carries `review_status` (`DOCUMENTS_REQUIRED`, `UNDER_REVIEW`, `REJECTED`, `APPROVED`), `next_screen` (`DOCUMENT_SUBMISSION`, `DOCUMENT_REVIEW` for under review and rejected, `DASHBOARD`), `can_resubmit` and `reject_reason`. Put `AgentApprovedGuard` after `AuthGuard` on every agent route that needs an approved agent.
- OTP is mocked as `1234` while `OTP_MOCK` is not `false`; there is no attempt limit yet. Set `OTP_MOCK=false` and wire a real provider into `SmsService.sendOTP` before going live.
- Files are uploaded first with `POST /uploads/file`; send back `{file_name, base_url, folders}`.

## Regions rules

- Regions are a collection (`/admin/regions`, same shape as categories): `POST` and `PATCH :id` (`SUPER_ADMIN`, `ADMIN_STAFF`), `GET` and `GET :id` (all panel roles). Names are free text such as "Mumbai, Maharashtra", unique ignoring case; `is_active` hides a region from new agents while existing agents keep it.
- Never store a region as a string. Reference it by `region_id` (agents use `region_id`; responses return `region: {_id, name, is_active}`), and validate new assignments against an active region. Adding a region inline in a form means calling `POST /admin/regions` first.
- `AdminSeeder.migrateAgentFlatten()` (startup, idempotent) converts old agents that still have a nested `agent` object into the flat fields, turns a legacy region name string into a region record, and renames the old `VERIFIED` document status to `APPROVED`.

## Customers and leads rules

- Customers (`/admin/customers`, all panel roles): one record per mobile (`country_code` + `phone_no` unique); `POST`, `GET` (search name, mobile, email; shows `leads_count`, `open_leads`, `last_activity_at`), `GET :id` (profile + leads; policies and claims to be added by those modules), `PATCH :id`. No delete.
- Leads (`/admin/leads`, all panel roles): `POST` capture, `GET` list (filters `stage`, `assigned_to`, `category_id`, `customer_id`, `follow_up_due`, `search`), `GET board` (a column per stage with counts), `GET assignees?category_id=`, `GET :id`, `PATCH :id` (category, assignee, estimated premium, notes), `PATCH :id/stage`, `POST :id/notes`.
- Capturing a lead finds the customer by mobile or creates it. Stages: `NEW` -> `IN_DISCUSSION` -> `QUOTATION_SHARED` -> `CONVERTED` or `LOST`. `LOST` can be reopened; `CONVERTED` is locked (the policy module will convert leads). Closing a lead clears its follow-up.
- Assignee must be an active Policy Staff or an APPROVED agent whose `category_ids` include the lead's category. Policy Staff see and work only leads assigned to them and can only assign to themselves; `SUPER_ADMIN` and `ADMIN_STAFF` see all.
- Lead history is embedded in the lead (`history`): `EVENT` entries store an action key translated on read (`LEAD_ACTIVITY_<action>`, stage names `STAGE_<STAGE>`); `NOTE` and `CALL` entries are the text people typed. `is_call` on a note sets `last_call_at`; `next_follow_up_at` is epoch ms and must be in the future.
- A lead can only reach `CONVERTED` through `POST /admin/leads/:id/convert-to-policy` (see Policies), not the stage endpoint.
- Not built yet: quote comparison and AI summary on a lead.

## Products rules

- Routes `/admin/products`: `POST` and `PATCH :id` (`SUPER_ADMIN`, `ADMIN_STAFF`); `GET`, `GET :id`, `GET :id/calculate?gross_premium=` for all panel roles. The list filters `category_id`, `insurer_id`, `is_active`, `search`, and `assignee_id` (an agent only gets products in their approved categories; Policy Staff get all).
- A product has name, category, insurer, `gst_rate` (percent), `commission_type` (`PERCENT_OF_NET` or `FLAT`) and `commission_value` (percent 0 to 100, or rupees per policy). Name is unique per insurer, ignoring case.
- New products need an active category and an active insurer; if the insurer lists categories it must include the product's category. Deactivating a category or insurer does not touch existing products.
- Net premium = gross / (1 + GST%); commission = net x % or the flat amount. Use `ProductService.calculatePremium()` for policies, and snapshot the result on the policy so later product edits do not change old policies.
- Policy Staff never see commission fields (commission is for agents only).
- Adding a category or insurer "inline" from the product form is done by the client calling the category or insurer endpoints first.

## Policies rules

- Routes `/admin/policies` (all panel roles; `POST :id/cancel` is `SUPER_ADMIN` and `ADMIN_STAFF` only): `POST` enter, `GET` list (filters `category_id`, `product_id`, `insurer_id`, `assigned_to`, `customer_id`, `status` ACTIVE/EXPIRED/CANCELLED, `issued` LAST_30_DAYS/LAST_90_DAYS/THIS_QUARTER, `expiring_in_days`, `search` over policy number, customer, insurer), `GET :id`, `PATCH :id`, `POST :id/cancel`. Converting a lead is `POST /admin/leads/:id/convert-to-policy` (customer and default assignee come from the lead; the product must be in the lead's category).
- Dates are epoch milliseconds. Policy number is unique ignoring case and stored uppercase. `status` in responses is derived: cancelled, expired (past `expiry_date`) or active, plus `days_left`.
- The assignee is an approved agent whose categories include the product's category, or a Policy Staff. Policy Staff see only policies they handled and can only assign to themselves.
- Net premium, GST and commission are calculated by `ProductService.calculatePremium()` and **snapshotted** on the policy (`gst_rate`, `commission_type`, `commission_value`, `commission_amount`), so later product edits do not change old policies. Editing `gross_premium` recalculates from those saved rates and is refused once the commission is `PAID`.
- `commission_status`: `PENDING` for agent policies, `NOT_APPLICABLE` for Policy Staff, `PAID` (set by the commission module) and `CANCELLED` (when the policy is cancelled while pending). Commission fields are hidden from Policy Staff.
- Customer detail returns `policies` and the customer list returns `active_policies` and `total_premium` (active policies only). Claims for the customer detail wait for the claims module.

## Conventions

- Import paths use the `src/...` alias.
- Guards: `AuthGuard` for authenticated routes, `@Public()` to opt out, plus the role and permission guards in `src/guard`.
- DB models are registered in `src/models` (`ModelsService`); inject `ModelsService` instead of registering schemas per module.
- Run `npx tsc --noEmit -p tsconfig.json` before finishing a change.
