-
Notifications
You must be signed in to change notification settings - Fork 2
✨ server: support manteca inquiry #680
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
base: main
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 3f12e45 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
WalkthroughThis PR introduces manteca inquiry support to the KYC and ramp integration system. It migrates the ramp route from "/onramp" to "/ramp", extends KYC scope validation to accept "manteca" alongside "basic", implements a full manteca onboarding flow with document and photo uploads, updates provider signatures to remove templateId parameters, and adds webhook handling for user status updates with deprecation response. Changes
Sequence DiagramssequenceDiagram
actor Client
participant KYC as KYC Endpoint
participant Persona as Persona Utilities
participant Manteca as Manteca Provider
participant MantecaAPI as Manteca API
Client->>KYC: GET /kyc?scope=manteca
KYC->>KYC: Validate scope (manteca allowed)
KYC->>Persona: getPendingInquiryTemplate(account, manteca)
Persona->>MantecaAPI: evaluateAccount(account)
alt Account Supported
MantecaAPI-->>Persona: template
Persona-->>KYC: template/undefined
alt Has Pending Inquiry
KYC-->>Client: { ok, status, inquiry }
else No Pending Inquiry
KYC-->>Client: { ok, status: "not started" }
end
else Account Not Supported
MantecaAPI-->>Persona: NOT_SUPPORTED error
Persona-->>KYC: NOT_SUPPORTED error
KYC-->>Client: 400 { code: "not supported" }
end
sequenceDiagram
actor Client
participant Onboarding as Ramp Onboarding
participant Bridge as Bridge Provider
participant Manteca as Manteca Provider
participant MantecaAPI as Manteca API
participant PersonaAPI as Persona API
Client->>Onboarding: POST /ramp onboarding request
Onboarding->>Onboarding: Load credential
alt Type: Bridge
Onboarding->>Bridge: bridgeOnboarding(credentialId, customerId, acceptedTermsId)
Bridge-->>Onboarding: result
else Type: Manteca
Onboarding->>MantecaAPI: Check chain/currency support
MantecaAPI-->>Onboarding: validation result
Onboarding->>PersonaAPI: Fetch account & documents
PersonaAPI-->>Onboarding: account data
Onboarding->>MantecaAPI: Upload identity document
MantecaAPI-->>Onboarding: document processed
Onboarding->>MantecaAPI: Upload front & back photos
MantecaAPI-->>Onboarding: photos processed
Onboarding->>Manteca: mantecaOnboarding(account, credentialId)
Manteca->>MantecaAPI: Accept terms & create user
MantecaAPI-->>Manteca: user created
Manteca-->>Onboarding: result
end
Onboarding-->>Client: { code: "ok" }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @mainqueg, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly enhances the server's capability to handle Know Your Customer (KYC) and on-ramp processes by integrating Manteca. It involves a substantial refactoring of the on-ramp module, introducing new logic for Manteca-specific user onboarding, and updating API interactions and data structures to support this new provider. The changes aim to provide a more robust and flexible system for managing user verification and fund transfers. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
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.
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.
🟡 Duplicate call to getMantecaDepositDetails in quote endpoint
In the /quote endpoint's manteca case, getMantecaDepositDetails is called twice with the same arguments - once inside the try block (line 150) and once outside (line 158). The second call on line 158 is redundant and wasteful.
Click to expand
Code flow
Lines 149-159 in server/api/ramp.ts:
try {
depositInfo = getMantecaDepositDetails(query.currency, mantecaUser.exchange);
} catch (error) {
// error handling...
}
depositInfo = getMantecaDepositDetails(query.currency, mantecaUser.exchange);
return c.json({ quote: await getMantecaQuote(`USDC_${query.currency}`), depositInfo });If the first call (line 150) succeeds, depositInfo is assigned. Then line 158 immediately overwrites it by calling the exact same function with the same arguments. This is unnecessary duplication.
Impact
This causes unnecessary computation and could potentially throw an error on the second call that wasn't caught by the try-catch block.
(Refers to line 158)
Recommendation: Remove the duplicate call on line 158. The depositInfo variable is already assigned in the try block on line 150.
Was this helpful? React with 👍 or 👎 to provide feedback.
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.
Code Review
This pull request introduces support for Manteca inquiries, significantly refactoring the KYC and ramp provider integration. Key changes include renaming the 'onramp' module to 'ramp', extending the KYC scope to include 'manteca', and streamlining the logic for retrieving provider information and handling Manteca onboarding. The changes also include comprehensive new tests for the Manteca scope, ensuring the correctness of the new flows. The refactoring improves maintainability by centralizing Persona-related logic and simplifying provider interfaces.
| const account = parse(Address, credential.account); | ||
| setUser({ id: account }); | ||
| setContext("exa", { credential }); | ||
|
|
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.
| const allowedMantecaCountries = new Map<Country, Allowed>([ | ||
| ["AR", { allowedIds: ["id", "pp"] }], | ||
| ["BR", { allowedIds: ["id", "dl", "pp"] }], | ||
| ["BR", { allowedIds: ["id", "pp", "dl"] }], |
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.
| account: string, | ||
| credentialId: string, | ||
| templateId: string, | ||
| countryCode?: string, |
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.
| if (!mantecaUser) { | ||
| const [inquiry, personaAccount] = await Promise.all([ | ||
| getInquiry(credentialId, templateId), | ||
| getAccount(credentialId, "manteca"), | ||
| ]); | ||
| if (!inquiry || !personaAccount) throw new Error(ErrorCodes.NO_KYC); | ||
| if (inquiry.attributes.status !== "approved" && inquiry.attributes.status !== "completed") { | ||
| throw new Error(ErrorCodes.KYC_NOT_APPROVED); | ||
| } | ||
|
|
||
| const country = personaAccount.attributes["country-code"]; | ||
|
|
||
| try { | ||
| validateIdentification(inquiry); | ||
| } catch (error) { | ||
| if (error instanceof Error && Object.values(ErrorCodes).includes(error.message)) { | ||
| switch (error.message) { | ||
| case ErrorCodes.COUNTRY_NOT_ALLOWED: | ||
| case ErrorCodes.ID_NOT_ALLOWED: | ||
| return { status: "NOT_AVAILABLE", currencies: [], cryptoCurrencies: [], pendingTasks: [] }; | ||
| case ErrorCodes.BAD_KYC_ADDITIONAL_DATA: { | ||
| let mantecaRedirectURL: undefined | URL = undefined; | ||
| if (redirectURL) { | ||
| mantecaRedirectURL = new URL(redirectURL); | ||
| mantecaRedirectURL.searchParams.set("provider", "manteca" satisfies (typeof shared.RampProvider)[number]); | ||
| } | ||
| const inquiryTask: InferOutput<typeof shared.PendingTask> = { | ||
| type: "INQUIRY", | ||
| link: await resumeOrCreateMantecaInquiryOTL(credentialId, mantecaRedirectURL?.toString()), | ||
| displayText: "We need more information to complete your KYC", | ||
| currencies: getSupportedByCountry(country), | ||
| cryptoCurrencies: [], | ||
| }; | ||
| return { status: "MISSING_INFORMATION", currencies, cryptoCurrencies: [], pendingTasks: [inquiryTask] }; | ||
| } | ||
| } | ||
| captureException(error, { contexts: { inquiry } }); | ||
| } | ||
| throw error; | ||
| } | ||
| return { status: "NOT_STARTED", currencies, cryptoCurrencies: [], pendingTasks: [] }; | ||
| return { onramp: { currencies, cryptoCurrencies: [] }, status: "NOT_STARTED" }; |
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.
| export async function mantecaOnboarding(account: string, credentialId: string) { | ||
| const supportedChainId = SupportedOnRampChainId[chain.id as (typeof shared.SupportedChainId)[number]]; | ||
| if (!supportedChainId) { | ||
| captureMessage("manteca_not_supported_chain_id", { contexts: { chain }, level: "error" }); | ||
| throw new Error(ErrorCodes.NOT_SUPPORTED_CHAIN_ID); | ||
| } | ||
|
|
||
| const mantecaUser = await getUser(account.replace("0x", "")); | ||
| if (mantecaUser?.status === "ACTIVE") return; | ||
| if (mantecaUser?.status === "INACTIVE") throw new Error(ErrorCodes.MANTECA_USER_INACTIVE); | ||
| const personaAccount = await getAccount(credentialId, "manteca"); | ||
| if (!personaAccount) throw new Error(ErrorCodes.NO_PERSONA_ACCOUNT); | ||
| const countryCode = personaAccount.attributes["country-code"]; | ||
|
|
||
| if (!mantecaUser) { | ||
| await initiateOnboarding({ | ||
| email: personaAccount.attributes["email-address"], | ||
| legalId: personaAccount.attributes.fields.tin.value, | ||
| externalId: account.replace("0x", ""), | ||
| type: "INDIVIDUAL", | ||
| exchange: getExchange(countryCode), | ||
| personalData: { | ||
| birthDate: personaAccount.attributes.fields.birthdate.value, | ||
| nationality: getNationality(countryCode), | ||
| phoneNumber: personaAccount.attributes.fields.phone_number.value, | ||
| surname: personaAccount.attributes.fields.name.value.last.value, | ||
| name: personaAccount.attributes.fields.name.value.first.value, | ||
| maritalStatus: "Soltero", // cspell:ignore soltero | ||
| sex: | ||
| personaAccount.attributes.fields.sex_1.value === "Male" | ||
| ? "M" | ||
| : personaAccount.attributes.fields.sex_1.value === "Female" | ||
| ? "F" | ||
| : "X", | ||
| isFacta: !personaAccount.attributes.fields.isnotfacta.value, // cspell:ignore isnotfacta | ||
| isPep: false, | ||
| isFep: false, | ||
| work: personaAccount.attributes.fields.economic_activity.value, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| const identityDocument = getDocumentForManteca(personaAccount.attributes.fields.documents.value, countryCode); | ||
| if (!identityDocument) { | ||
| captureException(new Error("no identity document"), { contexts: { personaAccount } }); | ||
| throw new Error(ErrorCodes.NO_DOCUMENT); | ||
| } | ||
|
|
||
| const document = await getDocument(identityDocument.id_document_id.value); | ||
| const frontDocumentURL = document.attributes["front-photo"]?.url; | ||
| const backDocumentURL = document.attributes["back-photo"]?.url; | ||
|
|
||
| const results = await Promise.allSettled([ | ||
| uploadIdentityFile( | ||
| account.replace("0x", ""), | ||
| "FRONT", | ||
| document.attributes["front-photo"]?.filename ?? "front-photo.jpg", | ||
| frontDocumentURL, | ||
| ), | ||
| uploadIdentityFile( | ||
| account.replace("0x", ""), | ||
| "BACK", | ||
| document.attributes["back-photo"]?.filename ?? "back-photo.jpg", | ||
| backDocumentURL, | ||
| ), | ||
| acceptTermsAndConditions(account.replace("0x", "")), | ||
| ]); | ||
|
|
||
| for (const result of results) { | ||
| result.status === "rejected" && captureException(result.reason, { extra: { account } }); | ||
| } | ||
| } |
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.
The implementation of mantecaOnboarding is a significant addition, detailing the steps required to onboard a user with Manteca, including initiating onboarding, fetching identity documents from Persona, uploading files, and accepting terms. This centralizes the onboarding logic and improves clarity.
| const getExchange = (countryCode: string): (typeof Exchange)[number] => { | ||
| const exchange = ExchangeByCountry[countryCode as (typeof CountryCode)[number]]; | ||
| // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition | ||
| if (!exchange) throw new Error(`Invalid country: ${countryCode}`); | ||
| return exchange; |
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.
The getExchange helper function includes a check for !exchange and throws an error. This approach aligns with the rule to throw an error when an expected record is not found, indicating a system or data integrity issue (5xx-level error). While throwing an error is appropriate here, validating the countryCode earlier could prevent reaching this state, improving robustness.
References
- Throw an error when an expected database record (like a card) is not found. This indicates a system or data integrity issue, not a client-side error that can be fixed by the user, so a 5xx-level error is more appropriate than a 4xx.
| const getNationality = (countryCode: string): string => { | ||
| const nationality = Nationality[countryCode as (typeof CountryCode)[number]]; | ||
| if (!nationality) throw new Error(`Invalid country: ${countryCode}`); | ||
| return nationality; |
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.
Similar to getExchange, the getNationality helper function also throws an error for an invalid countryCode. This consistency in error handling is good and aligns with the rule for handling missing expected records as system/data integrity issues. Earlier validation of countryCode could further enhance robustness.
References
- Throw an error when an expected database record (like a card) is not found. This indicates a system or data integrity issue, not a client-side error that can be fixed by the user, so a 5xx-level error is more appropriate than a 4xx.
| export const ErrorCodes = { | ||
| MULTIPLE_IDENTIFICATION_NUMBERS: "multiple identification numbers", | ||
| NO_IDENTIFICATION_NUMBER: "no identification number", | ||
| NO_IDENTIFICATION_CLASS: "no identification class", | ||
| BAD_KYC_ADDITIONAL_DATA: "bad kyc additional data", | ||
| NOT_SUPPORTED_CURRENCY: "not supported currency", | ||
| NOT_SUPPORTED_CHAIN_ID: "not supported chain id", | ||
| NOT_SUPPORTED_CURRENCY: "not supported currency", | ||
| MANTECA_USER_INACTIVE: "manteca user inactive", | ||
| COUNTRY_NOT_ALLOWED: "country not allowed", | ||
| MULTIPLE_DOCUMENTS: "multiple documents", | ||
| NO_PERSONA_ACCOUNT: "no persona account", | ||
| INVALID_ORDER_SIZE: "invalid order size", | ||
| KYC_NOT_APPROVED: "kyc not approved", | ||
| BAD_MANTECA_KYC: "bad manteca kyc", | ||
| ID_NOT_ALLOWED: "id not allowed", | ||
| NO_NON_FACTA: "no non facta", | ||
| NO_PERSONA_ACCOUNT: "no persona account", | ||
| NO_DOCUMENT: "no document", | ||
| NO_GENDER: "no gender", | ||
| NO_KYC: "no kyc", | ||
| }; |
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.
| export type OnRampNetworkType = (typeof CryptoNetwork)[number] | (typeof FiatNetwork)[number]; | ||
|
|
||
| export const ProviderStatus = ["NOT_STARTED", "ACTIVE", "ONBOARDING", "NOT_AVAILABLE", "MISSING_INFORMATION"] as const; | ||
| export const ProviderStatus = ["NOT_STARTED", "ACTIVE", "ONBOARDING", "NOT_AVAILABLE"] as const; |
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.
| export const ProviderInfo = object({ | ||
| onramp: object({ | ||
| currencies: array(string()), | ||
| cryptoCurrencies: array(object({ cryptoCurrency: picklist(Cryptocurrency), network: picklist(CryptoNetwork) })), | ||
| }), | ||
| status: picklist(ProviderStatus), | ||
| currencies: array(string()), | ||
| cryptoCurrencies: array(object({ cryptoCurrency: picklist(Cryptocurrency), network: picklist(CryptoNetwork) })), | ||
| pendingTasks: optional(array(PendingTask)), | ||
| }); |
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.
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.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@server/api/ramp.ts`:
- Around line 196-229: The onboarding handlers only return 400 for one specific
code each; update the error handling in the manteca and bridge cases (around
mantecaOnboarding and bridgeOnboarding) so that any error whose message is a
member of MantecaErrorCodes or BridgeErrorCodes returns c.json({ code:
error.message }, 400). Keep the existing captureException(error, { contexts: {
credential } }) and the instanceof Error check, replace the single-case switch
with a general check using
Object.values(MantecaErrorCodes).includes(error.message) and
Object.values(BridgeErrorCodes).includes(error.message) and return 400 for any
match instead of re-throwing.
In `@server/utils/ramps/manteca.ts`:
- Around line 250-321: In mantecaOnboarding the expression account.replace("0x",
"") is repeated multiple times; introduce a const userExternalId =
account.replace("0x", "") near the top of the function and replace all uses of
account.replace("0x", "") (calls to getUser, the externalId property passed to
initiateOnboarding, the first argument to uploadIdentityFile for FRONT/BACK, and
the argument to acceptTermsAndConditions, as well as any captureException extra
data that references the stripped account) with userExternalId to avoid repeated
string ops and improve readability.
- Around line 302-320: The current use of Promise.allSettled with only logging
can let critical operations silently fail; update the block around
Promise.allSettled (which runs uploadIdentityFile and acceptTermsAndConditions)
to treat failures as fatal: after awaiting results, inspect the results array
for any entry with status === "rejected", call captureException(result.reason, {
extra: { account } }) for each, and then throw a new Error (including a brief
context like "Manteca onboarding failed for account") or switch to Promise.all
so the function rejects immediately when any of uploadIdentityFile or
acceptTermsAndConditions fails; ensure you reference the uploadIdentityFile and
acceptTermsAndConditions calls and the results variable when making this change.
| switch (onboarding.provider) { | ||
| case "manteca": | ||
| try { | ||
| await mantecaOnboarding(credential.account, credentialId); | ||
| } catch (error) { | ||
| captureException(error, { contexts: { credential } }); | ||
| if (error instanceof Error && Object.values(MantecaErrorCodes).includes(error.message)) { | ||
| switch (error.message) { | ||
| case MantecaErrorCodes.NO_DOCUMENT: | ||
| return c.json({ code: error.message }, 400); | ||
| } | ||
| throw error; | ||
| } | ||
| break; | ||
| case "bridge": | ||
| try { | ||
| await bridgeOnboarding({ | ||
| credentialId, | ||
| customerId: credential.bridgeId, | ||
| templateId, | ||
| acceptedTermsId: onboarding.acceptedTermsId, | ||
| }); | ||
| } catch (error) { | ||
| captureException(error, { contexts: { credential } }); | ||
| if (error instanceof Error && Object.values(BridgeErrorCodes).includes(error.message)) { | ||
| switch (error.message) { | ||
| case BridgeErrorCodes.ALREADY_ONBOARDED: | ||
| return c.json({ code: error.message }, 400); | ||
| } | ||
| throw error; | ||
| } | ||
| break; | ||
| case "bridge": | ||
| try { | ||
| await bridgeOnboarding({ | ||
| credentialId, | ||
| customerId: credential.bridgeId, | ||
| acceptedTermsId: onboarding.acceptedTermsId, | ||
| }); | ||
| } catch (error) { | ||
| captureException(error, { contexts: { credential } }); | ||
| if (error instanceof Error && Object.values(BridgeErrorCodes).includes(error.message)) { | ||
| switch (error.message) { | ||
| case BridgeErrorCodes.ALREADY_ONBOARDED: | ||
| return c.json({ code: error.message }, 400); | ||
| } | ||
| throw error; | ||
| } | ||
| break; | ||
| } | ||
| return c.json({ code: "ok" }); | ||
| }, | ||
| ); | ||
| throw error; | ||
| } | ||
| break; | ||
| } |
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.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find where MantecaErrorCodes is defined
fd -t f "manteca" server/ | head -20
rg -l "MantecaErrorCodes" server/ --type tsRepository: exactly/exa
Length of output: 127
🏁 Script executed:
#!/bin/bash
# Get MantecaErrorCodes definition
rg -A 20 "export.*MantecaErrorCodes" server/ -t tsRepository: exactly/exa
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Get BridgeErrorCodes definition
rg -A 20 "export.*BridgeErrorCodes" server/ -t tsRepository: exactly/exa
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Check mantecaOnboarding function to understand error throwing behavior
rg -B 5 -A 30 "function mantecaOnboarding|export.*mantecaOnboarding" server/ -t tsRepository: exactly/exa
Length of output: 2884
🏁 Script executed:
#!/bin/bash
# Check bridgeOnboarding function to understand error throwing behavior
rg -B 5 -A 30 "function bridgeOnboarding|export.*bridgeOnboarding" server/ -t tsRepository: exactly/exa
Length of output: 37
🏁 Script executed:
#!/bin/bash
# Find ErrorCodes definition
rg -B 5 -A 30 "export.*ErrorCodes|const.*ErrorCodes" server/ -t ts | head -100Repository: exactly/exa
Length of output: 5703
🏁 Script executed:
#!/bin/bash
# Search for NO_DOCUMENT error
rg "NO_DOCUMENT" server/ -t ts -B 2 -A 2Repository: exactly/exa
Length of output: 1729
🏁 Script executed:
#!/bin/bash
# Search for bridgeOnboarding definition
rg -B 5 -A 30 "bridgeOnboarding" server/ -t tsRepository: exactly/exa
Length of output: 3174
🏁 Script executed:
#!/bin/bash
# Check if MantecaErrorCodes is imported or defined in ramp.ts
rg "MantecaErrorCodes|BridgeErrorCodes" server/api/ramp.ts -B 5 -A 5Repository: exactly/exa
Length of output: 3350
🏁 Script executed:
#!/bin/bash
# Check context around error handling in onboarding endpoint
cat -n server/api/ramp.ts | sed -n '185,235p'Repository: exactly/exa
Length of output: 2116
🏁 Script executed:
#!/bin/bash
# Look for comments explaining error handling strategy
rg -B 10 "case \"manteca\":" server/api/ramp.ts | head -30Repository: exactly/exa
Length of output: 959
🏁 Script executed:
#!/bin/bash
# Check if onboarding endpoint is called from webhooks or other places
rg "onboarding|POST /" server/hooks/ -t ts -B 2 -A 2Repository: exactly/exa
Length of output: 37
Handle all error codes consistently in onboarding endpoint.
The manteca and bridge onboarding error handling only catches specific error codes (NO_DOCUMENT for manteca, ALREADY_ONBOARDED for bridge) and returns 400, while other expected error codes like MANTECA_USER_INACTIVE, NO_PERSONA_ACCOUNT, and NOT_SUPPORTED_CHAIN_ID are re-thrown as 500 errors. This is inconsistent with the GET /details endpoint, which returns 400 for any error in MantecaErrorCodes or BridgeErrorCodes. All errors thrown by mantecaOnboarding and bridgeOnboarding are user-facing validation failures and should return 400 status.
🤖 Prompt for AI Agents
In `@server/api/ramp.ts` around lines 196 - 229, The onboarding handlers only
return 400 for one specific code each; update the error handling in the manteca
and bridge cases (around mantecaOnboarding and bridgeOnboarding) so that any
error whose message is a member of MantecaErrorCodes or BridgeErrorCodes returns
c.json({ code: error.message }, 400). Keep the existing captureException(error,
{ contexts: { credential } }) and the instanceof Error check, replace the
single-case switch with a general check using
Object.values(MantecaErrorCodes).includes(error.message) and
Object.values(BridgeErrorCodes).includes(error.message) and return 400 for any
match instead of re-throwing.
| export async function mantecaOnboarding(account: string, credentialId: string) { | ||
| const supportedChainId = SupportedOnRampChainId[chain.id as (typeof shared.SupportedChainId)[number]]; | ||
| if (!supportedChainId) { | ||
| captureMessage("manteca_not_supported_chain_id", { contexts: { chain }, level: "error" }); | ||
| throw new Error(ErrorCodes.NOT_SUPPORTED_CHAIN_ID); | ||
| } | ||
|
|
||
| const mantecaUser = await getUser(account.replace("0x", "")); | ||
| if (mantecaUser?.status === "ACTIVE") return; | ||
| if (mantecaUser?.status === "INACTIVE") throw new Error(ErrorCodes.MANTECA_USER_INACTIVE); | ||
| const personaAccount = await getAccount(credentialId, "manteca"); | ||
| if (!personaAccount) throw new Error(ErrorCodes.NO_PERSONA_ACCOUNT); | ||
| const countryCode = personaAccount.attributes["country-code"]; | ||
|
|
||
| if (!mantecaUser) { | ||
| await initiateOnboarding({ | ||
| email: personaAccount.attributes["email-address"], | ||
| legalId: personaAccount.attributes.fields.tin.value, | ||
| externalId: account.replace("0x", ""), | ||
| type: "INDIVIDUAL", | ||
| exchange: getExchange(countryCode), | ||
| personalData: { | ||
| birthDate: personaAccount.attributes.fields.birthdate.value, | ||
| nationality: getNationality(countryCode), | ||
| phoneNumber: personaAccount.attributes.fields.phone_number.value, | ||
| surname: personaAccount.attributes.fields.name.value.last.value, | ||
| name: personaAccount.attributes.fields.name.value.first.value, | ||
| maritalStatus: "Soltero", // cspell:ignore soltero | ||
| sex: | ||
| personaAccount.attributes.fields.sex_1.value === "Male" | ||
| ? "M" | ||
| : personaAccount.attributes.fields.sex_1.value === "Female" | ||
| ? "F" | ||
| : "X", | ||
| isFacta: !personaAccount.attributes.fields.isnotfacta.value, // cspell:ignore isnotfacta | ||
| isPep: false, | ||
| isFep: false, | ||
| work: personaAccount.attributes.fields.economic_activity.value, | ||
| }, | ||
| }); | ||
| } | ||
|
|
||
| const identityDocument = getDocumentForManteca(personaAccount.attributes.fields.documents.value, countryCode); | ||
| if (!identityDocument) { | ||
| captureException(new Error("no identity document"), { contexts: { personaAccount } }); | ||
| throw new Error(ErrorCodes.NO_DOCUMENT); | ||
| } | ||
|
|
||
| const document = await getDocument(identityDocument.id_document_id.value); | ||
| const frontDocumentURL = document.attributes["front-photo"]?.url; | ||
| const backDocumentURL = document.attributes["back-photo"]?.url; | ||
|
|
||
| const results = await Promise.allSettled([ | ||
| uploadIdentityFile( | ||
| account.replace("0x", ""), | ||
| "FRONT", | ||
| document.attributes["front-photo"]?.filename ?? "front-photo.jpg", | ||
| frontDocumentURL, | ||
| ), | ||
| uploadIdentityFile( | ||
| account.replace("0x", ""), | ||
| "BACK", | ||
| document.attributes["back-photo"]?.filename ?? "back-photo.jpg", | ||
| backDocumentURL, | ||
| ), | ||
| acceptTermsAndConditions(account.replace("0x", "")), | ||
| ]); | ||
|
|
||
| for (const result of results) { | ||
| result.status === "rejected" && captureException(result.reason, { extra: { account } }); | ||
| } | ||
| } |
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.
🧹 Nitpick | 🔵 Trivial
Consider extracting repeated account.replace("0x", "") to a variable.
The expression account.replace("0x", "") appears 6 times in mantecaOnboarding. Extracting it to a const userExternalId = account.replace("0x", "") at the start would improve readability and avoid repeated string operations.
Proposed refactor
export async function mantecaOnboarding(account: string, credentialId: string) {
+ const userExternalId = account.replace("0x", "");
const supportedChainId = SupportedOnRampChainId[chain.id as (typeof shared.SupportedChainId)[number]];
if (!supportedChainId) {
captureMessage("manteca_not_supported_chain_id", { contexts: { chain }, level: "error" });
throw new Error(ErrorCodes.NOT_SUPPORTED_CHAIN_ID);
}
- const mantecaUser = await getUser(account.replace("0x", ""));
+ const mantecaUser = await getUser(userExternalId);
if (mantecaUser?.status === "ACTIVE") return;
if (mantecaUser?.status === "INACTIVE") throw new Error(ErrorCodes.MANTECA_USER_INACTIVE);
// ... and so on for other occurrences🤖 Prompt for AI Agents
In `@server/utils/ramps/manteca.ts` around lines 250 - 321, In mantecaOnboarding
the expression account.replace("0x", "") is repeated multiple times; introduce a
const userExternalId = account.replace("0x", "") near the top of the function
and replace all uses of account.replace("0x", "") (calls to getUser, the
externalId property passed to initiateOnboarding, the first argument to
uploadIdentityFile for FRONT/BACK, and the argument to acceptTermsAndConditions,
as well as any captureException extra data that references the stripped account)
with userExternalId to avoid repeated string ops and improve readability.
| const results = await Promise.allSettled([ | ||
| uploadIdentityFile( | ||
| account.replace("0x", ""), | ||
| "FRONT", | ||
| document.attributes["front-photo"]?.filename ?? "front-photo.jpg", | ||
| frontDocumentURL, | ||
| ), | ||
| uploadIdentityFile( | ||
| account.replace("0x", ""), | ||
| "BACK", | ||
| document.attributes["back-photo"]?.filename ?? "back-photo.jpg", | ||
| backDocumentURL, | ||
| ), | ||
| acceptTermsAndConditions(account.replace("0x", "")), | ||
| ]); | ||
|
|
||
| for (const result of results) { | ||
| result.status === "rejected" && captureException(result.reason, { extra: { account } }); | ||
| } |
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.
Promise.allSettled silently swallows critical failures.
Using Promise.allSettled with only logging on rejection means the function succeeds even if all operations (document uploads and T&C acceptance) fail. The user would appear to complete onboarding but manteca won't have the required documents.
Consider either:
- Checking if any critical operation failed and throwing
- Using
Promise.allif all operations are required for successful onboarding
Proposed fix to fail on critical errors
const results = await Promise.allSettled([
uploadIdentityFile(
account.replace("0x", ""),
"FRONT",
document.attributes["front-photo"]?.filename ?? "front-photo.jpg",
frontDocumentURL,
),
uploadIdentityFile(
account.replace("0x", ""),
"BACK",
document.attributes["back-photo"]?.filename ?? "back-photo.jpg",
backDocumentURL,
),
acceptTermsAndConditions(account.replace("0x", "")),
]);
+ const failures = results.filter((result) => result.status === "rejected");
for (const result of results) {
result.status === "rejected" && captureException(result.reason, { extra: { account } });
}
+
+ // fail if T&C acceptance failed (critical)
+ if (results[2]?.status === "rejected") {
+ throw new Error(ErrorCodes.ONBOARDING_FAILED);
+ }🤖 Prompt for AI Agents
In `@server/utils/ramps/manteca.ts` around lines 302 - 320, The current use of
Promise.allSettled with only logging can let critical operations silently fail;
update the block around Promise.allSettled (which runs uploadIdentityFile and
acceptTermsAndConditions) to treat failures as fatal: after awaiting results,
inspect the results array for any entry with status === "rejected", call
captureException(result.reason, { extra: { account } }) for each, and then throw
a new Error (including a brief context like "Manteca onboarding failed for
account") or switch to Promise.all so the function rejects immediately when any
of uploadIdentityFile or acceptTermsAndConditions fails; ensure you reference
the uploadIdentityFile and acceptTermsAndConditions calls and the results
variable when making this change.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #680 +/- ##
==========================================
- Coverage 61.84% 61.80% -0.05%
==========================================
Files 169 169
Lines 5289 5304 +15
Branches 1496 1502 +6
==========================================
+ Hits 3271 3278 +7
- Misses 1845 1857 +12
+ Partials 173 169 -4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary by CodeRabbit
New Features
Tests
✏️ Tip: You can customize this high-level summary in your review settings.