# Business Rules

## Application Workflows

### Lead Lifecycle

```
[Raw Lead] -> [Verified Lead] -> [Client]
                |
                v
         [Not Interested]
                |
                v
          [Call Back]
```

- Leads are created as "raw" (default status)
- `POST /lead/verified/convert` - Converts raw to verified (sets `cnvt_to_verified_at`)
- `POST /lead/client/all/fetch` - Clients (further conversion; `cnvt_to_client_at` tracked)
- `POST /lead/not_interest/convert` - Marks as not interested
- `POST /lead/call_back/convert` - Creates callback entry
- Leads can be claimed, transferred (single/bulk), shared, and prioritized
- Lead phone numbers are normalized in `lead_phones` table (supports multiple phones per lead)
- Lead emails are normalized in `lead_emails` table (supports multiple emails per lead)

#### Inactive-Lead Reclamation

Raw and verified leads that see no activity for a configurable period are automatically released into an inactive pool visible only to holders of the `lead_management.inactive_lead.*` permission.

```
[Raw / Verified Lead]
        | no activity for lead_settings.inactivity_threshold_days
        v
[ReleaseInactiveLeadsJob] (daily 02:30)
        | detach owners, flag is_inactive, snapshot previous owners
        v
[Inactive Pool]  --- POST /lead/inactive/all/fetch (read/override)
        | POST /lead/inactive/single/transfer or /lead/inactive/bulk/transfer (update/override)
        v
[Raw / Verified Lead] (creator and/or assignee chosen by the super admin)
```

- "Activity" is the most recent of the lead's own timestamps plus every connected record: follow-ups, callbacks, notes, meetings, demos, visits, sales, quotations, tasks, ownership history, and any activity-log entry for the lead (`App\Services\LeadInactivityService`).
- Only `raw` and `verified` leads are eligible; `client` leads are never pooled.
- Release is flag-only (Approach C): the lead row stays in `leads` with `is_inactive = 1`, `created_by`/`assigned_to` nulled, and the prior owners kept in `previous_created_by`/`previous_assigned_to`. Nothing is deleted or moved to another table.
- Pooled leads are hidden from all normal lead lists (raw/verified/client/all/search/export).
- The threshold and an on/off toggle are configured via `lead/setting/*` (`lead_settings` singleton, default 30 days, enabled).
- A super admin brings pooled leads back via dedicated transfer endpoints (`lead/inactive/single/transfer`, `lead/inactive/bulk/transfer`), choosing the new creator and/or assignee; the transfer clears the inactive flags.
- Release is audited via `lead_ownership_histories` (`action` = `released`) and the activity log; transfers back are recorded with `action` = `transfer`.

### Meta Lead Ads Ingestion

```
Meta webhook (leadgen_id only)
        |
        v
X-Hub-Signature-256 verified against MetaConnection.app_secret
        |
        v
ad_webhook_logs row created (status: received)
        |
        v
Meta\ProcessMetaLead job (queue: meta)
        |
        v
Graph API fetch (full field_data) via page_access_token
        |
        v
ad_field_mappings applied (form_id + source_field -> crm_field)
        |
        v
ad_leads upserted (idempotency key: provider + provider_lead_id)
        |
        v
Lead created (company_id = null, source = 'Meta', status = 'raw')
```

- The webhook endpoint (`/webhook/meta/lead`) is never behind Sanctum — Meta has no CRM user token
  to send. Authenticity is proven by the `hub_verify_token` handshake (GET) and the
  `X-Hub-Signature-256` HMAC (POST), both checked against the single active `meta_connections` row.
- Duplicate protection is keyed on `(provider, provider_lead_id)` in `ad_leads`, not on the
  webhook's `leadgen_id` — a redelivered webhook re-fetches the same Graph lead id and the
  `crm_lead_id` already being set short-circuits a second `Lead` from being created.
- Leads from Meta always get `company_id = null` (no CRM Company is known at capture time) and
  `source = 'Meta'`, matching how the other legacy static webhook endpoints
  (`webhook_ivr_lead_capture`, etc.) attribute leads with no company.
- If a form has no `ad_field_mappings` configured yet, `MetaLeadService` falls back to Meta's
  common default field names (`full_name`/`email`/`phone_number`) so the pipeline still produces a
  usable lead rather than silently dropping it.
