+

Carrier financial-strength ratings, delivered nightly

A.M. Best is the insurance industry's standard for rating carrier financial strength. Plansight pulls the full A.M. Best universe on a recurring schedule and stamps every carrier in the application with current ratings, outlooks, and financial-size categories โ€” so brokers see the credit picture inline with every quote.

Direction: Inbound Transport: FTPS (CSV) Cadence: Daily (US carriers only) Surface: Per-carrier modal in side-by-side & compare
What it is

The credit-rating layer underneath every carrier

Every Plansight carrier record carries a small bundle of A.M. Best fields โ€” alpha rating, numeric rating, outlook, financial-size category, effective date โ€” that brokers can surface in one click while building a side-by-side. The data behind that single click comes from a recurring import job that mirrors A.M. Best's CSV feed into Plansight's common database, then propagates it onto each carrier.

10
A.M. Best fields ingested per company
US
Only carriers domiciled in the United States
FTPS
CSV pulled via secure FTP, latest file wins
1:N
One A.M. Best record stamped onto N matching carriers
๐Ÿ“Š

Why brokers care

A.M. Best ratings are how the industry quantifies carrier financial strength. When a broker is recommending an employer take a plan with Carrier X, knowing Carrier X carries an A (Excellent) rating with a Stable outlook is the difference between an informed recommendation and a guess.

โšก

Why it's inline, not a separate tool

The rating shows up where the broker is already working โ€” the side-by-side and the compare view โ€” via a click on the carrier name. No tab switching, no separate lookup. The integration exists to remove a step from the broker's day.

๐Ÿ›ก๏ธ

Why it's read-only inside Plansight

Plansight never authors A.M. Best data. The AmBestPolicy explicitly forbids users from creating, updating, or deleting rating records โ€” the artisan import is the only writer. Brokers see ground truth, not anyone's local edit.

How it works

From A.M. Best's FTP server to a Plansight carrier modal

A single Laravel artisan command, php artisan ambest:import, does the whole pipeline. It runs in production (and test) on the active DR region only.

1

Connect via FTPS

The job opens an ftp_ssl_connect to config('ambest.server') using AMBEST_USER + AMBEST_PASS. Passive mode is forced on, and FTP_USEPASVADDRESS=false is set first โ€” A.M. Best moved their server behind NAT at some point and the data connection breaks without it.

2

Pick the newest file in the FTP root

ftp_nlist lists the root, the list is reverse-sorted, and the first entry wins. A.M. Best drops a fresh CSV; Plansight always takes the latest. Downloaded to a UID-named tmp file, parsed in-memory.

3

Filter to US carriers

The CSV has every company A.M. Best rates, worldwide. The import skips any row where Country of Domicile != 'US'. International carriers aren't represented in Plansight's book of business and would only add noise.

4

Translate, normalize, and write to amBest table

Each US row maps 10 source columns onto 10 Plansight fields (see the field map below). Two columns get decoded inline: the numeric rating is rendered into human-readable text ("Excellent", "Good", "Marginal", etc.), and the outlook code (P/N/S) is expanded to "Positive" / "Negative" / "Stable". Non-printable characters are stripped, dates are Carbon-parsed.

5

Stamp every matching Carrier record

Once the amBest table is fully written, the import iterates every Carrier with a populated amBestNumber. For each one, it copies the matched amBest row's attributes onto the Carrier record and saves. This denormalization is what lets the broker UI render the rating from a single query instead of joining at display time.

6

Log the carrier count

The job logs the number of A.M. Best rows imported and the number of Carriers updated. That's the only observability on the success path โ€” no metrics, no alerting, no Slack notification on failure. The FTP login error path logs and returns; nothing else fires.

Where the schedule actually lives

Note the absence of a ->schedule() entry: app/Console/Kernel.php doesn't register ambest:import, and routes/console.php doesn't either. The cadence is set at the infrastructure layer (the production cron / ECS scheduled task), not in the Laravel codebase. If you want to know how often this runs without asking, you have to look at the EC2 crontab or the AWS scheduled-task config โ€” it isn't documented in the repo.

The Field Map

What A.M. Best gives us, and what we call it

Ten source columns from the A.M. Best CSV become ten Plansight fields. The mapping lives in AmBestImport::handle()'s $modelMap and is the single source of truth for what gets ingested.

A.M. Best column Plansight field Notes
A.M. Best Company NumberamBestNumberPrimary key. Also the join column to Carrier.
Company / Group NameamBestCompanyNameAuthoritative carrier name per A.M. Best.
A.M. Best Rating - AlphaamBestRatingA++, A+, A, A-, B++, etc. The familiar letter grade.
A.M. Best Rating - NumericamBestRatingNumeric2-digit code. First two chars decode into the rating-text column below.
(derived)amBestRatingText"(Superior)", "(Excellent)", "(Good)", etc. โ€” derived from amBestRatingNumeric at import time.
Rating OutlookamBestOutlookP/N/S โ†’ Positive / Negative / Stable. Empty if unrated.
Rating ModifieramBestRatingModifier"u" for under review, "pd" for public data, etc.
Financial Size Category - AlphaamBestFinancialSizeRoman numeral category Iโ€“XV โ€” A.M. Best's measure of policyholders' surplus.
Rating Effective DateamBestRatingEffectiveDateCarbon-parsed datetime. Surfaced in the modal as m/d/Y.
NAIC Company NumbernaicsCodeState-regulator identifier โ€” useful for state DOI cross-reference.
Federal Employers IDtaxIdEIN. Pulled for completeness; not currently displayed.

