cellex-web / src /app /api /group-buy /route.ts
eeshaAI
feat: Migrate frontend from static HTML to Next.js 16 App Router
a09d2a4
Raw
History Blame Contribute Delete
12.8 kB
import { NextRequest, NextResponse } from 'next/server';
const SUPABASE_URL = process.env.SUPABASE_URL || 'https://tcwdbokruvlizkxcpkzj.supabase.co';
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY || '';
const SUPABASE_TOKEN = process.env.SUPABASE_TOKEN || '';
const EDGE_FUNCTIONS_URL = `${SUPABASE_URL}/functions/v1`;
const COOKIE_NAME = 'cellex_session_id';
/**
* Group Buy API
*
* Operations:
* - start: Buyer starts a new group buy for a product (creates invite link)
* - join: Buyer joins an existing group buy via invite code
* - status: Get status of a group buy
* - active: Get active group buys for a product
* - mine: Get group buys the user has joined
* - enable: Seller enables group buy for a product (sets target + discount)
* - disable: Seller disables group buy for a product
*/
function generateInviteCode(): string {
return Math.random().toString(36).substring(2, 10) + Date.now().toString(36).substring(-4);
}
export async function POST(request: NextRequest) {
if (!SUPABASE_ANON_KEY) {
return NextResponse.json({ success: false, error: 'SUPABASE_ANON_KEY not set' }, { status: 500 });
}
const sessionId = request.cookies.get(COOKIE_NAME)?.value || '';
let body: any;
try { body = await request.json(); } catch { return NextResponse.json({ success: false, error: 'Invalid JSON' }, { status: 400 }); }
const sqlHeaders: Record<string, string> = {
'Authorization': `Bearer ${SUPABASE_TOKEN}`,
'Content-Type': 'application/json',
};
const sqlApiUrl = `https://api.supabase.com/v1/projects/tcwdbokruvlizkxcpkzj/database/query`;
// --- Operations that don't require auth ---
if (body.op === 'status') {
const gbId = body.groupBuyId;
if (!gbId) return NextResponse.json({ success: false, error: 'groupBuyId required' }, { status: 400 });
const resp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `SELECT gb.*, p.name as product_name, p.image_url, p.price, p.group_buy_enabled, p.group_buy_target_count, p.group_buy_discount_pct FROM group_buys gb JOIN products p ON gb.product_id = p.id WHERE gb.id = '${gbId}'::uuid;` }) });
const data = await resp.json();
if (Array.isArray(data) && data.length > 0) return NextResponse.json({ success: true, groupBuy: data[0] });
return NextResponse.json({ success: false, error: 'Group buy not found' }, { status: 404 });
}
if (body.op === 'open') {
const productId = body.productId;
if (!productId) return NextResponse.json({ success: false, error: 'productId required' }, { status: 400 });
const resp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `SELECT * FROM group_buys WHERE product_id = ${productId} AND status = 'open' ORDER BY created_at DESC;` }) });
const data = await resp.json();
return NextResponse.json({ success: true, groupBuys: Array.isArray(data) ? data : [] });
}
if (body.op === 'invite') {
// Get group buy by invite code (public — no auth needed)
const code = body.inviteCode;
if (!code) return NextResponse.json({ success: false, error: 'inviteCode required' }, { status: 400 });
const resp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `SELECT gb.*, p.name as product_name, p.image_url, p.price, p.group_buy_target_count, p.group_buy_discount_pct, u.email as initiator_email FROM group_buys gb JOIN products p ON gb.product_id = p.id LEFT JOIN auth.users u ON gb.initiator_id = u.id WHERE gb.invite_code = '${code.replace(/'/g, "''")}';` }) });
const data = await resp.json();
if (Array.isArray(data) && data.length > 0) return NextResponse.json({ success: true, groupBuy: data[0] });
return NextResponse.json({ success: false, error: 'Invalid invite code' }, { status: 404 });
}
// --- Operations that require auth ---
if (!sessionId) return NextResponse.json({ success: false, error: 'Not authenticated' }, { status: 401 });
// Get user
const authResp = await fetch(`${EDGE_FUNCTIONS_URL}/auth`, {
method: 'POST',
headers: { 'apikey': SUPABASE_ANON_KEY, 'Authorization': `Bearer ${sessionId}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ op: 'session' }),
});
const authData = await authResp.json();
if (!authData.success || !authData.user) return NextResponse.json({ success: false, error: 'Not authenticated' }, { status: 401 });
const userId = authData.user.id;
const userEmail = authData.user.email;
switch (body.op) {
case 'enable': {
// Seller enables group buy for a product
const { productId, targetCount, discountPct } = body;
if (!productId) return NextResponse.json({ success: false, error: 'productId required' }, { status: 400 });
const tc = targetCount || 3;
const dp = discountPct || 20;
const resp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `UPDATE products SET group_buy_enabled = true, group_buy_target_count = ${tc}, group_buy_discount_pct = ${dp} WHERE id = ${productId} AND seller_id = '${userId}'::uuid RETURNING *;` }) });
const data = await resp.json();
if (Array.isArray(data) && data.length > 0) return NextResponse.json({ success: true, product: data[0] });
return NextResponse.json({ success: false, error: 'Failed — not the seller or product not found' }, { status: 403 });
}
case 'disable': {
const { productId } = body;
if (!productId) return NextResponse.json({ success: false, error: 'productId required' }, { status: 400 });
const resp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `UPDATE products SET group_buy_enabled = false WHERE id = ${productId} AND seller_id = '${userId}'::uuid RETURNING *;` }) });
const data = await resp.json();
if (Array.isArray(data) && data.length > 0) return NextResponse.json({ success: true, product: data[0] });
return NextResponse.json({ success: false, error: 'Failed' }, { status: 403 });
}
case 'start': {
// Buyer starts a new group buy for a product
const { productId } = body;
if (!productId) return NextResponse.json({ success: false, error: 'productId required' }, { status: 400 });
// Check if product has group buy enabled
const checkResp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `SELECT group_buy_enabled, group_buy_target_count, group_buy_discount_pct, seller_id FROM products WHERE id = ${productId};` }) });
const checkData = await checkResp.json();
if (!Array.isArray(checkData) || checkData.length === 0) return NextResponse.json({ success: false, error: 'Product not found' }, { status: 404 });
const product = checkData[0];
if (!product.group_buy_enabled) return NextResponse.json({ success: false, error: 'Seller has not enabled group buy for this product' }, { status: 400 });
const inviteCode = generateInviteCode();
const targetCount = product.group_buy_target_count || 3;
const discountPct = product.group_buy_discount_pct || 20;
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // 7 days
// Create group buy
const createResp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `INSERT INTO group_buys (product_id, seller_id, initiator_id, target_count, current_count, discount_pct, status, expires_at, invite_code) VALUES (${productId}, '${product.seller_id}'::uuid, '${userId}'::uuid, ${targetCount}, 1, ${discountPct}, 'open', '${expiresAt}', '${inviteCode}') RETURNING *;` }) });
const createData = await createResp.json();
if (!Array.isArray(createData) || createData.length === 0) return NextResponse.json({ success: false, error: 'Failed to create group buy' }, { status: 500 });
const groupBuy = createData[0];
// Add initiator as first member
await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `INSERT INTO group_buy_members (group_buy_id, user_id) VALUES ('${groupBuy.id}'::uuid, '${userId}'::uuid);` }) });
// Create a conversation for this group buy (initiator ↔ seller)
// Only create if initiator is NOT the seller (avoid self-conversation)
let conversationId = null;
if (userId !== product.seller_id) {
const convResp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `INSERT INTO conversations (type, participant1, participant2, group_buy_id) VALUES ('group_buy', '${userId}'::uuid, '${product.seller_id}'::uuid, '${groupBuy.id}'::uuid) RETURNING *;` }) });
const convData = await convResp.json();
if (Array.isArray(convData) && convData.length > 0) {
conversationId = convData[0].id;
await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `UPDATE group_buys SET conversation_id = '${conversationId}'::uuid WHERE id = '${groupBuy.id}'::uuid;` }) });
}
}
return NextResponse.json({ success: true, groupBuy: { ...groupBuy, conversationId, inviteLink: `/group-buy-join?code=${inviteCode}` } });
}
case 'join': {
// Buyer joins a group buy via invite code
const { inviteCode } = body;
if (!inviteCode) return NextResponse.json({ success: false, error: 'inviteCode required' }, { status: 400 });
// Get group buy
const gbResp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `SELECT * FROM group_buys WHERE invite_code = '${inviteCode.replace(/'/g, "''")}' AND status = 'open';` }) });
const gbData = await gbResp.json();
if (!Array.isArray(gbData) || gbData.length === 0) return NextResponse.json({ success: false, error: 'Group buy not found or expired' }, { status: 404 });
const groupBuy = gbData[0];
// Check if already a member
const memberResp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `SELECT * FROM group_buy_members WHERE group_buy_id = '${groupBuy.id}'::uuid AND user_id = '${userId}'::uuid;` }) });
const memberData = await memberResp.json();
if (Array.isArray(memberData) && memberData.length > 0) return NextResponse.json({ success: false, error: 'You are already a member of this group buy' }, { status: 400 });
// Check if group buy is full
if (groupBuy.current_count >= groupBuy.target_count) return NextResponse.json({ success: false, error: 'Group buy is full' }, { status: 400 });
// Add member
await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `INSERT INTO group_buy_members (group_buy_id, user_id) VALUES ('${groupBuy.id}'::uuid, '${userId}'::uuid);` }) });
// Increment count
const newCount = groupBuy.current_count + 1;
const isComplete = newCount >= groupBuy.target_count;
const updateResp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `UPDATE group_buys SET current_count = ${newCount}, status = '${isComplete ? 'completed' : 'open'}', completed_at = ${isComplete ? 'NOW()' : 'NULL'} WHERE id = '${groupBuy.id}'::uuid RETURNING *;` }) });
const updateData = await updateResp.json();
// Add user to the group buy's conversation (create one between joiner and initiator)
// This creates a 1-on-1 conversation linked to the group buy
await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `INSERT INTO conversations (type, participant1, participant2, group_buy_id) VALUES ('group_buy', '${userId}'::uuid, '${groupBuy.initiator_id}'::uuid, '${groupBuy.id}'::uuid) ON CONFLICT DO NOTHING;` }) });
return NextResponse.json({ success: true, groupBuy: Array.isArray(updateData) && updateData.length > 0 ? updateData[0] : groupBuy });
}
case 'mine': {
// Get group buys the user has joined
const resp = await fetch(sqlApiUrl, { method: 'POST', headers: sqlHeaders, body: JSON.stringify({ query: `SELECT gb.*, p.name as product_name, p.image_url, p.price FROM group_buys gb JOIN group_buy_members gbm ON gb.id = gbm.group_buy_id JOIN products p ON gb.product_id = p.id WHERE gbm.user_id = '${userId}'::uuid ORDER BY gb.created_at DESC;` }) });
const data = await resp.json();
return NextResponse.json({ success: true, groupBuys: Array.isArray(data) ? data : [] });
}
default:
return NextResponse.json({ success: false, error: `Unknown op: ${body.op}` }, { status: 400 });
}
}