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:
root
2026-06-24 07:02:03 +02:00
parent 913fa94fb9
commit cfff74e2db
81 changed files with 12132 additions and 2101 deletions
@@ -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 }
);
}