feat: Souq native integration + auth simplification + CE-wide enhancements
Souq Marketplace: - Native souq pages: browse+filters, create listing, detail+seller profile, orders+chat - New API: reviews (POST), sellers/[id], upload (multipart), polar-checkout webhook - Listings API enhanced: minPrice, maxPrice, sortBy, deliveryDays filters - Seller workflow: start order, mark delivered, upload files, confirm delivery - Wallet: 5 Polar.sh FLH tiers, production checkout, mock fallback, success banner - Fix: order redirect /souq/history → /souq/orders Auth System: - Unified auth page, removed OAuth provider routing (2 files deleted) - Deleted oauth.ts (319 lines) — simplified auth chain - Streamlined login/register API routes Prisma Schema (+179 lines): - New models: Listing, Purchase, Review, Group/GroupMember - Gamification: DailyStreak, XpTransaction, Achievement, UserLevel - Learning: LearnCourse/Module/Enrollment/Certificate - Notifications, Referrals, Dhikr, PremiumBenefits Deleted legacy code: - src/app/api/marketplace/* (3 files) — replaced by /api/souq/* - src/app/api/files/[listingId]/route.ts — replaced by /api/souq/upload - src/app/api/seller/[sellerId]/route.ts — replaced by /api/souq/sellers/[id] Infrastructure: - Dockerfile: standalone output, Prisma runtime migration - docker-compose.yml: Polar env vars, healthcheck fix - docker-compose.staging.yml: staging on port 4014 - .gitea/workflows/ci.yml: CI pipeline
This commit is contained in:
@@ -3,9 +3,11 @@ import { prisma } from "@/lib/prisma";
|
||||
import { requireAuth } from "@/lib/auth";
|
||||
|
||||
const TOP_UP_AMOUNTS = {
|
||||
500: { usd: 4.99 },
|
||||
1100: { usd: 9.99 },
|
||||
3000: { usd: 24.99 },
|
||||
500: { usd: 0.99, bonus: 0, priceEnv: "POLAR_FLH_500" },
|
||||
1000: { usd: 0.99, bonus: 0, priceEnv: "POLAR_FLH_1000" },
|
||||
5000: { usd: 4.99, bonus: 500, priceEnv: "POLAR_FLH_5000" },
|
||||
10000: { usd: 9.99, bonus: 2000, priceEnv: "POLAR_FLH_10000" },
|
||||
50000: { usd: 49.99, bonus: 15000, priceEnv: "POLAR_FLH_50000" },
|
||||
} as const;
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
@@ -20,22 +22,19 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
if (!amount || !(amount in TOP_UP_AMOUNTS)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid amount. Choose 500, 1100, or 3000 FLH." },
|
||||
{ error: "Invalid amount. Choose 500, 1000, 5000, 10000, or 50000 FLH." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const pricing = TOP_UP_AMOUNTS[amount as keyof typeof TOP_UP_AMOUNTS];
|
||||
const totalFlh = amount + (pricing.bonus || 0);
|
||||
|
||||
// In production, this would create a Polar.sh checkout session.
|
||||
// For development, we mock the flow by directly adding FLH balance.
|
||||
// Replace with actual Polar API integration when ready.
|
||||
const useMock = process.env.MOCK_PAYMENTS !== "false";
|
||||
|
||||
if (useMock) {
|
||||
// Mock mode: use when POLAR_ACCESS_TOKEN is not configured
|
||||
if (!process.env.POLAR_ACCESS_TOKEN) {
|
||||
await prisma.user.update({
|
||||
where: { id: jwtPayload.userId },
|
||||
data: { flhBalance: { increment: amount } },
|
||||
data: { flhBalance: { increment: totalFlh } },
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -43,27 +42,67 @@ export async function POST(req: NextRequest) {
|
||||
url: "/wallet?topup=success",
|
||||
mock: true,
|
||||
amount,
|
||||
bonus: pricing.bonus || 0,
|
||||
amountUsd: pricing.usd,
|
||||
});
|
||||
}
|
||||
|
||||
// Production path — Polar checkout creation
|
||||
// const polarSession = await createPolarCheckout({
|
||||
// amount,
|
||||
// customerId: user.stripeCustomerId,
|
||||
// metadata: { userId: jwtPayload.userId, type: "top-up", amount },
|
||||
// });
|
||||
// Production — Polar.sh checkout session
|
||||
const productPriceId = process.env[pricing.priceEnv];
|
||||
if (!productPriceId) {
|
||||
return NextResponse.json(
|
||||
{ error: `Polar price not configured for ${amount} FLH. Set ${pricing.priceEnv} env var.` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const origin = req.headers.get("origin") || req.headers.get("host") || "";
|
||||
const baseUrl = origin.startsWith("http") ? origin : `https://${origin}`;
|
||||
|
||||
const polarResponse = await fetch("https://api.polar.sh/v1/checkouts/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.POLAR_ACCESS_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
product_price_id: productPriceId,
|
||||
success_url: `${baseUrl}/wallet?topup=success&amount=${amount}`,
|
||||
customer_email: jwtPayload.email,
|
||||
return_url: `${baseUrl}/wallet`,
|
||||
metadata: {
|
||||
type: "flh_purchase",
|
||||
flh_amount: totalFlh,
|
||||
user_id: jwtPayload.userId,
|
||||
user_email: jwtPayload.email,
|
||||
},
|
||||
allow_trial: false,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!polarResponse.ok) {
|
||||
const errorBody = await polarResponse.text();
|
||||
console.error("Polar API error:", polarResponse.status, errorBody);
|
||||
return NextResponse.json(
|
||||
{ error: `Checkout creation failed (HTTP ${polarResponse.status})` },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
|
||||
const polarData = await polarResponse.json();
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
url: "/wallet?topup=success", // would be polarSession.url
|
||||
url: polarData.url,
|
||||
checkout_id: polarData.id,
|
||||
amount,
|
||||
amountUsd: pricing.usd,
|
||||
bonus: pricing.bonus || 0,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Top-up checkout error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to create checkout" },
|
||||
{ error: error instanceof Error ? error.message : "Failed to create checkout" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user