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
4 changes: 2 additions & 2 deletions src/app/api/affiliates/offers/[id]/affiliates/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ vi.mock("@/lib/supabase/service", () => ({

function makeRequest(id: string) {
return new NextRequest(
`http://localhost/api/affiliates/offers/${id}/affiliates`
`http://localhost:3000/api/affiliates/offers/${id}/affiliates`
);
}

Expand Down Expand Up @@ -173,7 +173,7 @@ describe("GET /api/affiliates/offers/[id]/affiliates", () => {
expect(alice.username).toBe("alice");
expect(alice.status).toBe("approved");
expect(alice.tracking_url).toBe(
"https://ugig.net/api/affiliates/click?ugig_ref=alice-abc123"
"http://localhost:3000/api/affiliates/click?ugig_ref=alice-abc123"
);
expect(alice.clicks_30d).toBe(3);
expect(alice.conversions).toBe(2);
Expand Down
8 changes: 6 additions & 2 deletions src/app/api/affiliates/offers/[id]/affiliates/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { NextRequest, NextResponse } from "next/server";
import { getAuthContext } from "@/lib/auth/get-user";
import { createServiceClient } from "@/lib/supabase/service";
import {
buildAffiliateTrackingUrl,
getAffiliateBaseUrl,
} from "@/lib/affiliates/tracking-url";

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

/**
Expand Down Expand Up @@ -135,6 +138,7 @@ export async function GET(
}

// Build response with per-affiliate stats
const baseUrl = getAffiliateBaseUrl(request.url);
const affiliates = affiliateList.map(
(app: {
id: string;
Expand All @@ -159,7 +163,7 @@ export async function GET(
tracking_code: app.tracking_code,
tracking_url:
app.status === "approved" && app.tracking_code
? `https://ugig.net/api/affiliates/click?ugig_ref=${app.tracking_code}`
? buildAffiliateTrackingUrl(baseUrl, app.tracking_code)
: null,
clicks_30d: clicksByAffiliate[app.affiliate_id] || 0,
conversions: convStats.count,
Expand Down
10 changes: 6 additions & 4 deletions src/app/api/affiliates/offers/[id]/apply/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { NextRequest, NextResponse } from "next/server";
import { getAuthContext } from "@/lib/auth/get-user";
import { createServiceClient } from "@/lib/supabase/service";
import { checkRateLimit, rateLimitExceeded, getRateLimitIdentifier } from "@/lib/rate-limit";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnySupabase = any;
import { generateTrackingCode } from "@/lib/affiliates/tracking";
import {
buildAffiliateTrackingUrl,
getAffiliateBaseUrl,
} from "@/lib/affiliates/tracking-url";

type AnySupabase = any;

/**
* POST /api/affiliates/offers/[id]/apply - Apply to become an affiliate for an offer
Expand Down Expand Up @@ -127,7 +129,7 @@ export async function POST(
return NextResponse.json({
application,
tracking_code: trackingCode,
tracking_url: `${process.env.NEXT_PUBLIC_APP_URL || "https://ugig.net"}/ref/${trackingCode}`,
tracking_url: buildAffiliateTrackingUrl(getAffiliateBaseUrl(request.url), trackingCode),
}, { status: 201 });
Comment on lines 129 to 133

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 tracking_url format changed from short /ref/ link to raw click endpoint

The old response returned …/ref/{trackingCode} — the purpose-built short link that affiliates share with customers. This PR changes it to …/api/affiliates/click?ugig_ref={trackingCode}, which is the internal click-tracking handler that /ref/[code] redirects to. Any client or affiliate dashboard that stores or displays the tracking_url from the apply response will now see and distribute the raw API URL instead of the clean shareable link. Both resolve to the same destination, but this is a breaking change to the response contract that isn't covered by a test for apply/route.ts.

} catch {
return NextResponse.json({ error: "An unexpected error occurred" }, { status: 500 });
Expand Down
40 changes: 40 additions & 0 deletions src/app/ref/[code]/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { NextRequest } from "next/server";
import { GET } from "./route";

function makeParams(code: string) {
return { params: Promise.resolve({ code }) };
}

describe("GET /ref/[code]", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it("redirects to the current request origin when no app URL is configured", async () => {
vi.stubEnv("NEXT_PUBLIC_APP_URL", "");

const res = await GET(
new NextRequest("http://localhost:3000/ref/alice code/1"),
makeParams("alice code/1")
);

expect(res.status).toBe(307);
expect(res.headers.get("location")).toBe(
"http://localhost:3000/api/affiliates/click?ugig_ref=alice+code%2F1"
);
});

it("redirects to the configured public app URL when present", async () => {
vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://staging.ugig.example");

const res = await GET(
new NextRequest("http://localhost:3000/ref/alice"),
makeParams("alice")
);

expect(res.headers.get("location")).toBe(
"https://staging.ugig.example/api/affiliates/click?ugig_ref=alice"
);
});
});
6 changes: 4 additions & 2 deletions src/app/ref/[code]/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { buildAffiliateTrackingUrl, getAffiliateBaseUrl } from "@/lib/affiliates/tracking-url";

/**
* GET /ref/[code] - Short affiliate tracking link
Expand All @@ -9,6 +10,7 @@ export async function GET(
{ params }: { params: Promise<{ code: string }> }
) {
const { code } = await params;
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || "https://ugig.net";
return NextResponse.redirect(`${baseUrl}/api/affiliates/click?ugig_ref=${encodeURIComponent(code)}`);
return NextResponse.redirect(
buildAffiliateTrackingUrl(getAffiliateBaseUrl(request.url), code)
);
}
27 changes: 27 additions & 0 deletions src/lib/affiliates/tracking-url.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { buildAffiliateTrackingUrl, getAffiliateBaseUrl } from "./tracking-url";

describe("affiliate tracking URLs", () => {
afterEach(() => {
vi.unstubAllEnvs();
});

it("uses the current request origin when no app URL is configured", () => {
vi.stubEnv("NEXT_PUBLIC_APP_URL", "");

expect(getAffiliateBaseUrl("http://localhost:3000/api/affiliates/offers/offer-1/affiliates"))
.toBe("http://localhost:3000");
});

it("prefers the configured public app URL", () => {
vi.stubEnv("NEXT_PUBLIC_APP_URL", "https://staging.ugig.example");

expect(getAffiliateBaseUrl("http://localhost:3000/api/affiliates/offers/offer-1/affiliates"))
.toBe("https://staging.ugig.example");
});

it("builds encoded click URLs", () => {
expect(buildAffiliateTrackingUrl("https://staging.ugig.example", "alice code/1"))
.toBe("https://staging.ugig.example/api/affiliates/click?ugig_ref=alice+code%2F1");
});
});
9 changes: 9 additions & 0 deletions src/lib/affiliates/tracking-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function getAffiliateBaseUrl(requestUrl: string): string {
return process.env.NEXT_PUBLIC_APP_URL || new URL(requestUrl).origin;
}

export function buildAffiliateTrackingUrl(baseUrl: string, trackingCode: string): string {
const url = new URL("/api/affiliates/click", baseUrl);
url.searchParams.set("ugig_ref", trackingCode);
return url.toString();
}
Loading