Skip to content
Merged
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
35 changes: 35 additions & 0 deletions app/api/pymthouse/invoices/[invoiceId]/hosted-url/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NextRequest, NextResponse } from "next/server";
import { getDashboardUserInvoiceHostedUrl } from "@/lib/console/pymthouse-billing-bff";
import { requireConsoleSession } from "@/lib/console/session-user";
import {
PYMTHOUSE_NO_STORE_HEADERS,
pymthouseErrorResponse,
} from "@/app/api/pymthouse/route-helpers";

export const runtime = "nodejs";

export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ invoiceId: string }> }
) {
const { invoiceId: rawInvoiceId } = await params;
const invoiceId = decodeURIComponent(rawInvoiceId).trim();

if (!invoiceId) {
return NextResponse.json(
{ error: "invoiceId is required" },
{ status: 400 }
);
}

try {
const session = await requireConsoleSession();
const links = await getDashboardUserInvoiceHostedUrl(
session.externalUserId,
invoiceId
);
return NextResponse.json(links, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(error, "Failed to resolve invoice link");
}
}
28 changes: 28 additions & 0 deletions app/api/pymthouse/invoices/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from "next/server";
import { listDashboardUserInvoices } from "@/lib/console/pymthouse-billing-bff";
import { requireConsoleSession } from "@/lib/console/session-user";
import {
PYMTHOUSE_NO_STORE_HEADERS,
pymthouseErrorResponse,
} from "@/app/api/pymthouse/route-helpers";

export const runtime = "nodejs";

export async function GET(request: NextRequest) {
const pageRaw = Number(request.nextUrl.searchParams.get("page") || "1");
const pageSizeRaw = Number(
request.nextUrl.searchParams.get("pageSize") || "20"
);

try {
const session = await requireConsoleSession();
const result = await listDashboardUserInvoices(session.externalUserId, {
page: Number.isFinite(pageRaw) && pageRaw > 0 ? pageRaw : 1,
pageSize:
Number.isFinite(pageSizeRaw) && pageSizeRaw > 0 ? pageSizeRaw : 20,
});
return NextResponse.json(result, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(error, "Failed to load invoices");
}
}
135 changes: 135 additions & 0 deletions app/api/pymthouse/payment-methods/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { NextRequest, NextResponse } from "next/server";
import {
ensureDashboardUserDefaultPaymentMethod,
listDashboardUserPaymentMethods,
removeDashboardUserPaymentMethod,
setDashboardUserDefaultPaymentMethod,
startDashboardPaymentMethodCheckout,
} from "@/lib/console/pymthouse-billing-bff";
import { requireConsoleSession } from "@/lib/console/session-user";
import {
checkoutReturnOrigin,
PYMTHOUSE_NO_STORE_HEADERS,
pymthouseErrorResponse,
} from "@/app/api/pymthouse/route-helpers";

export const runtime = "nodejs";

export async function GET() {
try {
const session = await requireConsoleSession();
const paymentMethods = await listDashboardUserPaymentMethods(
session.externalUserId
);
return NextResponse.json(
{ paymentMethods },
{ headers: PYMTHOUSE_NO_STORE_HEADERS }
);
} catch (error) {
return pymthouseErrorResponse(error, "Failed to load payment methods");
}
}

export async function POST(request: NextRequest) {
let body: {
successUrl?: string;
cancelUrl?: string;
};
try {
body = (await request.json()) as typeof body;
} catch {
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
}

const origin = checkoutReturnOrigin(request);
const successUrl =
body.successUrl?.trim() ||
`${origin}/settings?tab=billing&checkout=success`;
const cancelUrl =
body.cancelUrl?.trim() || `${origin}/settings?tab=billing&checkout=cancel`;

try {
const session = await requireConsoleSession();
const result = await startDashboardPaymentMethodCheckout({
externalUserId: session.externalUserId,
successUrl,
cancelUrl,
});
return NextResponse.json(result, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(
error,
"Failed to start payment method checkout"
);
}
}

export async function PATCH(request: NextRequest) {
let body: {
paymentMethodId?: string;
ensureDefault?: boolean;
};
try {
body = (await request.json()) as typeof body;
} catch {
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
}

try {
const session = await requireConsoleSession();
if (body.ensureDefault === true) {
return NextResponse.json(
await ensureDashboardUserDefaultPaymentMethod(session.externalUserId),
{ headers: PYMTHOUSE_NO_STORE_HEADERS }
);
}

const paymentMethodId = body.paymentMethodId?.trim();
if (!paymentMethodId) {
return NextResponse.json(
{ error: "paymentMethodId is required" },
{ status: 400 }
);
}

return NextResponse.json(
await setDashboardUserDefaultPaymentMethod(
session.externalUserId,
paymentMethodId
),
{ headers: PYMTHOUSE_NO_STORE_HEADERS }
);
} catch (error) {
return pymthouseErrorResponse(error, "Failed to set default payment method");
}
}

export async function DELETE(request: NextRequest) {
let body: { paymentMethodId?: string };
try {
body = (await request.json()) as typeof body;
} catch {
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
}

const paymentMethodId = body.paymentMethodId?.trim();
if (!paymentMethodId) {
return NextResponse.json(
{ error: "paymentMethodId is required" },
{ status: 400 }
);
}

try {
const session = await requireConsoleSession();
return NextResponse.json(
await removeDashboardUserPaymentMethod(
session.externalUserId,
paymentMethodId
),
{ headers: PYMTHOUSE_NO_STORE_HEADERS }
);
} catch (error) {
return pymthouseErrorResponse(error, "Failed to remove payment method");
}
}
32 changes: 32 additions & 0 deletions app/api/pymthouse/wallet/invoices/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { NextRequest, NextResponse } from "next/server";
import { listDashboardWalletInvoices } from "@/lib/console/pymthouse-billing-bff";
import { requireConsoleSession } from "@/lib/console/session-user";
import {
PYMTHOUSE_NO_STORE_HEADERS,
pymthouseErrorResponse,
} from "@/app/api/pymthouse/route-helpers";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

function parsePageParam(raw: string | null): number | undefined {
if (!raw || !/^[1-9]\d*$/.test(raw)) return undefined;
return Number(raw);
}

export async function GET(request: NextRequest) {
const page = parsePageParam(request.nextUrl.searchParams.get("page"));
const pageSize = parsePageParam(request.nextUrl.searchParams.get("pageSize"));

try {
const session = await requireConsoleSession();
const result = await listDashboardWalletInvoices({
externalUserId: session.externalUserId,
page,
pageSize,
});
return NextResponse.json(result, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(error, "Failed to load wallet invoices");
}
}
91 changes: 91 additions & 0 deletions app/api/pymthouse/wallet/payment-methods/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { NextRequest, NextResponse } from "next/server";
import {
ensureDashboardWalletDefaultPaymentMethod,
listDashboardWalletPaymentMethods,
startDashboardWalletPaymentMethodCheckout,
} from "@/lib/console/pymthouse-billing-bff";
import { requireConsoleSession } from "@/lib/console/session-user";
import {
checkoutReturnOrigin,
PYMTHOUSE_NO_STORE_HEADERS,
pymthouseErrorResponse,
} from "@/app/api/pymthouse/route-helpers";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function GET() {
try {
const session = await requireConsoleSession();
const paymentMethods = await listDashboardWalletPaymentMethods(
session.externalUserId
);
return NextResponse.json(
{ paymentMethods },
{ headers: PYMTHOUSE_NO_STORE_HEADERS }
);
} catch (error) {
return pymthouseErrorResponse(error, "Failed to load wallet payment methods");
}
}

export async function POST(request: NextRequest) {
let body: {
successUrl?: string;
cancelUrl?: string;
};
try {
body = (await request.json()) as typeof body;
} catch {
body = {};
}

const origin = checkoutReturnOrigin(request);
const successUrl =
body.successUrl?.trim() || `${origin}/usage?topup=pm-saved`;
const cancelUrl = body.cancelUrl?.trim() || `${origin}/usage?topup=canceled`;

try {
const session = await requireConsoleSession();
const result = await startDashboardWalletPaymentMethodCheckout({
externalUserId: session.externalUserId,
successUrl,
cancelUrl,
});
return NextResponse.json(result, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(
error,
"Failed to start payment method checkout"
);
}
}

export async function PATCH(request: NextRequest) {
let body: { ensureDefault?: boolean };
try {
body = (await request.json()) as typeof body;
} catch {
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
}

if (body.ensureDefault !== true) {
return NextResponse.json(
{ error: "ensureDefault: true is required" },
{ status: 400 }
);
}

try {
const session = await requireConsoleSession();
const result = await ensureDashboardWalletDefaultPaymentMethod(
session.externalUserId
);
return NextResponse.json(result, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(
error,
"Failed to ensure default payment method"
);
}
}
20 changes: 20 additions & 0 deletions app/api/pymthouse/wallet/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextResponse } from "next/server";
import { getDashboardOwnerWallet } from "@/lib/console/pymthouse-billing-bff";
import { requireConsoleSession } from "@/lib/console/session-user";
import {
PYMTHOUSE_NO_STORE_HEADERS,
pymthouseErrorResponse,
} from "@/app/api/pymthouse/route-helpers";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function GET() {
try {
const session = await requireConsoleSession();
const wallet = await getDashboardOwnerWallet(session.externalUserId);
return NextResponse.json(wallet, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(error, "Failed to load wallet");
}
}
52 changes: 52 additions & 0 deletions app/api/pymthouse/wallet/top-up/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { NextRequest, NextResponse } from "next/server";
import { startDashboardWalletTopUp } from "@/lib/console/pymthouse-billing-bff";
import { requireConsoleSession } from "@/lib/console/session-user";
import {
checkoutReturnOrigin,
PYMTHOUSE_NO_STORE_HEADERS,
pymthouseErrorResponse,
} from "@/app/api/pymthouse/route-helpers";

export const runtime = "nodejs";

export async function POST(request: NextRequest) {
let body: {
amountUsd?: string | number;
successUrl?: string;
cancelUrl?: string;
};
try {
body = (await request.json()) as typeof body;
} catch {
return NextResponse.json({ error: "invalid_json" }, { status: 400 });
}

const amountUsd =
typeof body.amountUsd === "number"
? body.amountUsd.toFixed(2)
: body.amountUsd?.trim();
if (!amountUsd) {
return NextResponse.json(
{ error: 'amountUsd is required (e.g. "25.00")' },
{ status: 400 }
);
}

const origin = checkoutReturnOrigin(request);
const successUrl =
body.successUrl?.trim() || `${origin}/usage?topup=succeeded`;
const cancelUrl = body.cancelUrl?.trim() || `${origin}/usage?topup=canceled`;

try {
const session = await requireConsoleSession();
const result = await startDashboardWalletTopUp({
amountUsd,
externalUserId: session.externalUserId,
successUrl,
cancelUrl,
});
return NextResponse.json(result, { headers: PYMTHOUSE_NO_STORE_HEADERS });
} catch (error) {
return pymthouseErrorResponse(error, "Failed to start top-up checkout");
}
}
Loading