@borrowbetter/arpcsdk 0.4.0 → 0.6.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/README.md CHANGED
@@ -10,7 +10,7 @@ pnpm add @borrowbetter/arpcsdk
10
10
 
11
11
  ## Requirements
12
12
 
13
- - Node.js >= 18
13
+ - Node.js >= 22.12
14
14
  - FDR ARPC OAuth client credentials (provided by your FDR integration contact)
15
15
 
16
16
  ## Quick start
@@ -154,7 +154,7 @@ await sdk.api.selectProgram({
154
154
  Two things to know:
155
155
 
156
156
  - **Select from the lead's options, not the quote's.** Eligibility and `generateProgramV1()` return pre-lead estimates. `createLeadSync()` submits the full income/expense picture and FDR **recalculates the program**, so the lead's tiers differ — in a smoke run, `Best Value` moved from `$380/mo` to `$560/mo`. Pick after the lead exists.
157
- - **Request and response speak different vocabularies.** You send `draft_type: "bi-weekly" | "regular" | "split"`; the lead reports a capitalised label back on `application.draft_type` — `Bi-Weekly`, `Regular`, `Split`. (FDR's spec documents `Monthly [Regular]` and `Twice Monthly [Split]` for the latter two; neither value exists — see [Spec repairs](#spec-repairs).) The SDK exports the mapping so you don't hand-roll it:
157
+ - **Request and response speak different vocabularies.** You send `draft_type: "bi-weekly" | "regular" | "split"`; the lead reports a capitalised label back on `application.draft_type` — `Bi-Weekly`, `Regular`, `Split`. (Through 2026.16.0 the spec documented `Monthly [Regular]` and `Twice Monthly [Split]` for the latter two, neither of which the API returns; 16.1 corrected the enum. The two vocabularies still differ, so the mapping stays.) The SDK exports the mapping so you don't hand-roll it:
158
158
 
159
159
  ```typescript
160
160
  import { DRAFT_TYPE_CODE, DRAFT_TYPE_LABEL, draftTypeOf } from "@borrowbetter/arpcsdk";
@@ -169,11 +169,12 @@ Two things to know:
169
169
  ## Error handling & gotchas
170
170
 
171
171
  - **Branch on `res.status`, not exceptions.** A gated call returns e.g. `{ status: 400, data: { error_code: "ER40301", … } }`.
172
- - **UW submission rewrites the program selection to `Bi-Weekly`.** Not a clear — an overwrite, with the draft amount changed to match. Observed on both `regular` and `split`. Always call `selectProgram()` again after `uwSubmissionV2()`. It's invisible if you selected bi-weekly in the first place, which is why it's easy to miss. Note the UW response's own `application` is a thin projection (identity + status, no program fields), so re-read the lead to check what actually happened.
172
+ - **UW submission rewrites the program selection to `Bi-Weekly`.** Not a clear — an overwrite, with the draft amount changed to match. Observed on both `regular` and `split`. Always call `selectProgram()` again after `uwSubmissionV2()`. It's invisible if you selected bi-weekly in the first place, which is why it's easy to miss. Note the UW response's own `application` is a thin projection (identity + status, no program fields — typed as `UwSubmissionV2Application` since 2026.16.1), so re-read the lead to check what actually happened.
173
173
  - **`ER40301` at UW is often transient** — the readiness check hasn't caught up with the condition-verify you just did (live eventual consistency). Retry the UW submission after a short delay before treating it as terminal.
174
- - **`ER40303` at DRA = the `dra` readiness gate rejected.** Read `error_details[]` for the specific field. The draft/program dates it checks are backend-derived — no request field sets them — so they're populated by `selectProgram()` and `createProgramSummaryTask()` upstream rather than by the DRA call itself. The one case with no workaround today is **`split`**, which always fails on `secondDraftDateMissing` ("Second draft date is required when draft type is split or bi-weekly"); bi-weekly passes the same check via `second_draft_date_bi_weekly`, which has no split equivalent. `bi-weekly` and `regular` complete end-to-end; reported to FDR.
174
+ - **`ER40303` at DRA = the `dra` readiness gate rejected.** Read `error_details[]` for the specific field. The draft/program dates it checks are backend-derived — no request field sets them — so they're populated by `selectProgram()` and `createProgramSummaryTask()` upstream rather than by the DRA call itself. **`split` needs one extra call.** It fails the `secondDraftDateMissing` check ("Second draft date is required when draft type is split or bi-weekly") unless you `patchLead()` a `second_draft_date_split` **after UW submission and before generating the DRA** — added in 2026.16.1 in response to our report. Bi-weekly needs nothing: `second_draft_date_bi_weekly` is populated automatically at UW approval, and there is no patch field for it. `bi-weekly` and `regular` are what the smoke run exercises end-to-end; the `split` patch is documented but not yet driven by it.
175
175
  - **Hardship and goal fields are UW-required despite being spec-optional.** `CreateLeadRequest` marks nothing required, and lead creation happily returns `200` without them — then UW fails with `ER40301` / `HARDSHIP_CHECK_FAILED`. Empirically the gate needs **all three** of `hardship_category`, `hardship_category_other` and `goal_category`; `goal_category_other` is genuinely optional. Note `hardship_category_other` is required even when the category isn't `Other`, contradicting its own description — reported to FDR. Also don't trust the `field` on that error: it has been observed naming a field that *was* supplied.
176
- - **Read readiness failures off `error_details[]`, not `errors[]`.** Each `ReadinessCheck` now carries structured entries with a stable `reason_code` (`REQUIRED`, `DUPLICATE_EMAIL`, `CONDITION_NOT_VERIFIED`, `DEBT_TOTAL_MISMATCH`, …) and a dotted `field` path. `errors[]` is free text kept for compat — its wording changes.
176
+ - **Readiness gates return the breakdown on `error_details`.** Since 2026.16.1 `ARPCErrorResponse.error_details` is a typed `ErrorDetails` (`checks[]`, `is_program_refresh_required`, `is_uw_approved`, `message`), so `ER40301` at UW and `ER40303`/`ER40305` at DRA are readable without a cast. Failures are documented as **`400`**; the `422` is now a separate generic `ER42200` for seller-config/upstream failures.
177
+ - **Read per-check failures off `error_details[]`, not `errors[]`.** Each `ReadinessCheck` now carries structured entries with a stable `reason_code` (`REQUIRED`, `DUPLICATE_EMAIL`, `CONDITION_NOT_VERIFIED`, `DEBT_TOTAL_MISMATCH`, …) and a dotted `field` path. `errors[]` is free text kept for compat — its wording changes.
177
178
  - **Conditional vs full approval changes the email path** — always check `banking_disclosure_email_sent` before deciding whether to call `sendEmail()`.
178
179
  - **Program timing varies** — the poll can take 20s+; don't set a tight timeout around the loop.
179
180
 
@@ -251,6 +252,8 @@ const sdk = new ArpcSDK({
251
252
  pnpm install
252
253
  pnpm codegen # regenerate the typed client from the OpenAPI spec
253
254
  pnpm smoke # drive the full flow end-to-end against live STG
255
+ pnpm smoke <user-id> # ... as a specific test identity (see scripts/test-users.ts)
256
+ pnpm smoke --force-hard-conditions # ... auto-verifying hard-conditions (test-only shortcut)
254
257
  pnpm typecheck
255
258
  pnpm build # codegen + tsup → dist/ (esm + cjs + d.ts)
256
259
  ```
@@ -264,7 +267,7 @@ ARPC_OAUTH_PASSWORD=...
264
267
 
265
268
  These are the smoke script's, not the SDK's — the library never reads `process.env`. The script pins `environment: "stg"` in code rather than taking it from `.env`: it drives a real enrollment against the canned STG test identity, so it must not be pointable at PRD.
266
269
 
267
- The typed client (`src/generated/`) is generated from the committed spec (`openapi/api-v2026.16.0.json`) and is not checked in — `pnpm codegen` (run automatically by `build`) reproduces it. To move to a newer spec, drop the JSON in `openapi/` and update `input.target` in `codegen.ts`.
270
+ The typed client (`src/generated/`) is generated from the committed spec (`openapi/api-v2026.16.1.json`) and is not checked in — `pnpm codegen` (run automatically by `build`) reproduces it. To move to a newer spec, drop the JSON in `openapi/` and update `input.target` in `codegen.ts`.
268
271
 
269
272
  The spec's `Authentication` tag is excluded from codegen (`input.filters`): it documents `POST /v1/token`, which lives on the OAuth host with HTTP Basic and a form-encoded body, while every generated operation runs through the ky mutator and targets the gateway. `src/auth.ts` owns that exchange.
270
273
 
@@ -274,10 +277,10 @@ The vendored spec is byte-identical to what FDR published. Defects we've reporte
274
277
 
275
278
  | Repair | FDR ships | We generate |
276
279
  |---|---|---|
277
- | `program-selection-applicant` | `ProgramSelectionRequest.applicant` typed as the full registration `Applicant` — 10 required fields incl. `ssn`, `network_token` | `ProgramSelectionApplicant`, requiring only `fdr_applicant_id` — all the endpoint actually uses |
278
- | `readiness-error-envelope` | `ErrorDetails` / `ReadinessErrorResponse` deleted; UW readiness failures typed as `ARPCErrorResponse`, which has no `error_details` | Both schemas restored and the UW `400` repointed, so the readiness breakdown is typeable |
279
- | `lead-draft-type-values` | `LeadApplication.draft_type` enumerates `Bi-Weekly`, `Monthly [Regular]`, `Twice Monthly [Split]` | `Bi-Weekly`, `Regular`, `Split` — the values the API actually returns, confirmed against STG by selecting each in turn |
280
280
  | `select-program-error-responses` | `selectProgram` 401/500 → `ServerResponse`, an empty `{type: object}` | `ARPCErrorResponse`, matching every other operation; `ServerResponse` dropped |
281
+ | `program-selection-applicant-id` | `ProgramSelectionApplicant.fdr_applicant_id` optional, so `{ applicant: {} }` type-checks and 400s at runtime | the one field the endpoint reads, marked `required` |
282
+
283
+ 2026.16.1 fixed three of the four repairs this file carried against 16.0 — the `draft_type` response enum, the `ProgramSelectionRequest.applicant` reference, and the readiness error envelope (`ARPCErrorResponse.error_details`). Their guards fired on the version bump and the repairs were deleted, which is the mechanism working as designed. Note `ReadinessErrorResponse` is no longer exported: readiness detail now hangs off `ARPCErrorResponse.error_details` as FDR's own `ErrorDetails`.
281
284
 
282
285
  Each patched schema carries a `⚠️ Patched locally` note in its TSDoc, so the divergence is visible at the point of use.
283
286
 
@@ -292,6 +295,13 @@ Releases are automated via [Changesets](https://github.com/changesets/changesets
292
295
  pnpm changeset
293
296
  ```
294
297
  Pick the bump (patch/minor/major) and write a summary. Commit the generated `.changeset/*.md` alongside your code.
295
- 2. Merge to `main`. The **Release** workflow (`.github/workflows/release.yml`) then runs `changeset version` (bumps `package.json` + updates `CHANGELOG.md`, consuming the changeset), commits `chore: version packages`, and publishes to npm.
298
+ 2. Merge to `main`. The **Release** workflow (`.github/workflows/release.yml`) runs `changeset version` (bumps `package.json` + updates `CHANGELOG.md`, consuming the changeset) on a `changeset-release/main` branch and opens a **chore: version packages** PR.
299
+ 3. The same workflow run waits on that PR's checks and squash-merges it as soon as they're all green — no action needed from you. That merge lands on `main`, re-triggers Release with no changesets pending, and *that* run publishes to npm.
300
+
301
+ Nothing is pushed to `main` directly; the org rulesets don't allow it. Every repo-facing step runs as the `borrowbetter-automation` GitHub App, which is a `pull_request`-mode bypass actor on `org-require-pr-review` — that's what lets it merge a mechanical version bump without a human approval while still forcing the change through a PR. The merge gate is *every* check on the PR, not just the contexts the ruleset marks required, so a red typecheck stops the release even though CI isn't a required context.
302
+
303
+ If a release stalls, look for an open `chore: version packages` PR: a failed check leaves it sitting there and turns the Release run red. Fix forward on `main` and the next push rebuilds the branch from scratch.
304
+
305
+ > **Never write the literal CI-skip token in a commit message or PR body.** GitHub honours it anywhere in the message, not just the first line, and squash merges here inherit the branch's commit messages (`squash_merge_commit_message: COMMIT_MESSAGES`). Mentioning it in prose — even in backticks, even to say you removed it — silently suppresses CI on the PR and Release on the merge, which is exactly the run that publishes. It cost this repo a day of green-looking PRs with no CI on them. Write it as "the skip-ci marker" instead.
296
306
 
297
- Publishing uses **npm OIDC trusted publishing** — no `NPM_TOKEN` secret. The trusted-publisher policy on npm is scoped to this repo + the `release.yml` workflow.
307
+ Publishing uses **npm OIDC trusted publishing** — no `NPM_TOKEN` secret. The trusted-publisher policy on npm is scoped to this repo + the `release.yml` workflow, so don't rename or move that file. `workflow_dispatch` publishes whatever version `package.json` currently holds, for when a bump landed but the publish didn't.