Files
alva/supabase/functions/create-checkout/index.ts
T
gpt-engineer-app[bot] b1f5efcaad Changes
Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com>
2026-07-12 13:32:05 +00:00

285 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { z } from "npm:zod@3.23.8";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0";
import { type StripeEnv, createStripeClient } from "../_shared/stripe.ts";
const supabaseAdmin = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
);
async function sendOrderEmail(payload: Record<string, unknown>) {
try {
const res = await fetch(
`${Deno.env.get("SUPABASE_URL")}/functions/v1/send-order-email`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")}`,
},
body: JSON.stringify(payload),
},
);
if (!res.ok) console.error("send-order-email failed:", res.status, await res.text());
} catch (e) {
console.error("send-order-email error:", e);
}
}
async function insertOrder(row: {
method: "card" | "invoice";
b: any;
cleaned: string;
monthlyAmountOre: number;
stripeSessionId?: string | null;
stripeSubscriptionId?: string | null;
stripeCustomerId: string;
}) {
const { b, method, cleaned, monthlyAmountOre } = row;
const orderNumber = `ABO-${Date.now().toString(36).toUpperCase()}`;
const totalKr = monthlyAmountOre / 100;
const { error } = await supabaseAdmin.from("orders").insert({
order_number: orderNumber,
customer_email: b.email,
customer_name: b.company,
total_amount: totalKr,
currency: "SEK",
status: method === "invoice" ? "invoiced" : "pending",
items: [
{
typ: "Kakabonnemang",
betalning: method === "card" ? "Kort (månadsvis)" : "Faktura (månadsvis)",
antal_anställda: b.employees,
kg_per_vecka: b.kg_per_week,
pris_per_månad_exkl_moms: totalKr,
valuta: "SEK",
leverans: {
adress: b.address,
postnummer: cleaned,
stad: b.city,
},
telefon: b.phone,
kommentarer: b.comments,
stripe_session_id: row.stripeSessionId ?? null,
stripe_subscription_id: row.stripeSubscriptionId ?? null,
stripe_customer_id: row.stripeCustomerId,
},
],
});
if (error) console.error("Order insert failed:", error);
await sendOrderEmail({
order_number: orderNumber,
customer_email: b.email,
customer_name: b.company,
kg_per_week: b.kg_per_week,
employees: b.employees,
monthly_amount_sek: totalKr,
method,
address: b.address ?? "",
postal_code: cleaned,
phone: b.phone ?? "",
comments: b.comments ?? "",
});
}
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "authorization, x-client-info, x-supabase-client-platform, x-supabase-api-version, apikey, content-type",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
// TEMP TEST: 1 kr/kg (sätt tillbaka till 32500 efter testet)
const PRICE_PER_KG_ORE = 100; // 1 kr in öre
const WEEKS_PER_MONTH = 4;
const BodySchema = z.object({
method: z.enum(["card", "invoice"]),
employees: z.number().int().min(1).max(500),
kg_per_week: z.number().min(1).max(200),
company: z.string().trim().min(1).max(200),
email: z.string().trim().email().max(255),
phone: z.string().trim().max(30).optional().default(""),
address: z.string().trim().min(1).max(200),
postal_code: z.string().trim().min(1).max(20),
city: z.string().trim().min(1).max(100),
comments: z.string().trim().max(1000).optional().default(""),
return_url: z.string().url(),
environment: z.enum(["sandbox", "live"]),
});
Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });
if (req.method !== "POST") {
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
try {
const raw = await req.json();
const parsed = BodySchema.safeParse(raw);
if (!parsed.success) {
return new Response(
JSON.stringify({ error: "Ogiltiga fält", details: parsed.error.flatten().fieldErrors }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
const b = parsed.data;
// Stockholm postal enforcement
const cleaned = b.postal_code.replace(/\s/g, "");
if (!/^\d{5}$/.test(cleaned) || parseInt(cleaned, 10) < 10000 || parseInt(cleaned, 10) > 19999) {
return new Response(
JSON.stringify({ error: "Vi levererar endast inom Storstockholm (100 00 199 99)." }),
{ status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
const env: StripeEnv = b.environment;
const stripe = createStripeClient(env);
// Monthly amount in öre = kg × 4 veckor × 325 kr
const monthlyAmountOre = Math.round(b.kg_per_week * WEEKS_PER_MONTH * PRICE_PER_KG_ORE);
// Find or create customer
const existing = await stripe.customers.list({ email: b.email, limit: 1 });
const customer =
existing.data[0] ??
(await stripe.customers.create({
email: b.email,
name: b.company,
phone: b.phone || undefined,
address: b.address
? {
line1: b.address,
postal_code: cleaned,
city: b.city || undefined,
country: "SE",
}
: undefined,
metadata: {
company: b.company,
employees: String(b.employees),
kg_per_week: String(b.kg_per_week),
comments: b.comments,
},
}));
if (b.method === "card") {
// Embedded Checkout with dynamic recurring price_data
const session = await stripe.checkout.sessions.create({
mode: "subscription",
ui_mode: "embedded_page" as any,
return_url: b.return_url,
customer: customer.id,
line_items: [
{
quantity: 1,
price_data: {
currency: "sek",
product_data: {
name: "Kakabonnemang",
description: "Månatligt kakabonnemang för företag inom Storstockholm.",
tax_code: "txcd_40060003",
},
recurring: { interval: "month" },
unit_amount: monthlyAmountOre,
} as any,
},
],
automatic_tax: { enabled: true },
customer_update: { address: "auto", name: "auto" },
subscription_data: {
description: `Kakabonnemang ${b.kg_per_week} kg/vecka`,
metadata: {
company: b.company,
customer_email: b.email,
kg_per_week: String(b.kg_per_week),
employees: String(b.employees),
},
},
metadata: {
company: b.company,
customer_email: b.email,
kg_per_week: String(b.kg_per_week),
},
} as any);
await insertOrder({
method: "card",
b,
cleaned,
monthlyAmountOre,
stripeSessionId: session.id,
stripeSubscriptionId: null,
stripeCustomerId: customer.id,
});
return new Response(JSON.stringify({ clientSecret: session.client_secret }), {
status: 200,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}
// Invoice method: create subscription with send_invoice
const subscription = await stripe.subscriptions.create({
customer: customer.id,
collection_method: "send_invoice",
days_until_due: 30,
items: [
{
price_data: {
currency: "sek",
product_data: {
name: "Kakabonnemang",
description: "Månatligt kakabonnemang för företag inom Storstockholm.",
tax_code: "txcd_40060003",
},
recurring: { interval: "month" },
unit_amount: monthlyAmountOre,
tax_behavior: "exclusive",
} as any,
quantity: 1,
},
],
automatic_tax: { enabled: true },
description: `Kakabonnemang ${b.kg_per_week} kg/vecka`,
metadata: {
company: b.company,
customer_email: b.email,
kg_per_week: String(b.kg_per_week),
employees: String(b.employees),
},
} as any);
await insertOrder({
method: "invoice",
b,
cleaned,
monthlyAmountOre,
stripeSessionId: null,
stripeSubscriptionId: subscription.id,
stripeCustomerId: customer.id,
});
return new Response(
JSON.stringify({
invoice: true,
subscription_id: subscription.id,
message: "Faktura skickas till din e-post inom kort.",
}),
{ status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
} catch (err: any) {
console.error("create-checkout error:", err);
return new Response(
JSON.stringify({ error: err?.message ?? "Något gick fel vid checkout" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
});