semillero-special-hotel/src/app/api/sales/batch-save/route.ts

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 });
}
}