290 lines
No EOL
11 KiB
TypeScript
290 lines
No EOL
11 KiB
TypeScript
// app/api/staff/facturation/[id]/route.ts
|
|
import { NextResponse } from "next/server";
|
|
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
|
|
import { cookies } from "next/headers";
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
export const revalidate = 0;
|
|
export const runtime = 'nodejs';
|
|
|
|
// Lazily import AWS SDK pieces to avoid client bundle impact
|
|
async function getS3Presigner() {
|
|
const [{ S3Client, GetObjectCommand }, { getSignedUrl }] = await Promise.all([
|
|
import("@aws-sdk/client-s3"),
|
|
import("@aws-sdk/s3-request-presigner"),
|
|
]);
|
|
return { S3Client, GetObjectCommand, getSignedUrl };
|
|
}
|
|
|
|
function mapStatus(input?: string | null, row?: any):
|
|
| "payee"
|
|
| "annulee"
|
|
| "prete"
|
|
| "emise"
|
|
| "en_cours"
|
|
| string {
|
|
if (!input && row) {
|
|
if (row?.payment_date) return "payee";
|
|
return "emise";
|
|
}
|
|
const s = String(input || "").trim().toLowerCase();
|
|
if (!s) return "emise";
|
|
if (/(payée|payee|paid)/i.test(s)) return "payee";
|
|
if (/(annulée|annulee|cancell|cancel)/i.test(s)) return "annulee";
|
|
if (/(prête|prepar|ready|prete)/i.test(s)) return "prete";
|
|
if (/(en\s*cours|pending|due)/i.test(s)) return "en_cours";
|
|
if (/(émise|emise|issued)/i.test(s)) return "emise";
|
|
return s;
|
|
}
|
|
|
|
async function isStaffUser(supabase: any, userId: string): Promise<boolean> {
|
|
try {
|
|
const { data: staffRow } = await supabase
|
|
.from('staff_users')
|
|
.select('is_staff')
|
|
.eq('user_id', userId)
|
|
.maybeSingle();
|
|
return !!staffRow?.is_staff;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// GET - Récupérer une facture spécifique
|
|
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
try {
|
|
const supabase = createRouteHandlerClient({ cookies });
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
|
|
|
// Vérifier que l'utilisateur est staff
|
|
const isStaff = await isStaffUser(supabase, session.user.id);
|
|
if (!isStaff) {
|
|
return NextResponse.json({ error: 'forbidden', message: 'Staff access required' }, { status: 403 });
|
|
}
|
|
|
|
// Récupérer la facture
|
|
const { data: invoice, error } = await supabase
|
|
.from('invoices')
|
|
.select('*')
|
|
.eq('id', params.id)
|
|
.single();
|
|
|
|
if (error) {
|
|
if (error.code === 'PGRST116') {
|
|
return NextResponse.json({ error: 'not_found', message: 'Invoice not found' }, { status: 404 });
|
|
}
|
|
console.error('[api/staff/facturation/[id]] error:', error.message);
|
|
return NextResponse.json({ error: 'supabase_error', detail: error.message }, { status: 500 });
|
|
}
|
|
|
|
// Récupérer le nom de l'organisation
|
|
let organizationName = null;
|
|
if (invoice.org_id) {
|
|
const { data: orgData } = await supabase
|
|
.from('organizations')
|
|
.select('structure_api')
|
|
.eq('id', invoice.org_id)
|
|
.single();
|
|
organizationName = orgData?.structure_api || null;
|
|
}
|
|
|
|
// Presign S3 URL for PDF
|
|
let signedPdfUrl: string | null = null;
|
|
if (invoice.pdf_s3_key) {
|
|
try {
|
|
const { S3Client, GetObjectCommand, getSignedUrl } = await getS3Presigner();
|
|
const bucket = (process.env.AWS_S3_BUCKET || 'odentas-docs').trim();
|
|
const region = process.env.AWS_REGION || 'eu-west-3';
|
|
const expireSeconds = Math.max(60, Math.min(60 * 60, Number(process.env.INVOICE_URL_EXPIRES ?? 900)));
|
|
|
|
const client = new S3Client({ region });
|
|
const cmd = new GetObjectCommand({ Bucket: bucket, Key: invoice.pdf_s3_key });
|
|
signedPdfUrl = await getSignedUrl(client, cmd, { expiresIn: expireSeconds });
|
|
} catch (e) {
|
|
console.error('[api/staff/facturation/[id]] presign error:', e);
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
id: invoice.id,
|
|
numero: invoice.invoice_number || null,
|
|
periode: invoice.period_label || null,
|
|
date: invoice.invoice_date || null,
|
|
montant_ht: (typeof invoice.amount_ht === 'number' ? invoice.amount_ht : parseFloat(invoice.amount_ht || '0')) || 0,
|
|
montant_ttc: (typeof invoice.amount_ttc === 'number' ? invoice.amount_ttc : parseFloat(invoice.amount_ttc || '0')) || 0,
|
|
statut: mapStatus(invoice.status, invoice),
|
|
notified: invoice.notified || false,
|
|
gocardless_payment_id: invoice.gocardless_payment_id || null,
|
|
pdf: signedPdfUrl,
|
|
org_id: invoice.org_id,
|
|
organization_name: organizationName,
|
|
payment_date: invoice.payment_date || null,
|
|
payment_method: invoice.payment_method || null,
|
|
due_date: invoice.due_date || null,
|
|
sepa_day: invoice.sepa_day || null,
|
|
invoice_type: invoice.invoice_type || null,
|
|
site_name: invoice.site_name || null,
|
|
created_at: invoice.created_at,
|
|
updated_at: invoice.updated_at,
|
|
notes: invoice.notes || null,
|
|
};
|
|
|
|
return NextResponse.json(result);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return NextResponse.json({ error: 'internal_server_error', message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// PATCH - Modifier une facture
|
|
export async function PATCH(req: Request, { params }: { params: { id: string } }) {
|
|
try {
|
|
console.log('[api/staff/facturation/[id]] PATCH request for ID:', params.id);
|
|
|
|
const supabase = createRouteHandlerClient({ cookies });
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
|
|
|
// Vérifier que l'utilisateur est staff
|
|
const isStaff = await isStaffUser(supabase, session.user.id);
|
|
if (!isStaff) {
|
|
return NextResponse.json({ error: 'forbidden', message: 'Staff access required' }, { status: 403 });
|
|
}
|
|
|
|
const body = await req.json();
|
|
console.log('[api/staff/facturation/[id]] Request body:', body);
|
|
|
|
const {
|
|
numero,
|
|
periode,
|
|
date,
|
|
montant_ht,
|
|
montant_ttc,
|
|
statut,
|
|
notified,
|
|
payment_date,
|
|
payment_method,
|
|
due_date,
|
|
sepa_day,
|
|
invoice_type,
|
|
site_name,
|
|
notes
|
|
} = body;
|
|
|
|
// Construire l'objet de mise à jour
|
|
const updateData: any = {
|
|
updated_at: new Date().toISOString(),
|
|
};
|
|
|
|
if (numero !== undefined) updateData.invoice_number = numero || null;
|
|
if (periode !== undefined) updateData.period_label = periode || null;
|
|
if (date !== undefined) updateData.invoice_date = date || null;
|
|
if (montant_ht !== undefined) updateData.amount_ht = parseFloat(montant_ht) || 0;
|
|
if (montant_ttc !== undefined) updateData.amount_ttc = parseFloat(montant_ttc) || 0;
|
|
if (statut !== undefined) updateData.status = statut || null;
|
|
if (notified !== undefined) updateData.notified = Boolean(notified);
|
|
if (payment_date !== undefined) updateData.payment_date = payment_date || null;
|
|
if (payment_method !== undefined) updateData.payment_method = payment_method || null;
|
|
if (due_date !== undefined) updateData.due_date = due_date || null;
|
|
if (sepa_day !== undefined) updateData.sepa_day = sepa_day || null;
|
|
if (invoice_type !== undefined) updateData.invoice_type = invoice_type || null;
|
|
if (site_name !== undefined) updateData.site_name = site_name || null;
|
|
if (notes !== undefined) updateData.notes = notes || null;
|
|
|
|
console.log('[api/staff/facturation/[id]] Update data:', updateData);
|
|
|
|
// Mettre à jour la facture
|
|
const { data: updatedInvoice, error } = await supabase
|
|
.from('invoices')
|
|
.update(updateData)
|
|
.eq('id', params.id)
|
|
.select()
|
|
.single();
|
|
|
|
if (error) {
|
|
console.error('[api/staff/facturation/[id]] Supabase update error:', error);
|
|
if (error.code === 'PGRST116') {
|
|
return NextResponse.json({ error: 'not_found', message: 'Invoice not found' }, { status: 404 });
|
|
}
|
|
return NextResponse.json({ error: 'supabase_error', detail: error.message }, { status: 500 });
|
|
}
|
|
|
|
console.log('[api/staff/facturation/[id]] Update successful:', updatedInvoice.id);
|
|
return NextResponse.json({
|
|
id: updatedInvoice.id,
|
|
message: 'Invoice updated successfully'
|
|
});
|
|
} catch (error) {
|
|
console.error('[api/staff/facturation/[id]] PATCH error:', error);
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return NextResponse.json({ error: 'internal_server_error', message }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// DELETE - Supprimer une facture
|
|
export async function DELETE(req: Request, { params }: { params: { id: string } }) {
|
|
try {
|
|
const supabase = createRouteHandlerClient({ cookies });
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
if (!session) return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
|
|
|
// Vérifier que l'utilisateur est staff
|
|
const isStaff = await isStaffUser(supabase, session.user.id);
|
|
if (!isStaff) {
|
|
return NextResponse.json({ error: 'forbidden', message: 'Staff access required' }, { status: 403 });
|
|
}
|
|
|
|
// Récupérer la facture pour obtenir la clé S3 avant suppression
|
|
const { data: invoice, error: fetchError } = await supabase
|
|
.from('invoices')
|
|
.select('pdf_s3_key')
|
|
.eq('id', params.id)
|
|
.single();
|
|
|
|
if (fetchError) {
|
|
console.error('[api/staff/facturation/[id]] fetch error:', fetchError.message);
|
|
return NextResponse.json({ error: 'supabase_error', detail: fetchError.message }, { status: 500 });
|
|
}
|
|
|
|
// Supprimer le PDF de S3 si il existe
|
|
if (invoice?.pdf_s3_key) {
|
|
try {
|
|
const { S3Client, DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
|
const client = new S3Client({
|
|
region: process.env.AWS_REGION || 'eu-west-3'
|
|
});
|
|
|
|
const deleteCommand = new DeleteObjectCommand({
|
|
Bucket: (process.env.AWS_S3_BUCKET || 'odentas-docs').trim(),
|
|
Key: invoice.pdf_s3_key,
|
|
});
|
|
|
|
await client.send(deleteCommand);
|
|
console.log('[api/staff/facturation/[id]] PDF deleted from S3:', invoice.pdf_s3_key);
|
|
} catch (s3Error) {
|
|
console.error('[api/staff/facturation/[id]] S3 delete error:', s3Error);
|
|
// Continue même si la suppression S3 échoue
|
|
}
|
|
}
|
|
|
|
// Supprimer la facture de Supabase
|
|
const { error } = await supabase
|
|
.from('invoices')
|
|
.delete()
|
|
.eq('id', params.id);
|
|
|
|
if (error) {
|
|
console.error('[api/staff/facturation/[id]] delete error:', error.message);
|
|
return NextResponse.json({ error: 'supabase_error', detail: error.message }, { status: 500 });
|
|
}
|
|
|
|
return NextResponse.json({
|
|
message: 'Invoice and PDF deleted successfully'
|
|
});
|
|
} catch (error) {
|
|
console.error('[api/staff/facturation/[id]] DELETE error:', error);
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return NextResponse.json({ error: 'internal_server_error', message }, { status: 500 });
|
|
}
|
|
} |