hedge-broker 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +38 -9
- package/dist/index.js +295 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable changes to `hedge-broker` are documented here. This project follows
|
|
|
4
4
|
[Semantic Versioning](https://semver.org) and
|
|
5
5
|
[Keep a Changelog](https://keepachangelog.com).
|
|
6
6
|
|
|
7
|
+
## [0.2.0] - 2026-07-06
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `submit` now covers the full create schema: `--website`, `--fein`, `--entity-type`, `--naics`, `--business-phone`, `--business-email`, `--contact-first`, `--contact-last`, `--contact-email`, `--contact-phone`, `--address`, `--address2`, `--city`, `--zip`, `--tiv`, `--vehicles`, `--payroll`, `--insured-id`, `--producer-email`.
|
|
12
|
+
- `submit --body <file|->` sends a full JSON request body from a file or stdin; explicit flags take precedence over matching top-level keys, applicant flags merge over the body's applicant, and address flags merge into its mailing_address.
|
|
13
|
+
- `submit` sends an `Idempotency-Key` header: a random UUID per invocation by default, or your own via `--idempotency-key <key>`. Replay engages only when the same key is re-sent within 24h (per brokerage), so scripted retries that must not double-create should pass their own key.
|
|
14
|
+
- `submit` fails fast with a clear message when a partial mailing address is given; the API requires `--address`, `--city`, `--state`, and `--zip` to resolve together (fields supplied via `--body` count).
|
|
15
|
+
- `submissions` filters: `--limit`, `--offset`, `--updated-since` (alongside the existing `--status` and `--search`).
|
|
16
|
+
- `policy <policyId>`: policy detail (term, net premium, total billed, commission, payment plan, documents).
|
|
17
|
+
- `policy-doc <policyId> <kind> [-o file]`: download the binder, policy, or declarations PDF.
|
|
18
|
+
- `documents <submissionId>`: list a submission's finalized documents.
|
|
19
|
+
- `download <documentId> [-o file]`: download a finalized document PDF (filename from Content-Disposition when `-o` is omitted).
|
|
20
|
+
- `market-requirements <marketId> --lob <slug> [--state ST] [--programs keys]`: what a market needs to quote a line.
|
|
21
|
+
- `status` now renders the live per-carrier marketing table (carrier, line, status, quote premium), a status roll-up, the effective date, and the documents on file.
|
|
22
|
+
- `policies` table gains effective and expiration columns; `payments` gains an invoice_url column.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- `submit --state` now actually sends the state (top-level `primary_state`, or the mailing address state when a mailing address is present). In 0.1.0 the flag was accepted but dropped.
|
|
27
|
+
- Non-JSON API error bodies (HTML 502s from a proxy) no longer crash uploads with a SyntaxError; the raw text is surfaced instead.
|
|
28
|
+
- API validation errors (FastAPI `detail` arrays/objects) render as readable `loc: message` lines instead of `[object Object]`.
|
|
29
|
+
- Filenames taken from a server's Content-Disposition header are sanitized (path separators, control characters, and Windows-invalid characters stripped) before writing to disk.
|
|
30
|
+
|
|
7
31
|
## [0.1.0] - 2026-07-06
|
|
8
32
|
|
|
9
33
|
### Added
|
package/README.md
CHANGED
|
@@ -48,17 +48,22 @@ hedge status <submission-id>
|
|
|
48
48
|
| Command | What it does |
|
|
49
49
|
| --- | --- |
|
|
50
50
|
| `hedge appetite <class> [--state ST] [--lob slug]` | Which markets have appetite for a class of business. |
|
|
51
|
-
| `hedge
|
|
51
|
+
| `hedge market-requirements <marketId> --lob <slug> [--state ST] [--programs keys]` | What a market needs to quote a line: the application package plus per-program requirements. Market ids come from `hedge appetite --json`. |
|
|
52
|
+
| `hedge submit --insured <name> --narrative <text> [flags]` | Create a submission (does not market it yet). Applicant, contact, address, and exposure flags are listed in `hedge submit --help`; `--body <file\|->` sends a full JSON body, with explicit flags taking precedence. |
|
|
52
53
|
| `hedge upload <submissionId> <file.pdf> [--name label]` | Attach an ACORD, loss runs, or supplement. |
|
|
53
54
|
| `hedge requirements <submissionId>` | What the submission still needs (per market, forms, carrier questions). |
|
|
54
55
|
| `hedge finalize <submissionId>` | Start marketing the submission to carriers. |
|
|
55
|
-
| `hedge status <submissionId>` | Submission detail plus live marketing and
|
|
56
|
-
| `hedge submissions [--status state] [--search q]` | List your brokerage's submissions. |
|
|
56
|
+
| `hedge status <submissionId>` | Submission detail plus the live per-carrier marketing table, status roll-up, and documents on file. |
|
|
57
|
+
| `hedge submissions [--status state] [--search q] [--limit n] [--offset n] [--updated-since iso]` | List your brokerage's submissions, with filters and paging. |
|
|
58
|
+
| `hedge documents <submissionId>` | List a submission's finalized documents (ACORDs, quotes, binders). |
|
|
59
|
+
| `hedge download <documentId> [-o file]` | Download a finalized document PDF. |
|
|
57
60
|
| `hedge quotes <submissionId>` | List instant-quote carrier sessions and their open questions. |
|
|
58
61
|
| `hedge answer <submissionId> <sessionId> --set k=v [--set k=v ...]` | Answer a carrier session's questions. |
|
|
59
62
|
| `hedge request-quote <submissionId> <sessionId>` | Close a carrier session, request an indication, then a quote. |
|
|
60
|
-
| `hedge policies` | List bound policies. |
|
|
61
|
-
| `hedge
|
|
63
|
+
| `hedge policies` | List bound policies with term dates. |
|
|
64
|
+
| `hedge policy <policyId>` | Policy detail: term, premium, commission, payment plan, documents. Policy ids come from `hedge policies --json`. |
|
|
65
|
+
| `hedge policy-doc <policyId> <kind> [-o file]` | Download a policy PDF; kind is `binder`, `policy`, or `declarations`. |
|
|
66
|
+
| `hedge payments` | List payment and invoice status, with invoice links. |
|
|
62
67
|
| `hedge whoami` | Show the signed-in broker and brokerage. |
|
|
63
68
|
| `hedge login` / `hedge logout` | Sign in and out. |
|
|
64
69
|
|
|
@@ -78,15 +83,39 @@ hedge appetite "roofing contractor" --state CA
|
|
|
78
83
|
hedge submit \
|
|
79
84
|
--insured "Peak Roofing LLC" \
|
|
80
85
|
--narrative "Residential re-roofing, no hot tar, no work over 3 stories" \
|
|
81
|
-
--lob commercial_general_liability \
|
|
82
|
-
--
|
|
83
|
-
--
|
|
86
|
+
--lob commercial_general_liability,commercial_property \
|
|
87
|
+
--effective 2026-08-01 \
|
|
88
|
+
--website https://peakroofing.example.com \
|
|
89
|
+
--fein 12-3456789 \
|
|
90
|
+
--entity-type llc \
|
|
91
|
+
--naics 238160 \
|
|
92
|
+
--business-phone "415-555-0100" \
|
|
93
|
+
--business-email office@peakroofing.example.com \
|
|
94
|
+
--contact-first Dana --contact-last Rivera \
|
|
95
|
+
--contact-email dana@peakroofing.example.com \
|
|
96
|
+
--contact-phone "415-555-0101" \
|
|
97
|
+
--address "1200 Harrison St" --address2 "Suite 4" \
|
|
98
|
+
--city Oakland --state CA --zip 94612 \
|
|
99
|
+
--tiv 250000 --payroll 900000 --vehicles 3
|
|
84
100
|
hedge upload <submission-id> ./acord-125.pdf --name "ACORD 125"
|
|
85
101
|
hedge requirements <submission-id>
|
|
86
102
|
hedge finalize <submission-id>
|
|
87
103
|
hedge status <submission-id>
|
|
104
|
+
hedge documents <submission-id>
|
|
105
|
+
hedge download <document-id> -o quote.pdf
|
|
88
106
|
```
|
|
89
107
|
|
|
108
|
+
When a mailing address is in play, `--state` fills the mailing address state; the address must be complete (`--address`, `--city`, `--state`, `--zip`, counting fields supplied via `--body`). With `--state` alone it is sent as the submission's top-level `primary_state`.
|
|
109
|
+
|
|
110
|
+
Prefer building the request in a file (or another program)? Send a full JSON body. Explicit flags still win: top-level flag values replace matching body keys, applicant flags merge over the body's applicant, and address flags merge into its mailing_address:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
hedge submit --body ./submission.json
|
|
114
|
+
cat submission.json | hedge submit --body - --effective 2026-09-01
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Every `submit` sends an `Idempotency-Key` header (a random UUID each run). Replay only engages when the same key is re-sent within 24 hours, so a scripted retry that must not double-create should pass its own key with `--idempotency-key <key>`.
|
|
118
|
+
|
|
90
119
|
## Signing in
|
|
91
120
|
|
|
92
121
|
The CLI signs in with OAuth 2.1, so there are no API keys to copy around for the interactive flow, and it works over SSH.
|
|
@@ -98,7 +127,7 @@ The CLI signs in with OAuth 2.1, so there are no API keys to copy around for the
|
|
|
98
127
|
|
|
99
128
|
Credentials are stored per profile at `~/.config/taven-cli/hedge.<profile>.json` with `0600` permissions. Access tokens are refreshed automatically. Sign out with `hedge logout`.
|
|
100
129
|
|
|
101
|
-
The CLI requests the `broker_mcp` and `broker_submit` scopes. Submitting on a brokerage's behalf requires that the brokerage has connected apps and programmatic submission enabled by Hedge. Read commands (appetite, submissions, status, requirements, policies, payments) work with either scope. `submit`, `upload`, `finalize`, and
|
|
130
|
+
The CLI requests the `broker_mcp` and `broker_submit` scopes. Submitting on a brokerage's behalf requires that the brokerage has connected apps and programmatic submission enabled by Hedge. Read commands (appetite, market-requirements, submissions, status, requirements, documents, download, quotes, policies, policy, policy-doc, payments) work with either scope. `submit`, `upload`, `finalize`, `answer`, and `request-quote` require `broker_submit`.
|
|
102
131
|
|
|
103
132
|
## License
|
|
104
133
|
|
package/dist/index.js
CHANGED
|
@@ -103,7 +103,7 @@ async function deviceLogin(opts) {
|
|
|
103
103
|
}
|
|
104
104
|
throw new Error(pb.error === "access_denied" ? "Access was denied" : pb.error || `Login failed (${poll.status})`);
|
|
105
105
|
}
|
|
106
|
-
throw new Error("Login timed out
|
|
106
|
+
throw new Error("Login timed out. Run the command again");
|
|
107
107
|
}
|
|
108
108
|
async function loopbackLogin(opts) {
|
|
109
109
|
const { meta, clientId } = opts;
|
|
@@ -178,6 +178,7 @@ function sleep(ms) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// src/core/http.ts
|
|
181
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
181
182
|
var ApiError = class extends Error {
|
|
182
183
|
constructor(status, message) {
|
|
183
184
|
super(message);
|
|
@@ -185,11 +186,37 @@ var ApiError = class extends Error {
|
|
|
185
186
|
}
|
|
186
187
|
status;
|
|
187
188
|
};
|
|
189
|
+
function renderErrorDetail(detail, fallback) {
|
|
190
|
+
if (detail == null) return fallback;
|
|
191
|
+
if (typeof detail === "string") return detail.trim() || fallback;
|
|
192
|
+
if (Array.isArray(detail)) {
|
|
193
|
+
const lines = detail.map((entry) => {
|
|
194
|
+
if (entry && typeof entry === "object" && "msg" in entry) {
|
|
195
|
+
const loc = Array.isArray(entry.loc) ? entry.loc.join(".") : "";
|
|
196
|
+
return (loc ? loc + ": " : "") + String(entry.msg);
|
|
197
|
+
}
|
|
198
|
+
return JSON.stringify(entry);
|
|
199
|
+
});
|
|
200
|
+
return lines.join("\n") || fallback;
|
|
201
|
+
}
|
|
202
|
+
if (typeof detail === "object") return JSON.stringify(detail);
|
|
203
|
+
return String(detail);
|
|
204
|
+
}
|
|
205
|
+
function apiErrorFrom(status, parsed, fallback) {
|
|
206
|
+
let detail = void 0;
|
|
207
|
+
if (parsed && typeof parsed === "object") {
|
|
208
|
+
const obj = parsed;
|
|
209
|
+
detail = obj.detail ?? obj.error;
|
|
210
|
+
} else if (typeof parsed === "string") {
|
|
211
|
+
detail = parsed;
|
|
212
|
+
}
|
|
213
|
+
return new ApiError(status, renderErrorDetail(detail, fallback));
|
|
214
|
+
}
|
|
188
215
|
async function bearer(cfg) {
|
|
189
216
|
const tok = loadToken(cfg);
|
|
190
|
-
if (!tok) throw new ApiError(401, "Not signed in
|
|
217
|
+
if (!tok) throw new ApiError(401, "Not signed in. Run `login` first");
|
|
191
218
|
if (tok.expires_at - 60 > Math.floor(Date.now() / 1e3)) return tok.access_token;
|
|
192
|
-
if (!tok.refresh_token) throw new ApiError(401, "Session expired
|
|
219
|
+
if (!tok.refresh_token) throw new ApiError(401, "Session expired. Run `login` again");
|
|
193
220
|
try {
|
|
194
221
|
const r = await refresh(tok.token_endpoint, tok.client_id, tok.refresh_token);
|
|
195
222
|
const updated = {
|
|
@@ -203,7 +230,7 @@ async function bearer(cfg) {
|
|
|
203
230
|
return updated.access_token;
|
|
204
231
|
} catch {
|
|
205
232
|
clearToken(cfg);
|
|
206
|
-
throw new ApiError(401, "Session expired
|
|
233
|
+
throw new ApiError(401, "Session expired. Run `login` again");
|
|
207
234
|
}
|
|
208
235
|
}
|
|
209
236
|
async function apiRequest(opts, method, path, init) {
|
|
@@ -230,10 +257,7 @@ async function apiRequest(opts, method, path, init) {
|
|
|
230
257
|
parsed = text;
|
|
231
258
|
}
|
|
232
259
|
}
|
|
233
|
-
if (!res.ok) {
|
|
234
|
-
const detail = parsed && typeof parsed === "object" && "detail" in parsed && parsed.detail || parsed && typeof parsed === "object" && "error" in parsed && parsed.error || (typeof parsed === "string" ? parsed : `Request failed (${res.status})`);
|
|
235
|
-
throw new ApiError(res.status, String(detail));
|
|
236
|
-
}
|
|
260
|
+
if (!res.ok) throw apiErrorFrom(res.status, parsed, `Request failed (${res.status})`);
|
|
237
261
|
return parsed;
|
|
238
262
|
}
|
|
239
263
|
async function multipartRequest(opts, method, path, form) {
|
|
@@ -243,13 +267,60 @@ async function multipartRequest(opts, method, path, form) {
|
|
|
243
267
|
else headers.Authorization = `Bearer ${await bearer(opts.cfg)}`;
|
|
244
268
|
const res = await fetch(url, { method, headers, body: form });
|
|
245
269
|
const text = await res.text();
|
|
246
|
-
let parsed =
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
270
|
+
let parsed = void 0;
|
|
271
|
+
if (text) {
|
|
272
|
+
try {
|
|
273
|
+
parsed = JSON.parse(text);
|
|
274
|
+
} catch {
|
|
275
|
+
parsed = text;
|
|
276
|
+
}
|
|
250
277
|
}
|
|
278
|
+
if (!res.ok) throw apiErrorFrom(res.status, parsed, `Upload failed (${res.status})`);
|
|
251
279
|
return parsed;
|
|
252
280
|
}
|
|
281
|
+
function sanitizeFilename(name) {
|
|
282
|
+
const last = name.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
283
|
+
const cleaned = last.replace(/[:*?"<>|\u0000-\u001f]/g, "_").trim();
|
|
284
|
+
if (!cleaned || cleaned === "." || cleaned === "..") return "";
|
|
285
|
+
return cleaned;
|
|
286
|
+
}
|
|
287
|
+
function dispositionFilename(header) {
|
|
288
|
+
if (!header) return void 0;
|
|
289
|
+
const star = /filename\*=(?:UTF-8'')?"?([^";]+)"?/i.exec(header);
|
|
290
|
+
if (star) {
|
|
291
|
+
try {
|
|
292
|
+
return decodeURIComponent(star[1].trim());
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const plain = /filename="?([^";]+)"?/i.exec(header);
|
|
297
|
+
return plain ? plain[1].trim() : void 0;
|
|
298
|
+
}
|
|
299
|
+
async function downloadRequest(opts, method, path, outPath, fallbackName) {
|
|
300
|
+
const url = opts.apiBase.replace(/\/$/, "") + path;
|
|
301
|
+
const headers = {};
|
|
302
|
+
if (opts.apiKey) headers[opts.apiKeyHeader ?? "X-Api-Key"] = opts.apiKey;
|
|
303
|
+
else headers.Authorization = `Bearer ${await bearer(opts.cfg)}`;
|
|
304
|
+
const res = await fetch(url, { method, headers });
|
|
305
|
+
if (!res.ok) {
|
|
306
|
+
const text = await res.text();
|
|
307
|
+
let parsed = void 0;
|
|
308
|
+
if (text) {
|
|
309
|
+
try {
|
|
310
|
+
parsed = JSON.parse(text);
|
|
311
|
+
} catch {
|
|
312
|
+
parsed = text;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
throw apiErrorFrom(res.status, parsed, `Download failed (${res.status})`);
|
|
316
|
+
}
|
|
317
|
+
const serverName = dispositionFilename(res.headers.get("content-disposition"));
|
|
318
|
+
const safeName = serverName ? sanitizeFilename(serverName) : "";
|
|
319
|
+
const target = outPath ?? (safeName || void 0) ?? fallbackName;
|
|
320
|
+
if (!target) throw new ApiError(500, "No output filename (pass -o <file>)");
|
|
321
|
+
writeFileSync2(target, Buffer.from(await res.arrayBuffer()));
|
|
322
|
+
return target;
|
|
323
|
+
}
|
|
253
324
|
|
|
254
325
|
// src/core/output.ts
|
|
255
326
|
function printJson(data) {
|
|
@@ -377,18 +448,104 @@ function registerAuth(program2) {
|
|
|
377
448
|
}
|
|
378
449
|
|
|
379
450
|
// src/commands/submissions.ts
|
|
451
|
+
import { randomUUID } from "crypto";
|
|
380
452
|
import { readFileSync as readFileSync2 } from "fs";
|
|
381
453
|
import { basename } from "path";
|
|
454
|
+
var POLICY_DOC_KINDS = ["binder", "policy", "declarations"];
|
|
455
|
+
function intFlag(value, flag) {
|
|
456
|
+
if (!/^\d+$/.test(String(value).trim())) throw new Error(`${flag} expects a whole number, got "${value}"`);
|
|
457
|
+
return Number.parseInt(String(value).trim(), 10);
|
|
458
|
+
}
|
|
459
|
+
function readJsonBody(source) {
|
|
460
|
+
const raw = source === "-" ? readFileSync2(0, "utf8") : readFileSync2(source, "utf8");
|
|
461
|
+
let parsed;
|
|
462
|
+
try {
|
|
463
|
+
parsed = JSON.parse(raw);
|
|
464
|
+
} catch (e) {
|
|
465
|
+
throw new Error(`--body is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
466
|
+
}
|
|
467
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("--body must be a JSON object");
|
|
468
|
+
return parsed;
|
|
469
|
+
}
|
|
470
|
+
function formatBytes(size) {
|
|
471
|
+
const n = Number(size);
|
|
472
|
+
if (!Number.isFinite(n) || n <= 0) return "";
|
|
473
|
+
if (n < 1024) return `${n} B`;
|
|
474
|
+
if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
|
|
475
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
476
|
+
}
|
|
382
477
|
function registerSubmissions(program2) {
|
|
383
|
-
program2.command("submit").description("Create a submission (does not market it
|
|
478
|
+
program2.command("submit").description("Create a submission (does not market it; run `finalize` when ready)").option("--insured <name>", "insured business name (required unless --body supplies applicant.insured_name)").option("--narrative <text>", "operations description of the risk (required unless --body supplies narrative)").option("--lob <slugs>", "comma-separated lines of business, e.g. commercial_general_liability,workers_compensation").option("--effective <date>", "effective date, YYYY-MM-DD").option("--state <ST>", "primary state (2-letter); with any address flag it becomes the mailing address state instead").option("--website <url>", "insured website").option("--fein <id>", "FEIN (or SSN for a sole proprietor)").option("--entity-type <type>", "legal entity type, e.g. llc, corporation").option("--naics <code>", "NAICS code").option("--business-phone <phone>", "insured business phone").option("--business-email <email>", "insured business email").option("--contact-first <name>", "primary contact first name").option("--contact-last <name>", "primary contact last name").option("--contact-email <email>", "primary contact email").option("--contact-phone <phone>", "primary contact phone").option("--address <line1>", "mailing address line 1").option("--address2 <line2>", "mailing address line 2").option("--city <city>", "mailing address city").option("--zip <zip>", "mailing address ZIP code").option("--tiv <int>", "total insured property value in dollars (property_tiv_total)").option("--vehicles <int>", "number of vehicles (auto_vehicle_count)").option("--payroll <int>", "total annual payroll in dollars (wc_total_annual_payroll)").option("--insured-id <uuid>", "start from an existing insured in your book").option("--producer-email <email>", "producing broker to attribute (required for brokerage API-client credentials)").option(
|
|
479
|
+
"--body <file|->",
|
|
480
|
+
"full JSON request body from a file, or - for stdin. Precedence: flags win; a top-level flag value replaces the matching body key, applicant flags merge over the body's applicant, and address flags merge into its mailing_address"
|
|
481
|
+
).option(
|
|
482
|
+
"--idempotency-key <key>",
|
|
483
|
+
"Idempotency-Key header value (default: a random UUID per invocation). Re-send the same key within 24h to replay the original response instead of creating a duplicate"
|
|
484
|
+
).action(async (opts) => {
|
|
384
485
|
const ctx = makeCtx(program2.opts());
|
|
385
|
-
const body = {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
486
|
+
const body = opts.body ? readJsonBody(opts.body) : { lines_of_business: [] };
|
|
487
|
+
const applicant = {};
|
|
488
|
+
if (opts.insured) applicant.insured_name = opts.insured;
|
|
489
|
+
if (opts.businessPhone) applicant.business_phone = opts.businessPhone;
|
|
490
|
+
if (opts.businessEmail) applicant.business_email = opts.businessEmail;
|
|
491
|
+
if (opts.website) applicant.website = opts.website;
|
|
492
|
+
if (opts.fein) applicant.fein_or_ssn = opts.fein;
|
|
493
|
+
if (opts.entityType) applicant.entity_type = opts.entityType;
|
|
494
|
+
if (opts.naics) applicant.naics = opts.naics;
|
|
495
|
+
if (opts.contactFirst) applicant.contact_first_name = opts.contactFirst;
|
|
496
|
+
if (opts.contactLast) applicant.contact_last_name = opts.contactLast;
|
|
497
|
+
if (opts.contactEmail) applicant.contact_email = opts.contactEmail;
|
|
498
|
+
if (opts.contactPhone) applicant.contact_phone = opts.contactPhone;
|
|
499
|
+
const addressFlags = {};
|
|
500
|
+
if (opts.address) addressFlags.line1 = opts.address;
|
|
501
|
+
if (opts.address2) addressFlags.line2 = opts.address2;
|
|
502
|
+
if (opts.city) addressFlags.city = opts.city;
|
|
503
|
+
if (opts.zip) addressFlags.zip = opts.zip;
|
|
504
|
+
const hasAddressFlags = Object.keys(addressFlags).length > 0;
|
|
505
|
+
const state = opts.state ? String(opts.state).trim().toUpperCase() : void 0;
|
|
506
|
+
const bodyApplicant = body.applicant && typeof body.applicant === "object" && !Array.isArray(body.applicant) ? body.applicant : void 0;
|
|
507
|
+
const bodyAddress = bodyApplicant?.mailing_address && typeof bodyApplicant.mailing_address === "object" && !Array.isArray(bodyApplicant.mailing_address) ? bodyApplicant.mailing_address : void 0;
|
|
508
|
+
let mailing = bodyAddress ? { ...bodyAddress } : void 0;
|
|
509
|
+
if (hasAddressFlags) mailing = { ...mailing ?? {}, ...addressFlags };
|
|
510
|
+
if (state) {
|
|
511
|
+
if (mailing) {
|
|
512
|
+
mailing.state = state;
|
|
513
|
+
if (body.primary_state != null) body.primary_state = state;
|
|
514
|
+
} else {
|
|
515
|
+
body.primary_state = state;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (hasAddressFlags) {
|
|
519
|
+
const flagFor = { line1: "--address", city: "--city", state: "--state", zip: "--zip" };
|
|
520
|
+
const missing = Object.keys(flagFor).filter((k) => !mailing?.[k]).map((k) => flagFor[k]);
|
|
521
|
+
if (missing.length) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
"--address, --city, --state and --zip must be provided together (missing " + missing.join(", ") + "; fields already in --body's mailing_address count)"
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (mailing && (hasAddressFlags || state)) applicant.mailing_address = mailing;
|
|
528
|
+
if (Object.keys(applicant).length > 0) {
|
|
529
|
+
body.applicant = { ...bodyApplicant ?? {}, ...applicant };
|
|
530
|
+
}
|
|
531
|
+
if (opts.narrative) body.narrative = opts.narrative;
|
|
532
|
+
if (opts.lob) body.lines_of_business = String(opts.lob).split(",").map((s) => s.trim()).filter(Boolean);
|
|
390
533
|
if (opts.effective) body.effective_date = opts.effective;
|
|
391
|
-
|
|
534
|
+
if (opts.tiv != null) body.property_tiv_total = intFlag(opts.tiv, "--tiv");
|
|
535
|
+
if (opts.vehicles != null) body.auto_vehicle_count = intFlag(opts.vehicles, "--vehicles");
|
|
536
|
+
if (opts.payroll != null) body.wc_total_annual_payroll = intFlag(opts.payroll, "--payroll");
|
|
537
|
+
if (opts.insuredId) body.insured_id = opts.insuredId;
|
|
538
|
+
if (opts.producerEmail) body.producer_email = opts.producerEmail;
|
|
539
|
+
const finalApplicant = body.applicant;
|
|
540
|
+
if (!finalApplicant?.insured_name) throw new Error("--insured is required (or supply applicant.insured_name via --body)");
|
|
541
|
+
if (!body.narrative) throw new Error("--narrative is required (or supply narrative via --body)");
|
|
542
|
+
const res = await apiRequest(ctx.client, "POST", "/broker/submissions", {
|
|
543
|
+
body,
|
|
544
|
+
// Random per invocation unless the caller supplies a key. The API
|
|
545
|
+
// replays the original response when the same key is re-sent within
|
|
546
|
+
// 24h (per brokerage), so scripted retries should pass their own key.
|
|
547
|
+
headers: { "Idempotency-Key": opts.idempotencyKey ?? randomUUID() }
|
|
548
|
+
});
|
|
392
549
|
if (ctx.json) return printJson(res);
|
|
393
550
|
process.stdout.write(kv({ submission_id: res.submission_id, state: res.state, status: res.status_label }) + "\n");
|
|
394
551
|
process.stdout.write("\nNext: hedge upload " + res.submission_id + " <file.pdf>, hedge requirements " + res.submission_id + ", hedge finalize " + res.submission_id + "\n");
|
|
@@ -429,12 +586,27 @@ function registerSubmissions(program2) {
|
|
|
429
586
|
const ctx = makeCtx(program2.opts());
|
|
430
587
|
const s = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}`);
|
|
431
588
|
if (ctx.json) return printJson(s);
|
|
432
|
-
process.stdout.write(kv({ insured: s.insured_name, lines: (s.lines || []).join(", "), state: s.state, status: s.status_label, premium: s.premium }) + "\n");
|
|
589
|
+
process.stdout.write(kv({ insured: s.insured_name, lines: (s.lines || []).join(", "), state: s.state, status: s.status_label, premium: s.premium, effective: s.effective_date }) + "\n");
|
|
590
|
+
if (s.markets?.length) {
|
|
591
|
+
process.stdout.write("\nMarkets:\n" + table(s.markets.flatMap((m) => (m.lines || []).map((l) => ({
|
|
592
|
+
carrier: m.carrier_name,
|
|
593
|
+
line: l.lob_label ?? l.lob_slug,
|
|
594
|
+
status: l.status_label ?? l.status,
|
|
595
|
+
quote: l.quote_premium ?? ""
|
|
596
|
+
}))), ["carrier", "line", "status", "quote"]) + "\n");
|
|
597
|
+
}
|
|
598
|
+
const byStatus = s.status_summary?.by_status;
|
|
599
|
+
if (byStatus && Object.keys(byStatus).length) {
|
|
600
|
+
process.stdout.write("\nSummary: " + Object.entries(byStatus).map(([k, v]) => `${k}=${v}`).join(", ") + "\n");
|
|
601
|
+
}
|
|
602
|
+
if (s.documents?.length) {
|
|
603
|
+
process.stdout.write("\nDocuments: " + s.documents.map((d) => d.display_name).join(", ") + "\n");
|
|
604
|
+
}
|
|
433
605
|
});
|
|
434
|
-
program2.command("submissions").description("List your brokerage's submissions").option("--status <state>", "filter by state").option("--search <q>", "filter by insured name").action(async (opts) => {
|
|
606
|
+
program2.command("submissions").description("List your brokerage's submissions").option("--status <state>", "filter by state").option("--search <q>", "filter by insured name").option("--limit <n>", "max rows to return (1-500)").option("--offset <n>", "skip the first n rows (for paging)").option("--updated-since <iso>", "only rows updated at or after this ISO timestamp").action(async (opts) => {
|
|
435
607
|
const ctx = makeCtx(program2.opts());
|
|
436
608
|
const rows = await apiRequest(ctx.client, "GET", "/broker/submissions", {
|
|
437
|
-
query: { status: opts.status, search: opts.search }
|
|
609
|
+
query: { status: opts.status, search: opts.search, limit: opts.limit, offset: opts.offset, updated_since: opts.updatedSince }
|
|
438
610
|
});
|
|
439
611
|
if (ctx.json) return printJson(rows);
|
|
440
612
|
process.stdout.write(table(rows.map((r) => ({
|
|
@@ -451,21 +623,85 @@ function registerSubmissions(program2) {
|
|
|
451
623
|
if (ctx.json) return printJson(r);
|
|
452
624
|
process.stdout.write("Marketing started. Track with: hedge status " + submissionId + "\n");
|
|
453
625
|
});
|
|
626
|
+
program2.command("documents <submissionId>").description("List a submission's finalized documents (ACORDs, quotes, binders)").action(async (submissionId) => {
|
|
627
|
+
const ctx = makeCtx(program2.opts());
|
|
628
|
+
const rows = await apiRequest(ctx.client, "GET", `/broker/submissions/${submissionId}/finalized-documents`);
|
|
629
|
+
if (ctx.json) return printJson(rows);
|
|
630
|
+
process.stdout.write(table(rows.map((d) => ({
|
|
631
|
+
id: d.id,
|
|
632
|
+
name: d.display_name,
|
|
633
|
+
kind: d.source_label ?? d.source,
|
|
634
|
+
size: formatBytes(d.size_bytes ?? d.size),
|
|
635
|
+
added: String(d.created_at ?? "").slice(0, 10)
|
|
636
|
+
})), ["id", "name", "kind", "size", "added"]) + "\n");
|
|
637
|
+
});
|
|
638
|
+
program2.command("download <documentId>").description("Download a finalized document PDF").option("-o, --output <file>", "output file (default: the server's filename, else <documentId>.pdf)").action(async (documentId, opts) => {
|
|
639
|
+
const ctx = makeCtx(program2.opts());
|
|
640
|
+
const out = await downloadRequest(ctx.client, "GET", `/broker/finalized-documents/${documentId}/pdf`, opts.output, `${documentId}.pdf`);
|
|
641
|
+
process.stdout.write("Saved " + out + "\n");
|
|
642
|
+
});
|
|
454
643
|
program2.command("policies").description("List bound policies").action(async () => {
|
|
455
644
|
const ctx = makeCtx(program2.opts());
|
|
456
645
|
const rows = await apiRequest(ctx.client, "GET", "/broker/policies");
|
|
457
646
|
if (ctx.json) return printJson(rows);
|
|
458
|
-
process.stdout.write(table(rows.map((r) => ({
|
|
647
|
+
process.stdout.write(table(rows.map((r) => ({
|
|
648
|
+
insured: r.insured_name,
|
|
649
|
+
carrier: r.carrier_name,
|
|
650
|
+
policy: r.policy_number,
|
|
651
|
+
premium: r.premium ?? "",
|
|
652
|
+
effective: r.effective_date ?? "",
|
|
653
|
+
expiration: r.expiration_date ?? "",
|
|
654
|
+
status: r.status_label ?? r.status
|
|
655
|
+
})), ["insured", "carrier", "policy", "premium", "effective", "expiration", "status"]) + "\n");
|
|
656
|
+
});
|
|
657
|
+
program2.command("policy <policyId>").description("Policy detail: term, premium, commission, payment plan, documents").action(async (policyId) => {
|
|
658
|
+
const ctx = makeCtx(program2.opts());
|
|
659
|
+
const p = await apiRequest(ctx.client, "GET", `/broker/policies/${policyId}`);
|
|
660
|
+
if (ctx.json) return printJson(p);
|
|
661
|
+
process.stdout.write(kv({
|
|
662
|
+
insured: p.insured_name,
|
|
663
|
+
policy: p.policy_number,
|
|
664
|
+
carrier: p.carrier_name,
|
|
665
|
+
lines: (p.lines || []).join(", "),
|
|
666
|
+
term: [p.effective_date, p.expiration_date].filter(Boolean).join(" to "),
|
|
667
|
+
net_premium: p.net_premium,
|
|
668
|
+
total_billed: p.total_billed,
|
|
669
|
+
commission_rate: p.commission_rate,
|
|
670
|
+
commission_amount: p.commission_amount,
|
|
671
|
+
payment_plan: p.payment_plan
|
|
672
|
+
}) + "\n");
|
|
673
|
+
if (p.documents?.length) {
|
|
674
|
+
process.stdout.write("\nDocuments: " + p.documents.map((d) => d.kind).join(", ") + "\n");
|
|
675
|
+
process.stdout.write("Download with: hedge policy-doc " + policyId + " <kind>\n");
|
|
676
|
+
}
|
|
677
|
+
});
|
|
678
|
+
program2.command("policy-doc <policyId> <kind>").description("Download a policy PDF; kind is one of binder, policy, declarations").option("-o, --output <file>", "output file (default <policyId>-<kind>.pdf)").action(async (policyId, kind, opts) => {
|
|
679
|
+
if (!POLICY_DOC_KINDS.includes(kind)) throw new Error(`kind must be one of ${POLICY_DOC_KINDS.join(", ")} (got "${kind}")`);
|
|
680
|
+
const ctx = makeCtx(program2.opts());
|
|
681
|
+
const out = await downloadRequest(ctx.client, "GET", `/broker/policies/${policyId}/document/${kind}`, opts.output ?? `${policyId}-${kind}.pdf`);
|
|
682
|
+
process.stdout.write("Saved " + out + "\n");
|
|
459
683
|
});
|
|
460
684
|
program2.command("payments").description("List payment / invoice status").action(async () => {
|
|
461
685
|
const ctx = makeCtx(program2.opts());
|
|
462
686
|
const rows = await apiRequest(ctx.client, "GET", "/broker/payments");
|
|
463
687
|
if (ctx.json) return printJson(rows);
|
|
464
|
-
process.stdout.write(table(rows.map((r) => ({
|
|
688
|
+
process.stdout.write(table(rows.map((r) => ({
|
|
689
|
+
insured: r.insured_name,
|
|
690
|
+
status: r.status_label ?? r.status,
|
|
691
|
+
premium: r.premium ?? "",
|
|
692
|
+
invoice_url: r.invoice_url ?? ""
|
|
693
|
+
})), ["insured", "status", "premium", "invoice_url"]) + "\n");
|
|
465
694
|
});
|
|
466
695
|
}
|
|
467
696
|
|
|
468
697
|
// src/commands/appetite.ts
|
|
698
|
+
function requirementRows(items) {
|
|
699
|
+
return items.map((q) => ({
|
|
700
|
+
requirement: q.label + (q.conditional ? " (conditional)" : ""),
|
|
701
|
+
severity: q.severity,
|
|
702
|
+
provided_by: q.provided_by
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
469
705
|
function registerAppetite(program2) {
|
|
470
706
|
program2.command("appetite <class>").description('Which markets have appetite for a class, e.g. hedge appetite "roofing contractor" --state CA').option("--state <ST>", "2-letter state").option("--lob <slug>", "line of business filter").action(async (klass, opts) => {
|
|
471
707
|
const ctx = makeCtx(program2.opts());
|
|
@@ -481,6 +717,38 @@ function registerAppetite(program2) {
|
|
|
481
717
|
}));
|
|
482
718
|
process.stdout.write(table(rows, ["market", "lines", "programs", "turnaround"]) + "\n");
|
|
483
719
|
});
|
|
720
|
+
program2.command("market-requirements <marketId>").description("What a market needs to quote a line: the application package plus per-program requirements (market ids come from `hedge appetite --json`)").requiredOption("--lob <slug>", "line of business, e.g. commercial_general_liability").option("--state <ST>", "2-letter state").option("--programs <keys>", "comma-separated program keys (matched_program_keys from the appetite result) to narrow the panel").action(async (marketId, opts) => {
|
|
721
|
+
const ctx = makeCtx(program2.opts());
|
|
722
|
+
const r = await apiRequest(ctx.client, "GET", `/broker/markets/${marketId}/requirements`, {
|
|
723
|
+
query: { lob: opts.lob, state: opts.state, programs: opts.programs }
|
|
724
|
+
});
|
|
725
|
+
if (ctx.json) return printJson(r);
|
|
726
|
+
process.stdout.write(kv({
|
|
727
|
+
market: r.market_name,
|
|
728
|
+
line: r.lob?.label ?? r.lob?.slug,
|
|
729
|
+
state: r.state ?? "",
|
|
730
|
+
channel: r.market_channel
|
|
731
|
+
}) + "\n");
|
|
732
|
+
if (r.baseline?.length) {
|
|
733
|
+
process.stdout.write("\nApplication package (Hedge prepares these):\n" + table(requirementRows(r.baseline), ["requirement", "severity", "provided_by"]) + "\n");
|
|
734
|
+
}
|
|
735
|
+
for (const p of r.programs || []) {
|
|
736
|
+
const lines = (p.lines || []).map((l) => l.label ?? l.slug).join(", ");
|
|
737
|
+
process.stdout.write(`
|
|
738
|
+
Program: ${p.display_name} [${p.channel}]${lines ? " (" + lines + ")" : ""}
|
|
739
|
+
`);
|
|
740
|
+
if (p.has_authored_requirements && p.requirements?.length) {
|
|
741
|
+
process.stdout.write(table(requirementRows(p.requirements), ["requirement", "severity", "provided_by"]) + "\n");
|
|
742
|
+
} else {
|
|
743
|
+
process.stdout.write("Standard application package; no carrier-specific requirements.\n");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (r.other_program_count) {
|
|
747
|
+
process.stdout.write(`
|
|
748
|
+
${r.other_program_count} more program(s) use the standard package.
|
|
749
|
+
`);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
484
752
|
}
|
|
485
753
|
|
|
486
754
|
// src/commands/quotes.ts
|
|
@@ -512,21 +780,21 @@ function registerQuotes(program2) {
|
|
|
512
780
|
process.stdout.write(`status: ${res.status} | remaining questions: ${(res.missing_required_questions_json || []).length}
|
|
513
781
|
`);
|
|
514
782
|
});
|
|
515
|
-
program2.command("request-quote <submissionId> <sessionId>").description("Close a carrier session
|
|
783
|
+
program2.command("request-quote <submissionId> <sessionId>").description("Close a carrier session and request an indication then a quote").action(async (submissionId, sessionId) => {
|
|
516
784
|
const ctx = makeCtx(program2.opts());
|
|
517
785
|
const res = await apiRequest(ctx.client, "POST", `/broker/submissions/${submissionId}/api-quotes/sessions/${sessionId}/close`);
|
|
518
786
|
if (ctx.json) return printJson(res);
|
|
519
787
|
process.stdout.write(`outcome: ${res.outcome} | status: ${res.status}${res.quote_pdf_url ? " | quote: " + res.quote_pdf_url : ""}
|
|
520
788
|
`);
|
|
521
789
|
if ((res.missing_required_questions_json || []).length)
|
|
522
|
-
process.stdout.write(`still needs ${(res.missing_required_questions_json || []).length} answers
|
|
790
|
+
process.stdout.write(`still needs ${(res.missing_required_questions_json || []).length} answers; use \`hedge answer\`
|
|
523
791
|
`);
|
|
524
792
|
});
|
|
525
793
|
}
|
|
526
794
|
|
|
527
795
|
// src/index.ts
|
|
528
796
|
var program = new Command();
|
|
529
|
-
program.name("hedge").description("Submit risks to Hedge and track them from your terminal.").version("0.
|
|
797
|
+
program.name("hedge").description("Submit risks to Hedge and track them from your terminal.").version("0.2.0").option("--staging", "use the staging environment").option("--json", "output raw JSON (for scripting)");
|
|
530
798
|
registerAuth(program);
|
|
531
799
|
registerSubmissions(program);
|
|
532
800
|
registerAppetite(program);
|
package/package.json
CHANGED