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.
Typical sequence from the list:
queuedrfq_createdqcr_createdrfq_sentqcr_sentwon
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).
| Area | File |
|---|---|
| List + action dropdown | app/Views/leads_list.php |
| Non-EB add/edit form | app/Views/leads_non_eb.php |
| EB vs Non-EB form include | app/Views/leads_form_handler.php |
| All backend logic | app/Controllers/LeadsController.php |
| Google Sheets | GoogleSheetLib, Config\RfqConfig |
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.
Library: app/Libraries/GoogleSheetLib.php — auth via service account JSON at
{project-root}/nhance-ee8d1-e3c5269b1ec7.json, scopes DRIVE + SPREADSHEETS.
| Step | Before | Why |
|---|---|---|
| 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 |
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;
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:
https://docs.google.com/spreadsheets/d/{FILE_ID}/editphp public/index.php cli/list-sheet-folder-files {RFQ_PARENT_FOLDER_ID}
→ writes sheetid.json with name + sheetId pairs
(GoogleSheetController::listFolderSheetFilesCli)QCR and Placement: no separate template ID per product. They always copy from the lead’s existing sheets:
| Stage | Copy source | Stored on lead |
|---|---|---|
| RFQ | policy_type.misc.rfq_template_sheet_id | leads.misc.rfq_sheet_id |
| QCR | leads.misc.rfq_sheet_id | leads.misc.qcr_sheet_id |
| Placement | leads.misc.qcr_sheet_id (filename: QCR → Placement) | leads.misc.placement_sheet_id |
| Setting | Location | Purpose |
|---|---|---|
RFQ_PARENT_FOLDER_ID |
.env → Config\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 template | Filled 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.
/leads/list → click Add.lead_form_type=2).GET /util/getLeadNonEB/2/0.leads_non_eb.php)allocg = Non-EB or Marine.GET /util/getPolicyTypeFields → dynamic fields in #appendArea.fin_years_claims JSON.rfq_qcr_viewers (emails) become Google Sheet editors later.POST /leads/create with lead_form_type=2.createLead()prepareLeadData() → prepareSingleLeadData() (one lead row; EB uses multi-row).claim_history=1.insertNewLead() — no demography background job (EB-only).queued (In-Queued).From the list action menu → Edit (available for both EB and Non-EB):
getLeadsDataForEdit(lead_id, lead_form_type, actual_lead_id)GET /util/getLeadNonEB/{lead_form_type}/{actual_lead_id}/{lead_id}getLeadNonEB($type, $actual_lead_id, $id) (~line 4818)
$id present: fetches lead_edit_data, files, custom fields, date formattinggenerateViewPageHtml()rfq/claims_details_non_eb into claims_details_htmlleads_form_handler → includes leads_non_eb.php when selected_lead_type != 1POST /leads/create with hidden id → updateOldLead()
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).
| # | 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 |
.btnRfqSheetList → createRfqSheetFromList() |
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
|
.btnQcrSheetList → createQcrSheetFromList() |
GET
/leads/createQcrSheet?lead_id={id}LeadsController::createQcrSheet()
|
qcr_created |
| 3 | Send Internal Mail | Non-EB; status != queued |
.btnInternalMailList → openInternalMailFromList() |
GET
/leads/mailTemplate?lead_id={id}&template_type=rfqPOST /leads/sendMail — recipient_type=internal
|
— |
| 4 | Send Insurer Mail | Same as internal mail | .btnInsurerMailList → openInsurerMailFromList() |
GET
/leads/mailTemplate?lead_id={id}&template_type=rfqPOST /leads/sendMail — recipient_type=insurer (RFQ sheet attach)
|
rfq_sent |
| 5 | Send Client Mail | Same as internal mail | .btnClientMailList → openClientMailFromList() |
GET
/leads/mailTemplate?lead_id={id}&template_type=qcrPOST /leads/sendMail — recipient_type=client (QCR sheet attach)
|
qcr_sent |
| 6 | Placement | Same as internal mail | .btnPlacementList → openPlacementFromList() |
GET
/rfq/placementData/{id}GET /leads/mailTemplate?lead_id={id}&template_type=placementPOST /leads/sendMail — recipient_type=placement
|
won |
| — | Email History | Always (EB + Non-EB) | .btnHistory → getLeadsDataForMailHistory() |
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):
POST /leads/uploadLeadAttachment — optional extra files (lead_id, docs_name, file)constructURL_ForInternalMailSend(), insurer/client via constructURL_ForInsurerOrClientMailSend(), placement via constructURL_ForPlacementMailSend()#external_cc_disclaimer_modal) before client/placement send to non-user emailscreateRfqSheet())policy_type.misc.rfq_template_sheet_id.misc.rfq_sheet_id exists → return existing Google Sheet URL.GoogleSheetLib::copyTemplate() into RfqConfig::rfqParentFolderId.buildRfqPlaceholderData()): client, GST, PAN, policy period, claims table.leads.rfq_qcr_viewers email JSON.misc.rfq_sheet_id; set status = rfq_created.createQcrSheet())misc.rfq_sheet_id — returns 400 if RFQ not created yet.misc.qcr_sheet_id exists → return existing URL.rfq_qcr_viewers.misc.qcr_sheet_id; set status = qcr_created.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.
sendMailWithAttachement()For Non-EB (lead_form_type == 2), the Excel attachment comes from Google Sheets:
| recipient_type | Sheet used | Status 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().
Opened from list action → openPlacementFromList(leadId):
GET /rfq/placementData/{id} — pre-fills policy dates, premium, CD, installments, contacts.GET /leads/mailTemplate?template_type=placement — subject/body/attachments.POST /leads/sendMail with recipient_type=placement.won,
persists placement_date, premium_amount, cd_amount, installments, etc.| Status | Label | Set by |
|---|---|---|
queued | In-Queued | Create form (default) |
rfq_created | RFQ Created | createRfqSheet() |
rfq_sent | RFQ Sent | Insurer mail |
qcr_created | QCR Created | createQcrSheet() |
qcr_sent | QCR Sent | Client mail |
won | Won | Placement mail |
lost | Lost | User sets on edit form + lost reason |
{
"rfq_sheet_id": "…",
"qcr_sheet_id": "…",
"placement_sheet_id": "…"
}
Methods in LeadsController.php used by the list Non-EB flow:
| Method | Triggered 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) |
policy_type before first RFQ.policy_type.misc.rfq_template_sheet_id per product (see sheet ID per product).RFQ_PARENT_FOLDER_ID in .env and share folder/templates with the service account.Config\RfqConfig::$rfqPlaceholders.$isNonEb in leads_list.php.[1, 5, 2, 3] or BUSINESS_SUPPORT_TEAM_ID in user team.createQcrSheet fails if rfq_sheet_id is missing.queued.POST /leads/create; presence of hidden id triggers update.