From 4293fa76af532e64d51f0d15bbf280dcbc83bf5e Mon Sep 17 00:00:00 2001 From: balvarez Date: Sat, 3 Jan 2026 21:11:17 +0100 Subject: [PATCH] connexion nouvelle api --- MIGRATION_API.md | 123 ++++++++ src/.env | 6 +- src/package.json | 1 - src/src/config/orderFormConfig.jsx | 16 +- src/src/lib/apiClient.js | 106 +++++++ src/src/lib/supabaseClient.js | 6 - src/src/pages/ConfirmationPage.jsx | 20 +- src/src/pages/OrderPage.jsx | 110 ++----- .../create-checkout-session/index.js | 163 ++++++++++ .../send-order-confirmation-email/index.js | 290 ++++++++++++++++++ 10 files changed, 721 insertions(+), 120 deletions(-) create mode 100644 MIGRATION_API.md create mode 100644 src/src/lib/apiClient.js delete mode 100644 src/src/lib/supabaseClient.js create mode 100644 supabase/functions/create-checkout-session/index.js create mode 100644 supabase/functions/send-order-confirmation-email/index.js diff --git a/MIGRATION_API.md b/MIGRATION_API.md new file mode 100644 index 0000000..09bc9cc --- /dev/null +++ b/MIGRATION_API.md @@ -0,0 +1,123 @@ +# Migration du site vers l'API Laravel + +Ce document décrit la migration du projet `site` de Supabase vers l'API Laravel. + +## Changements effectués + +### 1. Remplacement du client Supabase + +**Fichier supprimé :** +- `src/lib/supabaseClient.js` + +**Fichier créé :** +- `src/lib/apiClient.js` - Client API pour communiquer avec l'API Laravel + +### 2. Adaptation des pages + +#### OrderPage.jsx +- **Avant :** Utilisait `supabase.functions.invoke('create-checkout-session')` +- **Après :** Utilise `api.stripe.createCheckoutSession()` +- Suppression de l'utilisation de `loadStripe` et `redirectToCheckout` car l'API retourne directement l'URL de checkout +- Simplification du code de redirection vers Stripe + +#### ConfirmationPage.jsx +- **Avant :** Utilisait `supabase.functions.invoke('send-order-confirmation-email')` +- **Après :** Utilise `api.orders.confirmOrder()` + +### 3. Dépendances + +**Supprimé :** +- `@supabase/supabase-js` (plus nécessaire) + +**Conservé :** +- `@stripe/stripe-js` (toujours utilisé pour la validation côté client si nécessaire) + +## Configuration + +### Variables d'environnement + +Ajoutez dans votre fichier `.env` : + +```env +VITE_API_URL=http://localhost:8000 +``` + +Pour la production, utilisez : +```env +VITE_API_URL=https://api.ditesleenchanson.fr +``` + +## Structure de l'API Client + +Le client API (`apiClient.js`) expose : + +### `api.stripe.createCheckoutSession(options)` +Crée une session de checkout Stripe. + +**Paramètres :** +- `priceId` (string, requis) - ID du prix Stripe +- `orderData` (object, requis) - Données de la commande +- `successUrl` (string, requis) - URL de succès +- `cancelUrl` (string, requis) - URL d'annulation +- `quantity` (number, optionnel) - Quantité (défaut: 1) +- `customerEmail` (string, optionnel) - Email du client + +**Retour :** +```javascript +{ + data: { + sessionId: string, + url: string // URL de redirection Stripe + }, + error: null | { message: string, status: number } +} +``` + +### `api.orders.confirmOrder(orderData, sessionId)` +Confirme une commande et envoie les emails. + +**Paramètres :** +- `orderData` (object, requis) - Données de la commande +- `sessionId` (string, requis) - ID de session Stripe + +**Retour :** +```javascript +{ + data: { + success: boolean, + orderId: string, + customerEmail: string, + emailSent: boolean, + customerEmailError: string | null, + adminEmailSent: boolean, + adminEmailError: string | null + }, + error: null | { message: string, status: number } +} +``` + +## Différences avec Supabase + +1. **Pas de gestion d'authentification** : Le site n'a pas besoin d'authentification, contrairement au backoffice +2. **URLs directes** : L'API retourne directement l'URL de checkout Stripe, pas besoin de `redirectToCheckout` +3. **Format de réponse** : Les réponses sont structurées différemment mais compatibles avec le code existant + +## Tests + +Pour tester la migration : + +1. Vérifiez que `VITE_API_URL` est correctement configuré +2. Testez le processus de commande complet : + - Sélection d'un produit + - Remplissage du formulaire + - Création de la session Stripe + - Redirection vers Stripe + - Confirmation après paiement + - Réception des emails + +## Notes + +- Le code de redirection vers Stripe a été simplifié car l'API retourne directement l'URL +- Les données de commande sont toujours stockées dans `localStorage` pour la page de confirmation +- La structure des données reste identique pour assurer la compatibilité + diff --git a/src/.env b/src/.env index ba4e8e5..b404e32 100644 --- a/src/.env +++ b/src/.env @@ -1,4 +1,4 @@ -VITE_SUPABASE_URL=https://supabase.abpcode.fr -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzY2Nzc3OTU5LCJleHAiOjE5MjQ0NTc5NTl9.I-qytVb1ef6QMR8IUDePJzESO3bJAnsGE075XQ2xiaI VITE_STRIPE_PUBLISHABLE_KEY=pk_live_51RPSGmEPL3QASpovp8Q6p8ehNMW7TzSrOaV6zvPE1OtflMFN5jChQBEj5kr84wontlLOe8uiHyJBiCduzxIZwj5A00DIEVs31n -VITE_STRIPE_PUBLISHABLE_KEY_DEV=pk_test_51RPSH1ERAUBjYKpgbz4GjZjDtI24rqfBky5SO6AwdBfZaqNmFN0zQSxx0Z1wfFKtKXIZXfx5IOQSt2ularULIsto00frDMNi03 +# VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RPSH1ERAUBjYKpgbz4GjZjDtI24rqfBky5SO6AwdBfZaqNmFN0zQSxx0Z1wfFKtKXIZXfx5IOQSt2ularULIsto00frDMNi03 + +VITE_API_URL=https://api.ditesleenchanson.fr diff --git a/src/package.json b/src/package.json index 4878024..9b8628e 100644 --- a/src/package.json +++ b/src/package.json @@ -24,7 +24,6 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@stripe/stripe-js": "^3.0.0", - "@supabase/supabase-js": "^2.39.8", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "framer-motion": "^10.16.4", diff --git a/src/src/config/orderFormConfig.jsx b/src/src/config/orderFormConfig.jsx index 2539e27..8dfb25d 100644 --- a/src/src/config/orderFormConfig.jsx +++ b/src/src/config/orderFormConfig.jsx @@ -38,14 +38,14 @@ import React from 'react'; stripePriceId: 'price_1RVTXtEPL3QASpovaWjXf30q', description: 'Un texte de chanson unique, écrit sur mesure pour vous.', imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg' - // }, - // { - // id: 'prod_SQMCFqFLaNHxx0', - // name: 'Test', - // price: 17.90, - // promotionPrice: null, - // stripePriceId: 'price_1RVVUBERAUBjYKpgYuQyuI76', - // description: 'Un texte de chanson unique, écrit sur mesure pour vous.', + }, + { + id: 'prod_SQMCFqFLaNHxx0', + name: 'Test', + price: 17.90, + promotionPrice: null, + stripePriceId: 'price_1RVVUBERAUBjYKpgYuQyuI76', + description: 'Un texte de chanson unique, écrit sur mesure pour vous.', // imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg' } ]; diff --git a/src/src/lib/apiClient.js b/src/src/lib/apiClient.js new file mode 100644 index 0000000..ebec23f --- /dev/null +++ b/src/src/lib/apiClient.js @@ -0,0 +1,106 @@ +/** + * Client API pour remplacer Supabase + * Communique avec l'API Laravel + */ + +const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; + +/** + * Effectue une requête HTTP vers l'API + */ +async function apiRequest(endpoint, options = {}) { + const url = `${API_URL}${endpoint}`; + + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + const config = { + ...options, + headers, + }; + + if (options.body && typeof options.body === 'object') { + config.body = JSON.stringify(options.body); + } + + try { + const response = await fetch(url, config); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Erreur inconnue' })); + throw { + message: errorData.error || `Erreur HTTP ${response.status}`, + status: response.status, + data: errorData, + }; + } + + return await response.json(); + } catch (error) { + if (error.status) { + throw error; + } + throw { + message: error.message || 'Erreur de connexion', + status: 0, + }; + } +} + +/** + * API client pour remplacer Supabase + */ +export const api = { + /** + * Stripe - Création de session de checkout + */ + stripe: { + /** + * Crée une session de checkout Stripe + */ + async createCheckoutSession({ priceId, orderData, successUrl, cancelUrl, quantity, customerEmail }) { + try { + const data = await apiRequest('/stripe/create-checkout-session', { + method: 'POST', + body: { + priceId, + orderData, + successUrl, + cancelUrl, + quantity, + customerEmail, + }, + }); + return { data, error: null }; + } catch (error) { + return { data: null, error }; + } + }, + }, + + /** + * Commandes + */ + orders: { + /** + * Confirme une commande et envoie les emails + */ + async confirmOrder(orderData, sessionId) { + try { + const data = await apiRequest('/orders/confirm', { + method: 'POST', + body: { + orderData, + sessionId, + }, + }); + return { data, error: null }; + } catch (error) { + return { data: null, error }; + } + }, + }, +}; + diff --git a/src/src/lib/supabaseClient.js b/src/src/lib/supabaseClient.js deleted file mode 100644 index 733b874..0000000 --- a/src/src/lib/supabaseClient.js +++ /dev/null @@ -1,6 +0,0 @@ -import { createClient } from '@supabase/supabase-js'; - -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; - -export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/src/src/pages/ConfirmationPage.jsx b/src/src/pages/ConfirmationPage.jsx index afbe88d..214072e 100644 --- a/src/src/pages/ConfirmationPage.jsx +++ b/src/src/pages/ConfirmationPage.jsx @@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { CheckCircle, Package, Loader2, AlertTriangle, Info } from 'lucide-react'; import { useToast } from "@/components/ui/use-toast"; - import { supabase } from '@/lib/supabaseClient'; + import { api } from '@/lib/apiClient'; const ConfirmationPage = () => { const [orderDetails, setOrderDetails] = useState(null); @@ -77,30 +77,28 @@ import React, { useEffect, useState } from 'react'; sessionStorage.setItem(processedKey, 'true'); try { - const payload = { orderData: orderDataForDB, sessionId: sessionId }; - const { data: functionResponse, error: functionError } = await supabase.functions.invoke('send-order-confirmation-email', { - body: JSON.stringify(payload), - }); + const { data: functionResponse, error: functionError } = await api.orders.confirmOrder(orderDataForDB, sessionId); if (functionError) { - console.error("Supabase function invocation error:", functionError); - throw new Error(functionError.message || "Erreur d'invocation de la fonction Edge."); + console.error("API error:", functionError); + throw new Error(functionError.message || "Erreur lors de l'appel à l'API."); } if (!functionResponse) { - console.error("No response data from edge function"); - throw new Error("Aucune donnée retournée par la fonction Edge."); + console.error("No response data from API"); + throw new Error("Aucune donnée retournée par l'API."); } if (functionResponse.orderId && functionResponse.orderId !== 'N/A') { + const orderIdShort = functionResponse.orderId.toString().substring(0, 8); setOrderDetails(prev => ({ ...prev, id: functionResponse.orderId, customer_email: functionResponse.customerEmail || prev.customer_email })); toast({ title: "Paiement Réussi!", - description: `Votre commande (ID: ${functionResponse.orderId.substring(0,8)}) est enregistrée et un email de confirmation vous a été envoyé.`, + description: `Votre commande (ID: ${orderIdShort}) est enregistrée et un email de confirmation vous a été envoyé.`, variant: "success", duration: 7000, }); - setMainStatus({ type: 'success', message: `Commande ${functionResponse.orderId.substring(0,8)} confirmée.` }); + setMainStatus({ type: 'success', message: `Commande ${orderIdShort} confirmée.` }); } else { toast({ title: "Problème Enregistrement Commande", diff --git a/src/src/pages/OrderPage.jsx b/src/src/pages/OrderPage.jsx index 053ef75..9ecb994 100644 --- a/src/src/pages/OrderPage.jsx +++ b/src/src/pages/OrderPage.jsx @@ -14,13 +14,9 @@ import { initialFormData, stepsConfig, formValidations, - products, - STRIPE_PUBLISHABLE_KEY + products } from '@/config/orderFormConfig'; -import { loadStripe } from '@stripe/stripe-js'; -import { supabase } from '@/lib/supabaseClient'; - -const stripePromise = loadStripe(STRIPE_PUBLISHABLE_KEY); +import { api } from '@/lib/apiClient'; const OrderPage = () => { const [ currentStep, setCurrentStep ] = useState(0); @@ -136,12 +132,6 @@ const OrderPage = () => { }); try { - const stripe = await stripePromise; - - if (!stripe) { - throw new Error("Stripe n'a pas pu être initialisé. Vérifiez votre clé publique."); - } - const selectedProductDetails = products.find(p => p.id === formData.selectedProduct); if (!selectedProductDetails || !selectedProductDetails.stripePriceId || selectedProductDetails.stripePriceId.includes('YOUR_')) { @@ -184,17 +174,16 @@ const OrderPage = () => { localStorage.setItem('confirmedOrderDataForConfirmationPage', JSON.stringify(finalFormDataForConfirmation)); localStorage.setItem('orderDataForDB', JSON.stringify(orderDataForDB)); - // Créer la session Checkout via une fonction Supabase Edge + // Créer la session Checkout via l'API Laravel console.log("Création de la session Checkout Stripe..."); - const {data: sessionData, error: sessionError} = await supabase.functions.invoke('create-checkout-session', { - body: JSON.stringify({ - priceId: selectedProductDetails.stripePriceId, - quantity: 1, - customerEmail: formData.email, - successUrl: `${window.location.origin}/confirmation?session_id={CHECKOUT_SESSION_ID}`, - cancelUrl: `${window.location.origin}/commander`, - }), + const {data: sessionData, error: sessionError} = await api.stripe.createCheckoutSession({ + priceId: selectedProductDetails.stripePriceId, + orderData: orderDataForDB, + quantity: 1, + customerEmail: formData.email, + successUrl: `${window.location.origin}/confirmation?session_id={CHECKOUT_SESSION_ID}`, + cancelUrl: `${window.location.origin}/commander`, }); if (sessionError) { @@ -211,16 +200,16 @@ const OrderPage = () => { } // Vérifier la structure de la réponse - console.log("Réponse complète de la fonction Edge:", sessionData); + console.log("Réponse complète de l'API:", sessionData); - // La réponse peut être directement sessionId ou dans un objet - const sessionId = sessionData?.sessionId || sessionData?.id || sessionData; + // La réponse contient sessionId et url + const checkoutUrl = sessionData?.url; - if (!sessionId || typeof sessionId !== 'string') { - console.error("Aucune session ID valide retournée:", sessionData); + if (!checkoutUrl) { + console.error("Aucune URL de checkout retournée:", sessionData); toast({ title: "Erreur de paiement", - description: "La session de paiement n'a pas pu être créée. Veuillez réessayer.", + description: "L'URL de paiement n'a pas pu être générée. Veuillez réessayer.", variant: "destructive", }); localStorage.removeItem('confirmedOrderDataForConfirmationPage'); @@ -229,70 +218,9 @@ const OrderPage = () => { return; } - // Rediriger vers Stripe avec le sessionId - console.log("Redirection vers Stripe avec sessionId:", sessionId); - - // Vérifier si on a une URL de checkout dans la réponse - const checkoutUrl = sessionData?.url || sessionData?.checkoutUrl; - - if (checkoutUrl) { - // Si on a l'URL directement, l'utiliser - console.log("Redirection vers l'URL de checkout fournie:", checkoutUrl); - window.location.href = checkoutUrl; - return; - } - - // Sinon, utiliser redirectToCheckout de Stripe.js - try { - // Mémoriser l'URL actuelle pour détecter si la redirection a eu lieu - const currentUrl = window.location.href; - - const {error: redirectError} = await stripe.redirectToCheckout({ - sessionId: sessionId, - }); - - if (redirectError) { - console.error("Erreur Stripe lors de la redirection:", redirectError); - toast({ - title: "Erreur de paiement", - description: redirectError.message || "Une erreur est survenue lors de la redirection vers Stripe.", - variant: "destructive", - }); - localStorage.removeItem('confirmedOrderDataForConfirmationPage'); - localStorage.removeItem('orderDataForDB'); - setIsProcessingPayment(false); - } else { - // La redirection devrait se produire - console.log("Redirection initiée avec succès via redirectToCheckout"); - - // Vérifier après un court délai si la redirection a réellement eu lieu - // (fallback si redirectToCheckout ne fonctionne pas silencieusement) - setTimeout(() => { - if (window.location.href === currentUrl && document.visibilityState === 'visible') { - console.warn("La redirection automatique n'a pas fonctionné, tentative alternative..."); - // Essayer une redirection manuelle via l'API Stripe - // Note: Cette approche nécessite que la fonction Edge retourne l'URL de checkout - // Pour l'instant, on affiche un message à l'utilisateur - toast({ - title: "Redirection en cours...", - description: "Si la redirection ne fonctionne pas, veuillez cliquer sur le lien de paiement.", - variant: "default", - }); - setIsProcessingPayment(false); - } - }, 1500); - } - } catch (redirectErr) { - console.error("Exception lors de la redirection:", redirectErr); - toast({ - title: "Erreur de paiement", - description: redirectErr.message || "Une erreur est survenue lors de la redirection vers Stripe.", - variant: "destructive", - }); - localStorage.removeItem('confirmedOrderDataForConfirmationPage'); - localStorage.removeItem('orderDataForDB'); - setIsProcessingPayment(false); - } + // Rediriger vers Stripe avec l'URL de checkout + console.log("Redirection vers l'URL de checkout:", checkoutUrl); + window.location.href = checkoutUrl; } catch (err) { console.error("Erreur lors de la préparation du paiement:", err); toast({ diff --git a/supabase/functions/create-checkout-session/index.js b/supabase/functions/create-checkout-session/index.js new file mode 100644 index 0000000..6c02b1b --- /dev/null +++ b/supabase/functions/create-checkout-session/index.js @@ -0,0 +1,163 @@ +/* eslint-disable no-undef */ +// Edge Function: create-checkout-session +// Environnement: Deno (Supabase Edge Functions self-host) +// Secrets/vars à définir (ex: volumes/functions/.env ou env docker): +// STRIPE_SECRET_KEY=sk_live_... +// SUPABASE_URL=https://supabase.abcdcode.fr (ou ce que tu utilises) +// SUPABASE_SERVICE_ROLE_KEY=xxxxxxxxxxxxxxxx + +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; +import Stripe from "https://esm.sh/stripe@12.0.0"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; + +const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY") ?? ""; +const SUPABASE_URL = Deno.env.get("SUPABASE_URL") ?? ""; +const SUPABASE_SERVICE_ROLE_KEY = + Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? ""; + +if (!STRIPE_SECRET_KEY) { + console.warn("⚠️ STRIPE_SECRET_KEY is not set – Stripe calls will fail."); +} +if (!SUPABASE_URL || !SUPABASE_SERVICE_ROLE_KEY) { + console.warn( + "⚠️ SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY is not set – database writes may fail.", + ); +} + +const stripe = new Stripe(STRIPE_SECRET_KEY, { + apiVersion: "2022-11-15", + httpClient: Stripe.createFetchHttpClient(), +}); + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type", +}; + +serve(async (req) => { + // Préflight CORS + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + try { + const body = await req.json().catch(() => null); + + if (!body) { + return new Response( + JSON.stringify({ error: "Invalid JSON body" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + const { priceId, orderData, successUrl, cancelUrl } = body; + + if (!priceId) { + return new Response( + JSON.stringify({ error: "priceId is required" }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + if (!successUrl || !cancelUrl) { + return new Response( + JSON.stringify({ + error: "successUrl and cancelUrl are required", + }), + { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + if (!STRIPE_SECRET_KEY) { + return new Response( + JSON.stringify({ + error: "Stripe secret key is not configured on the server", + }), + { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } + + // 1. Client Supabase pour sauver la commande + const supabaseClient = createClient( + SUPABASE_URL, + SUPABASE_SERVICE_ROLE_KEY, + { + global: { + // On forwarde l'Authorization si tu en as besoin côté RLS + headers: { + Authorization: req.headers.get("Authorization") ?? "", + }, + }, + }, + ); + + // 2. Insertion dans la table "orders" + const { data: order, error: orderError } = await supabaseClient + .from("orders") + .insert({ + ...orderData, + status: "pending_payment", + created_at: new Date().toISOString(), + }) + .select() + .single(); + + if (orderError) { + console.error("Error saving order:", orderError); + throw new Error(`Error saving order: ${orderError.message}`); + } + + // 3. Création de la session de paiement Stripe + const session = await stripe.checkout.sessions.create({ + payment_method_types: ["card"], + line_items: [ + { + price: priceId, + quantity: 1, + }, + ], + mode: "payment", + + // ⚠️ Champs Stripe doivent être en snake_case + success_url: `${successUrl}&session_id={CHECKOUT_SESSION_ID}&order_id=${order?.id}`, + cancel_url: cancelUrl, + + metadata: { + order_id: order?.id?.toString(), + product_name: orderData?.productName ?? "", + }, + customer_email: orderData?.email ?? undefined, + }); + + return new Response( + JSON.stringify({ sessionId: session.id, url: session.url }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + status: 200, + }, + ); + } catch (error) { + console.error("create-checkout-session error:", error); + + return new Response( + JSON.stringify({ error: error.message ?? "Unknown error" }), + { + headers: { ...corsHeaders, "Content-Type": "application/json" }, + status: 400, + }, + ); + } +}); diff --git a/supabase/functions/send-order-confirmation-email/index.js b/supabase/functions/send-order-confirmation-email/index.js new file mode 100644 index 0000000..d2dae43 --- /dev/null +++ b/supabase/functions/send-order-confirmation-email/index.js @@ -0,0 +1,290 @@ +/* eslint-disable no-undef */ +// Edge Function: send-order-confirmation-email +// Self-host Supabase (Docker) + +import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; +import { createClient } from "https://esm.sh/@supabase/supabase-js@2"; +import Stripe from "https://esm.sh/stripe@12.0.0"; + +const RESEND_API_KEY = Deno.env.get("RESEND_API_KEY"); +const STRIPE_SECRET_KEY = Deno.env.get("STRIPE_SECRET_KEY") || ""; +const SUPABASE_URL = Deno.env.get("SUPABASE_URL") || ""; +const SUPABASE_SERVICE_ROLE_KEY = + Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") || ""; +const ADMIN_EMAIL = Deno.env.get("ADMIN_EMAIL") || ""; + +const stripe = new Stripe(STRIPE_SECRET_KEY, { + apiVersion: "2022-11-15", + httpClient: Stripe.createFetchHttpClient(), +}); + +const corsHeaders = { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type", +}; + +serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + try { + const { orderData, sessionId } = await req.json(); + + // 1️⃣ Récupérer l'email vérifié depuis la session Stripe + let customerEmail = null; + + if (sessionId && STRIPE_SECRET_KEY) { + try { + const session = await stripe.checkout.sessions.retrieve(sessionId); + + // Priorité : customer_details.email puis customer_email + customerEmail = + session?.customer_details?.email || session?.customer_email; + } catch (stripeError) { + console.error("Error retrieving Stripe session:", stripeError); + // Fallback : email envoyé par le front + customerEmail = orderData?.email; + } + } else { + // Sans sessionId (tests par ex.) + customerEmail = orderData?.email; + } + + if (!customerEmail) { + console.warn("No email found in Stripe session or order data."); + // On continue quand même : la commande sera enregistrée mais sans email + } + + // 2️⃣ Client Supabase (SERVICE ROLE pour pouvoir écrire dans la table) + const supabaseClient = createClient( + SUPABASE_URL, + SUPABASE_SERVICE_ROLE_KEY, + { + global: { + headers: { + Authorization: req.headers.get("Authorization") ?? "", + }, + }, + }, + ); + + // 3️⃣ Enregistrer la commande dans Supabase + const finalOrderData = { + ...orderData, + customer_email: customerEmail, + session_id: sessionId, + status: "En attente de traitement", + created_at: new Date().toISOString(), + }; + + const { data: order, error: orderError } = await supabaseClient + .from("orders") + .insert(finalOrderData) + .select() + .single(); + + if (orderError) { + console.error("Error saving order to Supabase:", orderError); + // on continue quand même pour tenter les mails + } + + // 4️⃣ Email client (joli template violet) + let customerEmailError = null; + let customerEmailResult = null; + + if (RESEND_API_KEY && customerEmail) { + const res = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${RESEND_API_KEY}`, + }, + body: JSON.stringify({ + from: "Dites le en chanson ", + to: [customerEmail], + subject: `Votre commande Dites le en chanson est confirmée ! (ID: ${order?.id?.slice(0, 8) ?? ""})`, + html: ` + + + + + +Confirmation de votre commande - Dites le en chanson + + + + + +
+
+ Dites-le en chanson Logo +

