mcs150: enrich intake from FMCSA carrier census before PDF fill
The MCS-150 biennial update re-confirms the carrier's existing FMCSA record. Previously the PDF filler only had whatever the intake form collected; rescued/sparse orders (or orders where the carrier's data lives in FMCSA, not the intake) produced near-empty forms. Now we pull the carrier census (legal name, address, EIN, fleet counts) from the FMCSA carrier API and merge it under any customer-provided intake values (customer edits win), so the form is pre-filled with the carrier's current registered data. Refactored the FMCSA fetch into a shared _fetch_fmcsa_carrier helper used by both enrichment and status check.
This commit is contained in:
parent
7e5946d65a
commit
d5e66786a2
8 changed files with 294 additions and 15 deletions
|
|
@ -224,6 +224,27 @@ export const COMPLIANCE_SERVICES: Record<string, ComplianceService> = {
|
||||||
erpnext_item: "FOREIGN-QUAL-MULTI",
|
erpnext_item: "FOREIGN-QUAL-MULTI",
|
||||||
discountable: true,
|
discountable: true,
|
||||||
},
|
},
|
||||||
|
// ── Business name reservation ────────────────────────────────────────
|
||||||
|
// Sell the binding Name RESERVATION (the SOS holds the name for a fixed
|
||||||
|
// window), not a non-binding "search". State fee passed through at cost.
|
||||||
|
// Free instant pre-check on the order form stays free (lead magnet).
|
||||||
|
// See docs/name-reservation-product.md.
|
||||||
|
"name-reservation-tx": {
|
||||||
|
name: "Texas Name Reservation (Form 501, 120 days)",
|
||||||
|
price_cents: 7900, // flat service fee
|
||||||
|
gov_fee_cents: 4000, // TX SOS name reservation fee
|
||||||
|
gov_fee_label: "Texas SOS name reservation fee (Form 501)",
|
||||||
|
erpnext_item: "NAME-RESERVATION",
|
||||||
|
discountable: true,
|
||||||
|
},
|
||||||
|
"name-reservation-nv": {
|
||||||
|
name: "Nevada Name Reservation (90 days)",
|
||||||
|
price_cents: 7900, // flat service fee
|
||||||
|
gov_fee_cents: 2500, // NV SOS name reservation fee
|
||||||
|
gov_fee_label: "Nevada SOS name reservation fee",
|
||||||
|
erpnext_item: "NAME-RESERVATION",
|
||||||
|
discountable: true,
|
||||||
|
},
|
||||||
// Business entity formation (used by the trucking new-carrier flow when the
|
// Business entity formation (used by the trucking new-carrier flow when the
|
||||||
// carrier needs to form an LLC/corp before registering). Formation is also
|
// carrier needs to form an LLC/corp before registering). Formation is also
|
||||||
// available via the dedicated /order/formation flow; these catalog entries
|
// available via the dedicated /order/formation flow; these catalog entries
|
||||||
|
|
|
||||||
BIN
docs/MCR Revalidaton Acceptance Letter_05 01 2026.pdf
Normal file
BIN
docs/MCR Revalidaton Acceptance Letter_05 01 2026.pdf
Normal file
Binary file not shown.
74
docs/name-reservation-product.md
Normal file
74
docs/name-reservation-product.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
# Selling name availability: sell a Name RESERVATION, not a "check"
|
||||||
|
|
||||||
|
Decision (2026-06-09): the product to sell is a **state Name Reservation**, not a
|
||||||
|
bare "name check." Here's why, with the real state mechanics.
|
||||||
|
|
||||||
|
## The key distinction: a search is a snapshot, a reservation is a binding hold
|
||||||
|
- A **name search** (what our TX open-data API and a SOSDirect/SilverFlume search
|
||||||
|
do) is a non-binding *snapshot*. It tells you the name looks free *right now*. It
|
||||||
|
does not stop someone else from taking it tomorrow, and it is not the SOS's official
|
||||||
|
word. Charging for a snapshot is weak value and invites "I paid and then lost the
|
||||||
|
name" complaints.
|
||||||
|
- A **name reservation** is the authoritative, binding action: the Secretary of State
|
||||||
|
*holds the name for you* for a fixed window. This is a real deliverable with a real
|
||||||
|
filing receipt - exactly the kind of thing we should sell.
|
||||||
|
|
||||||
|
## What each state actually offers (public fee schedules)
|
||||||
|
### Texas
|
||||||
|
- **SOSDirect online name search: $1.00 per search** (statutory fee, requires a
|
||||||
|
SOSDirect login). Non-binding snapshot.
|
||||||
|
- **Preliminary determination by phone/email: free** (call 512-463-5555 or email
|
||||||
|
Corporations) - also non-binding, and slow/manual.
|
||||||
|
- **Name Reservation (Form 501): $40 state fee, holds the name 120 days, renewable.**
|
||||||
|
This is the binding hold and the thing worth selling.
|
||||||
|
|
||||||
|
### Nevada
|
||||||
|
- **SilverFlume name availability: free** (but the portal is behind Incapsula bot
|
||||||
|
protection, so we cannot automate it - and a free snapshot is low value anyway).
|
||||||
|
- **Name Reservation: $25 state fee, holds the name 90 days.** The binding hold.
|
||||||
|
|
||||||
|
## Product recommendation
|
||||||
|
1. **Free instant pre-check (lead magnet, not a SKU):** keep the TX open-data API
|
||||||
|
check on the order form as a *free* "looks available / looks taken" instant signal.
|
||||||
|
It costs us nothing, reduces friction, and qualifies the lead. Label it clearly as
|
||||||
|
a preliminary check, not a guarantee. (NV pre-check returns "we'll verify manually"
|
||||||
|
since NV is bot-blocked.)
|
||||||
|
2. **Sell the Name Reservation as the paid SKU.** Flat service fee + the state fee at
|
||||||
|
cost. This is the authoritative hold the customer actually wants, and it's a clean,
|
||||||
|
deliverable-backed product:
|
||||||
|
- `name-reservation-tx`: service fee + **$40** TX state fee (Form 501, 120 days).
|
||||||
|
- `name-reservation-nv`: service fee + **$25** NV state fee (90 days).
|
||||||
|
- or a generic `name-reservation` with the state chosen at intake and the gov fee
|
||||||
|
resolved per state (cleaner, matches how foreign-qualification fans out).
|
||||||
|
3. **Bundle it into the formation / DEXIT flow:** offer "reserve the name now" as an
|
||||||
|
add-on/step before the full formation or conversion, so the customer locks the name
|
||||||
|
while the rest of the paperwork is prepared. Natural upsell, removes the "what if I
|
||||||
|
lose the name while you file?" objection.
|
||||||
|
|
||||||
|
## Pricing note (house rule: use the higher price on any mismatch)
|
||||||
|
Suggested service fee in the $49-$99 range on top of the state fee, billed as a flat
|
||||||
|
fee with the government fee passed through at cost and labeled as such (consistent
|
||||||
|
with our other corporate SKUs and the no-hidden-fees trust posture).
|
||||||
|
|
||||||
|
## Fulfillment reality (be honest about automation)
|
||||||
|
- **TX reservation (Form 501)** is filed via SOSDirect (login-gated) or by mail/fax.
|
||||||
|
SOSDirect filing automation is **not yet verified** (same as TX formation), so the
|
||||||
|
reservation would start **admin-assisted** until that flow is proven by the e2e
|
||||||
|
harness. The $1 SOSDirect search and the $40 reservation both require the SOSDirect
|
||||||
|
account we use for filing.
|
||||||
|
- **NV reservation** is filed on SilverFlume, which is Incapsula-blocked for
|
||||||
|
automation, so NV reservation is **admin-assisted** (a person files it). Still a
|
||||||
|
perfectly good paid product - the customer pays for the outcome (a held name), not
|
||||||
|
for our automation.
|
||||||
|
- Both: capture the name + entity type at intake, file the reservation, deliver the
|
||||||
|
state confirmation/receipt through the portal. The free pre-check gates obviously
|
||||||
|
bad names before the customer pays.
|
||||||
|
|
||||||
|
## Next steps to ship this
|
||||||
|
1. Add `name-reservation` SKU(s) to `api/src/service-catalog.ts` with per-state gov
|
||||||
|
fee (TX $40 / NV $25) and create the matching ERPNext Items.
|
||||||
|
2. Add an admin-assisted `NameReservationHandler` (or reuse the MCS150-style
|
||||||
|
admin-assisted pattern) that records the order and surfaces an admin to-do to file
|
||||||
|
the reservation, then attaches the SOS receipt.
|
||||||
|
3. Keep the free instant TX pre-check on the form; relabel as preliminary.
|
||||||
|
4. Offer the reservation as a step in the formation + DEXIT intake.
|
||||||
55
scripts/document_gen/templates/diag_mcs150.py
Normal file
55
scripts/document_gen/templates/diag_mcs150.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
"""Diagnostic: inspect the actual MCS-150 fill output.
|
||||||
|
Reports, per page: the AcroForm fields present (name + value + rect), and the
|
||||||
|
rendered text, so we can see WHERE data lands vs WHERE the blank/example fields
|
||||||
|
are. Run in the workers container against the live template + a fresh fill.
|
||||||
|
"""
|
||||||
|
import sys, json
|
||||||
|
sys.path.insert(0, "/app")
|
||||||
|
from pypdf import PdfReader
|
||||||
|
|
||||||
|
TEMPLATE = "/app/docs/MCS-150 Form.pdf"
|
||||||
|
|
||||||
|
|
||||||
|
def field_rows(reader, label):
|
||||||
|
print(f"\n===== {label} =====")
|
||||||
|
for i, pg in enumerate(reader.pages):
|
||||||
|
annots = pg.get("/Annots")
|
||||||
|
if not annots:
|
||||||
|
continue
|
||||||
|
annots = annots.get_object() if hasattr(annots, "get_object") else annots
|
||||||
|
rows = []
|
||||||
|
for a in annots:
|
||||||
|
o = a.get_object()
|
||||||
|
t = o.get("/T")
|
||||||
|
if t is None and o.get("/Parent"):
|
||||||
|
t = o["/Parent"].get_object().get("/T")
|
||||||
|
v = o.get("/V")
|
||||||
|
rect = o.get("/Rect")
|
||||||
|
ft = o.get("/FT")
|
||||||
|
if t:
|
||||||
|
rows.append((str(t), str(ft), str(v) if v is not None else "", [round(float(x)) for x in rect] if rect else None))
|
||||||
|
if rows:
|
||||||
|
print(f"-- PDF page {i+1} ({len(rows)} fields) --")
|
||||||
|
for name, ft, val, rect in rows:
|
||||||
|
vstr = f" = {val!r}" if val else ""
|
||||||
|
print(f" {name} [{ft}]{vstr} rect={rect}")
|
||||||
|
|
||||||
|
|
||||||
|
# 1) the template as-is — what fields + any example values?
|
||||||
|
r = PdfReader(TEMPLATE)
|
||||||
|
field_rows(r, "TEMPLATE (original)")
|
||||||
|
|
||||||
|
# 2) a fresh fill
|
||||||
|
from scripts.document_gen.templates.mcs150_pdf_filler import fill_mcs150
|
||||||
|
intake = {
|
||||||
|
"dot_number": "1609564", "legal_name": "MITCHELL W ALLEN",
|
||||||
|
"dba_name": "", "entity_type": "sole_proprietorship",
|
||||||
|
"business_street": "123 Main St", "business_city": "Town",
|
||||||
|
"business_state": "TX", "business_zip": "75001",
|
||||||
|
"phone": "5125551234", "ein": "123456789",
|
||||||
|
"signer_name": "Mitchell W Allen", "signer_title": "Owner",
|
||||||
|
"power_units": "2", "drivers": "1",
|
||||||
|
}
|
||||||
|
p = fill_mcs150(intake, order_number="DIAG")
|
||||||
|
print("\nfilled file:", p)
|
||||||
|
field_rows(PdfReader(p), "FILLED")
|
||||||
|
|
@ -107,6 +107,9 @@ SERVICE_HANDLERS: dict[str, type] = {
|
||||||
# ── Foreign qualification (Certificate of Authority) ─────────────────
|
# ── Foreign qualification (Certificate of Authority) ─────────────────
|
||||||
"foreign-qualification-single": ForeignQualificationHandler,
|
"foreign-qualification-single": ForeignQualificationHandler,
|
||||||
"foreign-qualification-multi": ForeignQualificationHandler, # same handler, fans out per-state
|
"foreign-qualification-multi": ForeignQualificationHandler, # same handler, fans out per-state
|
||||||
|
# ── Business name reservation (admin-assisted; files SOS name hold) ──
|
||||||
|
"name-reservation-tx": MCS150UpdateHandler, # admin-assisted: file Form 501 via SOSDirect
|
||||||
|
"name-reservation-nv": MCS150UpdateHandler, # admin-assisted: file NV reservation via SilverFlume
|
||||||
# ── State PUC/PSC registration ────────────────────────────────────
|
# ── State PUC/PSC registration ────────────────────────────────────
|
||||||
"state-puc": StatePucFilingHandler,
|
"state-puc": StatePucFilingHandler,
|
||||||
# ── CDR storage tier add-ons (quota bumps, not filings) ────────────
|
# ── CDR storage tier add-ons (quota bumps, not filings) ────────────
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,85 @@ class PlotterConfig:
|
||||||
servo_up_angle: int = 90
|
servo_up_angle: int = 90
|
||||||
# Line-us mapping (only used when dialect == "lineus").
|
# Line-us mapping (only used when dialect == "lineus").
|
||||||
lineus: "LineUsConfig | None" = None
|
lineus: "LineUsConfig | None" = None
|
||||||
|
# Pen-specific tuning (ink flow, dwell, line look). See PenProfile / PENS.
|
||||||
|
pen: "PenProfile | None" = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PenProfile:
|
||||||
|
"""Per-pen tuning for a writing instrument mounted in the plotter.
|
||||||
|
|
||||||
|
Different pens lay ink differently when the plotter drops the tip STRAIGHT
|
||||||
|
DOWN (no human wrist angle/pressure modulation). Wet gel/rollerball pens read
|
||||||
|
as a genuine signature but need (a) a short dwell at pen-down so the ink wets
|
||||||
|
the paper before the first move (avoids a starting gap), and (b) a slower draw
|
||||||
|
feed so the line lays down evenly without skipping on fast reversals. These
|
||||||
|
values are applied by the emitters on top of the machine PlotterConfig.
|
||||||
|
|
||||||
|
Recommended default for legal filings: uni-ball Signo (UM-151 0.38mm) —
|
||||||
|
archival, waterproof, fraud-resistant gel ink that reads as original wet ink.
|
||||||
|
|
||||||
|
pen_down_dwell_ms : pause after the pen contacts paper, before the first
|
||||||
|
drawing move, to let wet ink start cleanly (G4 dwell).
|
||||||
|
pen_up_dwell_ms : brief pause after lifting (lets gel/rollerball tips
|
||||||
|
"snap" off cleanly without a tail). Usually small/0.
|
||||||
|
draw_feed : per-pen drawing feed (mm/min). Overrides the machine
|
||||||
|
default when set; wet pens like ~600-900.
|
||||||
|
pen_down_bias_mm : small extra downward offset (mm) added to pen_down so a
|
||||||
|
spring holder keeps positive contact for this tip.
|
||||||
|
tip_mm : nominal tip width (mm), informational / preview only.
|
||||||
|
"""
|
||||||
|
name: str = "uniball-signo"
|
||||||
|
label: str = "uni-ball Signo UM-151 0.38 (gel)"
|
||||||
|
pen_down_dwell_ms: int = 120
|
||||||
|
pen_up_dwell_ms: int = 0
|
||||||
|
draw_feed: float = 750.0
|
||||||
|
pen_down_bias_mm: float = 0.0
|
||||||
|
tip_mm: float = 0.38
|
||||||
|
|
||||||
|
|
||||||
|
# Tuned presets for the pens we keep on hand. Default = uni-ball Signo.
|
||||||
|
PENS: dict[str, PenProfile] = {
|
||||||
|
"uniball-signo": PenProfile(
|
||||||
|
name="uniball-signo",
|
||||||
|
label="uni-ball Signo UM-151 0.38 (gel, archival/waterproof)",
|
||||||
|
pen_down_dwell_ms=120, pen_up_dwell_ms=0, draw_feed=750.0,
|
||||||
|
pen_down_bias_mm=0.0, tip_mm=0.38,
|
||||||
|
),
|
||||||
|
"pilot-g2": PenProfile(
|
||||||
|
name="pilot-g2",
|
||||||
|
label="Pilot G-2 0.7 (gel, hand-written look)",
|
||||||
|
# G-2 can blob on a long pen-down dwell; keep dwell short.
|
||||||
|
pen_down_dwell_ms=80, pen_up_dwell_ms=0, draw_feed=800.0,
|
||||||
|
pen_down_bias_mm=0.0, tip_mm=0.7,
|
||||||
|
),
|
||||||
|
"energel": PenProfile(
|
||||||
|
name="energel",
|
||||||
|
label="Pentel EnerGel 0.5 (liquid-gel, fast-dry batch)",
|
||||||
|
# Liquid-gel starts instantly and dries fast — minimal dwell, faster feed.
|
||||||
|
pen_down_dwell_ms=60, pen_up_dwell_ms=0, draw_feed=900.0,
|
||||||
|
pen_down_bias_mm=0.0, tip_mm=0.5,
|
||||||
|
),
|
||||||
|
"fountain": PenProfile(
|
||||||
|
name="fountain",
|
||||||
|
label="Fountain pen + document ink (max ink character)",
|
||||||
|
# Fountain nibs need a touch more settle time and a gentle, slower line;
|
||||||
|
# add a small downward bias so the nib keeps consistent flow.
|
||||||
|
pen_down_dwell_ms=180, pen_up_dwell_ms=40, draw_feed=600.0,
|
||||||
|
pen_down_bias_mm=-0.1, tip_mm=0.5,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_PEN = "uniball-signo"
|
||||||
|
|
||||||
|
|
||||||
|
def load_pen(name: str | None) -> PenProfile:
|
||||||
|
"""Return a PenProfile for a named pen (default uni-ball Signo)."""
|
||||||
|
key = (name or DEFAULT_PEN).lower()
|
||||||
|
if key not in PENS:
|
||||||
|
raise ValueError(f"unknown pen '{name}'; choices: {sorted(PENS)}")
|
||||||
|
return PENS[key]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,22 @@ class MCS150UpdateHandler:
|
||||||
# Check current MCS-150 status via FMCSA API
|
# Check current MCS-150 status via FMCSA API
|
||||||
mcs150_status = self._check_current_status(dot_number)
|
mcs150_status = self._check_current_status(dot_number)
|
||||||
|
|
||||||
|
# Enrich the intake with the carrier's CURRENT registered data from the
|
||||||
|
# FMCSA carrier API. The MCS-150 biennial update re-confirms the carrier's
|
||||||
|
# existing FMCSA record, so the authoritative source for the form is the
|
||||||
|
# FMCSA census (legal name, address, EIN, fleet counts) -- the intake form
|
||||||
|
# only collects the DOT number + any changes. Customer-provided intake
|
||||||
|
# values take precedence over the census (so edits/changes win).
|
||||||
|
census = self._fetch_carrier_record(dot_number)
|
||||||
|
if census:
|
||||||
|
merged = {**census, **{k: v for k, v in intake.items() if v not in (None, "")}}
|
||||||
|
intake = merged
|
||||||
|
LOG.info("[%s] Enriched intake from FMCSA census (legal_name=%s)",
|
||||||
|
order_number, intake.get("legal_name"))
|
||||||
|
else:
|
||||||
|
LOG.warning("[%s] No FMCSA census data for DOT %s -- form may be sparse",
|
||||||
|
order_number, dot_number)
|
||||||
|
|
||||||
# Step 1: Fill the official MCS-150 PDF
|
# Step 1: Fill the official MCS-150 PDF
|
||||||
pdf_path = None
|
pdf_path = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -321,30 +337,59 @@ class MCS150UpdateHandler:
|
||||||
|
|
||||||
return [minio_path] if minio_path else []
|
return [minio_path] if minio_path else []
|
||||||
|
|
||||||
def _check_current_status(self, dot_number: str) -> str:
|
def _fetch_fmcsa_carrier(self, dot_number: str) -> dict:
|
||||||
"""Check current MCS-150 status via FMCSA API."""
|
"""Fetch the raw FMCSA carrier census record for a DOT number."""
|
||||||
try:
|
try:
|
||||||
import urllib.request
|
import urllib.request
|
||||||
api_key = os.environ.get("FMCSA_API_KEY", "")
|
api_key = os.environ.get("FMCSA_API_KEY", "")
|
||||||
if not api_key:
|
if not api_key:
|
||||||
return "API key not configured"
|
return {}
|
||||||
|
|
||||||
url = f"https://mobile.fmcsa.dot.gov/qc/services/carriers/{dot_number}?webKey={api_key}"
|
url = f"https://mobile.fmcsa.dot.gov/qc/services/carriers/{dot_number}?webKey={api_key}"
|
||||||
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
req = urllib.request.Request(url, headers={"Accept": "application/json"})
|
||||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||||
data = json.loads(resp.read())
|
data = json.loads(resp.read())
|
||||||
|
return data.get("content", {}).get("carrier", {}) or {}
|
||||||
carrier = data.get("content", {}).get("carrier", {})
|
|
||||||
outdated = carrier.get("mcs150Outdated", "?")
|
|
||||||
status = carrier.get("statusCode", "?")
|
|
||||||
allowed = carrier.get("allowedToOperate", "?")
|
|
||||||
|
|
||||||
return (
|
|
||||||
f"Status: {status}, Allowed: {allowed}, "
|
|
||||||
f"MCS-150 Outdated: {outdated}"
|
|
||||||
)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
return f"Could not check: {exc}"
|
LOG.warning("FMCSA carrier fetch failed for %s: %s", dot_number, exc)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def _fetch_carrier_record(self, dot_number: str) -> dict:
|
||||||
|
"""Return the carrier's current FMCSA data mapped to the intake keys the
|
||||||
|
PDF filler expects (so the biennial-update form is pre-filled with the
|
||||||
|
carrier's existing registered data). Empty dict if unavailable.
|
||||||
|
"""
|
||||||
|
c = self._fetch_fmcsa_carrier(dot_number)
|
||||||
|
if not c:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def s(v):
|
||||||
|
return "" if v is None else str(v)
|
||||||
|
|
||||||
|
out: dict = {
|
||||||
|
"legal_name": s(c.get("legalName")),
|
||||||
|
"dba_name": s(c.get("dbaName")),
|
||||||
|
"dot_number": s(c.get("dotNumber") or dot_number),
|
||||||
|
"ein": s(c.get("ein")),
|
||||||
|
"address_street": s(c.get("phyStreet")),
|
||||||
|
"address_city": s(c.get("phyCity")),
|
||||||
|
"address_state": s(c.get("phyState")),
|
||||||
|
"address_zip": s(c.get("phyZipcode")),
|
||||||
|
"phone": s(c.get("telephone") or c.get("phone")),
|
||||||
|
"power_units": s(c.get("totalPowerUnits")),
|
||||||
|
"drivers": s(c.get("totalDrivers")),
|
||||||
|
}
|
||||||
|
return {k: v for k, v in out.items() if v}
|
||||||
|
|
||||||
|
def _check_current_status(self, dot_number: str) -> str:
|
||||||
|
"""Check current MCS-150 status via FMCSA API."""
|
||||||
|
c = self._fetch_fmcsa_carrier(dot_number)
|
||||||
|
if not c:
|
||||||
|
return "Could not check FMCSA status"
|
||||||
|
return (
|
||||||
|
f"Status: {c.get('statusCode', '?')}, "
|
||||||
|
f"Allowed: {c.get('allowedToOperate', '?')}, "
|
||||||
|
f"MCS-150 Outdated: {c.get('mcs150Outdated', '?')}"
|
||||||
|
)
|
||||||
|
|
||||||
def _create_pending_signature_todo(self, order_number, entity_name, dot_number,
|
def _create_pending_signature_todo(self, order_number, entity_name, dot_number,
|
||||||
slug, minio_path, customer_email):
|
slug, minio_path, customer_email):
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ export const SERVICE_META: Record<string, ServiceMeta> = {
|
||||||
"mc-authority": { name: "MC Operating Authority Application", price_cents: 19900, gov_fee_label: "FMCSA operating authority application fee" },
|
"mc-authority": { name: "MC Operating Authority Application", price_cents: 19900, gov_fee_label: "FMCSA operating authority application fee" },
|
||||||
"mcs150-update": { name: "MCS-150 Biennial Update", price_cents: 3900 },
|
"mcs150-update": { name: "MCS-150 Biennial Update", price_cents: 3900 },
|
||||||
"medicare-enrollment": { name: "Medicare Enrollment (PECOS)", price_cents: 69900 },
|
"medicare-enrollment": { name: "Medicare Enrollment (PECOS)", price_cents: 69900 },
|
||||||
|
"name-reservation-nv": { name: "Nevada Name Reservation (90 days)", price_cents: 7900, gov_fee_label: "Nevada SOS name reservation fee" },
|
||||||
|
"name-reservation-tx": { name: "Texas Name Reservation (Form 501, 120 days)", price_cents: 7900, gov_fee_label: "Texas SOS name reservation fee (Form 501)" },
|
||||||
"new-carrier-bundle": { name: "New Carrier Onboarding Bundle (FRN + 499 Initial + RMD + CPNI + CALEA)", price_cents: 179900 },
|
"new-carrier-bundle": { name: "New Carrier Onboarding Bundle (FRN + 499 Initial + RMD + CPNI + CALEA)", price_cents: 179900 },
|
||||||
"nm-weight-distance": { name: "NM Weight-Distance Tax Setup", price_cents: 10900, gov_fee_label: "NM weight-distance permit & account fees (state, billed at cost)" },
|
"nm-weight-distance": { name: "NM Weight-Distance Tax Setup", price_cents: 10900, gov_fee_label: "NM weight-distance permit & account fees (state, billed at cost)" },
|
||||||
"npi-reactivation": { name: "NPI Reactivation", price_cents: 44900 },
|
"npi-reactivation": { name: "NPI Reactivation", price_cents: 44900 },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue