Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fancy-ends-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@exactly/server": patch
---

🐛 handle failed onboarding tasks in manteca
16 changes: 16 additions & 0 deletions server/test/utils/manteca.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,22 @@ describe("manteca utils", () => {
expect(result.status).toBe("NOT_AVAILABLE");
});

it("returns ONBOARDING when user has failed required tasks", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
mockFetchResponse({
...mockOnboardingUser,
onboarding: {
EMAIL_VALIDATION: { required: true, status: "COMPLETED" },
IDENTITY_VALIDATION: { required: true, status: "FAILED" },
},
}),
);

const result = await manteca.getProvider(account, "AR");

expect(result.status).toBe("ONBOARDING");
});
Comment on lines +353 to +367
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Strengthen this scenario to lock precedence and response shape.

Add a required PENDING task alongside FAILED and assert onramp fields so this test protects both branch order and payload contract.

♻️ Suggested test hardening
     it("returns ONBOARDING when user has failed required tasks", async () => {
       vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
         mockFetchResponse({
           ...mockOnboardingUser,
           onboarding: {
-            EMAIL_VALIDATION: { required: true, status: "COMPLETED" },
+            EMAIL_VALIDATION: { required: true, status: "PENDING" },
             IDENTITY_VALIDATION: { required: true, status: "FAILED" },
           },
         }),
       );

       const result = await manteca.getProvider(account, "AR");

       expect(result.status).toBe("ONBOARDING");
+      expect(result.onramp.currencies).toEqual(["ARS", "USD"]);
+      expect(result.onramp.cryptoCurrencies).toEqual([]);
     });


it("returns NOT_STARTED when user has pending required tasks", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
mockFetchResponse({
Expand Down
24 changes: 18 additions & 6 deletions server/utils/ramps/manteca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,23 @@ export async function getProvider(
if (mantecaUser.status === "INACTIVE") {
return { onramp: { currencies: [], cryptoCurrencies: [] }, status: "NOT_AVAILABLE" };
}
const hasPendingTasks = Object.values(mantecaUser.onboarding).some(
(task) => task.required && task.status === "PENDING",
);
if (hasPendingTasks) {

if (Object.values(mantecaUser.onboarding).some((task) => task.required && task.status === "FAILED")) {
// TODO handle failed required tasks
withScope((scope) => {
scope.addEventProcessor((event) => {
if (event.exception?.values?.[0]) event.exception.values[0].type = "has failed tasks";
return event;
});
captureException(new Error("has failed tasks"), {
level: "warning",
fingerprint: ["{{ default }}", "has failed tasks"],
});
});
return { onramp: { currencies, cryptoCurrencies: [] }, status: "ONBOARDING" };
}

if (Object.values(mantecaUser.onboarding).some((task) => task.required && task.status === "PENDING")) {
withScope((scope) => {
scope.addEventProcessor((event) => {
if (event.exception?.values?.[0]) event.exception.values[0].type = "has pending tasks";
Expand All @@ -279,7 +292,6 @@ export async function getProvider(
captureException(new Error("has pending tasks"), {
level: "warning",
fingerprint: ["{{ default }}", "has pending tasks"],
contexts: { mantecaUser },
});
});
return { onramp: { currencies, cryptoCurrencies: [] }, status: "NOT_STARTED" };
Expand Down Expand Up @@ -545,7 +557,7 @@ export const BalancesResponse = object({
updatedAt: string(),
});

const onboardingTaskStatus = ["PENDING", "COMPLETED", "IN_PROGRESS"] as const;
const onboardingTaskStatus = ["COMPLETED", "FAILED", "IN_PROGRESS", "PENDING"] as const;
const OnboardingTaskInfo = optional(
object({
required: boolean(),
Expand Down