136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { getPrisma } from '@/lib/db';
|
|
|
|
export async function POST(req: NextRequest) {
|
|
try {
|
|
const signature = req.headers.get('x-n8n-signature');
|
|
const expectedSecret = process.env.N8N_WEBHOOK_SECRET;
|
|
|
|
if (!expectedSecret || signature !== expectedSecret) {
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: {
|
|
code: 'UNAUTHORIZED',
|
|
metadata: {}
|
|
}
|
|
}, { status: 401 });
|
|
}
|
|
|
|
const body = await req.json();
|
|
const { idempotencyKey, uploaderId, sales } = body;
|
|
|
|
if (!idempotencyKey || !uploaderId || !Array.isArray(sales)) {
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: {
|
|
code: 'BAD_REQUEST',
|
|
metadata: {}
|
|
}
|
|
}, { status: 400 });
|
|
}
|
|
|
|
const prisma = getPrisma();
|
|
|
|
const result = await prisma.$transaction(async (tx: any) => {
|
|
// Elevate privileges to admin role to bypass RLS for n8n batch operations
|
|
await tx.$executeRawUnsafe(`SET LOCAL app.current_user_role = 'admin';`);
|
|
|
|
// 1. Check idempotency
|
|
const existing = await tx.salesResult.findMany({
|
|
where: {
|
|
idempotencyKey: {
|
|
startsWith: `${idempotencyKey}-`
|
|
}
|
|
}
|
|
});
|
|
|
|
if (existing.length > 0) {
|
|
return {
|
|
alreadyProcessed: true,
|
|
count: existing.length
|
|
};
|
|
}
|
|
|
|
// 2. Resolve usernames and hotel codes
|
|
const uniqueUsernames = Array.from(new Set(sales.map(s => s.username).filter(Boolean))) as string[];
|
|
const uniqueHotelCodes = Array.from(new Set(sales.map(s => s.hotelCode).filter(Boolean))) as string[];
|
|
|
|
const dbUsers = await tx.user.findMany({
|
|
where: { username: { in: uniqueUsernames } }
|
|
});
|
|
|
|
const dbHotels = await tx.hotel.findMany({
|
|
where: { code: { in: uniqueHotelCodes } }
|
|
});
|
|
|
|
const userMap = new Map(dbUsers.map(u => [u.username, u]));
|
|
const hotelMap = new Map(dbHotels.map(h => [h.code, h]));
|
|
|
|
// 3. Save records
|
|
const createdSales = [];
|
|
for (let i = 0; i < sales.length; i++) {
|
|
const s = sales[i];
|
|
const user = userMap.get(s.username);
|
|
const hotel = hotelMap.get(s.hotelCode);
|
|
|
|
if (!user || !hotel) {
|
|
throw new Error(`User or Hotel not found for record: ${JSON.stringify(s)}`);
|
|
}
|
|
|
|
const created = await tx.salesResult.create({
|
|
data: {
|
|
source: 'API',
|
|
hotelId: hotel.id,
|
|
userId: user.id,
|
|
period: s.period,
|
|
amount: Number(s.amount),
|
|
salesCount: Number(s.salesCount),
|
|
idempotencyKey: `${idempotencyKey}-${i}`,
|
|
transactionId: s.transactionId || null,
|
|
uploadedBy: uploaderId,
|
|
status: 'PENDING'
|
|
}
|
|
});
|
|
createdSales.push(created);
|
|
}
|
|
|
|
// 4. Create Audit Log
|
|
await tx.auditLog.create({
|
|
data: {
|
|
userId: uploaderId,
|
|
action: 'CREATE',
|
|
targetTable: 'sales_results',
|
|
targetId: createdSales[0]?.id || 0,
|
|
newValue: {
|
|
count: createdSales.length,
|
|
idempotencyKey
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
alreadyProcessed: false,
|
|
count: createdSales.length
|
|
};
|
|
});
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
code: result.alreadyProcessed ? 'IMPORT_ALREADY_PROCESSED' : 'IMPORT_SUCCESSFUL',
|
|
metadata: {
|
|
count: result.count,
|
|
idempotencyKey
|
|
}
|
|
}, { status: 201 });
|
|
|
|
} catch (err: any) {
|
|
console.error('Batch save error:', err);
|
|
return NextResponse.json({
|
|
success: false,
|
|
error: {
|
|
code: 'BATCH_SAVE_FAILED',
|
|
metadata: { message: err.message }
|
|
}
|
|
}, { status: 500 });
|
|
}
|
|
}
|