feat(sase): refund + bonus extension — Phase E
Pairs with sase.tr#30.
Refund
- admin-sdk: refundPayment({ paymentId, amount?, reason, founderId }) +
RefundResult type; SASE_ADMIN_ENDPOINTS lists the new route.
- POST /api/sase/payments/[paymentId]/refund — auth + spoke-wired,
reason ≥ 5 chars, amount > 0 when provided (full refund if omitted).
Audit on success and failure.
- _refund-button.tsx: per-row [Refund] button in the Billing tab
payments table. Modal has [✓] full vs partial input, reason, and a
destructive submit. Hidden for payments that aren't refundable
(status !== completed/partially_refunded).
- Payment row now exposes an "İşlem" column (only when spoke is wired).
Bonus extension (goodwill)
- BillingActions: new "Bonus +7g/+14g/+30g + özel" buttons surfaced
for active subscriptions. Hits the same /trial/extend wire under the
hood — the spoke generalized that endpoint to active too — but the
panel uses "Bonus süre ekle (goodwill)" copy instead of "Trial uzat".
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { headers } from "next/headers";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { createSaseAdmin, saseAdminWired } from "@/lib/admin-sdk/sase";
|
||||
import { writeAudit } from "@/lib/audit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST(
|
||||
req: Request,
|
||||
ctx: { params: Promise<{ paymentId: string }> },
|
||||
) {
|
||||
const session = await auth.api.getSession({ headers: await headers() });
|
||||
if (!session) {
|
||||
return NextResponse.json({ ok: false, error: "unauthenticated" }, { status: 401 });
|
||||
}
|
||||
if (!saseAdminWired()) {
|
||||
return NextResponse.json({ ok: false, error: "spoke_not_wired" }, { status: 503 });
|
||||
}
|
||||
|
||||
const { paymentId } = await ctx.params;
|
||||
const body = (await req.json().catch(() => ({}))) as {
|
||||
amount?: number | null;
|
||||
reason?: string;
|
||||
};
|
||||
const reason = (body.reason ?? "").trim();
|
||||
if (reason.length < 5) {
|
||||
return NextResponse.json({ ok: false, error: "reason_required" }, { status: 400 });
|
||||
}
|
||||
let amount: number | undefined;
|
||||
if (body.amount !== undefined && body.amount !== null) {
|
||||
if (typeof body.amount !== "number" || body.amount <= 0) {
|
||||
return NextResponse.json({ ok: false, error: "invalid_amount" }, { status: 400 });
|
||||
}
|
||||
amount = Math.round(body.amount);
|
||||
}
|
||||
|
||||
const endpoint = `/api/sase/payments/${paymentId}/refund`;
|
||||
try {
|
||||
const sdk = createSaseAdmin();
|
||||
const result = await sdk.refundPayment({
|
||||
paymentId,
|
||||
amount,
|
||||
reason,
|
||||
founderId: session.user.id,
|
||||
});
|
||||
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "POST",
|
||||
requestPayload: { paymentId, amount, reasonLen: reason.length },
|
||||
responseStatus: 200,
|
||||
});
|
||||
|
||||
return NextResponse.json({ ok: true, result });
|
||||
} catch (err) {
|
||||
const status = (err as { status?: number } | undefined)?.status ?? 500;
|
||||
const message = err instanceof Error ? err.message : "unknown";
|
||||
await writeAudit({
|
||||
projectKey: "sase",
|
||||
endpoint,
|
||||
method: "POST",
|
||||
requestPayload: { paymentId, amount, reasonLen: reason.length },
|
||||
responseStatus: status,
|
||||
});
|
||||
return NextResponse.json({ ok: false, error: message }, { status });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user