- A lead with no phone number after mapping cannot be created (`leads.phone` is required/unique) —
  this raises rather than silently creating a broken row, which fails the job and is visible via
  `ad_leads.status = 'failed'` / `ad_webhook_logs.error_message`.

### Meeting Lifecycle

```
[Created] -> [Arrived] -> [Started] -> [OTP Sent] -> [OTP Verified] -> [Completed]
                                                                             |
                                                                        [Interested/Not Interested]
                              [Cancelled] (any stage)
```

**Status flow rules:**
1. `meeting_arrived`: Lead arrives within the arrival window of scheduled time — `meeting_settings.arrival_window_minutes` (default 30)
2. `meeting_started`: Meeting started with GPS coordinates (`start_lat`, `start_long`)
3. `meeting_otp_send`: Sends 4-digit OTP to lead's phone number (country_code + phone)
4. `meeting_otp_verify`: Verifies OTP; clears OTP fields after success
5. `meeting_completed`: Marks completion with interest status, reason, end GPS
6. `meeting_cancel`: Can be cancelled at any stage with reason
7. Active user tracked via `meeting_users.is_active`
8. Rescheduling creates new `meeting_schedule` record (history preserved)
9. `overdue` status: a meeting is only considered overdue once `meeting_date` is more than
   `meeting_settings.overdue_buffer_minutes` (default 30) in the past (and not yet arrived/started/completed/
   cancelled). This grace buffer applies consistently to: the `status` field on meeting
   responses (`FormatsEntityData::getMeetingStatus`), the overdue meetings list
   (`MeetingController::meeting_overdue_fetch`), the dashboard "upcoming meetings" query
   (`DashboardController`), and the `meeting_overdue` reminder notification
   (`DispatchMeetingStateNotifications`).

**OTP rules:**
- 4-digit random (1000-9999)
- Expires in `meeting_settings.otp_expiry_minutes` (default 5) minutes
- Sent via SMS API to lead's phone number
- Cleared after successful verification (one-time use)

All three meeting thresholds are admin-configurable via the Settings section
(`meeting/setting/*` endpoints — see `docs/api.md`), stored in the `meeting_settings` table
and read through `App\Models\MeetingSetting::get()`.

### Login OTP (User & Client Portal Auth)

Applies to `AuthController` (`/api/login`, `/api/verify`) and `ClientAuthController`
(`/api/client/login`, `/api/client/verify`):

- 4-digit random (1000-9999), generated with `random_int()`
- Stored **hashed** (`Hash::make`) in the `otp` column, never in plaintext — verified with `Hash::check`
- Expires in 10 minutes
- Cleared after successful verification (one-time use)
- Rate-limited: max 5 OTP requests per phone per 10 minutes, and max 5 verify attempts per phone
  per 10 minutes — both return `429` once exceeded

### Demo Lifecycle

```
[Created] -> [Scheduled] -> [Started] -> [Completed]
                                       -> [Cancelled]
```

- Demos integrate with Google Calendar (optional)
- Google Meet link generated for online demos
- Attendee responses synced via Google Calendar API (`syncAttendeeResponses`)
- Multiple assignees, products, and schedules supported

### Sales & Finance Flow

1. Sale created on a lead (`/lead/sale/create`)
2. Sale has approval status: pending -> approved/rejected
3. Finance team approves via `/finance/sale/approve`
4. Bill raised via `/finance/sale/bill/mark`
5. Payments recorded via `/lead/sale/payment/create`
6. Payment status auto-synced: `unpaid` -> `partial` -> `paid` (via `syncPaymentStatus()`)
7. Incentives can be manually overridden on sales (`/lead/sale/incentive/update`, requires `report_management.sales.override`)

### Sale Incentive Slabs

- Slabs are managed via `/incentive/slab/{create,update,delete,fetch}` and are scoped to a single product **and** a single user (`user_id` is required) — there is no product-wide slab that applies to all salespeople.
- A slab also defines a `min_quantity`/`max_quantity` and `min_price`/`max_price` range (nullable max = unbounded).
- **Priority slabs**: a slab may be flagged `priority = true`. A priority slab outranks every non-priority
  match for a sale. To keep matching unambiguous, create/update **rejects** a second priority slab (same
  user + product) that overlaps an existing active priority slab on *every dimension both slabs constrain*
  (quantity, price, date window). Only the dimensions where both sides set a bound are compared — an
  unbounded dimension on either side never separates them, and two dated slabs conflict only if their
  windows intersect. Non-priority slabs have no such restriction (they are the fallback pool).
