# 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)

### 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).
- **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. When both a special slab and a normal
  slab match the same sale, **the special slab always wins**, regardless of which has the narrower
  quantity/price range — the narrowest-range tie-break (below) only applies to compare slabs within the
  same tier (two overlapping specials, or two overlapping normals). To revert a slab back to "normal",
  clear both `start_date` and `end_date` together via `/incentive/slab/update`.
- **Incentive is only applied once a sale is fully paid.** It is *not* computed at sale creation or on
  price/quantity/product edits — it is driven entirely by `LeadSale::syncPaymentStatus()`, the same method
  that recomputes `payment_status` (called from sale creation with initial payments, and from every
  payment create/update/delete and sale edit):
  - When `payment_status` becomes `paid`, `SaleIncentiveService::findMatchingSlab()` looks for an active
    slab matching the sale's `product_id`, `created_by` (the lead creator), whose ranges contain the sale's
    current quantity and price, and — for special slabs — whose date window contains the sale's `sale_date`.
    If multiple slabs match, a special-occasion slab always outranks a normal one; within the same tier the
    narrowest combined range wins (ties broken by lowest id). The matched slab's `incentive_percentage` is
    applied to the sale's `total` to compute `incentive`; `incentive_slab_id`/`incentive_percentage` are
    stored on the `lead_sales` row and `incentive_applied_at` is stamped.
  - If no slab matches, `incentive` is `0` and `incentive_slab_id` is `null`, but `incentive_applied_at`
    is still stamped (the sale is "settled", just with no incentive).
  - If a previously `paid` sale drops back to `partial`/`unpaid` (a payment is edited or deleted), the
    incentive is **reversed**: `incentive`/`incentive_slab_id`/`incentive_percentage` are reset and
    `incentive_applied_at` is cleared. Incentive always reflects the sale's *current* fully-paid state.
- 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 | create, read, update, delete, override, export, contact |
| `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 | read, update, delete, override, export |
| `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 `******`
