@borrowbetter/arpcsdk 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BorrowBetter
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,163 @@
1
+ # @borrowbetter/arpcsdk
2
+
3
+ Typed, agnostic TypeScript client for FDR's **ARPC DEX** API — Achieve Resolution Partner Connect, Digital Enrollment Experience. It owns the two-host routing, the OAuth token lifecycle, and bearer injection, and exposes the full endpoint surface as typed operations generated from FDR's OpenAPI spec. Thin by design: you assemble payloads and drive the enrollment sequence; the SDK handles auth + transport + types.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @borrowbetter/arpcsdk
9
+ ```
10
+
11
+ ## Requirements
12
+
13
+ - Node.js >= 18
14
+ - FDR ARPC OAuth client credentials (provided by your FDR integration contact)
15
+
16
+ ## Quick start
17
+
18
+ ```typescript
19
+ import { ArpcSDK } from "@borrowbetter/arpcsdk";
20
+
21
+ const sdk = new ArpcSDK({
22
+ oauthUrl: "https://oauth.stg.ffngcp.com",
23
+ gatewayUrl: "https://apis-gateway-v2.stg.fdrgcp.com",
24
+ username: "borrowbetter@seller.com", // OAuth client_id (also the seller_agent_email)
25
+ password: process.env.FDR_OAUTH_PASSWORD!,
26
+ });
27
+
28
+ await sdk.authenticate(); // exchange credentials → bearer JWT (~900s TTL)
29
+
30
+ const elig = await sdk.api.checkEligibility({ /* … */ });
31
+ const faid = elig.data.application?.applicant?.fdr_applicant_id;
32
+
33
+ await sdk.api.registerApplicantV2({ /* … */ });
34
+ // … thread `faid` through the rest of the sequence (see below)
35
+ ```
36
+
37
+ Every operation returns `{ status, data, headers }` and **never throws on a non-2xx** — a business gate (a UW/DRA readiness failure, a duplicate) comes back as a normal response you inspect via `status`/`data`. Only transport errors throw. `scripts/smoke.ts` is the full end-to-end reference sequence (including the program poll loop).
38
+
39
+ ## How it works — the mental model
40
+
41
+ Four things to hold in your head:
42
+
43
+ - **Two hosts.** Auth happens on the **OAuth host** (`POST /v1/token`, HTTP Basic). Everything else goes to the **API gateway**. The SDK routes each call to the right one.
44
+ - **The passport.** `fdr_applicant_id` is minted once at eligibility and threads through *every* subsequent call. You carry it between operations.
45
+ - **Token lifecycle.** `authenticate()` exchanges credentials for a bearer JWT with a **~900s (15 min) TTL**, stored and attached to every `api` call. Re-call to refresh on a long run.
46
+ - **No-throw responses.** Operations return `{ status, data, headers }`. Branch on `status` — don't wrap every call in try/catch.
47
+
48
+ ## The enrollment workflow
49
+
50
+ The SDK is agnostic — it doesn't impose an order. This is the sequence FDR's DEX flow expects, and what `scripts/smoke.ts` drives.
51
+
52
+ ```mermaid
53
+ sequenceDiagram
54
+ participant App as Your app (sdk.api.*)
55
+ participant OAuth as FDR OAuth host
56
+ participant GW as FDR API gateway
57
+ participant DS as DocuSign
58
+
59
+ App->>OAuth: authenticate() — client_credentials
60
+ OAuth-->>App: bearer JWT (~900s)
61
+
62
+ Note over App,GW: Phase 1 — identity
63
+ App->>GW: checkEligibility() POST /v2/application/eligibility
64
+ GW-->>App: fdr_applicant_id (the "passport")
65
+ App->>GW: registerApplicantV2() POST /v2/applicant/register
66
+ GW-->>App: 200 — triggers async credit fetch
67
+
68
+ Note over App,GW: Phase 2 — program (poll)
69
+ loop until 200 (credit ready)
70
+ App->>GW: generateProgramV1() POST /v1/application/program
71
+ GW-->>App: 202 + Retry-After (… then 200)
72
+ end
73
+
74
+ Note over App,GW: Phase 3 — lead & underwriting
75
+ App->>GW: createLeadSync() POST /v3/application
76
+ App->>GW: readLead() → patchLead() clear hard-conditions
77
+ App->>GW: uwSubmissionV2() POST /v2/application/uw-submission/{id}
78
+ GW-->>App: conditional approval (disclosure email suppressed)
79
+ App->>GW: sendEmail() (only when not auto-sent)
80
+
81
+ Note over App,GW: Phase 4 — enrollment & signing
82
+ App->>GW: updateApplicationBankDetails() PATCH /v1/application/bank-update/{id}
83
+ App->>GW: createProgramSummaryTask() (gates DRA)
84
+ App->>GW: getApplicationReadiness() GET …/readiness
85
+ App->>GW: generateDra() POST /v2/application/{id}/dra
86
+ GW->>DS: create envelope
87
+ DS-->>App: embedded signing URL
88
+ ```
89
+
90
+ ### Step by step
91
+
92
+ **Auth** — `sdk.authenticate()`. One call up front; refresh if the run runs long.
93
+
94
+ **Phase 1 — identity**
95
+ - `checkEligibility()` — stateless quote (no PII). Its critical job: it **mints `fdr_applicant_id`**. Registration doesn't return the id yet, so call eligibility first. *(FDR is working on making registerV2 return the id — not shipped yet.)*
96
+ - `registerApplicantV2()` — writes applicant PII and **kicks off an async credit pull**, which is why Phase 2 polls.
97
+
98
+ **Phase 2 — program (poll it yourself)**
99
+ - `generateProgramV1()` returns `202` + a `Retry-After` while the credit pull runs, then `200` with the program. **Loop until 200**, honoring `Retry-After` (usually ~12s, occasionally 20s+). See `scripts/smoke.ts` for the reference loop.
100
+
101
+ **Phase 3 — lead & underwriting**
102
+ - `createLeadSync()` — promotes to a real lead. Data authority shifts to FDR/Salesforce here.
103
+ - `readLead()` → `patchLead()` — the lead comes back with **hard-conditions** that block underwriting. Read their ids from `readLead()`, then `patchLead()` with each set `verified_by_debt_consultant: true`.
104
+ - `uwSubmissionV2()` — for these leads it comes back as **conditional approval almost every time**, which *suppresses* the automatic banking-disclosure email.
105
+ - `sendEmail()` — send the disclosure manually **only when UW didn't auto-send** (check `uw.data.banking_disclosure_email_sent`). Full approval auto-sends; conditional approval suppresses.
106
+
107
+ **Phase 4 — enrollment & signing**
108
+ - `updateApplicationBankDetails()` — attach bank details + banking-disclosure confirmation.
109
+ - `createProgramSummaryTask()` — records the program-summary review. **This gates DRA generation** — skip it and the `dra` readiness check fails. Returns `201`, not `200`, on success.
110
+ - `getApplicationReadiness()` — pre-flight; confirms the `uw_submission` and `dra` gates are verified.
111
+ - `generateDra()` — returns a **DocuSign embedded signing URL** (`dra.data.applicant?.url`). Single-use, ~5 min TTL. Successful signing triggers enrollment downstream.
112
+
113
+ ### Endpoint reference
114
+
115
+ | `sdk.api.*` | Endpoint |
116
+ |-------------|----------|
117
+ | `checkEligibility` | `POST /v2/application/eligibility` (mints the applicant id) |
118
+ | `registerApplicantV2` | `POST /v2/applicant/register` |
119
+ | `generateProgramV1` | `POST /v1/application/program` (202 → … → 200 poll) |
120
+ | `createLeadSync` | `POST /v3/application` |
121
+ | `readLead` / `patchLead` | `GET` / `PATCH /v1/application/{id}` |
122
+ | `uwSubmissionV2` | `POST /v2/application/uw-submission/{id}` |
123
+ | `sendEmail` | `POST /v1/application/send-email/{id}` (only if UW didn't auto-send) |
124
+ | `updateApplicationBankDetails` | `PATCH /v1/application/bank-update/{id}` |
125
+ | `createProgramSummaryTask` | `POST /v2/application/program-summary-task/{id}` — **gates DRA** |
126
+ | `getApplicationReadiness` | `GET /v1/application/{id}/readiness` |
127
+ | `generateDra` | `POST /v2/application/{id}/dra` (DocuSign embedded signing URL) |
128
+ | `leadTransfer` | `POST /v1/application/lead-transfer` (retail handoff — see below) |
129
+
130
+ ## Error handling & gotchas
131
+
132
+ - **Branch on `res.status`, not exceptions.** A gated call returns e.g. `{ status: 400, data: { error_code: "ER40301", … } }`.
133
+ - **`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.
134
+ - **`ER40303` at DRA = missing draft/program dates** — a known FDR-side gap: the `dra` gate can reject on empty draft/program dates that no request field sets (they're backend-derived). Not a bug in your call.
135
+ - **Conditional vs full approval changes the email path** — always check `banking_disclosure_email_sent` before deciding whether to call `sendEmail()`.
136
+ - **Program timing varies** — the poll can take 20s+; don't set a tight timeout around the loop.
137
+
138
+ ## Retail transfer (the drop-off path)
139
+
140
+ `sdk.api.leadTransfer()` hands a lead to FDR's **retail sales floor** — a human calls the applicant to help them enroll by phone. Two uses: **reactive** (the digital flow stalls or the applicant abandons → hand off to recover) or **proactive** (offer phone vs. online checkout up front).
141
+
142
+ Key property: **no inactivation.** Transferring does *not* close the digital lead — both stay active, and whichever path enrolls first wins. If retail enrolls first, a later digital enroll returns a duplicate / "already enrolled" error (expected). So calling it early is safe.
143
+
144
+ ## Development
145
+
146
+ ```bash
147
+ cp .env.example .env # fill in STG credentials
148
+ pnpm install
149
+ pnpm codegen # regenerate the typed client from the OpenAPI spec
150
+ pnpm smoke # drive the full flow end-to-end against live STG
151
+ pnpm typecheck
152
+ pnpm build # codegen + tsup → dist/ (esm + cjs + d.ts)
153
+ ```
154
+
155
+ The typed client (`src/generated/`) is generated from the committed spec (`openapi/api-v2026.15.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`.
156
+
157
+ ## Releasing
158
+
159
+ ```bash
160
+ ./publish.sh <npm-otp> [patch|minor|major]
161
+ ```
162
+
163
+ Requires a clean tree on `main` and an npm login. Typechecks, builds, bumps the version, tags, pushes, and publishes `@borrowbetter/arpcsdk`.