- **Special-occasion slabs**: a slab may additionally carry `start_date`/`end_date` (always set together —
  validation rejects one without the other, and `end_date` must be on/after `start_date`). Such a slab is
  only eligible while the sale's `sale_date` falls inside that window; a normal (dateless) slab is always
  eligible, subject only to its quantity/price/product/user match.
- **Match tie-break order** when more than one slab matches a sale: (1) priority slabs first, (2) then
  special-occasion over normal, (3) then the narrowest combined quantity+price range, (4) then lowest id.
- **Incentive is per-participant (creator + assignee).** Each sale is attributed to its lead creator and,
  when distinct, its assignee, split by `IncentiveSetting.creator_share_percentage` (a sole-owner sale gives
  the creator 100%). Each participant gets a `lead_sale_incentives` row; `lead_sales.incentive` is the sum.
- **Incentive is earned on the FIFO EXCESS above the sale target.** A participant earns incentive on a sale
  only when it is fully `paid`, not blacked out for them, **and** a `UserProductSaleTarget` for the product
  contains the sale's `sale_date`. When several targets contain the date, the one with the **smallest**
  `sale_amount_target` defines the consumption pool. Within that target's window (extended by
  `IncentiveSetting.monthly_buffer_days` grace), the participant's **paid, non-blackout** sales of the
  product are consumed **oldest-first** (by `sale_date`, then `id`) up to the target amount — the consumed
  portion earns nothing. Only the portion of a sale **above** the target line earns, with the slab
  percentage applied to that excess (the participant's share). A sale straddling the boundary is
  **partially** incentivized. When no target contains the sale's date, the sale is excluded
  (`target_not_met`) — only an admin recalculate with `ignore_target` pays it (full share, no FIFO).
  Slab selection is unchanged (matched on the sale's full price/quantity); only the base the percentage
  applies to shrinks to the excess. Per participant row: `full_share_amount` = `total * share`,
  `consumed_amount` = portion filling the target, `incentive_base` = earning excess, `incentive` =
  slab% * `incentive_base`.
- Incentive is driven by `LeadSale::syncPaymentStatus()` (called from sale creation with initial payments,
  and from every payment create/update/delete and sale edit) and by sale-target changes
  (`SaleTargetService::upsert`):
  - On any paid/unpaid transition, `SaleIncentiveService::recomputeForSale()` re-evaluates the trigger sale
    **and every other paid sale** sharing a containing target window (same `sale_by` + product). This is
    necessary because one sale becoming paid can push the period's paid total over target and retroactively
    qualify earlier paid sales (and the reverse on un-payment).
  - For each qualifying sale, `findMatchingSlab()` finds the best active slab (tie-break order above) and
    applies its `incentive_percentage` to the sale's **FIFO excess** (`incentive_base`), not the full total;
    `incentive_slab_id`/`incentive_percentage` are stored and `incentive_applied_at` is stamped on the paid
    transition.
  - When a sale is not paid, no containing target exists, or no slab matches, `incentive` is `0` and
    `incentive_slab_id` is `null`. A sale fully below its target line is **not** excluded: it is
    `target_consumed` (zero incentive, `incentive_exclusion_reason` null) so it still shows in the report.
  - When a sale-target is created/updated/cleared, every paid, non-locked sale for that user+product in the
    period window is recomputed, since qualification is a period-level condition.
- **Per-participant audit trail** (`lead_sale_incentives.incentive_exclusion_reason` + `incentive_status`):
  every evaluation records the outcome for each participant. `incentive_exclusion_reason` holds the
  exclusion cause — `not_paid`, `blackout`, `target_not_met`, or `no_matching_slab` — and is `null` for a
  participating row (earned or fully consumed). `incentive_status` describes a non-excluded row:
  `incentivized` (earned on the full share), `partially_incentivized` (the target boundary split the sale),
  or `target_consumed` (fully consumed by the target, zero earned). The sale row's
  `incentive_exclusion_reason` is a roll-up — `null` if any participant was not excluded, else the shared
  reason, and `locked` on manual override via `/lead/sale/incentive/update` (which deletes the participant
  rows since the amount is manually set). The incentive report shows every participating row (including
  `target_consumed`) and hides only truly-excluded rows.
- **Calculate-once with deferred settlement.** A sale's incentive is calculated **automatically once**, but
  only after its settlement date — the latest containing target window end **plus**
  `IncentiveSetting.monthly_buffer_days` — so the full period's paid sales are known before targets are
  judged. `incentive_applied_at` marks a sale as settled.
  - When a sale becomes `paid`, it settles immediately only if its settlement date has already passed
    (e.g. a late payment, or no containing target at all); otherwise it waits.
  - The `SettleDueSaleIncentives` job sweeps paid, not-yet-settled sales whose settlement date has passed
    and settles them once. It is dispatched opportunistically from the `TriggerScheduledWork` middleware
    (like `GenerateSaleTargetRenewals`), idempotent and lock-guarded.
  - **Once settled, no automatic path re-derives a sale.** Target edits and slab edits do not rewrite
    settled sales. The one automatic exception is un-payment: a sale dropping out of `paid` is reversed
    (participant rows deleted, `incentive_applied_at` cleared) so it can settle again if re-paid.
- **Admin manual recalculate** (`POST /finance/incentive/recalculate`, gate `incentive_management.incentive.override`):
  force-recomputes every paid, non-locked sale in a resolved period (optional `user_id`/`product_id`),
  **including already-settled sales**, and re-stamps them settled. When `ignore_target` is true the sale-target
  gate is skipped for that run only — a one-time exemption that is not persisted (it survives because nothing
  automatic overwrites a settled sale afterward). The sale still must be paid, non-blacked-out, and slab-matched.
- A manual override via `/lead/sale/incentive/update` sets `incentive_locked = true` on the sale. Once
  locked, `syncPaymentStatus()` never touches that sale's incentive fields again (only `payment_status`
  keeps syncing) — there is no endpoint to unlock it.

### Payment Reminders

1. A sale is a reminder candidate while `payment_status` is `unpaid` or `partial` and not deleted
   — regardless of age. Every candidate is annotated with two independent escalation flags:
- `is_overdue` — the sale's last activity (most recent `SalePayment.payment_date`, or
      `LeadSale.sale_date` if nothing has been paid yet) is more than
      `credit_settings.overdue_window_days` (default 60) old.
   - `credit_term_exceeded` — the sale's company is currently over its `credit_limit`
      (`Company::is_credit_limit_exceeded`) AND has been over it for longer than the company's
      `credit_duration_days` credit term (falls back to `credit_settings.default_credit_duration_days`,
      default 60, if unset). "How long over limit" is
      approximated as the oldest last-activity date among the company's unpaid/partial sales.