Merci pour votre commande !

+
+ +
+

Bonjour ${orderData?.recipient_name ?? "cher client"},

+ +

+ Nous sommes ravis de vous confirmer que votre commande a bien été enregistrée. + Notre équipe de créateurs est déjà prête à composer votre chanson personnalisée ! +

+ +
+

Récapitulatif de votre commande (ID: ${order?.id?.slice(0,8) ?? ""}) :

+ +

Produit: ${orderData?.product_name ?? "Chanson personnalisée"}

+

Prix Payé: ${orderData?.price ? orderData.price + " €" : "—"}

+

Pour: ${orderData?.recipient_name ?? "—"}

+

Langue: ${orderData?.language ?? "—"}

+

Voix: ${orderData?.voice_gender ?? "—"}

+

Style: ${orderData?.musical_style ?? "—"}

+

Ambiance: ${orderData?.mood ?? "—"}

+ + ${orderData?.anecdote1 ? `

Anecdote 1: ${orderData.anecdote1}

` : ""} + ${orderData?.anecdote2 ? `

Anecdote 2: ${orderData.anecdote2}

` : ""} + ${orderData?.anecdote3 ? `

Anecdote 3: ${orderData.anecdote3}

` : ""} +
+ +
+

Délai de création : Votre chanson unique sera prête et vous sera livrée par email dans les 48 heures.

