Skip to content
Closed
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
84 changes: 82 additions & 2 deletions src/app/api/affiliates/offers/[id]/conversions/route.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { GET, POST } from "./route";
import { GET, POST, PUT } from "./route";
import { NextRequest } from "next/server";

// Mock auth
Expand All @@ -18,8 +18,10 @@ vi.mock("@/lib/supabase/service", () => ({

// Mock recordConversion
const mockRecordConversion = vi.fn();
const mockCalculateCommission = vi.fn();
vi.mock("@/lib/affiliates/commission", () => ({
recordConversion: (...args: unknown[]) => mockRecordConversion(...args),
calculateCommission: (...args: unknown[]) => mockCalculateCommission(...args),
}));

function makeGetRequest(id: string) {
Expand All @@ -39,6 +41,17 @@ function makePostRequest(id: string, body: Record<string, unknown>) {
);
}

function makePutRequest(id: string, body: Record<string, unknown>) {
return new NextRequest(
`http://localhost/api/affiliates/offers/${id}/conversions`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}
);
}

function makeParams(id: string) {
return { params: Promise.resolve({ id }) };
}
Expand Down Expand Up @@ -319,6 +332,73 @@ describe("POST /api/affiliates/offers/[id]/conversions", () => {
);
expect(res2.status).toBe(400);
const body2 = await res2.json();
expect(body2.error).toBe("sale_amount_sats must be a positive number");
expect(body2.error).toBe("sale_amount_sats must be a positive integer");
});

it("rejects fractional sale amounts", async () => {
mockGetAuthContext.mockResolvedValue({
user: { id: "user-seller", authMethod: "session" },
});

mockFrom.mockImplementation((table: string) => {
if (table === "affiliate_offers") {
return chainable({
id: "offer-1",
seller_id: "user-seller",
});
}
return chainable([]);
});

const res = await POST(
makePostRequest("offer-1", {
affiliate_id: "aff-1",
sale_amount_sats: 100.5,
}),
makeParams("offer-1")
);

expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toBe("sale_amount_sats must be a positive integer");
expect(mockRecordConversion).not.toHaveBeenCalled();
});
});

describe("PUT /api/affiliates/offers/[id]/conversions", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("rejects fractional sale amount updates", async () => {
mockGetAuthContext.mockResolvedValue({
user: { id: "user-seller", authMethod: "session" },
});

mockFrom.mockImplementation((table: string) => {
if (table === "affiliate_offers") {
return chainable({
id: "offer-1",
seller_id: "user-seller",
commission_rate: 10,
commission_type: "percentage",
commission_flat_sats: null,
});
}
return chainable({ data: null, error: null });
});

const res = await PUT(
makePutRequest("offer-1", {
conversion_id: "conv-1",
sale_amount_sats: 100.5,
}),
makeParams("offer-1")
);

expect(res.status).toBe(400);
const body = await res.json();
expect(body.error).toBe("sale_amount_sats must be a positive integer");
expect(mockCalculateCommission).not.toHaveBeenCalled();
});
});
17 changes: 13 additions & 4 deletions src/app/api/affiliates/offers/[id]/conversions/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { getAuthContext } from "@/lib/auth/get-user";
import { createServiceClient } from "@/lib/supabase/service";
import { recordConversion } from "@/lib/affiliates/commission";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySupabase = any;

function isPositiveIntegerSats(value: unknown): value is number {
return typeof value === "number" && Number.isInteger(value) && value > 0;
}

/**
* GET /api/affiliates/offers/[id]/conversions - List conversions for an offer (seller only)
*/
Expand Down Expand Up @@ -141,9 +144,9 @@ export async function POST(
);
}

if (!sale_amount_sats || typeof sale_amount_sats !== "number" || sale_amount_sats <= 0) {
if (!isPositiveIntegerSats(sale_amount_sats)) {
return NextResponse.json(
{ error: "sale_amount_sats must be a positive number" },
{ error: "sale_amount_sats must be a positive integer" },
{ status: 400 }
);
}
Expand Down Expand Up @@ -242,7 +245,13 @@ export async function PUT(
if (typeof status === "string" && ["pending", "paid", "clawed_back"].includes(status)) {
updateData.status = status;
}
if (typeof sale_amount_sats === "number" && sale_amount_sats > 0) {
if (sale_amount_sats !== undefined && !isPositiveIntegerSats(sale_amount_sats)) {
return NextResponse.json(
{ error: "sale_amount_sats must be a positive integer" },
{ status: 400 }
);
}
if (typeof sale_amount_sats === "number") {
updateData.sale_amount_sats = sale_amount_sats;
const { calculateCommission } = await import("@/lib/affiliates/commission");
updateData.commission_sats = calculateCommission(offer, sale_amount_sats);
Expand Down
Loading