2. `POST /lead/sale/reminder/fetch` lists all candidates, each annotated with `due_amount`,
   `last_payment_at`, `days_since_last_payment`, `is_overdue`, `credit_term_exceeded`, and the
   company's credit context (`credit_limit`, `credit_duration_days`, `outstanding_balance`,
   `credit_remaining`, `is_credit_limit_exceeded`) for reference (see `Company` credit accessors
   under Company/Finance rules). An optional `bucket=current|overdue|credit_exceeded` request
   param filters to one of the three groups.
3. `SendPaymentRemindersJob` runs daily (09:00) and pushes an FCM reminder to both the lead's
   assigned user (`Lead.assigned_to`) and the sale's assigned/sales-by user, deduplicated per
   recipient. Message wording escalates by flag: routine "Payment Reminder" when neither flag is
   set, "Overdue Payment Reminder" when `is_overdue`, "Credit Limit Overdue" when
   `credit_term_exceeded`. Each send is logged in `scheduled_notification_logs` with
   `reminder_type = 'payment_reminder'`, keyed by `(user_id, notifiable_type=LeadSale,
   notifiable_id=sale_id, reminder_at=today)` so a given sale/recipient pair is reminded at most
   once per day.

### Leave Management

1. Leave types defined (Sick, Casual, Annual, etc.)
2. Leave entitlements assigned to users per period
3. User applies for leave (`/leave/application/create`)
4. Manager approves/rejects (`/leave/application/approve` or `/leave/application/reject`)
5. Approval recorded in `leave_approvals` table
6. Attendance system respects approved leaves

### Attendance Rules

- Check-in and check-out with GPS coordinates and photo
- Boolean status flags (mutually exclusive):
  - `is_present`, `is_absent`, `is_holiday`, `is_weekoff`, `is_halfday`, `is_late`
- Backfill service auto-marks attendance based on:
  - Holidays (from holidays table)
  - Week-offs (from user_working_days)
  - Approved leave applications
  - Remaining: marked absent
- Week-off is always derived per-user from `user_working_days.day_index` (`App\Support\AttendanceSchedule::isWeekOff`),
  never a hardcoded Saturday/Sunday check. A user with no working-days schedule configured is never
  auto-marked as week-off. When an attendance record already exists for the day, its stored
  `is_present`/`is_absent`/etc. always win over the schedule-derived flag in the formatted API response
  (`FormatsEntityData::formatAttendanceData`) — e.g. checking in on a day nominally outside the schedule
  still reports `status: "present"`, not `"weekoff"`.
- Location tracking during check-in/out period
- Late/halfday determined by shift settings
- Check-in is still allowed on a day with an approved leave (`attendance.leave_type_id` set): the
  punch just records `check_in_time`/lat/lng/image/remarks on top of the existing record. It does
  **not** clear `leave_type_id` or flip `is_present`/`is_absent` — those stay whatever the approved
  leave set them to, since leave applications have no full-day/half-day distinction and the leave
  status must not be silently overwritten by a punch-in.

### Task Management

- A task can be assigned to multiple users at once, via the `task_assignees` pivot table
  (`task_id`, `user_id`) — same pattern as `demo_assignees`/`project_assignees`. Create/update
  accept an `assignees` array of user IDs; the API response returns an `assignees` array (not a
  single `assignee` object).
- Any assignee (not just the task creator) may log time (`task/log/create`, `task/log/delete`),
  complete the task (`task/complete`), add a subtask (`task/subtask/create`), or stop its recurrence
  (`task/recurrence/stop`) — access is granted if the acting user is present in that task's
  `task_assignees`.
- Support recurring tasks: daily, weekly, fortnightly, monthly, quarterly, half-yearly, yearly.
  When a recurring task spawns its next occurrence, all of the original task's assignees, tags,
  and products are copied to the new occurrence, along with any subtask flagged
  `carry_forward_on_recurrence` (see below). Generation runs daily via the `GenerateRecurringTasks`
  job (`app/Jobs/GenerateRecurringTasks.php`), which can backfill several missed occurrences of the
  same task in one run. Each occurrence is generated inside its own try/catch: if one occurrence
  fails (e.g. bad copied data), it's logged and skipped without aborting the remaining occurrences
  of that task or the job's processing of other recurring tasks. A task's `has_recurred` flag is
  only flipped to `true` once its next occurrence's trigger date (due date minus the interval's
  buffer) has actually been reached in a given run — not merely because the job looked at it — so a
  task that isn't due yet stays eligible for the daily query on subsequent runs. A user with
  `task_management.task.override` can also trigger the full generation pass on demand via
  `task/recurrence/generate` — it runs `GenerateRecurringTasks` inline against every eligible
  recurring task with the exact same due-date/buffer rule as the daily scheduler (not scoped to one
  task or user), useful when the scheduler/queue worker has been down and tasks are stuck past
  their due date.
- Tasks have subtasks (Monday.com-style checklist items) — a `Subtask` (`task_subtasks` table) has
  a `title`, optional `due_date`, single `assigned_to` (independent of the task's own
  `task_assignees`), an `is_completed` flag, and a `position` used to drive drag-reorder within the
  task. Only the subtask's assignee can mark it complete (`task/subtask/complete`) — and doing so
  requires submitting a `log` and `duration_seconds` in the same request; this creates an
  `EventLog` under the *parent task* (same as `task/log/create`, not a subtask-scoped log), then
  marks the subtask complete, both inside one DB transaction. The creator (or a user with
  `task_management.task.override`) can delete a subtask (`task/subtask/delete`); any task
  assignee can reorder a task's subtasks (`task/subtask/reorder`). Subtasks cannot be added to or
  removed from a completed or overdue task.
