-
Notifications
You must be signed in to change notification settings - Fork 0
feat(console): wire onboarding to real /orgs + eERC onboarding + treasury #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { afterEach, describe, expect, it, vi } from "vitest"; | ||
| import type { CreatePaymentRequest } from "@benzo/types"; | ||
| import type { CreatePaymentRequest, OnboardingStatus } from "@benzo/types"; | ||
| import { ACTIVE_ORG_KEY, api, apiHref } from "./api"; | ||
|
|
||
| function jsonResponse(body: unknown, status = 200): Response { | ||
|
|
@@ -15,6 +15,27 @@ function callHeaders(call: unknown[]): Headers { | |
| : new Headers(); | ||
| } | ||
|
|
||
| function onboardingStatus(status: OnboardingStatus["status"]): OnboardingStatus { | ||
| return { | ||
| id: "onb_1", | ||
| userId: "usr_1", | ||
| address: "0x1234567890abcdef1234567890abcdef12345678", | ||
| chainEnv: "testnet", | ||
| chainId: 43113, | ||
|
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== relevant files =="
git ls-files | rg '^(apps/console/src/lib/api\.test\.ts|packages/.*/config|.*config.*)$' || true
echo
echo "== search for `@benzo/config` usage =="
rg -n "`@benzo/config`|chainEnv|chainId|testnet|43113" apps/console/src packages -g '!**/node_modules/**' || true
echo
echo "== map api.test.ts =="
if [ -f apps/console/src/lib/api.test.ts ]; then
wc -l apps/console/src/lib/api.test.ts
cat -n apps/console/src/lib/api.test.ts
fi
echo
echo "== map candidate config files =="
for f in $(git ls-files | rg '^.*config.*\.(ts|tsx|js|mjs|cjs|json)$' | head -n 40); do
echo "--- $f"
wc -l "$f"
doneRepository: Miny-Labs/benzo-console Length of output: 25688 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== file list =="
git ls-files | rg '^(apps/console/src/lib/api\.test\.ts|.*`@benzo/config`.*|.*config.*)$' || true
echo
echo "== api.test.ts =="
sed -n '1,200p' apps/console/src/lib/api.test.ts
echo
echo "== `@benzo/config` references =="
rg -n "`@benzo/config`|chainEnv|chainId|testnet|43113" apps/console/src packages -g '!**/node_modules/**' || trueRepository: Miny-Labs/benzo-console Length of output: 15752 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== packages/config/src/index.ts (relevant ranges) =="
sed -n '1,120p' packages/config/src/index.ts
echo
sed -n '120,240p' packages/config/src/index.ts
echo
sed -n '240,320p' packages/config/src/index.ts
echo
echo "== packages/config/test/index.test.ts =="
sed -n '1,120p' packages/config/test/index.test.ts
echo
echo "== apps/console/src/lib/api.ts =="
sed -n '1,220p' apps/console/src/lib/api.tsRepository: Miny-Labs/benzo-console Length of output: 17830 Import the shared Fuji chain metadata here. 🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| status, | ||
| error: status === "failed" ? "failed" : null, | ||
| createdAt: "2026-07-10T00:00:00.000Z", | ||
| updatedAt: "2026-07-10T00:00:02.000Z", | ||
| mockKyc: null, | ||
| steps: { | ||
| kyc: { completedAt: status === "pending_kyc" ? null : "2026-07-10T00:00:01.000Z", provider: status === "pending_kyc" ? null : "mock" }, | ||
| allowlist: { completedAt: ["allowlisted", "gas_dripped", "awaiting_registration", "complete"].includes(status) ? "2026-07-10T00:00:02.000Z" : null, result: null, txHash: null }, | ||
| gas: { completedAt: ["gas_dripped", "awaiting_registration", "complete"].includes(status) ? "2026-07-10T00:00:03.000Z" : null, result: null, txHash: null }, | ||
| registration: { completedAt: status === "complete" ? "2026-07-10T00:00:04.000Z" : null, lastCheckedAt: ["awaiting_registration", "complete"].includes(status) ? "2026-07-10T00:00:04.000Z" : null }, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| describe("console API idempotency", () => { | ||
| afterEach(() => { | ||
| localStorage.clear(); | ||
|
|
@@ -73,12 +94,11 @@ describe("console API idempotency", () => { | |
| amount: { amount: "10000000", assetCode: "USDC" }, | ||
| } satisfies CreatePaymentRequest; | ||
| const actions: Array<() => Promise<unknown>> = [ | ||
| () => api.saveOnboarding({ name: "Acme" }), | ||
| () => api.createOrg({ name: "Acme", slug: "acme" }), | ||
| () => api.startOnboarding({ name: "Acme Inc.", country: "US" }), | ||
| () => api.siweVerify("m", "0xsig"), | ||
| () => api.logout(), | ||
| () => api.submitKyb({ legalName: "Acme Inc." }), | ||
| () => api.provisionTreasury(), | ||
| () => api.finishOnboarding(), | ||
| () => api.provisionTreasury("org_1"), | ||
| () => api.proveBalance("1"), | ||
| () => api.proveTotal(), | ||
| () => api.proveSolvency(), | ||
|
|
@@ -129,6 +149,73 @@ describe("console API idempotency", () => { | |
| expect(callHeaders(fetchMock.mock.calls[1]).get("idempotency-key")).toBe(firstKey); | ||
| }); | ||
|
|
||
| it("uses the real org and eERC onboarding endpoints", async () => { | ||
| const complete = onboardingStatus("complete"); | ||
| const fetchMock = vi.fn() | ||
| .mockResolvedValueOnce(jsonResponse({ org: { id: "org_1", name: "Acme", slug: "acme", role: "owner", createdAt: "2026-07-10T00:00:00.000Z" }, role: "owner" })) | ||
| .mockResolvedValueOnce(jsonResponse({ jobId: "job_1", onboarding: onboardingStatus("pending_kyc") }, 202)) | ||
| .mockResolvedValueOnce(jsonResponse({ onboarding: complete })) | ||
| .mockResolvedValueOnce(jsonResponse({ address: "0xtreasury", custody: "managed", registered: true, consented: true, registrationTxHash: "0xreg" }, 201)); | ||
| vi.stubGlobal("fetch", fetchMock); | ||
|
|
||
| await expect(api.createOrg({ name: "Acme", slug: "acme" })).resolves.toMatchObject({ role: "owner", org: { id: "org_1" } }); | ||
| await expect(api.startOnboarding({ name: "Acme Inc.", country: "US" })).resolves.toMatchObject({ jobId: "job_1" }); | ||
| await expect(api.onboardingStatus()).resolves.toMatchObject({ onboarding: { status: "complete" } }); | ||
| await expect(api.provisionTreasury("org_1")).resolves.toMatchObject({ address: "0xtreasury", custody: "managed" }); | ||
|
|
||
| expect(fetchMock.mock.calls.map((call) => call[0])).toEqual([ | ||
| apiHref("/orgs"), | ||
| apiHref("/onboarding/start"), | ||
| apiHref("/onboarding/status"), | ||
| apiHref("/orgs/org_1/treasury"), | ||
| ]); | ||
| expect(fetchMock.mock.calls[0][1]).toMatchObject({ method: "POST", body: JSON.stringify({ name: "Acme", slug: "acme" }), credentials: "include" }); | ||
| expect(fetchMock.mock.calls[1][1]).toMatchObject({ method: "POST", body: JSON.stringify({ mockKyc: { name: "Acme Inc.", country: "US" } }), credentials: "include" }); | ||
| expect(callHeaders(fetchMock.mock.calls[2]).get("idempotency-key")).toBeNull(); | ||
| expect(fetchMock.mock.calls[3][1]).toMatchObject({ method: "POST", body: JSON.stringify({ consent: true }), credentials: "include" }); | ||
| }); | ||
|
|
||
| it("subscribes to onboarding status with credentialed EventSource", () => { | ||
| class FakeEventSource { | ||
| static instances: FakeEventSource[] = []; | ||
| url: string | URL; | ||
| init?: EventSourceInit; | ||
| private listeners = new Map<string, Array<(event: MessageEvent<string>) => void>>(); | ||
|
|
||
| constructor(url: string | URL, init?: EventSourceInit) { | ||
| this.url = url; | ||
| this.init = init; | ||
| FakeEventSource.instances.push(this); | ||
| } | ||
|
|
||
| addEventListener(type: string, listener: EventListenerOrEventListenerObject | null) { | ||
| if (typeof listener !== "function") return; | ||
| const listeners = this.listeners.get(type) ?? []; | ||
| listeners.push(listener as (event: MessageEvent<string>) => void); | ||
| this.listeners.set(type, listeners); | ||
| } | ||
|
|
||
| close() {} | ||
|
|
||
| emit(type: string, data: unknown) { | ||
| const event = new MessageEvent(type, { data: JSON.stringify(data) }); | ||
| for (const listener of this.listeners.get(type) ?? []) listener(event); | ||
| } | ||
| } | ||
| vi.stubGlobal("EventSource", FakeEventSource as unknown as typeof EventSource); | ||
| const seen: OnboardingStatus["status"][] = []; | ||
|
|
||
| const subscription = api.subscribeOnboardingStatus((status) => seen.push(status.status)); | ||
| const source = FakeEventSource.instances[0]; | ||
| source.emit("status", { onboarding: onboardingStatus("kyc_approved") }); | ||
| source.emit("status", { onboarding: onboardingStatus("complete") }); | ||
| subscription.close(); | ||
|
|
||
| expect(source.url).toBe(apiHref("/onboarding/status/stream")); | ||
| expect(source.init).toMatchObject({ withCredentials: true }); | ||
| expect(seen).toEqual(["kyc_approved", "complete"]); | ||
| }); | ||
|
|
||
| it("loads proof receipts through the authenticated API without mutation idempotency", async () => { | ||
| const fetchMock = vi.fn().mockResolvedValue(jsonResponse([{ id: "prf_1", action: "payroll.policy.cap", vkId: "SPENDCAP", verified: true, createdAt: "2026-06-26T00:00:00.000Z" }])); | ||
| vi.stubGlobal("fetch", fetchMock); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reset
orgOnboardingwhen an active organization appears.This effect only changes the flag to
true. If a refresh later suppliessession.activeOrg, the stale flag still routes the user to onboarding because Line 45 checksorgOnboardingfirst.Proposed fix
useEffect(() => { - if (!session) { - setOrgOnboarding(false); - return; - } - if (!session.activeOrg) setOrgOnboarding(true); + setOrgOnboarding(Boolean(session && !session.activeOrg)); }, [session]);📝 Committable suggestion
🤖 Prompt for AI Agents