// supabase/functions/send-accounting/index.ts import "jsr:@supabase/functions-js/edge-runtime.d.ts"; import JSZip from "npm:jszip@3.10.1"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; // --- 🧩 Utilitaires de nommage fichier --- function sanitizeTitle(raw: string | null, maxLen = 50) { const safe = (raw ?? "ticket") .normalize("NFKD") .replace(/[^\x00-\x7F]/g, "") // retire non-ASCII .replace(/[^a-zA-Z0-9-_]+/g, "_") // caractères sûrs .replace(/_+/g, "_") // underscores consécutifs .replace(/^_+|_+$/g, "") // trim .toLowerCase(); return safe.substring(0, Math.max(1, maxLen)); } function extFromPath(path?: string) { if (!path) return "jpg"; const i = path.lastIndexOf("."); if (i < 0) return "jpg"; const ext = path.substring(i + 1).toLowerCase(); return ext.length <= 5 ? ext : "jpg"; } function buildFilename(dateIso: string, amount: number, title: string, ext: string) { const date = new Date(dateIso).toISOString().split("T")[0]; // YYYY-MM-DD const base = `${date}_${amount.toFixed(2)}_`; const maxTotal = 120; const remain = Math.max(10, maxTotal - (base.length + 1 + ext.length)); const clipped = title.length > remain ? title.substring(0, remain) : title; return `${base}${clipped}.${ext}`; } // --- 🧩 Fonction principale --- Deno.serve(async (req) => { if (req.method !== "POST") { return new Response(JSON.stringify({ error: "Only POST allowed" }), { status: 405, headers: { "Content-Type": "application/json" }, }); } try { const { user_id, month, emails } = await req.json(); if (!user_id || !month || !emails || !Array.isArray(emails) || emails.length === 0) { return new Response(JSON.stringify({ error: "Missing parameters (user_id, month 'YYYY-MM', emails[])", }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const SUPABASE_URL = Deno.env.get("SUPABASE_URL"); const SERVICE_ROLE = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY"); const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY"); const FROM_EMAIL = Deno.env.get("FROM_EMAIL") ?? "TickTrack "; const EXPORT_BUCKET = Deno.env.get("EXPORT_BUCKET") ?? "exports"; if (!SUPABASE_URL || !SERVICE_ROLE || !RESEND_API_KEY) { return new Response(JSON.stringify({ error: "Missing env vars" }), { status: 500, headers: { "Content-Type": "application/json" }, }); } const supabase = createClient(SUPABASE_URL, SERVICE_ROLE); // --- 🗓️ Détermination de la période --- const [yearStr, monthStr] = String(month).split("-"); const y = parseInt(yearStr, 10); const m = parseInt(monthStr, 10); if (!y || !m || m < 1 || m > 12) { return new Response(JSON.stringify({ error: "Invalid month format" }), { status: 400, headers: { "Content-Type": "application/json" }, }); } const start = new Date(y, m - 1, 1); const end = new Date(y, m, 1); // --- 👤 Récupération des infos utilisateur --- const { data: profile } = await supabase .from("profiles") .select("first_name, last_name") .eq("id", user_id) .single(); const fullName = profile ? `${profile.first_name ?? ""} ${profile.last_name ?? ""}`.trim() : "Utilisateur TickTrack"; // --- 🧾 Récupération des tickets --- const { data: receipts, error: fetchErr } = await supabase .from("receipts") .select("title, amount, date, photo_path") .eq("user_id", user_id) .gte("date", start.toISOString()) .lt("date", end.toISOString()); if (fetchErr) throw new Error(fetchErr.message); if (!receipts || receipts.length === 0) { return new Response(JSON.stringify({ message: "Aucun ticket trouvé pour ce mois." }), { status: 404, headers: { "Content-Type": "application/json" }, }); } // --- 📦 Création du ZIP --- const zip = new JSZip(); for (const r of receipts) { if (!r.photo_path) continue; const { data: file } = await supabase.storage.from("receipts").download(r.photo_path); if (!file) continue; const buf = await file.arrayBuffer(); const title = sanitizeTitle(String(r.title ?? ""), 50); const ext = extFromPath(r.photo_path); const filename = buildFilename(String(r.date), Number(r.amount ?? 0), title, ext); zip.file(filename, buf); } const zipBytes = await zip.generateAsync({ type: "uint8array", compression: "DEFLATE", compressionOptions: { level: 6 }, }); // --- ☁️ Upload dans le bucket privé --- const zipName = `tickets_${y}-${String(m).padStart(2, "0")}.zip`; const storagePath = `${user_id}/${zipName}`; const zipBlob = new Blob([zipBytes], { type: "application/zip" }); const { error: upErr } = await supabase.storage .from(EXPORT_BUCKET) .upload(storagePath, zipBlob, { contentType: "application/zip", upsert: true, }); if (upErr) throw new Error(`Upload failed: ${upErr.message}`); // --- 🔗 URL signée valable 7 jours --- const { data: signed } = await supabase.storage .from(EXPORT_BUCKET) .createSignedUrl(storagePath, 7 * 24 * 60 * 60); if (!signed?.signedUrl) throw new Error("Failed to create signed URL"); // --- 🗓️ Format du mois (en lettres FR) --- const monthLabel = new Intl.DateTimeFormat("fr-FR", { month: "long", year: "numeric", }).format(new Date(y, m - 1, 1)); // --- 💌 Envoi d’e-mail au comptable via Resend --- const html = `

Tickets du mois de ${monthLabel}

Bonjour,

Veuillez trouver ci-dessous le lien de téléchargement contenant les justificatifs de ${fullName} pour la période de ${monthLabel}.

📥 Télécharger les tickets (${receipts.length})

Ce lien est valable pendant 7 jours.


Email automatique envoyé par TickTrack • Ne pas répondre

`; const mailRes = await fetch("https://api.resend.com/emails", { method: "POST", headers: { Authorization: `Bearer ${RESEND_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ from: FROM_EMAIL, to: emails, subject: `TickTrack • Tickets de ${monthLabel}`, html, }), }); if (!mailRes.ok) { const t = await mailRes.text(); console.error("Resend error:", t); throw new Error("Email send failed"); } return new Response( JSON.stringify({ ok: true, count: receipts.length, download_url: signed.signedUrl, storage_path: `${EXPORT_BUCKET}/${storagePath}`, }), { status: 200, headers: { "Content-Type": "application/json" } }, ); } catch (e) { console.error("send-accounting error:", e); return new Response(JSON.stringify({ error: String(e?.message ?? e) }), { status: 500, headers: { "Content-Type": "application/json" }, }); } });