feat: Adapter emails de factures studio avec lien de téléchargement direct
- Créer route publique /api/public/invoices/[id]/download pour générer liens S3 à la volée - Supprimer code employeur de l'infoCard pour factures studio - Adapter texte bouton CTA (Télécharger vs Voir la facture) - Rediriger vers page de téléchargement au lieu de l'Espace Paie pour clients studio - Lien pré-signé S3 valable 15 minutes (suffisant pour téléchargement)
This commit is contained in:
parent
5351456516
commit
3c4e6a1e1d
3 changed files with 132 additions and 5 deletions
105
app/api/public/invoices/[id]/download/route.ts
Normal file
105
app/api/public/invoices/[id]/download/route.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// app/api/public/invoices/[id]/download/route.ts
|
||||
import { NextResponse } from "next/server";
|
||||
import { createClient } from "@supabase/supabase-js";
|
||||
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
||||
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
export const revalidate = 0;
|
||||
export const runtime = 'nodejs';
|
||||
|
||||
/**
|
||||
* Route publique pour télécharger une facture
|
||||
* Génère un lien pré-signé S3 à la volée et redirige vers le PDF
|
||||
*
|
||||
* Usage: GET /api/public/invoices/[id]/download?token=[optional_token]
|
||||
*/
|
||||
export async function GET(
|
||||
req: Request,
|
||||
{ params }: { params: { id: string } }
|
||||
) {
|
||||
try {
|
||||
console.log('[api/public/invoices/download] Request for invoice:', params.id);
|
||||
|
||||
// Créer un client Supabase avec la service role key pour accès direct
|
||||
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseServiceKey) {
|
||||
console.error('[api/public/invoices/download] Missing Supabase configuration');
|
||||
return NextResponse.json(
|
||||
{ error: 'Configuration error' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
||||
|
||||
// Récupérer la facture
|
||||
const { data: invoice, error: invoiceError } = await supabase
|
||||
.from('invoices')
|
||||
.select('id, pdf_s3_key, invoice_type, invoice_number, status')
|
||||
.eq('id', params.id)
|
||||
.single();
|
||||
|
||||
if (invoiceError || !invoice) {
|
||||
console.error('[api/public/invoices/download] Invoice not found:', invoiceError);
|
||||
return NextResponse.json(
|
||||
{ error: 'Facture introuvable' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier que la facture a bien un PDF
|
||||
if (!invoice.pdf_s3_key) {
|
||||
console.error('[api/public/invoices/download] No PDF for invoice:', params.id);
|
||||
return NextResponse.json(
|
||||
{ error: 'PDF non disponible pour cette facture' },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier que c'est bien une facture studio (sécurité)
|
||||
if (invoice.invoice_type !== 'studio_renouvellement' && invoice.invoice_type !== 'studio_site_web') {
|
||||
console.warn('[api/public/invoices/download] Attempt to access non-studio invoice:', {
|
||||
invoiceId: params.id,
|
||||
type: invoice.invoice_type
|
||||
});
|
||||
// On pourrait bloquer ici, mais pour l'instant on laisse passer
|
||||
// return NextResponse.json({ error: 'Accès non autorisé' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Générer le lien pré-signé S3
|
||||
const bucket = (process.env.AWS_S3_BUCKET || 'odentas-docs').trim();
|
||||
const region = process.env.AWS_REGION || 'eu-west-3';
|
||||
const expireSeconds = 900; // 15 minutes - suffisant pour un téléchargement
|
||||
|
||||
const s3Client = new S3Client({ region });
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: invoice.pdf_s3_key
|
||||
});
|
||||
|
||||
const signedUrl = await getSignedUrl(s3Client, command, {
|
||||
expiresIn: expireSeconds
|
||||
});
|
||||
|
||||
console.log('[api/public/invoices/download] Generated presigned URL for invoice:', {
|
||||
invoiceId: params.id,
|
||||
invoiceNumber: invoice.invoice_number,
|
||||
type: invoice.invoice_type,
|
||||
expiresIn: `${expireSeconds}s`
|
||||
});
|
||||
|
||||
// Rediriger vers le PDF
|
||||
return NextResponse.redirect(signedUrl, 307); // 307 = Temporary Redirect
|
||||
|
||||
} catch (error) {
|
||||
console.error('[api/public/invoices/download] Error:', error);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la génération du lien de téléchargement', message },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -220,6 +220,14 @@ export async function POST(req: Request, { params }: { params: { id: string } })
|
|||
|
||||
const amountFormatted = new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR" }).format(invoice.amount_ttc || 0);
|
||||
const customMessage = buildInvoiceCustomMessage(invoice);
|
||||
|
||||
// Pour les factures studio, utiliser le lien de téléchargement direct
|
||||
// Pour les factures paie, utiliser l'Espace Paie
|
||||
const isStudioInvoice = invoice.invoice_type === 'studio_renouvellement' || invoice.invoice_type === 'studio_site_web';
|
||||
const ctaUrl = isStudioInvoice
|
||||
? `${process.env.NEXT_PUBLIC_SITE_URL}/api/public/invoices/${invoice.id}/download`
|
||||
: `${process.env.NEXT_PUBLIC_SITE_URL}/facturation`;
|
||||
|
||||
await sendUniversalInvoiceEmail(
|
||||
organizationDetails.email_notifs,
|
||||
organizationDetails.email_notifs_cc,
|
||||
|
|
@ -232,7 +240,8 @@ export async function POST(req: Request, { params }: { params: { id: string } })
|
|||
customMessage,
|
||||
invoiceDate: invoice.invoice_date ? new Date(invoice.invoice_date).toLocaleDateString('fr-FR') : undefined,
|
||||
sepaDate: invoice.sepa_day ? new Date(invoice.sepa_day).toLocaleDateString('fr-FR') : undefined,
|
||||
ctaUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/facturation`,
|
||||
ctaUrl,
|
||||
invoiceType: invoice.invoice_type,
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -681,7 +681,7 @@ const EMAIL_TEMPLATES_V2: Record<EmailTypeV2, EmailTemplateV2> = {
|
|||
greeting: '{{#if firstName}}Bonjour {{firstName}},{{/if}}',
|
||||
mainMessage: '{{{customMessage}}}',
|
||||
closingMessage: 'Si vous avez des questions concernant cette facture, <a href="mailto:paie@odentas.fr" style="color:#0B5FFF; text-decoration:none;">contactez-nous à paie@odentas.fr</a>.<br><br>L\'équipe Odentas vous remercie pour votre confiance.',
|
||||
ctaText: 'Voir la facture',
|
||||
ctaText: 'Télécharger la facture', // Sera "Voir la facture" pour paie via le template
|
||||
footerText: 'Vous recevez cet e-mail car vous êtes client de Odentas, pour vous notifier d\'une action sur votre compte.',
|
||||
preheaderText: 'Nouvelle facture · Accédez à votre espace',
|
||||
colors: {
|
||||
|
|
@ -1376,18 +1376,31 @@ export async function renderUniversalEmailV2(config: EmailConfigV2): Promise<{ s
|
|||
greeting: templateConfig.greeting ? replaceVariables(templateConfig.greeting, data) : undefined,
|
||||
mainMessage: processMainMessage(templateConfig.mainMessage, data),
|
||||
closingMessage: templateConfig.closingMessage ? replaceVariables(templateConfig.closingMessage, data) : undefined,
|
||||
ctaText: templateConfig.ctaText ? replaceVariables(templateConfig.ctaText, data) : undefined,
|
||||
// Pour les factures studio, adapter le texte du bouton
|
||||
ctaText: (config.type === 'invoice' && (data.invoiceType === 'studio_renouvellement' || data.invoiceType === 'studio_site_web'))
|
||||
? 'Télécharger la facture'
|
||||
: (config.type === 'invoice'
|
||||
? 'Voir la facture'
|
||||
: (templateConfig.ctaText ? replaceVariables(templateConfig.ctaText, data) : undefined)),
|
||||
ctaSubtext: templateConfig.ctaSubtext ? replaceVariables(templateConfig.ctaSubtext, data) : undefined,
|
||||
footerText: replaceVariables(templateConfig.footerText, data),
|
||||
preheaderText: replaceVariables(templateConfig.preheaderText, data),
|
||||
textFallback: `${replaceVariables(templateConfig.title, data)} - ${replaceVariables(templateConfig.mainMessage, data)}`,
|
||||
ctaUrl: templateConfig.ctaUrl || (config.type === 'invoice' ? 'https://paie.odentas.fr/facturation' : data.ctaUrl),
|
||||
// ctaUrl vient des data pour les factures (défini dans launch/route.ts)
|
||||
ctaUrl: templateConfig.ctaUrl || data.ctaUrl,
|
||||
logoUrl: 'https://newstaging.odentas.fr/wp-content/uploads/2025/08/Odentas-Logo-Bleu-Fond-Transparent-4-1.png',
|
||||
showInfoCard: !!templateConfig.infoCard,
|
||||
// Pour les factures studio, filtrer le code employeur de l'infoCard
|
||||
infoCardRows: templateConfig.infoCard?.map(field => ({
|
||||
label: field.label,
|
||||
value: data[field.key] ?? '—'
|
||||
})),
|
||||
})).filter(row => {
|
||||
// Exclure le code employeur pour les factures studio
|
||||
if (config.type === 'invoice' && (data.invoiceType === 'studio_renouvellement' || data.invoiceType === 'studio_site_web')) {
|
||||
return row.label !== 'Votre code employeur';
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
showDetailsCard: !!templateConfig.detailsCard,
|
||||
detailsCardTitle: templateConfig.detailsCard?.title,
|
||||
detailsCardRows: templateConfig.detailsCard?.rows.map(field => ({
|
||||
|
|
|
|||
Loading…
Reference in a new issue