Purpose

EB rack rate config is how Nhance authors premium rack rates for a client policy: rate tables (SI, age, grade, relationship, etc.) plus two policy-wide behaviours that apply to every grid type (1–13): Premium calculation and Applicable family members. GMC policies may also define multiple named rack rates (Primary + additional tabs). Technical save/load paths reference ClientController, policy_grid.php, and policy_grid_excel.php. For the Excel employee onboarding pipeline that consumes this configuration and runs calculate_premium_new, see EB rack rate calculation.

Premium calculation (all grid types)

The modal exposes Premium calculation as three radios: Individual, Family floater, and Family floater cum Individual. This choice is stored with the rack (e.g. premium_type on premium rows where used) and interpreted when premiums are calculated in downstream flows (enrollment, endorsements, etc.) — not re-derived from the grid layout alone.

ModeMeaning (at calculation time)
Individual Sum insured is covered per individual family member, and premium is calculated (and applied) per member according to the rack rules and member attributes.
Family floater Sum insured applies to the whole family as one floater cover. Premium is calculated for the family unit, but the amount is stored / represented against the self member only (single premium bucket for the floater).
Family floater cum Individual Combines both behaviours where the product rules require floater cover together with individually rated members (exact split depends on policy / insurer rules in the calculation engine).
i
For developers: the rack modal captures the mode and the rate table; always trace how premium_type (and policy terms) are read in the premium calculation path you are debugging — not only in createClientPolicyPremium.

Applicable family members (all grid types)

The section “Choose applicable family members” is driven by what the policy terms allow for that client policy (who can exist on the cover). The user then selects which of those relationships are in scope for this specific rack rate (Self / Spouse / Children / Parents / Parents in law) using the radio options below.

Radio option meanings (per relationship row)

Each relationship (Self, Spouse, Children, Parents, Parents in law) uses the same vocabulary of choices. These define eligibility rules for this rack rate, not the member list itself.

ChoiceMeaning for this rack rate
Yes That relationship must be present in the incoming family for this rack rate to apply.
No That relationship must not be present — if it is, this rack rate does not match.
Maybe If that relationship is available on the family, it is included in this rack rate’s match; if not present, the rack can still match without it.
NA Even if members of that relationship exist on the policy, they are not applicable to this rack rate (this rack never prices or targets them).
Any Any number of members with that same relationship is allowed for a match (no fixed count).
1, 2, 3, 4, … (numeric) An exact count of members with that relationship must be present for this rack rate to match (e.g. exactly two parents).

How rack rates are picked at premium time

At calculation time (enrollment / endorsement / etc.), the engine compares the incoming family’s members and relationships (who is on the cover and in what roles) against each configured rack rate’s applicable members (the rules saved in additional_relationship for that tab’s rack). Racks whose rules match the family pattern are candidates for premium.

More than one rack rate can match the same family (e.g. a base employee rack and a separate parents top-up). In those cases multiple rack rates may apply and premium is calculated accordingly (combined according to product rules in the calculation path — not in the modal UI alone).

The diagram below is conceptual: the exact class or function name lives in your premium / onboarding pipeline, but the decision order is what new developers should internalize.

