+
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.
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.
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.
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.
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.
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.
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.
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.
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.
amBest tableEach 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.
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.
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.
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.
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 Number | amBestNumber | Primary key. Also the join column to Carrier. |
| Company / Group Name | amBestCompanyName | Authoritative carrier name per A.M. Best. |
| A.M. Best Rating - Alpha | amBestRating | A++, A+, A, A-, B++, etc. The familiar letter grade. |
| A.M. Best Rating - Numeric | amBestRatingNumeric | 2-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 Outlook | amBestOutlook | P/N/S โ Positive / Negative / Stable. Empty if unrated. |
| Rating Modifier | amBestRatingModifier | "u" for under review, "pd" for public data, etc. |
| Financial Size Category - Alpha | amBestFinancialSize | Roman numeral category IโXV โ A.M. Best's measure of policyholders' surplus. |
| Rating Effective Date | amBestRatingEffectiveDate | Carbon-parsed datetime. Surfaced in the modal as m/d/Y. |
| NAIC Company Number | naicsCode | State-regulator identifier โ useful for state DOI cross-reference. |
| Federal Employers ID | taxId | EIN. Pulled for completeness; not currently displayed. |
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)
The integration's entire job is to put one click between the broker and the credit picture of every carrier on screen.
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.
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.
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.
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.
Eight files cover the entire integration โ import, model, policy, UI plumbing, and the surfacing in the broker views.
| Path | Role |
|---|---|
app/Console/Commands/AmBestImport.php | The artisan command โ FTPS pull, CSV parse, table write, Carrier merge. |
app/Models/AmBest.php | Eloquent model; amBestNumber primary key, lives on the common DB. |
app/Models/Carrier.php | Carries the denormalized A.M. Best fields; defines the amBestForceNR override accessor and getAmBestRatingFields() helper. |
app/Policies/AmBestPolicy.php | Read-only โ view returns true, create / update / delete all return false. |
app/Helpers/BladePlansight.php | generateAmBestModalDataAttributes() โ stamps the rating onto the DOM trigger element. |
config/ambest.php | Three env vars: AMBEST_SERVER, AMBEST_USER, AMBEST_PASS. |
resources/views/includes/app/ambestModal.blade.php | The 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. |
A few observable rough edges. None are blocking โ the integration has been quietly running for years.
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.
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.
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.
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).
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.
amBestForceNR override is a per-carrier globalIf 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.