This page documents the Non-EB Opportunities workflow from the Opportunities list (/leads/list). A user creates a Non-EB opportunity, then progresses through RFQ → QCR → mail actions → placement — all from the list row action menu.

Non-EB rows are identified by leads.lead_form_type = 2 (EB = 1). RFQ and QCR are managed in Google Sheets; sheet IDs are stored in leads.misc JSON.

End-to-end flow

flowchart TD A["/leads/list — Add → Non-EB"] --> B["Add form leads_non_eb.php"] B --> C["POST /leads/create"] C --> D["status: queued"] D --> E["Action: RFQ"] E --> F["GET /leads/createRfqSheet"] F --> G["status: rfq_created"] G --> H["Action: QCR"] H --> I["GET /leads/createQcrSheet"] I --> J["status: qcr_created"] J --> K["Action: Send Internal Mail"] K --> L["POST /leads/sendMail internal"] L --> M["Action: Send Insurer Mail"] M --> N["POST /leads/sendMail insurer → rfq_sent"] N --> O["Action: Send Client Mail"] O --> P["POST /leads/sendMail client → qcr_sent"] P --> Q["Action: Placement"] Q --> R["POST /leads/sendMail placement → won"]

Typical sequence from the list:

  1. Create opportunity (form submit) → queued
  2. RFQ — create/open Google Sheet → rfq_created
  3. QCR — copy RFQ sheet → qcr_created
  4. Send Internal Mail — team mail with RFQ or QCR attachment (by current status)
  5. Send Insurer Mail — RFQ sheet attached → rfq_sent
  6. Send Client Mail — QCR sheet attached → qcr_sent
  7. Placement — placement sheet + policy/payment fields → won

Edit is available at any stage from the same action menu and reuses the add form with pre-filled data (getLeadNonEB + POST /leads/create with lead id).

Key files

AreaFile
List + action dropdownapp/Views/leads_list.php
Non-EB add/edit formapp/Views/leads_non_eb.php
EB vs Non-EB form includeapp/Views/leads_form_handler.php
All backend logicapp/Controllers/LeadsController.php
Google SheetsGoogleSheetLib, Config\RfqConfig

Google Sheet config

Non-EB RFQ, QCR, and Placement sheets are Google Drive files created at runtime by GoogleSheetLib using a service account. Only RFQ needs a pre-configured template per product; QCR and Placement are copies of the lead’s RFQ/QCR sheets.

App ↔ Google Drive flow

flowchart TB subgraph setup ["One-time / per product setup"] T["Drive: master RFQ template per product"] PT["policy_type.misc.rfq_template_sheet_id"] ENV[".env RFQ_PARENT_FOLDER_ID"] SA["Service account JSON + share folder/templates"] end subgraph rfq ["RFQ — createRfqSheet"] R1["Read template ID from policy_type.misc"] R1 --> C1["Drive copyTemplate → parent folder"] C1 --> F1["Sheets find/replace placeholders"] F1 --> P1["Drive applyPermissions viewers"] P1 --> PR1["Sheets applyProtections"] PR1 --> S1["Save leads.misc.rfq_sheet_id"] end subgraph qcr ["QCR — createQcrSheet"] S1 --> C2["Drive copyTemplate from rfq_sheet_id"] C2 --> P2["applyPermissions viewers"] P2 --> S2["Save leads.misc.qcr_sheet_id"] end subgraph placement ["Placement — createAndDownloadPlacementSheet"] S2 --> C3["Drive copyTemplate from qcr_sheet_id"] C3 --> P3["applyPermissions viewers"] P3 --> S3["Save leads.misc.placement_sheet_id"] S3 --> X1["Drive export .xlsx → mail attachment"] end subgraph mail ["Mail — downloadFileFromGoogleSheet"] S1 --> X2["Export RFQ or QCR as Excel"] S2 --> X2 end PT --> R1 ENV --> C1 T --> PT SA --> C1

Library: app/Libraries/GoogleSheetLib.php — auth via service account JSON at {project-root}/nhance-ee8d1-e3c5269b1ec7.json, scopes DRIVE + SPREADSHEETS.

What must be done before RFQ / QCR

StepBeforeWhy
1 Google service account JSON present; account email shared on template folder + each template sheet GoogleSheetLib cannot copy or edit without Drive access
2 RFQ_PARENT_FOLDER_ID set in .env Target folder for every copied RFQ/QCR/Placement file
3 Master RFQ Google Sheet template created per product (Fire, Marine, GPA, etc.) with placeholder tokens RFQ copy source — one template per policy_type row
4 policy_type.misc JSON updated with rfq_template_sheet_id for that product createRfqSheet() reads this; fails with “RFQ template not found” if missing
5 Non-EB opportunity created with correct policy_type_id and rfq_qcr_viewers emails Lead must exist; viewers become sheet editors after copy
6 Before QCR: RFQ action completed (leads.misc.rfq_sheet_id set) QCR copies the lead’s RFQ sheet, not the policy template
7 Before Placement mail: QCR action completed (leads.misc.qcr_sheet_id set) Placement copies the QCR sheet

Where to configure sheet ID per product

Template sheet IDs are stored per product on the policy_type table — one row per product (Fire, Marine, Burglary, etc.). The lead’s selected policy_type_id determines which template is copied when RFQ is clicked.

Database column: policy_type.misc (JSON text)

{
  "rfq_template_sheet_id": "1GXNDNXoWriClb5HCqPYd2GAY1T8aie_Hos0yAOoTaC0"
}

Example — set or update for policy type id 12:

UPDATE policy_type
SET misc = JSON_SET(COALESCE(misc, '{}'), '$.rfq_template_sheet_id', 'YOUR_GOOGLE_DRIVE_FILE_ID')
WHERE id = 12;
i
There is no admin UI field for rfq_template_sheet_id today — configure via DB (or extend MasterController::editPolicyType / policy_type_onboarding if you add a form field). Model allow-list: app/Models/PolicyTypeModel.php includes misc.

How to find a template file ID:

QCR and Placement: no separate template ID per product. They always copy from the lead’s existing sheets:

StageCopy sourceStored on lead
RFQpolicy_type.misc.rfq_template_sheet_idleads.misc.rfq_sheet_id
QCRleads.misc.rfq_sheet_idleads.misc.qcr_sheet_id
Placementleads.misc.qcr_sheet_id (filename: QCR → Placement)leads.misc.placement_sheet_id

App-level config files

SettingLocationPurpose
RFQ_PARENT_FOLDER_ID .envConfig\RfqConfig::$rfqParentFolderId Google Drive folder where copied RFQ/QCR/Placement files are created
rfqPlaceholders app/Config/RfqConfig.php Maps template tokens like {{INSURED_NAME}} to lead field keys filled on RFQ create
rfqClaimsPlaceholder app/Config/RfqConfig.php Default {{CLAIMS_DETAILS}} — multi-row claims table from fin_years_claims
protections app/Config/RfqConfig.php Locked cell ranges on new RFQ sheets only (e.g. RFQ Page!B12:C12)
Service account key nhance-ee8d1-e3c5269b1ec7.json (project root) Google API authentication for all sheet operations

Placeholder tokens to embed in each product’s RFQ master template:

Token in templateFilled from
{{INSURED_NAME}}Client name / short name
{{COMMUNICATION_ADDRESS}}Client or custom field address
{{GST}} / {{PAN}}Lead GST / PAN
{{POLICY_PERIOD}}Policy start – end dates
{{OPPORTUNITY_TYPE}}Fresh / Renewal label
{{RISK_LOCATION}} / {{OCCUPANCY}}Custom policy-type fields
{{CLAIMS_DETAILS}}Claim history table (renewal leads)

After RFQ copy, editors are granted from the lead’s rfq_qcr_viewers JSON email list (selected on the create/edit form). The same list is applied to QCR and Placement copies.