- Each subtask has a `carry_forward_on_recurrence` flag (default `false`, set at creation via
  `task/subtask/create`). When a recurring task regenerates, only subtasks with this flag set to
  `true` are copied onto the new occurrence — with `is_completed` reset to false and, if the
  subtask had a `due_date`, that date shifted by the same number of days the task's own `due_date`
  moved for that occurrence (not a flat one-interval step — this matters when the job backfills
  several missed occurrences of the same task in a single run, keeping the subtask's due date in
  sync with its parent task's due date on every occurrence, not just the first). The copied
  subtask keeps `carry_forward_on_recurrence` set so it continues to carry forward on subsequent
  occurrences. Subtasks without the flag are one-off and do not appear on the next occurrence.
- Task stages trackable
- Products can be linked to tasks

### Campaign & Marketing Lifecycle

```
[Draft] -> [Scheduled] -> [Running] -> [Completed]
                                           |
                                      [Failed] (unrecoverable job error)
```

- A campaign sends a provider-approved template across SMS, RCS, and/or WhatsApp.
- `strategy: parallel` sends via every enabled channel to each recipient (recipient marked `sent` if any
  channel succeeds); `strategy: failover` tries channels in `priority` order, stopping at first success.
- Audience comes from exactly one of: a reusable Lead Group (preferred), an Excel upload, or a manually-typed
  test list (`select_test_audience`, capped at 100 numbers, for a quick test round before committing to a
  real audience) — selecting one source always wipes whichever of the other two the campaign previously had.
- Template variables (`{{1}}`, `{{2}}`...) resolve **per channel** — the same variable name can
  independently resolve to different values on SMS vs. RCS vs. WhatsApp within one campaign, via
  `campaign_variable_mappings.channel_type`.
- Launching requires a persistently running queue worker (`php artisan queue:work --queue=campaigns`) — a
  "launched" API response only means the job was queued, not that anything has sent yet.
- `sent` means the provider **accepted** the message; it does not mean it was delivered. There is no
  webhook receiver for provider delivery reports, so `delivered_count` reads `0` in practice — real
  delivery/rejection status must be checked on the provider's own dashboard.
- Pause/Resume/Stop endpoints exist but currently return `400` ("temporarily unavailable") — the execution
  job never re-checks campaign status mid-run, so it can't yet safely interrupt an in-progress send.

Full detail (endpoints, per-channel config quirks, known provider failure codes): see
[campaign-management.md](campaign-management.md).

### Lead Groups

- A Lead Group is a reusable, named list of leads built from explicit `lead_ids` (multi-select) — not a
  live filter. Membership is a fixed snapshot, set at creation and adjusted via add/remove-members calls.
- Managed under Lead Management (`/api/lead/group/*`); consumed by campaigns via `POST
  /api/campaign/select_lead_group` (a dedicated post-creation step, not part of campaign create/update).
- Deleting a group only soft-deletes it and never retroactively affects recipients a campaign already
  imported from it.

## User Roles & Permissions

### Permission Structure

Format: `{module}.{sub_module}.{action}`

| Module | Sub-modules | Actions |
|--------|-------------|---------|
| `account_management` | account, document | create, read, update, delete, override |
| `task_management` | task | create, read, update, delete, override |
| `project_management` | project | create, read, update, delete, override |
| `user_management` | user, live_location | create, read, update, delete, override, export, contact |
| `lead_management` | company, raw_lead, verified_lead, client, lead_group, ad_lead, inactive_lead, lead_setting | create, read, update, delete, override, export, contact, transfer |
| `quotation_management` | quotation, terms, email_body | create, read, update, delete, override, export |
| `meeting_management` | meeting | create, read, update, delete, override, export |
| `demo_management` | demo | create, read, update, delete, override, export |
| `visit_management` | visit | create, read, delete, export, override |
| `payroll_management` | payroll | create, read, update, delete, override, export |
| `product_management` | product | create, read, update, delete, override, export |
| `leave_management` | leave_type, leave_entitlement, leave_application | create, read, update, delete, override |
| `holiday_management` | holiday | create, read, update, delete, override |
| `finance_management` | finance; allocation; expense; expense_setting; reimbursement | finance: read, update, delete, override, export. allocation (budget pool: create/update/delete/fetch allocations, holder balance): create, read, update, delete, override. expense (company expenses + reimbursement approval): create, read, update, delete, override. expense_setting: read, update, override. reimbursement: create, read, update, delete, override |
| `marketing_management` | templates, campaign | create, read, update, delete, override (campaign also has launch, pause, resume, stop — though pause/resume/stop are currently disabled at the controller level, see Campaign Lifecycle above) |
| `report_management` | sales, attendances, calls, activity_log, client_analytics, user_analytics, campaign | read, update, delete, override, export, create |
| `setting` | role, smtp_credential, sms_credential, waba_credential, rcs_credential, campaign_credential, meta_credential, payment_type, api_credentials, payslip_signature | create, read, update, delete, override |

### "override" Permission
Acts as a super-admin permission for the given sub-module, bypassing ownership/data-scoping checks (e.g., viewing all leads vs. only assigned leads).

### "contact" Permission (Lead Management)
Allows viewing contact details (phone numbers, emails) of leads. Users without this see masked phone numbers.

## Notifications

- **Push notifications** via Firebase Cloud Messaging (FCM HTTP v1)
- Tokens stored in `user_app_settings.fcm_token` (mobile) and `user_app_settings.web_fcm_token` (browser)
- Mobile and web use **separate Firebase projects**; `PushNotificationService::sendToUser` delivers to
  each present token independently (one `push_notifications` record per platform, `channel` =
  `fcm_mobile` / `fcm_web`)
- Notifications triggered for upcoming activities (meetings, demos, follow-ups, callbacks)
- `DispatchUpcomingActivityNotifications` job runs every minute, checks each activity type within the
  next `notification_settings.<type>_reminder_minutes` (default 15 each) minutes — per-type keys:
  `follow_up_reminder_minutes`, `callback_reminder_minutes`, `meeting_reminder_minutes`,
  `demo_reminder_minutes` — all admin-configurable via `notification/setting/*`
- `ScheduledNotificationLog` tracks delivery status
- Activity logging via Spatie Activitylog for audit trail

## Google Calendar Integration

- Two service implementations: `GoogleCalendarService` and `OptimizedGoogleCalendarService`
- Optimized version features: circuit breaker (5 failures = 5min cooldown), token caching (1h TTL), exponential backoff (3 retries)
- Used for demo creation/update/delete/sync
- Credentials via `credentials.json` + stored tokens in `google_tokens`/`google_access_tokens` tables
- Default timezone: `Asia/Kolkata`

## Quick Actions

These are dashboard/shortcut operations:
- Lead claiming: First user to claim gets ownership (only allowed while lead is `raw` and has no prior activity); recorded in `lead_ownership_histories` as an `assigned_to` change with `action = 'claim'`
- Lead sharing: Share lead with another user
- Impersonation: Admin can impersonate another user (for troubleshooting)
- Bulk operations: Transfer, delete leads in bulk

## Analytics Reports Available

- Dashboard counts (leads, meetings, tasks, projects, demos, visits, sales)
- Call logs chart data
- Sales chart data
- Aggregated lead sales
- Attendance reports (aggregated + detailed)
- User activity logs
- User KPI tracking (calls, meetings, visits, demos, sales, notes, callbacks, follow-ups, quotations, lead conversions)
- Timesheet data per user
- Lead analytics
- User analytics
- Client-wise comprehensive report (`POST /api/lead/report`): given any single Lead id (raw, verified,
  or client — no status restriction), rolls up every related record — follow-ups, notes, callbacks,
  tasks, quotations, sales + payments, meetings, demos, visits, projects, ownership history, credit
  exceeded requests, campaign engagement, and activity log — plus a computed summary (total sales
  value, total paid, outstanding balance, task/follow-up counts). Guarded by
  `report_management.client_analytics.read`/`.override` (same module as Lead analytics), with `.read`
  scoped to leads whose `created_by`/`assigned_to` falls in the requester's `accessibleUserIdsFor`.

## Assumptions & Constraints

- **Single timezone**: Asia/Kolkata (Indian market CRM)
- **Indian mobile numbers**: Country code + phone, SMS via Indian provider (Bluewaves Media)
- **Fiscal year**: Supports both calendar (Jan-Dec) and fiscal year (custom start month)
- **No multi-language support**: English only
- **No soft deletes on all tables**: Some tables hard-delete, others soft-delete
- **Phone uniqueness**: `lead_phones.phone` has unique constraint
- **Phone masking**: For users without `contact` permission, phone numbers show `******`