flowchart TD Start([Start premium run]) --> Family["Incoming family snapshot:
members, relationship, counts"] Family --> Load["Load active rack config for policy
policy_premium_1 / 2, is_active = 1
each rack_rate_name + rules + grid rows"] Load --> Compare["For each rack rate:
compare family vs applicable members
Yes / No / Maybe / NA / Any / count"] Compare --> Matched["Build matched rack list
0, 1, or many racks"] Matched --> Calc["Premium engine:
use premium_type + rate table per matched rack"] Calc --> Multi{"Several racks matched?"} Multi -->|Yes| Combine["Combine premium
per product rules"] Multi -->|No| Single["Single-rack premium"] Combine --> Done([Allocate to members / floater]) Single --> Done

How to configure a rack rate (checklist)

  1. Policy terms first. Ensure client_policy.policy_terms reflects allowed members, family floater flags, SI ladders, etc. The rack UI inherits what is allowed.
  2. Open the Rack Rate modal (.btnPolicyModel) for the target client_policy_id.
  3. Set Premium calculation — pick Individual, Family floater, or Family floater cum Individual per product rules (see Premium calculation).
  4. Set applicable family members for this rack rate tab — narrow from policy-allowed members to who this table applies to (see Applicable family members).
  5. Choose Policy premium type — maps to policy_grid_id 1–13 from policy_grid_master (see Grid types).
  6. Fill the grid — manual rows, “+” rows, and/or Copy from excel using headers from policy_grid_excel.php.
  7. Save — POST to client/premimum/create; confirm no validation errors (Parsley, SI vs policy terms, duplicate family composition across tabs where enforced).
  8. Repeat for additional GMC tabs if the product uses more than one named rack rate.

Where it appears in the UI

Load grid data — ClientController::getpolicyGridData

HTTP: GET util/policy-premium with query client_policy_id (see app/Config/Routes.php under the /util group). The front end calls base_url("util/policy-premium") with that query parameter.

What it does:

  1. Loads client_policy joined to policy_type and reads policy_terms JSON (family floater flags, family_floaters, etc.).
  2. Resolves terms_si_amount_array via getPolicyTerms($client_policy_id) and branch units via getBranchUnitsByBranchId.
  3. Counts active employees on the policy for UI gating (hideShowSubmitButton).
  4. Derives a search token from policy type name / id: GMC (regex on type name, or type id 72) vs GPA (regex or type ids 6 / 7).
  5. Fetches grid templates from policy_grid_master via PolicyGridModel::like('policy_type', $search_term) — these rows drive which “Policy Premium Type” options exist and what columns each grid id expects.
  6. GPA: Loads saved rows from policy_premium_1 for this client + client_policy where is_active = 1.
  7. GMC: Loads saved rows from policy_premium_2 for the same scope. Also builds jsonArray: grouped by rack_rate_name, each value is json_decode(additional_relationship) (family-composition flags used when re-rendering checkboxes).
  8. GMC family floater filter: If family_floater == 0, when existing data exists it keeps premiums whose policy_grid_id is in 3–9; if family_floater == 1, it prefers grid ids 10–11 when data exists. Empty data passes through unchanged.

Response shape (success): data (grid master rows), premiumData (JSON string of premium rows for the UI), jsonArray, branch_units, family_floater, self, count, terms_si_amount_array, client_policy_id. GPA and GMC branches return the same keys; non-GPA/GMC types still return grid templates but premium payload may be empty.

Save rack rate — ClientController::createClientPolicyPremium

HTTP: POST client/premimum/create (spelling premimum matches routes). Body is multipart form data from the modal (FormData in JS).

What it does:

  1. Sanitizes POST via sanitizeInputArrayAdvanced; requires a valid client_policy_id and loads client_policy for client_id / branch.
  2. Builds additional_relationship JSON from checkboxes: self, spouse, childrens, parents, parents-in-law. For grid ids 1 or 2 it forces a fixed relation map (self only).
  3. Deactivate old rows (soft replace): For policy_grid_id 1 or 2, sets is_active = 0 on all policy_premium_1 rows for that client policy. Otherwise sets is_active = 0 on policy_premium_2 rows matching the same rack_rate_name (so additional GMC tabs do not wipe other rack rates).
  4. Insert new active rows: One insert per SI/premium row (arrays in POST). Units default from branch if a slot is empty or undefined.

Branching by policy_grid_id (high level):

Success response: status: true, rack_rate_json — a small JSON map used by the UI to prevent duplicate family-floater combinations across tabs (rarc_rate_json_array in policy_grid.php).

Frontend flow (policy_grid.php)

  1. Open: Click .btnPolicyModel → GET util/policy-premium?client_policy_id=….
  2. Populate dropdown: appendGridData(res.data, …) fills “Policy Premium Type” from grid master rows.
  3. Build inputs: Changing the dropdown calls addGridHTML, which injects large HTML templates for grid ids 1–13 (SI/basic/grade layouts, GMC age bands, etc.). Existing premiumData pre-fills values when editing.
  4. Family composition: createCheckboxes uses res.self / policy terms and, for GMC, saved additional_relationship from jsonArray.
  5. Submit: Delegated submit handler on form[id^="GridForm_"] — Parsley validation, duplicate SI checks against terms_si_amount_array (checkPolicyTermsSI), unit checks, then POST client/premimum/create with FormData. On success, appends res.rack_rate_json for duplicate-tab prevention; for grid 1 or 2 the modal may auto-close.
  6. Excel: “Copy from excel” toggles a textarea; policy_grid_excel.php defines per-grid header order and parsing to fill the grid.

Grid IDs and database tables

ArtifactRole
policy_grid_master Catalog of available premium grid layouts filtered by policy type (GPA vs GMC).
policy_premium_1 Stores GPA primary grid (id 1), GPA-style grid 2, and other rows where the controller routes to PolicyPremium1Model.
policy_premium_2 Stores most GMC grids (3+), grid 9, and additional rack rates distinguished by rack_rate_name.
client_policy.policy_terms JSON: drives family floater behaviour in getpolicyGridData and which checkbox defaults appear.

Grid types (1–13) — policy_grid_master

Every grid below uses the same two layers documented above: Premium calculation and Applicable family members. The only difference between ids 1–13 is the shape of the rate table (which columns appear and how POST fields are named). Downstream premium logic must combine premium_type, additional_relationship, and these rows.

UI pattern: grid 4 vs grid 5 (age vs age + SI per row)

These two GMC layouts are easy to confuse; the modal layout differs as follows (reviewed UI):

Grid ID Line Master label Rate table (what differs) Persisted in
1 GPA Sum Insured (SI) * Multiplier GPA primary: sub-mode si_or_bp1 SI × multiplier rows, 2 basic pay ladder with multipliers, 3 band/grade + SI + premium. Multiple unit/SI/premium lines. Controller forces additional_relationship to self-only for grids 1–2. Same premium calculation radios apply when shown. policy_premium_1
2 GPA Flat Rate for all SI Simple ladder: gpa_unit29[], gpa_si29[], gpa_premium29[] per row — no age/grade columns. policy_premium_1
3 GMC SI Unit + SI + premium per row (no age/relationship in the standard template). Prefix 3_ on POST keys. policy_premium_2
4 GMC Employees Age band One SI for the table; rows = age band + premium only (see Grid 4 vs 5). Backend still stores si, age_from, age_to, premium per insert; UI collects one SI context then many age rows. policy_premium_2
5 GMC Employees Age + SI Each row: SI + from age + to age + premium (see Grid 4 vs 5). Prefix 5_. policy_premium_2
6 GMC Employees + Dependent Age band Age-band table where dependents are in product scope (policy_grid_master dependent flags). Same POST pattern as other GMC age grids with prefix 6_. policy_premium_2
7 GMC Employees + Dependent Age + SI Dependent-aware age bands and SI on each row (prefix 7_) for combined pricing dimensions. policy_premium_2
8 GMC SI as per Grade or Band Adds grade/band per row with SI and premium (8_grade[], etc.). For corporate grade–based insurer tables. policy_premium_2
9 GMC Flat Rate for all Same row shape as grid 2 (gpa_si29[] / gpa_premium29[]) but saved to policy_premium_2 for GMC. policy_premium_2
10 GMC Maximum age of Dependents Floater-oriented table (age + SI + premium); getpolicyGridData prefers ids 10–11 when family_floater = 1 and premium data exists. Prefix 10_. policy_premium_2
11 GMC Maximum count per Family Similar family/floater use case as 10; includes max SI column (11_max_si[]) for family-count / cap rules. Prefix 11_. policy_premium_2
12 GMC Employees + relationship Each row carries a relationship value plus SI/premium (and unit) so rates differ by member type. Prefix 12_. policy_premium_2
13 GMC Employees age + relationship age Full row: relationship + age from/to + SI + premium. Prefix 13_. policy_premium_2
i
POST naming for GMC grids 3–13: fields use the {gridId}_ prefix, e.g. 5_age_from[], 5_age_to[], 5_si[], 5_premium[], 5_unit[]. See policy_grid_excel.phpexcel_headers for paste column order per id.

Excel paste path

policy_grid_excel.php defines excel_headers keyed by grid id (and nested keys for GPA grid 1 variants). Users paste tab-separated data; helpers build a preview table and map columns into the same field names the manual grid uses. Use this when bulk-entering many SI/premium lines instead of row-by-row “+” buttons.

!
Production caution: Saving grid 1 or 2 deactivates all rows in policy_premium_1 for the policy before insert. Other grids deactivate only rows sharing the same rack_rate_name in policy_premium_2. Test on a copy of client policy data first.