Step 1 — Create opportunity

From the list

  1. Open /leads/list → click Add.
  2. Modal Select Opportunity Type → choose Non-EB (lead_form_type=2).
  3. Redirect: GET /util/getLeadNonEB/2/0.

Form (leads_non_eb.php)

Controller: createLead()

Edit flow

From the list action menu → Edit (available for both EB and Non-EB):

  1. JS: getLeadsDataForEdit(lead_id, lead_form_type, actual_lead_id)
  2. Redirect: GET /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}
  3. Controller: getLeadNonEB($type, $actual_lead_id, $id) (~line 4818)
  4. Form pre-fills client, branch, policy, files, viewers, status, lost reason, etc.
  5. Submit same endpoint: POST /leads/create with hidden idupdateOldLead()

Steps 2–7 — List table actions (Non-EB)

When lead_form_type === 2, the row action menu in app/Views/leads_list.php (lines ~297–343) uses modal/AJAX flows instead of navigating to /rfq/list/{id}/1|2 (EB behaviour).

i
Menu order on screen (after opportunity is created): Edit → RFQ → QCR → Send Internal Mail → Send Insurer Mail → Send Client Mail → Placement → Email History.

Action → view handler → controller endpoint

# Action Visible when View (JS) / handler class Controller endpoint(s) Status after
Edit Always (EB + Non-EB) getLeadsDataForEdit() GET /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}
POST /leads/create (update when id posted)
User-selected on form
1 RFQ lead_form_type === 2 only .btnRfqSheetListcreateRfqSheetFromList() GET /leads/createRfqSheet?lead_id={id}
LeadsController::createRfqSheet() — opens Google Sheet URL in new tab
rfq_created
2 QCR Non-EB; status not queued or rfq_created; role 1, 5, 2, 3 or Business Support team .btnQcrSheetListcreateQcrSheetFromList() GET /leads/createQcrSheet?lead_id={id}
LeadsController::createQcrSheet()
qcr_created
3 Send Internal Mail Non-EB; status != queued .btnInternalMailListopenInternalMailFromList() GET /leads/mailTemplate?lead_id={id}&template_type=rfq
POST /leads/sendMailrecipient_type=internal
4 Send Insurer Mail Same as internal mail .btnInsurerMailListopenInsurerMailFromList() GET /leads/mailTemplate?lead_id={id}&template_type=rfq
POST /leads/sendMailrecipient_type=insurer (RFQ sheet attach)
rfq_sent
5 Send Client Mail Same as internal mail .btnClientMailListopenClientMailFromList() GET /leads/mailTemplate?lead_id={id}&template_type=qcr
POST /leads/sendMailrecipient_type=client (QCR sheet attach)
qcr_sent
6 Placement Same as internal mail .btnPlacementListopenPlacementFromList() GET /rfq/placementData/{id}
GET /leads/mailTemplate?lead_id={id}&template_type=placement
POST /leads/sendMailrecipient_type=placement
won
Email History Always (EB + Non-EB) .btnHistorygetLeadsDataForMailHistory() GET /util/getLeadEmailHistory/{id}getLeadEmailHistory()

EB contrast (same menu, lead_form_type === 1): RFQ/QCR are links to /rfq/list/{id}/1 and /rfq/list/{id}/2; internal/insurer/client/placement mail items are not shown.

Shared mail modal helpers (all in leads_list.php):

Step 2 — RFQ (createRfqSheet())

  1. Load lead + policy_type.misc.rfq_template_sheet_id.
  2. If misc.rfq_sheet_id exists → return existing Google Sheet URL.
  3. Copy template via GoogleSheetLib::copyTemplate() into RfqConfig::rfqParentFolderId.
  4. Fill placeholders (buildRfqPlaceholderData()): client, GST, PAN, policy period, claims table.
  5. Grant editors from leads.rfq_qcr_viewers email JSON.
  6. Save misc.rfq_sheet_id; set status = rfq_created.
  7. UI opens sheet in a new browser tab.

