Compare commits
15 Commits
4293fa76af
...
1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b130ce07b | ||
|
|
b170d54b0e | ||
|
|
4157880669 | ||
|
|
0a8e61948b | ||
|
|
e06c43542a | ||
|
|
ed4b4f6e5a | ||
|
|
2df974371c | ||
|
|
31bdef2407 | ||
|
|
f0109b703a | ||
|
|
50921780a9 | ||
|
|
7487efd9dc | ||
|
|
460ac07f0f | ||
|
|
fd3dcfee51 | ||
|
|
d4583f5e50 | ||
|
|
2dda7c334a |
119
TROUBLESHOOTING_API.md
Normal file
119
TROUBLESHOOTING_API.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Résolution des problèmes d'accès à l'API
|
||||||
|
|
||||||
|
## Problème : `net::ERR_NAME_NOT_RESOLVED`
|
||||||
|
|
||||||
|
Si vous rencontrez l'erreur `net::ERR_NAME_NOT_RESOLVED` lors de l'accès à l'API, cela signifie que le navigateur ne peut pas résoudre le nom de domaine `api.ditesleenchanson.fr`.
|
||||||
|
|
||||||
|
### Causes possibles
|
||||||
|
|
||||||
|
1. **Le domaine n'existe pas encore**
|
||||||
|
- Le sous-domaine `api.ditesleenchanson.fr` n'a pas été créé dans votre gestionnaire DNS
|
||||||
|
|
||||||
|
2. **Le domaine n'est pas configuré correctement**
|
||||||
|
- Les enregistrements DNS ne pointent pas vers le bon serveur
|
||||||
|
- Le DNS n'a pas encore propagé les changements (peut prendre jusqu'à 48h)
|
||||||
|
|
||||||
|
3. **Problème de configuration locale**
|
||||||
|
- La variable d'environnement `VITE_API_URL` pointe vers un domaine inaccessible
|
||||||
|
- Vous êtes en développement local mais l'URL pointe vers la production
|
||||||
|
|
||||||
|
### Solutions
|
||||||
|
|
||||||
|
#### 1. Vérifier la configuration de l'URL de l'API
|
||||||
|
|
||||||
|
Vérifiez le fichier `.env` dans `site/src/.env` :
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Pour le développement local
|
||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# Pour la production (uniquement si le domaine est configuré)
|
||||||
|
VITE_API_URL=https://api.ditesleenchanson.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Vérifier que l'API est accessible
|
||||||
|
|
||||||
|
**En développement local :**
|
||||||
|
- Assurez-vous que l'API Laravel est démarrée sur `http://localhost:8000`
|
||||||
|
- Testez l'accès à `http://localhost:8000/health` dans votre navigateur
|
||||||
|
|
||||||
|
**En production :**
|
||||||
|
- Vérifiez que le domaine `api.ditesleenchanson.fr` existe et pointe vers votre serveur
|
||||||
|
- Testez l'accès à `https://api.ditesleenchanson.fr/health` dans votre navigateur
|
||||||
|
- Vérifiez les enregistrements DNS de votre domaine
|
||||||
|
|
||||||
|
#### 3. Configuration DNS
|
||||||
|
|
||||||
|
Pour que `api.ditesleenchanson.fr` fonctionne, vous devez :
|
||||||
|
|
||||||
|
1. **Créer un enregistrement DNS de type A ou CNAME**
|
||||||
|
- Type A : pointe directement vers l'IP de votre serveur
|
||||||
|
- Type CNAME : pointe vers un autre domaine (ex: `ditesleenchanson.fr`)
|
||||||
|
|
||||||
|
2. **Configurer le serveur web** (Nginx/Apache)
|
||||||
|
- Créer un virtual host pour `api.ditesleenchanson.fr`
|
||||||
|
- Configurer le SSL/TLS (certificat Let's Encrypt recommandé)
|
||||||
|
|
||||||
|
3. **Vérifier la configuration Laravel**
|
||||||
|
- Dans `api/src/.env`, définir `APP_URL=https://api.ditesleenchanson.fr`
|
||||||
|
- Configurer `FRONTEND_URL` pour autoriser les requêtes depuis le frontend
|
||||||
|
|
||||||
|
#### 4. Test de connectivité
|
||||||
|
|
||||||
|
Pour tester si le domaine est accessible :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Test DNS
|
||||||
|
nslookup api.ditesleenchanson.fr
|
||||||
|
|
||||||
|
# Test HTTP
|
||||||
|
curl https://api.ditesleenchanson.fr/health
|
||||||
|
|
||||||
|
# Test depuis le navigateur
|
||||||
|
# Ouvrir : https://api.ditesleenchanson.fr/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration recommandée
|
||||||
|
|
||||||
|
#### Développement local
|
||||||
|
|
||||||
|
**Fichier `site/src/.env` :**
|
||||||
|
```env
|
||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
**Démarrer l'API Laravel :**
|
||||||
|
```bash
|
||||||
|
cd api/src
|
||||||
|
php artisan serve
|
||||||
|
# L'API sera accessible sur http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Production
|
||||||
|
|
||||||
|
**Fichier `site/src/.env` :**
|
||||||
|
```env
|
||||||
|
VITE_API_URL=https://api.ditesleenchanson.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fichier `api/src/.env` :**
|
||||||
|
```env
|
||||||
|
APP_URL=https://api.ditesleenchanson.fr
|
||||||
|
FRONTEND_URL=https://dites-le-en-chanson.fr
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification après déploiement
|
||||||
|
|
||||||
|
1. Vérifier que l'API répond : `https://api.ditesleenchanson.fr/health`
|
||||||
|
2. Vérifier les logs de l'API pour les erreurs CORS
|
||||||
|
3. Vérifier la console du navigateur pour les erreurs de connexion
|
||||||
|
4. Tester le processus de paiement complet
|
||||||
|
|
||||||
|
### Support
|
||||||
|
|
||||||
|
Si le problème persiste après avoir vérifié ces points :
|
||||||
|
1. Vérifiez les logs de l'API Laravel (`api/src/storage/logs/laravel.log`)
|
||||||
|
2. Vérifiez les logs du serveur web (Nginx/Apache)
|
||||||
|
3. Vérifiez la configuration DNS avec votre hébergeur
|
||||||
|
4. Contactez le support technique
|
||||||
|
|
||||||
6
src/.env
6
src/.env
@@ -1,4 +1,2 @@
|
|||||||
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_51RPSGmEPL3QASpovp8Q6p8ehNMW7TzSrOaV6zvPE1OtflMFN5jChQBEj5kr84wontlLOe8uiHyJBiCduzxIZwj5A00DIEVs31n
|
VITE_API_URL=https://api.dites-le-en-chanson.fr
|
||||||
# VITE_STRIPE_PUBLISHABLE_KEY=pk_test_51RPSH1ERAUBjYKpgbz4GjZjDtI24rqfBky5SO6AwdBfZaqNmFN0zQSxx0Z1wfFKtKXIZXfx5IOQSt2ularULIsto00frDMNi03
|
#VITE_API_URL=http://127.0.0.1:8000
|
||||||
|
|
||||||
VITE_API_URL=https://api.ditesleenchanson.fr
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "web-app",
|
"name": "web-app",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.0.0",
|
"version": "1.2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --host :: --port 3000",
|
"dev": "vite --host :: --port 3000",
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ShoppingBag, Edit2, Music, Send } from 'lucide-react';
|
import { ShoppingBag, Edit2, Music, Send } from 'lucide-react';
|
||||||
export const STRIPE_PUBLISHABLE_KEY = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY;
|
|
||||||
|
|
||||||
export const products = [
|
export const products = [
|
||||||
{
|
{
|
||||||
id: 'prod_SQK3M9UvfXBUh0',
|
id: 'prod_SQK3M9UvfXBUh0',
|
||||||
@@ -38,14 +36,14 @@ import React from 'react';
|
|||||||
stripePriceId: 'price_1RVTXtEPL3QASpovaWjXf30q',
|
stripePriceId: 'price_1RVTXtEPL3QASpovaWjXf30q',
|
||||||
description: 'Un texte de chanson unique, écrit sur mesure pour vous.',
|
description: 'Un texte de chanson unique, écrit sur mesure pour vous.',
|
||||||
imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg'
|
imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg'
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 'prod_SQMCFqFLaNHxx0',
|
// id: 'prod_SQMCFqFLaNHxx0',
|
||||||
name: 'Test',
|
// name: 'Test',
|
||||||
price: 17.90,
|
// price: 17.90,
|
||||||
promotionPrice: null,
|
// promotionPrice: null,
|
||||||
stripePriceId: 'price_1RVVUBERAUBjYKpgYuQyuI76',
|
// stripePriceId: 'price_1RVVUBERAUBjYKpgYuQyuI76',
|
||||||
description: 'Un texte de chanson unique, écrit sur mesure pour vous.',
|
// description: 'Un texte de chanson unique, écrit sur mesure pour vous.',
|
||||||
// imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg'
|
// imageUrl: 'https://files.dites-le-en-chanson.fr/products/texte-personalise.jpg'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -87,13 +87,14 @@ export const api = {
|
|||||||
/**
|
/**
|
||||||
* Confirme une commande et envoie les emails
|
* Confirme une commande et envoie les emails
|
||||||
*/
|
*/
|
||||||
async confirmOrder(orderData, sessionId) {
|
async confirmOrder(orderData, sessionId, orderId = null) {
|
||||||
try {
|
try {
|
||||||
const data = await apiRequest('/orders/confirm', {
|
const data = await apiRequest('/orders/confirm', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: {
|
body: {
|
||||||
orderData,
|
orderData,
|
||||||
sessionId,
|
sessionId,
|
||||||
|
orderId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { data, error: null };
|
return { data, error: null };
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
const [orderDetails, setOrderDetails] = useState(null);
|
const [orderDetails, setOrderDetails] = useState(null);
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const sessionId = searchParams.get('session_id');
|
const sessionId = searchParams.get('session_id');
|
||||||
|
const orderId = searchParams.get('order_id');
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [mainStatus, setMainStatus] = useState({ type: 'info', message: 'Traitement en cours...' });
|
const [mainStatus, setMainStatus] = useState({ type: 'info', message: 'Traitement en cours...' });
|
||||||
@@ -77,7 +78,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
sessionStorage.setItem(processedKey, 'true');
|
sessionStorage.setItem(processedKey, 'true');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: functionResponse, error: functionError } = await api.orders.confirmOrder(orderDataForDB, sessionId);
|
const { data: functionResponse, error: functionError } = await api.orders.confirmOrder(orderDataForDB, sessionId, orderId);
|
||||||
|
|
||||||
if (functionError) {
|
if (functionError) {
|
||||||
console.error("API error:", functionError);
|
console.error("API error:", functionError);
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ const OrderPage = () => {
|
|||||||
orderData: orderDataForDB,
|
orderData: orderDataForDB,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
customerEmail: formData.email,
|
customerEmail: formData.email,
|
||||||
successUrl: `${window.location.origin}/confirmation?session_id={CHECKOUT_SESSION_ID}`,
|
successUrl: `${window.location.origin}/confirmation`,
|
||||||
cancelUrl: `${window.location.origin}/commander`,
|
cancelUrl: `${window.location.origin}/commander`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,163 +0,0 @@
|
|||||||
/* 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,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
/* 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 <contact@dites-le-en-chanson.fr>",
|
|
||||||
to: [customerEmail],
|
|
||||||
subject: `Votre commande Dites le en chanson est confirmée ! (ID: ${order?.id?.slice(0, 8) ?? ""})`,
|
|
||||||
html: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Confirmation de votre commande - Dites le en chanson</title>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
body { font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin:0; padding:0; background:#f4f4f9; color:#333; }
|
|
||||||
.container { max-width:600px; margin:20px auto; background:#fff; border-radius:8px; box-shadow:0 4px 15px rgba(0,0,0,.1); overflow:hidden; }
|
|
||||||
.header { background:linear-gradient(135deg,#6B46C1,#9F7AEA); color:#fff; padding:30px 20px; text-align:center; }
|
|
||||||
.header img { max-width:100px; margin-bottom:15px; }
|
|
||||||
.header h1 { margin:0; font-size:28px; font-weight:600; }
|
|
||||||
.content { padding:30px; line-height:1.6; }
|
|
||||||
.content h2 { color:#6B46C1; font-size:22px; margin-top:0; }
|
|
||||||
.content p { margin-bottom:15px; }
|
|
||||||
.order-details { background:#f9fafb; padding:20px; border-radius:6px; margin-bottom:20px; border:1px solid #e5e7eb; }
|
|
||||||
.order-details p { margin:5px 0; }
|
|
||||||
.order-details strong { color:#555; }
|
|
||||||
.delivery-info { background:#eef2ff; padding:15px; border-radius:6px; text-align:center; margin-bottom:25px; border-left:4px solid #6366f1; }
|
|
||||||
.delivery-info p { margin:0; color:#4338ca; }
|
|
||||||
.button { display:inline-block; background:#6B46C1; color:#fff; padding:12px 25px; text-decoration:none; border-radius:5px; font-size:1em; font-weight:500; }
|
|
||||||
.button:hover { background:#553C9A; }
|
|
||||||
.footer { text-align:center; padding:20px; font-size:.9em; color:#777; background:#f1f1f1; }
|
|
||||||
.footer a { color:#6B46C1; text-decoration:none; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<img src="https://storage.googleapis.com/hostinger-horizons-assets-prod/72f15596-7338-40f3-8565-8548388d2677/4ac040560780878558644b6783d4f976.png" alt="Dites-le en chanson Logo" />
|
|
||||||
<h1>Merci pour votre commande !</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<h2>Bonjour ${orderData?.recipient_name ?? "cher client"},</h2>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
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 !
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="order-details">
|
|
||||||
<h3>Récapitulatif de votre commande (ID: ${order?.id?.slice(0,8) ?? ""}) :</h3>
|
|
||||||
|
|
||||||
<p><strong>Produit:</strong> ${orderData?.product_name ?? "Chanson personnalisée"}</p>
|
|
||||||
<p><strong>Prix Payé:</strong> ${orderData?.price ? orderData.price + " €" : "—"}</p>
|
|
||||||
<p><strong>Pour:</strong> ${orderData?.recipient_name ?? "—"}</p>
|
|
||||||
<p><strong>Langue:</strong> ${orderData?.language ?? "—"}</p>
|
|
||||||
<p><strong>Voix:</strong> ${orderData?.voice_gender ?? "—"}</p>
|
|
||||||
<p><strong>Style:</strong> ${orderData?.musical_style ?? "—"}</p>
|
|
||||||
<p><strong>Ambiance:</strong> ${orderData?.mood ?? "—"}</p>
|
|
||||||
|
|
||||||
${orderData?.anecdote1 ? `<p><strong>Anecdote 1:</strong> ${orderData.anecdote1}</p>` : ""}
|
|
||||||
${orderData?.anecdote2 ? `<p><strong>Anecdote 2:</strong> ${orderData.anecdote2}</p>` : ""}
|
|
||||||
${orderData?.anecdote3 ? `<p><strong>Anecdote 3:</strong> ${orderData.anecdote3}</p>` : ""}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="delivery-info">
|
|
||||||
<p><strong>Délai de création :</strong> Votre chanson unique sera prête et vous sera livrée par email dans les <strong>48 heures</strong>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>Nous mettons tout notre cœur pour transformer vos histoires en mélodies inoubliables.</p>
|
|
||||||
<p>Si vous avez la moindre question, n'hésitez pas à nous contacter.</p>
|
|
||||||
|
|
||||||
<p style="text-align:center; margin-top:30px;color: white !important;">
|
|
||||||
<a href="https://dites-le-en-chanson.fr" class="button">Visiter notre site</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>© ${new Date().getFullYear()} <a href="https://dites-le-en-chanson.fr">Dites-le en chanson</a>. Tous droits réservés.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
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 <contact@dites-le-en-chanson.fr>",
|
|
||||||
to: [ADMIN_EMAIL],
|
|
||||||
subject: `Nouvelle commande reçue (ID: ${order?.id?.slice(0,8) ?? "N/A"})`,
|
|
||||||
html: `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="fr">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>Nouvelle Commande Reçue</title>
|
|
||||||
</head>
|
|
||||||
<body style="font-family: Arial, sans-serif; color:#333;">
|
|
||||||
<h1>Nouvelle Commande Reçue !</h1>
|
|
||||||
<p>Une nouvelle commande a été passée sur votre site.</p>
|
|
||||||
|
|
||||||
<p><strong>ID de Commande :</strong> ${order?.id ?? "N/A"}</p>
|
|
||||||
<p><strong>ID de Session Stripe :</strong> ${sessionId ?? "N/A"}</p>
|
|
||||||
<p><strong>Email du Client :</strong> ${customerEmail ?? "N/A"}</p>
|
|
||||||
<p><strong>Statut :</strong> ${finalOrderData.status}</p>
|
|
||||||
|
|
||||||
<h2>Détails de la commande :</h2>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Recipient Name:</strong> ${orderData?.recipient_name ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Song For Whom:</strong> ${orderData?.song_for_whom ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Occasion:</strong> ${orderData?.occasion ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Language:</strong> ${orderData?.language ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Anecdote1:</strong> ${orderData?.anecdote1 ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Anecdote2:</strong> ${orderData?.anecdote2 ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Anecdote3:</strong> ${orderData?.anecdote3 ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Voice Gender:</strong> ${orderData?.voice_gender ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Musical Style:</strong> ${orderData?.musical_style ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Mood:</strong> ${orderData?.mood ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Price:</strong> ${orderData?.price ?? "Non spécifié"}</li>
|
|
||||||
<li><strong>Product Name:</strong> ${orderData?.product_name ?? "Non spécifié"}</li>
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
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" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user