Reputation: 1
I have a problem. I created a website that allows restaurants to sell their products. I am setting up payments using Stripe, but I am encountering an issue. When the payment is successful, it correctly captures the success, but the entire webhook code that is supposed to mark the order as paid does not execute, and I don't understand why.
Here are the captured events:
Here is the code:
import { createClient } from "npm:@supabase/[email protected]";
import Stripe from "https://esm.sh/stripe@14?target=denonext";
export const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
};
export default async function handler(req: Request) {
if (req.method !== 'POST') {
return new Response(JSON.stringify({ error: "Method not allowed" }), { status: 405, headers: corsHeaders });
}
return new Response(JSON.stringify({ success: true }), { status: 200, headers: corsHeaders });
}
// Configuration Supabase
const supabaseUrl = "https://hf...";
const supabaseServiceKey = "eyJhbGci...";
const resendApiKey = "re_TMQ...";
// Configuration Stripe
const stripeSecretKey = Deno.env.get("STRIPE_SECRET_KEY") || "sk_test_5...";
const endpointSecret = Deno.env.get("STRIPE_WEBHOOK_SECRET") || "whsec_wFt...";
const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey);
const stripe = new Stripe(stripeSecretKey, {
apiVersion: '2024-11-20'
});
const cryptoProvider = stripe.createSubtleCryptoProvider();
// Fonction d'envoi de notification
async function sendNewOrderNotification(expoPushToken, orderNumber, type, amount_total) {
const orderType = type === 'DELIVERY' ? 'Livraison' : 'À emporter';
const message = {
to: expoPushToken,
sound: "default",
title: "Nouvelle commande !",
body: `${orderNumber} - ${orderType} - ${amount_total.toFixed(2)}€`,
data: {
type: "NEW_ORDER",
orderNumber,
orderType,
amount: amount_total
}
};
try {
const response = await fetch("https://exp.host/--/api/v2/push/send", {
method: "POST",
headers: {
"host": "exp.host",
"accept": "application/json",
"accept-encoding": "gzip, deflate",
"content-type": "application/json",
},
body: JSON.stringify(message),
});
const data = await response.json();
console.log("Notification envoyée:", data);
return data;
} catch (error) {
console.error("Erreur d'envoi de notification:", error);
throw error;
}
}
await Deno.permissions.request({ name: "net", host: "hfby..." });
await Deno.permissions.request({ name: "net", host: "api.stripe.com" });
await Deno.permissions.request({ name: "net", host: "exp.host" });
await Deno.permissions.request({ name: "net", host: "api.resend.com" });
async function sendOrderConfirmationEmail(order) {
const { data: customer } = await supabaseAdmin
.from('customers')
.select('first_name, last_name, email')
.eq('id', order.customer_id)
.single();
const { data: restaurant } = await supabaseAdmin
.from('restaurants')
.select('name')
.eq('id', order.restaurant_id)
.single();
if (!customer || !customer.email) {
console.error("Impossible d'envoyer l'email: informations client manquantes");
return;
}
let addressHtml = '';
if (order.type === 'DELIVERY' && order.address_id) {
const { data: address } = await supabaseAdmin
.from('addresses')
.select('street, city, postal_code, country')
.eq('id', order.address_id)
.single();
if (address) {
addressHtml = `
<p><strong>Adresse de livraison :</strong><br>
${address.street}<br>
${address.postal_code} ${address.city}<br>
${address.country}</p>
`;
}
} else {
addressHtml = `
<p><strong>Type de commande :</strong> À emporter</p>
`;
}
const emailSubject = "Confirmation de votre commande";
const emailBody = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Merci pour votre commande !</h1>
<p>Bonjour ${customer.first_name} ${customer.last_name},</p>
<p>Nous vous confirmons que votre commande <strong>${order.order_number}</strong> a bien été reçue et payée avec succès.</p>
<div style="background-color: #f8f8f8; padding: 15px; border-radius: 5px; margin: 20px 0;">
<h2 style="color: #333; margin-top: 0;">Détails de la commande :</h2>
<p><strong>Restaurant :</strong> ${restaurant?.name || 'Restaurant'}</p>
<p><strong>Montant total :</strong> ${order.amount_total.toFixed(2)}€</p>
<p><strong>Statut :</strong> Payée</p>
${addressHtml}
</div>
<p style="color: #666; font-size: 14px; margin-top: 30px;">
L'équipe YumCo vous remercie pour votre confiance !
</p>
</div>
`;
try {
const emailResponse = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
Authorization: `Bearer ${resendApiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "[email protected]",
to: customer.email,
subject: emailSubject,
html: emailBody,
}),
});
if (!emailResponse.ok) {
const errorText = await emailResponse.text();
console.error('Erreur lors de l\'envoi de l\'email:', errorText);
throw new Error(`Erreur Resend: ${errorText}`);
}
const emailResult = await emailResponse.json();
console.log('Email envoyé avec succès:', emailResult);
return emailResult;
} catch (error) {
console.error('Erreur lors de l\'envoi de l\'email:', error);
throw error;
}
}
Deno.serve(async (req) => {
console.log("⭐ Webhook appelé !", new Date().toISOString());
console.log("⭐ URL complète:", req.url);
console.log("⭐ Méthode:", req.method);
console.log("⭐ Headers:", Object.fromEntries(req.headers.entries()));
if (req.method === 'OPTIONS') {
return new Response('ok', {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'Content-Type, Stripe-Signature',
}
});
}
if (req.method !== 'POST') {
return new Response('Méthode non autorisée', { status: 405 });
}
const signature = req.headers.get('stripe-signature');
if (!signature) {
return new Response('Signature Stripe manquante', { status: 400 });
}
try {
const payload = await req.text();
console.log("Payload reçu de taille:", payload.length);
let event;
try {
console.log("Tentative de vérification de signature...");
event = await stripe.webhooks.constructEventAsync(
payload,
signature,
endpointSecret,
undefined,
cryptoProvider
);
console.log("Signature vérifiée avec succès, événement:", event.type);
} catch (err) {
console.error(`⚠️ Erreur de signature du webhook: ${err.message}`);
console.log("Début du payload:", payload.substring(0, 100) + "...");
// Décommentez les lignes suivantes pour les tests uniquement
/*
try {
event = JSON.parse(payload);
console.log("⚠️ AVERTISSEMENT: Traitement de l'événement sans vérification de signature:", event.type);
} catch (parseErr) {
console.error("Erreur de parsing du payload:", parseErr);
return new Response(`Erreur de parsing: ${parseErr.message}`, { status: 400 });
}
*/
return new Response(`Erreur de signature du webhook: ${err.message}`, { status: 401 });
}
switch (event.type) {
case 'checkout.session.completed': {
const session = event.data.object;
console.log('Checkout session completed:', session.id);
const { data: order, error: orderError } = await supabaseAdmin
.from('orders')
.select('*')
.eq('checkout_session', session.id)
.single();
if (orderError) {
console.error('Erreur lors de la récupération de la commande:', orderError);
break;
}
if (!order) {
console.error('Commande non trouvée pour la session:', session.id);
if (session.client_reference_id) {
console.log('Tentative de récupération par order_number:', session.client_reference_id);
const { data: orderByRef, error: refError } = await supabaseAdmin
.from('orders')
.select('*')
.eq('order_number', session.client_reference_id)
.single();
if (refError || !orderByRef) {
console.error('Commande également non trouvée par order_number:', session.client_reference_id);
break;
}
console.log('Commande trouvée par order_number:', orderByRef.id);
const { error: updateError } = await supabaseAdmin
.from('orders')
.update({
status: 'PENDING',
payment_status: 'PAID',
checkout_session: session.id,
payment_intent: session.payment_intent,
updated_at: new Date().toISOString()
})
.eq('id', orderByRef.id);
if (updateError) {
console.error('Erreur lors de la mise à jour de la commande:', updateError);
break;
}
const { error: historyError } = await supabaseAdmin
.from('order_status_history')
.insert({
order_id: orderByRef.id,
status: 'PENDING',
comment: 'Paiement confirmé via Stripe'
});
if (historyError) {
console.error('Erreur lors de la mise à jour de l\'historique:', historyError);
}
try {
const { data: restaurantUsers } = await supabaseAdmin
.from('roles')
.select(`
owner_id,
owner:owner_id (
id,
expo_push_token
)
`)
.eq('restaurant_id', orderByRef.restaurant_id);
if (restaurantUsers) {
for (const user of restaurantUsers) {
if (user.owner?.expo_push_token) {
try {
await sendNewOrderNotification(
user.owner.expo_push_token,
orderByRef.order_number,
orderByRef.type,
orderByRef.amount_total
);
console.log("Notification envoyée à l'utilisateur:", user.owner.id);
} catch (notifError) {
console.error(
"Erreur lors de l'envoi de la notification à l'utilisateur:",
user.owner.id,
notifError
);
}
}
}
}
try {
await sendOrderConfirmationEmail(orderByRef);
} catch (emailError) {
console.error("Erreur lors de l'envoi de l'email de confirmation:", emailError);
}
} catch (notificationError) {
console.error("Erreur lors de l'envoi des notifications:", notificationError);
}
break;
}
break;
}
const { error: updateError } = await supabaseAdmin
.from('orders')
.update({
status: 'PENDING',
payment_status: 'PAID',
updated_at: new Date().toISOString()
})
.eq('id', order.id);
if (updateError) {
console.error('Erreur lors de la mise à jour de la commande:', updateError);
break;
}
const { error: historyError } = await supabaseAdmin
.from('order_status_history')
.insert({
order_id: order.id,
status: 'PENDING',
comment: 'Paiement confirmé via Stripe'
});
if (historyError) {
console.error('Erreur lors de la mise à jour de l\'historique:', historyError);
}
try {
const { data: restaurantUsers } = await supabaseAdmin
.from('roles')
.select(`
owner_id,
owner:owner_id (
id,
expo_push_token
)
`)
.eq('restaurant_id', order.restaurant_id);
if (restaurantUsers) {
for (const user of restaurantUsers) {
if (user.owner?.expo_push_token) {
try {
await sendNewOrderNotification(
user.owner.expo_push_token,
order.order_number,
order.type,
order.amount_total
);
console.log("Notification envoyée à l'utilisateur:", user.owner.id);
} catch (notifError) {
console.error(
"Erreur lors de l'envoi de la notification à l'utilisateur:",
user.owner.id,
notifError
);
}
}
}
}
try {
await sendOrderConfirmationEmail(order);
} catch (emailError) {
console.error("Erreur lors de l'envoi de l'email de confirmation:", emailError);
}
} catch (notificationError) {
console.error("Erreur lors de l'envoi des notifications:", notificationError);
}
break;
}
case 'payment_intent.succeeded': {
const paymentIntent = event.data.object;
console.log('PaymentIntent was successful:', paymentIntent.id);
break;
}
case 'payment_intent.payment_failed': {
const paymentIntent = event.data.object;
console.log('Payment failed:', paymentIntent.id);
const { data: order, error: orderError } = await supabaseAdmin
.from('orders')
.select('*')
.eq('payment_intent', paymentIntent.id)
.single();
if (orderError || !order) {
console.error('Erreur ou commande non trouvée pour le paiement échoué:', paymentIntent.id);
break;
}
const { error: updateError } = await supabaseAdmin
.from('orders')
.update({
payment_status: 'FAILED',
updated_at: new Date().toISOString()
})
.eq('id', order.id);
if (updateError) {
console.error('Erreur lors de la mise à jour du statut de paiement:', updateError);
}
const { error: historyError } = await supabaseAdmin
.from('order_status_history')
.insert({
order_id: order.id,
status: 'PAYMENT_FAILED',
comment: 'Paiement échoué via Stripe'
});
if (historyError) {
console.error('Erreur lors de la mise à jour de l\'historique:', historyError);
}
break;
}
case 'charge.refunded': {
const charge = event.data.object;
console.log('Charge was refunded:', charge.id);
const paymentIntentId = charge.payment_intent;
if (paymentIntentId) {
const { data: order, error: orderError } = await supabaseAdmin
.from('orders')
.select('*')
.eq('payment_intent', paymentIntentId)
.single();
if (orderError || !order) {
console.error('Erreur ou commande non trouvée pour le remboursement:', paymentIntentId);
break;
}
const { error: updateError } = await supabaseAdmin
.from('orders')
.update({
payment_status: 'REFUNDED',
status: 'CANCELED',
updated_at: new Date().toISOString()
})
.eq('id', order.id);
if (updateError) {
console.error('Erreur lors de la mise à jour pour le remboursement:', updateError);
}
const { error: historyError } = await supabaseAdmin
.from('order_status_history')
.insert({
order_id: order.id,
status: 'CANCELED',
comment: 'Commande remboursée via Stripe'
});
if (historyError) {
console.error('Erreur lors de la mise à jour de l\'historique:', historyError);
}
}
break;
}
default:
console.log(`Événement non géré: ${event.type}`);
}
return new Response(JSON.stringify({ received: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
} catch (error) {
console.error(`⚠️ Erreur lors du traitement du webhook: ${error.message}`);
return new Response(
JSON.stringify({ error: error.message || 'Erreur lors du traitement du webhook' }),
{
status: 400,
headers: { 'Content-Type': 'application/json' }
}
);
}
});
On Stripe, my webhook has an endpoint identical to the URL of my webhook function hosted on Supabase, and I am using the correct secret key for my webhook.
Upvotes: 0
Views: 19