Step 3 — QCR (createQcrSheet())

  1. Requires misc.rfq_sheet_id — returns 400 if RFQ not created yet.
  2. If misc.qcr_sheet_id exists → return existing URL.
  3. Copy the RFQ sheet (not the policy template) with a QCR filename.
  4. Same editor permissions from rfq_qcr_viewers.
  5. Save misc.qcr_sheet_id; set status = qcr_created.

Steps 4–6 — Mail actions

Load template — getLeadMailTemplate()

Called before each mail modal opens. Query params: lead_id + template_type (rfq | qcr | placement). Returns subject, HTML body, and attachment checkbox HTML from lead_files.

Send — sendMailWithAttachement()

For Non-EB (lead_form_type == 2), the Excel attachment comes from Google Sheets:

recipient_typeSheet usedStatus after send
internal QCR if status is qcr_created/qcr_sent, else RFQ Unchanged
insurer misc.rfq_sheet_id rfq_sent (unless already past that stage)
client misc.qcr_sheet_id qcr_sent
placement createAndDownloadPlacementSheet() — copies QCR sheet won + saves placement/payment/installment fields

Implementation detail: downloadFileFromGoogleSheet() exports the chosen sheet to a temp .xlsx under writable/tmp/ before MailHelper::send_email().

Step 7 — Placement

Opened from list action → openPlacementFromList(leadId):

  1. GET /rfq/placementData/{id} — pre-fills policy dates, premium, CD, installments, contacts.
  2. GET /leads/mailTemplate?template_type=placement — subject/body/attachments.
  3. User fills placement modal: dates, premium/CD/total, installment rows, To/CC, optional external CC.
  4. POST /leads/sendMail with recipient_type=placement.
  5. Backend copies QCR → placement Google Sheet, attaches Excel, updates lead to won, persists placement_date, premium_amount, cd_amount, installments, etc.

Status lifecycle

StatusLabelSet by
queuedIn-QueuedCreate form (default)
rfq_createdRFQ CreatedcreateRfqSheet()
rfq_sentRFQ SentInsurer mail
qcr_createdQCR CreatedcreateQcrSheet()
qcr_sentQCR SentClient mail
wonWonPlacement mail
lostLostUser sets on edit form + lost reason

leads.misc JSON

{
  "rfq_sheet_id": "…",
  "qcr_sheet_id": "…",
  "placement_sheet_id": "…"
}

Controller reference (list flow)

Methods in LeadsController.php used by the list Non-EB flow:

MethodTriggered from
createLead()Form submit (create + edit)
getLeadNonEB()Add / Edit navigation
getPolicyTypeFields()Policy type change on form
createRfqSheet()RFQ action
createQcrSheet()QCR action
getLeadMailTemplate()All mail modals
getPlacementData()Placement modal pre-fill
uploadLeadAttachment()Attachment upload in mail modals
sendMailWithAttachement()All mail sends + placement
downloadFileFromGoogleSheet()Mail attachment (internal/insurer/client)
createAndDownloadPlacementSheet()Placement mail attachment
getLeadEmailHistory()Email History modal
buildRfqPlaceholderData()RFQ sheet placeholder fill (helper)

Developer checklist

  1. Complete Google Sheet config for each Non-EB/Marine policy_type before first RFQ.
  2. Set policy_type.misc.rfq_template_sheet_id per product (see sheet ID per product).
  3. Configure RFQ_PARENT_FOLDER_ID in .env and share folder/templates with the service account.
  4. Ensure placeholders in master templates match Config\RfqConfig::$rfqPlaceholders.
  5. Gate new list actions with $isNonEb in leads_list.php.
  6. Respect sheet order: RFQ → QCR → mails → placement.
  7. QCR button: roles [1, 5, 2, 3] or BUSINESS_SUPPORT_TEAM_ID in user team.

Common pitfalls