Decoding the numeric rating

The 2-digit numeric is a compressed form of A.M. Best's full rating scale. Plansight decodes it inline:

10 / 11 โ†’ (Superior)   12 / 13 โ†’ (Excellent)   20 / 21 โ†’ (Good)   22 / 23 โ†’ (Fair)   30 / 31 โ†’ (Marginal)   32 / 33 โ†’ (Weak)   42 โ†’ (Poor)   46 โ†’ (Under Regulatory Supervision)   48 โ†’ (In Liquidation)   90 โ†’ (Rating Suspended)

In the broker UI

Where this shows up for the user

The integration's entire job is to put one click between the broker and the credit picture of every carrier on screen.

The trigger

On the side-by-side and the compare view, every plan card has a "show carrier info" link rendered by BladePlansight::generateAmBestModalDataAttributes(). That helper stamps the carrier's A.M. Best fields onto the DOM element as data-* attributes โ€” no extra round trip when the broker clicks.

The modal

resources/views/includes/app/ambestModal.blade.php renders the rating fields as labeled rows, with the A.M. Best logo on the left, the carrier logo on the right, and a configurable disclaimer at the bottom. The modal body is empty at render โ€” the JavaScript handler at groupAppPages.js:15136 populates it from the trigger's data attributes when the user clicks.

Where it's included

Two view files include the modal: appPages/group/plansight.blade.php (the side-by-side) and appPages/group/plansightCompare.blade.php (the compare view). Both pass the carrier and a per-app-controller $amBestDisclaimer string.

The admin "force NR" override

Carriers have an amBestForceNR checkbox in adminPages/carrierUpdate.blade.php. When set, Carrier::getAmBestRatingAttribute short-circuits to empty regardless of what's actually in the database โ€” useful when a brokerage wants to suppress a rating that the carrier publicly disputes, or for white-label situations where the rating doesn't belong on screen.

In the repo

Where it lives in code

Eight files cover the entire integration โ€” import, model, policy, UI plumbing, and the surfacing in the broker views.

Path Role
app/Console/Commands/AmBestImport.phpThe artisan command โ€” FTPS pull, CSV parse, table write, Carrier merge.
app/Models/AmBest.phpEloquent model; amBestNumber primary key, lives on the common DB.
app/Models/Carrier.phpCarries the denormalized A.M. Best fields; defines the amBestForceNR override accessor and getAmBestRatingFields() helper.
app/Policies/AmBestPolicy.phpRead-only โ€” view returns true, create / update / delete all return false.
app/Helpers/BladePlansight.phpgenerateAmBestModalDataAttributes() โ€” stamps the rating onto the DOM trigger element.
config/ambest.phpThree env vars: AMBEST_SERVER, AMBEST_USER, AMBEST_PASS.
resources/views/includes/app/ambestModal.blade.phpThe modal markup. Body is populated client-side from the trigger's data attributes.
resources/assets/js/app/groupAppPages.js (lines ~14107, 15136)Click handler โ€” copies data-* attrs into the modal body, swaps in the carrier logo, formats the effective date.
Items worth a developer's review

What we could improve

A few observable rough edges. None are blocking โ€” the integration has been quietly running for years.

Schedule isn't in code

Neither Kernel.php nor routes/console.php registers ambest:import. The cadence is an infrastructure-layer cron that isn't documented in the repo. Moving it into Laravel's scheduler would put the schedule in version control next to the command it triggers.

Silent failure on FTP error

If ftp_login fails, the job logs "FTP login error" and returns. Nothing alerts. The next day's broker still sees the previous day's ratings (which is mostly fine), but a multi-week outage would go unnoticed unless someone tails the logs.

Latest-file-wins is fragile

The job rsorts the FTP listing and takes [0]. If A.M. Best ever leaves a stray file in the root with a higher-sorting name (a backup, a README, a debug drop), it gets parsed. A filename-pattern filter would harden this.

API instead of FTPS

A.M. Best offers a REST product as well. Migrating away from FTPS would remove the NAT-workaround, give us a structured contract, and make incremental pulls possible (today the job re-pulls and re-writes every US row, every run).

No carrier-update audit

When the merge step overwrites a Carrier's A.M. Best fields, the prior value is gone โ€” no history table, no diff log. For a downgrade event (Carrier X drops from A to B+), the only record that the rating ever was A is what's in the previous day's CSV on A.M. Best's side.

The amBestForceNR override is a per-carrier global

If a brokerage wants to suppress one carrier's rating, they suppress it everywhere โ€” there's no per-brokerage flag. Plansight today is multi-tenant on the data side but the A.M. Best override is global to the carrier record.