+
+ +

Nous mettons tout notre cœur pour transformer vos histoires en mélodies inoubliables.

+

Si vous avez la moindre question, n'hésitez pas à nous contacter.

+ +

+ Visiter notre site +

+
+ + +
+ + +`, + }), + }); + + const data = await res.json(); + + if (!res.ok) { + console.error("Resend API Error (Customer):", data); + customerEmailError = data.message || "Unknown Resend Error"; + } else { + customerEmailResult = data; + } + } else { + if (!RESEND_API_KEY) console.error("RESEND_API_KEY is missing"); + if (!customerEmail) console.error("No customer email available to send confirmation."); + } + + // 5️⃣ Email admin "Nouvelle Commande Reçue" + let adminEmailError = null; + let adminEmailResult = null; + + if (RESEND_API_KEY && ADMIN_EMAIL) { + const adminRes = await fetch("https://api.resend.com/emails", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${RESEND_API_KEY}`, + }, + body: JSON.stringify({ + from: "Dites le en chanson ", + to: [ADMIN_EMAIL], + subject: `Nouvelle commande reçue (ID: ${order?.id?.slice(0,8) ?? "N/A"})`, + html: ` + + + + +Nouvelle Commande Reçue + + +

Nouvelle Commande Reçue !

+

Une nouvelle commande a été passée sur votre site.

+ +

ID de Commande : ${order?.id ?? "N/A"}

+

ID de Session Stripe : ${sessionId ?? "N/A"}

+

Email du Client : ${customerEmail ?? "N/A"}

+

Statut : ${finalOrderData.status}

+ +

Détails de la commande :

+
    +
  • Recipient Name: ${orderData?.recipient_name ?? "Non spécifié"}
  • +
  • Song For Whom: ${orderData?.song_for_whom ?? "Non spécifié"}
  • +
  • Occasion: ${orderData?.occasion ?? "Non spécifié"}
  • +
  • Language: ${orderData?.language ?? "Non spécifié"}
  • +
  • Anecdote1: ${orderData?.anecdote1 ?? "Non spécifié"}
  • +
  • Anecdote2: ${orderData?.anecdote2 ?? "Non spécifié"}
  • +
  • Anecdote3: ${orderData?.anecdote3 ?? "Non spécifié"}
  • +
  • Voice Gender: ${orderData?.voice_gender ?? "Non spécifié"}
  • +
  • Musical Style: ${orderData?.musical_style ?? "Non spécifié"}
  • +
  • Mood: ${orderData?.mood ?? "Non spécifié"}
  • +
  • Price: ${orderData?.price ?? "Non spécifié"}
  • +
  • Product Name: ${orderData?.product_name ?? "Non spécifié"}
  • +
+ + + `, + }), + }); + + const adminData = await adminRes.json(); + + if (!adminRes.ok) { + console.error("Resend API Error (Admin):", adminData); + adminEmailError = adminData.message || "Unknown Resend Error"; + } else { + adminEmailResult = adminData; + } + } else { + if (!ADMIN_EMAIL) console.error("ADMIN_EMAIL is missing"); + } + + return new Response( + JSON.stringify({ + success: true, + orderId: order?.id || "N/A", + customerEmail, + emailSent: !!customerEmailResult, + customerEmailError, + adminEmailSent: !!adminEmailResult, + adminEmailError, + dbError: orderError ? orderError.message : null, + }), + { + status: 200, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }, + ); + } catch (error) { + console.error("Function Critical Error:", error); + return new Response(JSON.stringify({ error: error.message }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } +});