Skip to content

Commit 3967c85

Browse files
Refactor: Migrate Firebase Functions to Supabase
1 parent e27500b commit 3967c85

File tree

4 files changed

+685
-0
lines changed

4 files changed

+685
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
2+
import Stripe from "https://esm.sh/stripe@14.21.0";
3+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";
4+
5+
const corsHeaders = {
6+
"Access-Control-Allow-Origin": "*",
7+
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
8+
};
9+
10+
serve(async (req) => {
11+
// Handle CORS preflight requests
12+
if (req.method === "OPTIONS") {
13+
return new Response(null, { headers: corsHeaders });
14+
}
15+
16+
try {
17+
console.log('💳 STRIPE PAYMENT METHODS - Début traitement');
18+
19+
// Initialize Supabase client
20+
const supabaseClient = createClient(
21+
Deno.env.get("SUPABASE_URL") ?? "",
22+
Deno.env.get("SUPABASE_ANON_KEY") ?? ""
23+
);
24+
25+
// Get authenticated user
26+
const authHeader = req.headers.get("Authorization")!;
27+
const token = authHeader.replace("Bearer ", "");
28+
const { data } = await supabaseClient.auth.getUser(token);
29+
const user = data.user;
30+
31+
if (!user?.email) {
32+
throw new Error("User not authenticated");
33+
}
34+
35+
console.log('👤 STRIPE PAYMENT METHODS - Utilisateur:', user.email);
36+
37+
// Initialize Stripe
38+
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY") || "", {
39+
apiVersion: "2023-10-16",
40+
});
41+
42+
// Handle different HTTP methods
43+
if (req.method === "GET") {
44+
return await handleGetPaymentMethods(stripe, user.email);
45+
} else if (req.method === "POST") {
46+
const body = await req.json();
47+
return await handlePaymentMethodAction(stripe, body, user.email);
48+
} else if (req.method === "DELETE") {
49+
const body = await req.json();
50+
return await handleDeletePaymentMethod(stripe, body);
51+
}
52+
53+
return new Response(JSON.stringify({ error: "Method not allowed" }), {
54+
status: 405,
55+
headers: { ...corsHeaders, "Content-Type": "application/json" },
56+
});
57+
58+
} catch (error) {
59+
console.error('❌ STRIPE PAYMENT METHODS - Erreur:', error);
60+
return new Response(JSON.stringify({
61+
error: error instanceof Error ? error.message : "Internal server error"
62+
}), {
63+
status: 500,
64+
headers: { ...corsHeaders, "Content-Type": "application/json" },
65+
});
66+
}
67+
});
68+
69+
async function handleGetPaymentMethods(stripe: Stripe, userEmail: string) {
70+
console.log('📋 GET PAYMENT METHODS - Récupération pour:', userEmail);
71+
72+
try {
73+
// Find customer by email
74+
const customers = await stripe.customers.list({
75+
email: userEmail,
76+
limit: 1
77+
});
78+
79+
if (customers.data.length === 0) {
80+
console.log('👤 GET PAYMENT METHODS - Aucun client Stripe trouvé');
81+
return new Response(JSON.stringify({ paymentMethods: [] }), {
82+
headers: { ...corsHeaders, "Content-Type": "application/json" },
83+
status: 200,
84+
});
85+
}
86+
87+
const customer = customers.data[0];
88+
console.log('👤 GET PAYMENT METHODS - Client trouvé:', customer.id);
89+
90+
// Get payment methods for customer
91+
const paymentMethods = await stripe.paymentMethods.list({
92+
customer: customer.id,
93+
type: 'card',
94+
});
95+
96+
console.log('💳 GET PAYMENT METHODS - Méthodes trouvées:', paymentMethods.data.length);
97+
98+
// Format payment methods for frontend
99+
const formattedMethods = paymentMethods.data.map(pm => ({
100+
id: pm.id,
101+
type: pm.type,
102+
card: pm.card ? {
103+
brand: pm.card.brand,
104+
last4: pm.card.last4,
105+
exp_month: pm.card.exp_month,
106+
exp_year: pm.card.exp_year,
107+
} : null,
108+
created: pm.created,
109+
}));
110+
111+
return new Response(JSON.stringify({
112+
paymentMethods: formattedMethods,
113+
customerId: customer.id
114+
}), {
115+
headers: { ...corsHeaders, "Content-Type": "application/json" },
116+
status: 200,
117+
});
118+
119+
} catch (error) {
120+
console.error('❌ GET PAYMENT METHODS - Erreur:', error);
121+
throw error;
122+
}
123+
}
124+
125+
async function handlePaymentMethodAction(stripe: Stripe, body: any, userEmail: string) {
126+
const { action, customerId, paymentMethodId } = body;
127+
128+
console.log('⚙️ PAYMENT METHOD ACTION - Action:', action);
129+
130+
if (action === 'attach') {
131+
// Attach payment method to customer
132+
await stripe.paymentMethods.attach(paymentMethodId, {
133+
customer: customerId,
134+
});
135+
136+
console.log('✅ PAYMENT METHOD ACTION - Méthode attachée');
137+
return new Response(JSON.stringify({ success: true }), {
138+
headers: { ...corsHeaders, "Content-Type": "application/json" },
139+
status: 200,
140+
});
141+
142+
} else if (action === 'set_default') {
143+
// Set as default payment method
144+
await stripe.customers.update(customerId, {
145+
invoice_settings: {
146+
default_payment_method: paymentMethodId,
147+
},
148+
});
149+
150+
console.log('✅ PAYMENT METHOD ACTION - Méthode définie par défaut');
151+
return new Response(JSON.stringify({ success: true }), {
152+
headers: { ...corsHeaders, "Content-Type": "application/json" },
153+
status: 200,
154+
});
155+
}
156+
157+
return new Response(JSON.stringify({ error: "Invalid action" }), {
158+
status: 400,
159+
headers: { ...corsHeaders, "Content-Type": "application/json" },
160+
});
161+
}
162+
163+
async function handleDeletePaymentMethod(stripe: Stripe, body: any) {
164+
const { paymentMethodId } = body;
165+
166+
console.log('🗑️ DELETE PAYMENT METHOD - ID:', paymentMethodId);
167+
168+
if (!paymentMethodId) {
169+
return new Response(JSON.stringify({ error: "Payment method ID required" }), {
170+
status: 400,
171+
headers: { ...corsHeaders, "Content-Type": "application/json" },
172+
});
173+
}
174+
175+
// Detach payment method (this effectively deletes it from the customer)
176+
await stripe.paymentMethods.detach(paymentMethodId);
177+
178+
console.log('✅ DELETE PAYMENT METHOD - Méthode supprimée');
179+
return new Response(JSON.stringify({ success: true }), {
180+
headers: { ...corsHeaders, "Content-Type": "application/json" },
181+
status: 200,
182+
});
183+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
2+
import Stripe from "https://esm.sh/stripe@14.21.0";
3+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";
4+
5+
const corsHeaders = {
6+
"Access-Control-Allow-Origin": "*",
7+
"Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type",
8+
};
9+
10+
serve(async (req) => {
11+
// Handle CORS preflight requests
12+
if (req.method === "OPTIONS") {
13+
return new Response(null, { headers: corsHeaders });
14+
}
15+
16+
if (req.method !== "POST") {
17+
return new Response(JSON.stringify({ error: "Method not allowed" }), {
18+
status: 405,
19+
headers: { ...corsHeaders, "Content-Type": "application/json" },
20+
});
21+
}
22+
23+
try {
24+
console.log('🔧 STRIPE SETUP - Début création Setup Intent');
25+
26+
// Initialize Supabase client
27+
const supabaseClient = createClient(
28+
Deno.env.get("SUPABASE_URL") ?? "",
29+
Deno.env.get("SUPABASE_ANON_KEY") ?? ""
30+
);
31+
32+
// Get authenticated user
33+
const authHeader = req.headers.get("Authorization")!;
34+
const token = authHeader.replace("Bearer ", "");
35+
const { data } = await supabaseClient.auth.getUser(token);
36+
const user = data.user;
37+
38+
if (!user?.email) {
39+
throw new Error("User not authenticated");
40+
}
41+
42+
console.log('👤 STRIPE SETUP - Utilisateur authentifié:', user.email);
43+
44+
// Initialize Stripe
45+
const stripe = new Stripe(Deno.env.get("STRIPE_SECRET_KEY") || "", {
46+
apiVersion: "2023-10-16",
47+
});
48+
49+
const { userEmail, campaignName, campaignId } = await req.json();
50+
51+
if (!userEmail || !campaignName) {
52+
return new Response(JSON.stringify({ error: "Missing required fields" }), {
53+
status: 400,
54+
headers: { ...corsHeaders, "Content-Type": "application/json" },
55+
});
56+
}
57+
58+
// Get or create Stripe customer
59+
let customers = await stripe.customers.list({
60+
email: userEmail,
61+
limit: 1
62+
});
63+
64+
let customer;
65+
if (customers.data.length === 0) {
66+
console.log('👤 STRIPE SETUP - Création nouveau client Stripe');
67+
customer = await stripe.customers.create({
68+
email: userEmail,
69+
metadata: {
70+
campaign: campaignName,
71+
userId: user.id
72+
}
73+
});
74+
} else {
75+
console.log('👤 STRIPE SETUP - Client Stripe existant trouvé');
76+
customer = customers.data[0];
77+
}
78+
79+
// Create setup intent
80+
const setupIntent = await stripe.setupIntents.create({
81+
customer: customer.id,
82+
payment_method_types: ['card'],
83+
usage: 'off_session',
84+
metadata: {
85+
campaign_id: campaignId || '',
86+
user_id: user.id
87+
}
88+
});
89+
90+
console.log('✅ STRIPE SETUP - Setup Intent créé:', setupIntent.id);
91+
92+
// Update campaign with Stripe customer info if campaignId is provided
93+
if (campaignId) {
94+
const supabaseService = createClient(
95+
Deno.env.get("SUPABASE_URL") ?? "",
96+
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") ?? "",
97+
{ auth: { persistSession: false } }
98+
);
99+
100+
await supabaseService
101+
.from('campaigns')
102+
.update({
103+
stripe_customer_id: customer.id,
104+
stripe_setup_intent_id: setupIntent.id,
105+
updated_at: new Date().toISOString()
106+
})
107+
.eq('id', campaignId)
108+
.eq('user_id', user.id);
109+
110+
console.log('📊 STRIPE SETUP - Campagne mise à jour avec infos Stripe');
111+
}
112+
113+
return new Response(JSON.stringify({
114+
clientSecret: setupIntent.client_secret,
115+
customerId: customer.id,
116+
setupIntentId: setupIntent.id
117+
}), {
118+
headers: { ...corsHeaders, "Content-Type": "application/json" },
119+
status: 200,
120+
});
121+
122+
} catch (error) {
123+
console.error('❌ STRIPE SETUP - Erreur:', error);
124+
return new Response(JSON.stringify({
125+
error: error instanceof Error ? error.message : "Internal server error"
126+
}), {
127+
status: 500,
128+
headers: { ...corsHeaders, "Content-Type": "application/json" },
129+
});
130+
}
131+
});

0 commit comments

Comments
 (0)