Conversation
Resolved 7 conflicts, all where staging's refactors met the email notification work: - models/index.ts, vars.ts, phase-processor.ts, alfredpay-customer.service.ts: both sides added adjacent declarations; kept both. Dropped the TaxId associations, since staging removed that model in the provider_customers cutover. - security-spec/README.md: kept the Resend row, dropped the Stellar Anchors row that staging deleted along with the spec file. - alfredpay.md: both sides added an invariant 24; kept staging's at 24 and renumbered the notification invariants to 25 and 26. - brla.md: took staging's anonymous-quote row, which matches the merged registerRamp (rejects only when quote and request users both exist and differ); this branch's row described pre-cutover behavior. Kept the four added webhook threat rows.
…ed ramps The 055 prefix collides with 055-create-financial-operations on staging -- the exact duplicate-prefix situation MIGRATION_RENAMES in migrator.ts was added to clean up. Renumbering now, while branch-only, avoids another rename shim entry. The backfill closes a first-deploy hazard: reconcileMissedRampCompletedEmails deliberately has no age cutoff, so an empty table would re-enqueue every historical completed ramp and mass-mail users about months-old completions. Tombstoning them as skipped at table creation keeps the sweep anomaly-only.
getEffectiveUserId fills rampState.userId with the credential's linked profile on API-key requests, so the null-userId check alone mailed the partner once per end-customer ramp -- the opposite of the documented behavior. A ramp whose quote carries an api_credential_id now records a skipped tombstone instead, which also stops the hourly reconcile sweep from re-surfacing it forever. The reconciliation integration test now seeds real ramps and asserts the anti-join's semantics instead of running it against an empty database.
quote amounts are DECIMAL(38,18) and come back as "1250.000000000000000000"; the API response path already trims them, the email path did not.
bun's mock.module patches are process-global and leak into every file loaded
afterwards. The two Avenia suites and the dispatch suite left @vortexfi/shared,
the logger, the transport, Slack, and the template renderer stubbed for the
rest of the run. Each now restores the real namespaces in afterAll, matching
the priceFeed convention.
The canary's name check cannot see this stub shape -- an object literal's
{ getInstance: () => ... } infers the name "getInstance" -- so it now also
asserts the singletons are classes, which an object-literal stub is not.
New coverage for the properties the feature leans on but nothing verified:
the transactional SKIP LOCKED claim shape, the non-production recipient
allowlist (absent recipient, empty list, production bypass), the failure
path (backoff scheduling, abandon-at-cap with Slack alert, error-text cap),
the Avenia terminal-status mapping and reason cap (previously only its
Alfredpay twin was tested), the HTML-escaping invariant from the security
spec, and shared's {attemptId} path templating that the KYB poller's URLs
depend on. Also drops a tautological constant self-assertion.
…uristic The status route's catch handled provider errors and local enqueue/persist errors alike, so an enqueue failure whose message contained "not found" reset the customer to Consulted and wiped the observed state. The local writes now carry their own catch. The service-path swallow is raised from info to warn with the stack, since it also hides enqueue failures that block terminal persistence. The universally-fresh-submission-id dedupe claim is softened to match what the retry endpoints actually guarantee.
The poller never writes terminal outcomes back to kyc_cases, so every settled attempt cost one Avenia request per hour until it aged out of the 60-day window. The queue row the enqueue writes is the natural retirement marker: an anti-join against email_notifications excludes those attempts before any provider call. The batch is also capped at 250 cases (oldest writes first), matching the Alfredpay worker's bound.
Duplicates are the common case -- webhook replays and re-polled settled attempts -- and each one paid a Supabase admin API call to resolve a locale that was never used. A key lookup now short-circuits first. Also restores the claim-consumed attempt when the not-configured guard requeues a row, stubs getUserLocale in the fake auth world so integration enqueues stop dialing the neutralized Supabase host, and stubs findOne in the enqueue unit tests to keep them off the database.
- The unauthenticated Avenia webhook route no longer buffers the API's full 20mb body limit before the signature check; events are a few KB, 100kb is generous. - Supabase auth-mail subjects and the Terms/Privacy links now localize for pt-BR (the Dashboard subject field needs a self-contained conditional, and the site serves /pt/ legal pages). - The Avenia contract test sweeps stale contractRun webhooks left by crashed runs before checking the 3-slot cap, and a failed cleanup delete no longer masks the error that failed the test.
docs/README.md allows directory trees only for security-spec/ and api/; general documents live flat as docs/<kind>-<topic>.md and must be indexed. The architecture doc moves accordingly, the alfredpay-kyc-notification-gap follow-up note folds into its open-follow-ups section (it was a 40-line progress journal, which the conventions disallow as a standalone file), and the index and spec links are repaired in the same change.
…path AlfredpayCustomerView exposes alfredPayId, not id; the new error log in the 404-heuristic split referenced a field the view does not have.
Empirically (2-file probe): mock.module mutates already-imported namespace objects in place, so spreading the namespace at restore time copies the stubs back -- the restore was a silent no-op. Snapshots are now plain-object copies taken before the first mock.module call, and the new Avenia enqueue suite resolves its subject in beforeAll because the webhook controller test mocks that exact module path and bun's file execution order is not the CLI order.
Same in-place-mutation flaw fixed for the email suites earlier: spreading a live namespace at restore time copies the stub back, so the fund-ephemeral and offramp-subsidy suites and the FakeSquidRouter harness restore() were silent no-ops. All three now restore from a plain-object snapshot taken before the first mock.module call. The canary additionally checks that shared's getRoute is not a leftover FakeSquidRouter stub, which the name and class-shape checks cannot see.
The route persisted Approved/Rejected without ever queuing an email. Once a case is terminal both this route's short-circuit and the KYB worker stop observing the attempt, so a client polling ahead of a lost webhook lost the notification forever. The outcome is queued before the terminal writes -- mirroring the Alfredpay enqueue-before-persist invariant -- and a failed enqueue fails the request, leaving the case pollable.
…empts Three review findings on the hourly sweep: partner-owned cases passed the row filter and could permanently occupy the 250-slot batch (now excluded in the join); a capped oldest-first prefix re-selected the same rows every cycle since pending cases are never modified (now an id-ordered keyset like the Alfredpay sweep); and the returned attempt id was trusted, letting a malformed provider response enqueue another attempt's outcome and reason for this case's profile (now discarded, mirroring the authenticated route's guard). Spec updated in the same change, including its stale enqueueCompletionEmail/055 identifiers.
sendRequest used a bare fetch, so one hung Avenia connection stalled its caller indefinitely -- for cron workers with waitForCompletion that means no future cycle ever runs. Timeouts land in the existing transport-failure path (BrlaApiError status 0) that callers already normalize to a 502.
The recovery query had no limit, so a prolonged outage made a single hourly cycle arbitrarily large (one quote plus locale lookup per missing ramp). 250 oldest-first per cycle, with a log line when the cap is hit; processed ramps gain a queue row and leave the anti-join, so the backlog drains across cycles without giving up the no-age-cutoff guarantee.
The architecture doc's security-spec link broke when the doc moved into docs/ (its ../ now resolved outside the tree), and the section 7 comparison table still named migration 055.
Send ramp completion and verification outcome emails
The Settings checkboxes were uncontrolled defaultChecked stubs -- toggling persisted nothing and reset on reload. They now load and mutate /v1/notifications/preferences with optimistic rollback. Two categories remain: onboarding updates fan out to the three verification_* types and transfer status maps to ramp_completed -- the stored type strings the dispatch worker consults, pinned by a test because the PUT endpoint accepts any keys without validation. The recipient-approvals toggle is dropped (no such notification type exists yet) and the transfer caption no longer mentions failures.
…icing-metadata Add transparent AlfredPay quote pricing metadata
…contract The dashboard's preference toggles write prefs keyed by these strings and the API's dispatch worker mutes on them, but each side carried its own literals -- the preferences endpoint accepts arbitrary keys, so drift would mute nothing, silently. The enum now lives in shared and the API model re-exports it under its historical NotificationType name.
Review follow-ups on the Settings toggles: a checkbox now reflects emailEnabled AND the per-type keys, and enabling a category under a global mute lifts the switch while pinning the other category to its effective (muted) state. Toggling is blocked until the GET resolves -- a PUT built from a fallback would replace the saved document -- and while a PUT is in flight, since overlapping full-document snapshots can complete out of order. The optimistic handlers are extracted and tested against a real QueryClient (apply, rollback, no-op without loaded data), and the stale operations spec that still declared email dispatch unimplemented now documents the delivery-time preference gating.
…ion-preferences Wire the Settings email notification toggles to the backend
…ramps Re-enable Ethereum onramps
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vrtx-dashboard ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vortexfi ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
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.
No description provided.