[DONT MERGE] Add mocked dashboard app#1236
Open
Sharqiewicz wants to merge 38 commits into
Open
Conversation
❌ Deploy Preview for vortex-sandbox failed. Why did it fail? →
|
❌ Deploy Preview for vortexfi failed. Why did it fail? →
|
…hain/vortex into feature/dashboard-app-staging
… dual-write) Realizes the complete data model from the two architecture docs in one migration set, per the amended plan (migrate data once, cut reads over next, keep legacy tables as read-only backup): - 038 customer_entities + backfill (one individual entity per profile) - 039 partners split: partner_pricing_configs, dedup to unique-name partners, FK repoint (quotes/assignments) before fold, partners_legacy snapshot, assignments partner_id collapse, idempotent vortex re-seed - 040 provider_customers + kyc_cases + backfills from mykobo_customers, alfredpay_customers, tax_ids (owned rows only; statuses verbatim; raw tax ids replaced by sha256 hash + masked display; ownerless rows quarantined) - 041 api_keys.partner_id FK (backfill from partner_name) + scopes + revoked_at (user_id from 034 already is the doc's profile_id) - 042 recipient_invitations (link-based, D1) + sender_recipients + recipient_payout_references (thin provider pointer, D3) - 043 notifications + notification_preferences All new tables ENABLE ROW LEVEL SECURITY (Supabase default privileges grant anon/authenticated ALL on future tables; RLS-no-policy denies PostgREST). New status columns are VARCHAR + CHECK (plan D6). Verified: full chain 001-043 clean on fresh PG16; backfills exercised against seeded legacy fixtures (dup alfredpay rows, IND+BUS same country, multi-tax-id user, empty-string subaccount sentinel, orphan tax_id, divergent per-direction partner activity, ghost partner_name key) with all assertions passing. NOTE: not independently deployable — existing code still reads the pre-split partners shape; the cutover commits follow. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
zod was added to packages/shared and @vortexfi/sdk pinned shared to =0.2.0 without a lockfile update; the missing zod entry broke bun build:shared. Pure bun install output — no package.json changes here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Existing functionality unchanged; pricing now resolves through the split
schema (migrate-once cutover, legacy columns unread):
- New partners/partner-pricing.service: findPartnerWithPricing merges the
unique-name partner row with its per-direction pricing config — the exact
semantics of the pre-split Partner.findOne({name|id, isActive, rampType})
- partner-resolution: assignment branch reads the collapsed
profile_partner_assignments.partner_id (buy/sell pair now unread backup)
- quote-fees: single pricing-config lookup replaces the findAll loop;
CalculateFeeComponentsRequest.partnerName renamed to partnerId (it always
carried an id)
- discount helpers: ActivePartner sourced from pricing configs; in-memory
dynamic-difference state keyed by (partner_id, ramp_type) to preserve the
per-direction isolation the split rows used to give for free
- feeDistribution: vortex + markup payout addresses read from pricing configs
- apiKeyAuth: secret/public keys resolve partners via api_keys.partner_id FK;
partner_name never read; user-scoped branch keys off partner_id IS NULL
- admin controllers: unique-name partner resolution; assignments write a
single partner_id; api-key CRUD keyed by partner_id; revocation stamps
revoked_at; ambiguity 409 retired (structurally impossible)
- factories: createTestPartner creates partner + config (findOrCreate by
unique name); seedVortexPartners = 1 partner + BUY/SELL configs;
updatePartnerPricing helper for per-direction test overrides
Verified: full API suite green (414 pass / 0 fail) including the golden
pricing test (fee parity through the split), corridor scenarios and
sdk-contract suites. Security-spec updated in the same change set:
01-auth/api-keys.md, 03-ramp-engine/{profile-partner-pricing,
discount-mechanism,fee-integrity,quote-lifecycle}.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mers/kyc_cases
Migrate-once cutover of the EUR (Mykobo) and BRL (Avenia) provider identity;
existing functionality unchanged (full suite green: 415 pass / 0 fail):
- customer-entity.service: getOrCreateCustomerEntityForProfile — eager create
in verify-otp (outside the OTP error mapping so a DB failure cannot surface
as "Invalid OTP") + lazy fallback on every entity-scoped read (covers users
with pre-existing sessions who never re-verify)
- mykobo-customer.service: KYC mirror upserts/reads provider_customers
(provider=mykobo, provider_customer_id = last-synced email); module path and
exports unchanged (integration-test mock seam preserved)
- avenia-customer.service (new): hash/mask helpers, tax-hash lookups,
kyc_cases upkeep, idempotent Requested→Accepted/Rejected outcome transition
- avenia-account/brla.controller/ramp.service/phase handlers/worker: all
tax_ids reads keyed by tax_reference_hash against provider_customers;
ownership checks compare the effective user's customer_entity; SDK-matched
error strings byte-identical ("Subaccount not found", "KYC invalid", ...)
- createSubaccount keeps claim-on-create: quarantined (ownerless) legacy
tax_ids rows are adoptable via a one-time read-only probe of the backup;
owned legacy rows still 409
- Ownership gaps closed (the real remaining Phase 5): fetchSubaccountKycStatus
(which also writes status transitions) and getSelfieLivenessUrl now enforce
account ownership. Known residual: getKybAttemptStatus cannot be bound
server-side (attempt response carries no subaccount linkage)
- Migration 040 amended (pre-deploy): provider_customers.tax_reference retains
the raw normalized tax id — documented deviation from the "no raw tax IDs"
non-goal; it is the join/aggregation key for in-flight ramp state and
already persists in ramp_states.state JSONB
- Quote-provenance writes dropped (write-only columns); tests updated
(brla.controller.test rewritten for the new model incl. legacy-adoption
scenarios; factories create provider_customers; unique-subaccount fix in
sdk-contract)
Security-spec updated in the same change set: 05-integrations/brla.md
(+ cutover section documenting the raw-tax-reference deviation and the KYB
residual), 05-integrations/mykobo.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…odels Completes the provider cutover (full suite green: 415 pass / 0 fail): - alfredpay-customer.service (new): findAlfredpayCustomer resolves the caller's customer_entity and preserves the legacy updatedAt-DESC tie-break; a legacy-shaped view keeps the controller's status machine verbatim (alfredPayId/type/status accessors); status transitions mirror into the account's kyc_cases row; createAlfredpayCustomer maps country → rail - alfredpay.controller: all 22 lookup/create sites ported (mechanical); status vocabulary unchanged — and UPDATE_REQUIRED writes now succeed (the legacy Postgres enum lacked the value the shared enum had) - alfredpay.helpers/quote resolvers: customerType ASC keeps the type-ASC precedence; KYC gates read provider_customers - Removed models with zero remaining readers: AlfredPayCustomer, MykoboCustomer, KycLevel2 (tables stay as read-only backup). TaxId model retained solely for the createSubaccount legacy-adoption probe Security-spec updated: 05-integrations/alfredpay.md (+ cutover section), 07-operations/api-surface.md and 03-ramp-engine/quote-lifecycle.md resolution chains. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dashboard backend surface (plan §7/§8/D5), reading the new tables directly: - /v1/recipients: link-token invites (sha256-hashed at rest, 14-day TTL, optional email binding), token-bound acceptance creating the sender_recipients relationship, listing, nickname/block/archive PATCH, and the transfer-eligibility gate with per-check blocking reasons. - /v1/notifications: owner-scoped feed + unread count, read/read-all, preferences (defaults on first read). emitNotification never throws; email dispatch (D7) deliberately not wired yet. - GET /v1/onboarding/status: per-entity aggregate over provider_customers + kyc_cases with a normalized approved/pending/rejected state. New security specs 03-ramp-engine/recipient-transfers.md and 07-operations/notifications.md; plan Phase 1 marked landed (§7.1 payout instrument mechanism still open, so verified payout references have no production writer yet). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Phase 0 core connection plus the swap-execution engine, ported from the
widget's ramp machine per plan §9:
- Real auth: /v1/auth request-otp/verify-otp/refresh via a ported
AuthService (dashboard-scoped storage keys) + api-client with bearer
auth and single-flight 401 refresh; login page drops the fake OTP.
- Real quotes: POST /v1/quotes with a two-pass inversion so the
payout-driven form ("recipient gets X") maps onto the input-driven
quote endpoint.
- transfer.machine.ts: the widget's RegisterRamp → UpdateRamp/sign →
StartRamp → RampFollowUp core as a dashboard-owned XState machine —
register with fresh ephemerals, presign ephemeral txs client-side
(signUnsignedTransactions), sign user-owned txs with the wagmi wallet
(typed-data permits + squidRouter broadcasts, EVM-only), start, then
poll to terminal. An app-lifetime actor keeps polling after the form
unmounts and feeds the transactions table.
- CORS: allow :5174 in development; dashboard gains @vortexfi/shared
(runtime signing helpers), .env.example, esnext build target.
AssetHub payin is disabled (no substrate wallet in the dashboard).
Alfredpay/BRL offramp additionalData stays incomplete until the §7.1
payout-instrument capture lands — EUR is the fully-wirable corridor.
Recipients/notifications/onboarding pages still run on mocks; wiring
them to the new /v1 endpoints is the remaining §9-D work.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Code-verified per corridor: registration is structurally a self-offramp — destinations are already sender-bound on mykobo (anchor-side IBAN) and alfredpay (fiatAccountId scoped to the derived customer); only BRL takes a third-party destination (pix key + receiverTaxId consistency check). Sender→recipient transfers therefore need a second principal in registration (resolve the payout side from the recipient's provider identity), not a bolted-on eligibility check. Recorded as a pressing open decision (plan §12.7) and corrected in the recipient-transfers spec. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the mocked recipient store with real data: - Fetch third-party recipients + pending invitations from GET /v1/recipients (recipients.service.ts, recipient.mappers.ts, useRecipients hook). - Derive self-recipients from fetched payout accounts: one per saved AlfredPay fiat account (GET /v1/alfredpay/fiatAccounts), carrying the real fiatAccountId; a single entry for BR/EU. - Wire invite creation to POST /v1/recipients/invite (real one-time token link), invalidating the list. - Transfer selector: only self-recipients are selectable; third-party rows are disabled with a "coming soon" tooltip (no third-party sending yet). - Self-offramp additionalData now matches what the backend derives: AlfredPay sends fiatAccountId, BR sends only the user's PIX key (taxId/receiverTaxId are derived server-side) via a new PIX-key field, EU sends the receiving wallet (anchor email derived server-side). - Remove the orphaned recipient mock machinery (store recipient state/methods, ensureSelfRecipient, recipientFlow simulate, SEED_RECIPIENTS, invite-code/ profile mocks); persist bumped to v6. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive the approved-corridor gate from GET /v1/onboarding/status instead of the mocked account onboardings: - onboarding.service.ts + useApprovedCorridors hook derive the set of corridors the authenticated profile is provider-approved for (rail→corridor, with a provider+country fallback for null rails). - useRecipients gates self-recipient synthesis on that real set and returns it so the recipients page can offer only invite-able corridors. - recipients.tsx builds its "Add recipient" corridors from the real approved set rather than the mock account's selectedCorridors/onboardings. Self-recipients (AlfredPay fiat accounts, BR/EU entries) and invite corridors now reflect the user's actual provider_customers/kyc_cases, not seed data. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Delete the mock Zustand store and seed data (SEED_ACCOUNTS/SEED_TRANSACTIONS/
SEED_RECIPIENTS) and drive every surface from the authenticated session:
- Account identity is derived from the Supabase session + GET /v1/onboarding/status
(type + per-corridor status); no account switcher (one session = one account).
- Transactions come from GET /v1/ramp/history/:walletAddress for the connected
wallet (transaction.mappers + useTransactions); the transfer actor invalidates
that query instead of writing a local row.
- Onboarding is a redirect: the mock KYC/KYB wizard, headless/external machines,
country-select and add-corridor are deleted; overview shows real per-corridor
status and "Start" opens the widget (?kybLocked=<region> via VITE_WIDGET_URL).
- Funding drops the fake Privy "embedded wallet" (mockPayinAddress) — the ramp is
signed by the connected wallet, so both funding paths resolve to it.
- Remove now-dead helpers (routeFor/OnboardingRoute, simulated notify* helpers).
Third-party (non-self) recipients remain visible-but-disabled ("coming soon");
that is the only intentional restriction.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the provider-status rollup so submitted-and-under-review statuses (Mykobo PENDING, AlfredPay UserCompleted/Verifying, Avenia Requested) map to a distinct `in_review` state instead of collapsing into `pending`, which still means "awaiting the customer". Thread it through the dashboard's status mapping. Also point onboardingUrl at /widget, which the widget actually serves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The plan routed both sender and recipient onboarding to the widget via ?kybLocked=, and a later commit deleted the dashboard's onboarding wizards on that basis. That bounced an authenticated sender to another origin with no return path. Senders now onboard in the dashboard again; recipients keep the widget link-out for now. Restores OnboardingWizard / HeadlessFlow / ExternalFlow / WizardStepFields / DocumentDropzone and their state machines as they were, plus routeFor() and the CorridorCard route hints. The wizard submits nothing, so GET /v1/onboarding/status cannot see it. A session-only useOnboardingOverrideStore overlays wizard-advanced statuses on the aggregator's real ones so the corridor card advances; a reload drops back to real provider status. That store is scaffolding and is deleted when the wizard drives the real KYC machines. Plan §6 is rewritten accordingly (sender in-dashboard, recipient via widget), and the follow-up plan gains the reuse-vs-port analysis for lifting the widget's KYC state machines. Also records two gaps: sender onboarding is mocked end-to-end, and the widget has no KYB region for EU or AR, so recipient onboarding is unreachable there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ract AR was absent from KYB_REGIONS by omission, not by decision. alfredpayKyc already handled AR everywhere it handled MX/CO (plus an AR-only selfie upload), ArKycFormScreen existed, KYC_CHILD_BY_FIAT mapped ARS to it, and AlfredPayCountry.AR was already accepted server-side. Adding the region entry plus both locale strings makes ?kybLocked=AR resolve. EU stays excluded for the documented reason: Mykobo is individual-KYC-only and needs a connected wallet, so it cannot complete a quote-less KYB deep link. Corrects the mappers.ts comment that lumped AR in with EU. Follow-up plan §3 is reframed. "Move to shared" and "import the file from the frontend" both fail for the same reason: the machines import the frontend's module-scoped api-client and provider services, and vortex-frontend is private with no exports. The blocker is coupling, not packaging. Proposal is to inject the provider service at the machine boundary, then extract to a new packages/kyc, starting with alfredpayKyc (zero ramp coupling, four corridors) so the widget's ramp machine is untouched. No server behavior changes, so no security-spec update. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move the Alfredpay KYC/KYB state machine out of the widget into a new
@vortexfi/kyc package so the dashboard can run the real verification flow
instead of a visual mock. Three couplings had to be inverted for the machine
to leave the widget:
- the module-scoped AlfredpayService import became an injected AlfredpayKycApi
port (the machine no longer boots a Supabase client on import, so the test's
auth mock is gone);
- window.open became an injected openVerificationUrl (no DOM in the package);
- AlfredpayKycContext no longer extends RampContext — the machine never read a
ramp field, so kyc.states.ts now passes only { country, business }.
The zod schemas and phone normalisers move into the package too: both apps
resolve zod 4.4.3, and this is the one place a dashboard/widget divergence
would silently build a payload Alfredpay rejects. The widget's form screens now
import them.
Dashboard: the mock wizard collected no data (WizardStepFields submits nothing;
DocumentDropzone has no file input), so real screens were written —
KycFormScreen (MX/CO/AR), DocumentUploadScreen (real uploads, AR selfie), and
AlfredpayKycFlow mapping machine states to screens. OnboardingWizard routes
individual KYC on Alfredpay corridors to the real machine; everything else,
including Alfredpay company KYB, stays mocked (split by account.type). The
dashboard apiClient now passes FormData through for multipart uploads, and the
real flow refetches onboarding status rather than writing the mock override.
Tests: packages/kyc has 36 tests (18 machine incl. one new link-seam case,
18 schema). Frontend suite drops exactly the 17 moved tests; no regressions.
Per-app typecheck clean; both apps build.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
apps/dashboard had no tests. Port the frontend's hermetic Playwright setup (page.route-mocked backend, no API server, database, or chain access) and cover the auth surface: the email/OTP login flow incl. a rejected code, and the _app route gate in both directions. The dashboard gets its own config rather than a project inside the frontend's: it runs its own Vite server on 5174 under a /dashboard/ base path and consumes a different set of endpoints. Notes: - Specs other than login skip the OTP walk by seeding the session into localStorage before app scripts run; useAuthStore reads it at module init. The login spec asserts the flow writes exactly that session, so the shortcut cannot drift from what the app actually stores. - mockBackend records unmatched API paths and auth-gate.spec asserts the list is empty, so a newly-called endpoint fails the suite instead of escaping to a real server. - webServer passes --host 127.0.0.1: vite's default binds IPv6 ::1 only, which Playwright's url check never reaches. - Biome now excludes test-results/ and playwright-report/. The root .gitignore already covered the frontend's, but Biome resolves ignores relative to its invocation directory, so a workspace-local `verify` still linted them. Nightly and non-PR-blocking, matching the existing E2E policy. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Introduce docs/dashboard-app-spec.md: purpose, user stories collected from the dashboard routes/domain/machines, and high-level strategy. The dashboard's core feature is a third-party payment funded by either crypto or fiat. Fiat funding is not a ramp — RampDirection is BUY|SELL, one fiat side each — so it is an onramp chained to an offramp, and that composition is still an open design question. Decide invited-recipient onboarding (followup plan item 3): the invite link opens the widget at ?kybLocked=<country>&invite=<token>. The widget already has OTP login and the shipped KYC/KYB flows; it gains a redemption step. No /invite route in the dashboard. A second login for recipients who later use the dashboard is the accepted cost this iteration. The token is a bearer capability (24 random bytes, stored hashed, shown once), not the invitation id, and it does not authenticate — accept needs both a session and the token, forcing authenticate -> accept -> KYC. Deprecate dashboard-full-product-connection.md in favor of the spec, retaining it for migration phasing and open decisions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Accepting an invite burned it: reopening the link mid-KYC returned 409 INVITE_ALREADY_ACCEPTED, so a recipient who closed the tab could not get back into onboarding. Worse, an expiry passing after acceptance marked the row expired and stranded a relationship that already existed — and KYC can outlast the 14-day TTL. acceptInvite is now idempotent for the profile that accepted: - The invite binds to accepted_by_profile_id on first acceptance. Any other profile presenting the token still gets 409. - Expiry closes a pending invite only; re-entry survives it. - Re-entry returns 200 with the existing relationship, does not re-notify the sender, does not move accepted_at, and does not revive an archived relationship (the old !created path unconditionally set active, letting a recipient silently un-archive themselves). Sender revocation (410) and a blocked relationship (409) remain the kill switches. The transaction body was already idempotent; only the guards changed. Security spec: invariant 3 no longer claims invites are single-use — they bind to one recipient — plus a new 3a for re-entry, and the threat model now states that an intercepted link is only useful before the intended recipient redeems it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cover the dashboard's money path: a SELL offramp of USDC-on-Polygon to an MXN payout account, against the mocked backend and injected mock wallet. The signing is real — the page generates ephemeral keypairs and viem serializes EIP-1559 transactions offline — and the spec catches them at the network boundary: all three ephemeral txs arrive as raw 0x02 payloads on the first /ramp/update, the user-owned squidRouterNoPermitTransfer is broadcast by the wallet and its hash reported in a second update, then start + polling reach a terminal phase while the form navigates to /transactions. A second test pins payout-account selection: the mock serves two saved fiat accounts, and choosing the non-default one must register against that fiatAccountId — a broken selector would silently pay the wrong account. Hermeticity is enforced, not assumed. A catch-all route aborts every request outside the app origin that is not in an explicit third-party blocklist (WalletConnect/Reown, ConnectKit's Family probe, Coinbase Wallet SDK, Google Fonts), and unmatched API paths 404. Both lists are recorded and asserted empty, so a new endpoint or a changed viem default RPC URL fails the suite instead of silently reaching the network — which is how the real Polygon default (polygon.drpc.org, not polygon-rpc.com) was caught. mockWallet.ts is copied from apps/frontend/e2e/support with the tx-hash constant exported for the reported-hash assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive real Alfredpay MX individual KYC end to end against the mocked backend: add the corridor, create the customer, fill the form, upload the ID documents, and reach the "In review" screen. The KYC machine itself is unit-tested in packages/kyc, so this covers the dashboard's wiring around it — the six-screen flow and how approval surfaces. Three variants exercise the approval seam, each a different mechanism: - approval arrives while the wizard stays open (the machine's own status poll advances it to Approved, and the completion toast fires); - "Continue in background" tears the machine down, and the corridor card flips to approved with no reload, purely via the 15s onboarding-status refetch; - the wizard is dismissed mid-review and reopened, resuming into In review and then approval. Approval timing is deterministic: the mock's route handlers are Node closures, so the spec flips a kyc.approved flag between assertions and the browser's next poll observes it. GET /onboarding/status reflects KYC progress so the card tracks it; reflectOnboarding:false keeps the aggregator behind the provider so the reopen variant's card action stays enabled (the UI disables it once a corridor reads in-review). Verified the background variant is not vacuous by disabling the onboarding-status refetchInterval: that variant fails while the other two pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a new
apps/dashboardsub-app (mocked) with auth, onboarding, recipients, transactions, and transfer UI.Changes
apps/dashboard/Vite + shadcn/ui apppackage.json:build:dashboard/dev:dashboardscripts wired inapps/frontend/_redirects: SPA fallback for/dashboard/*Branch is rebuilt on top of the current
stagingtip so it compares cleanly.