From 0280b8d0d10e54d8e0cce95954e599d85c4b3a40 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:22:15 +0000 Subject: [PATCH 1/3] Changes Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com> --- .../update-subscription-quantity/index.ts | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 supabase/functions/update-subscription-quantity/index.ts diff --git a/supabase/functions/update-subscription-quantity/index.ts b/supabase/functions/update-subscription-quantity/index.ts new file mode 100644 index 0000000..839e86e --- /dev/null +++ b/supabase/functions/update-subscription-quantity/index.ts @@ -0,0 +1,132 @@ +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0"; +import { z } from "npm:zod@3.23.8"; +import { type StripeEnv, createStripeClient } from "../_shared/stripe.ts"; + +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", +}; + +const PRICE_PER_KG_ORE = 32500; // 325 kr exkl. moms +const WEEKS_PER_MONTH = 4; + +const supabase = createClient( + Deno.env.get("SUPABASE_URL")!, + Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!, +); + +const BodySchema = z.object({ + new_kg_per_week: z.number().min(1).max(200), + 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 token = req.headers.get("Authorization")?.replace("Bearer ", "") ?? ""; + const { data: userData, error: authErr } = await supabase.auth.getUser(token); + if (authErr || !userData?.user) { + return new Response(JSON.stringify({ error: "Ej inloggad" }), { + status: 401, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const parsed = BodySchema.safeParse(await req.json()); + if (!parsed.success) { + return new Response(JSON.stringify({ error: "Ogiltiga fält" }), { + status: 400, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const env: StripeEnv = parsed.data.environment; + const newKg = parsed.data.new_kg_per_week; + + const { data: sub } = await supabase + .from("subscriptions") + .select("id, stripe_subscription_id, kg_per_week") + .eq("user_id", userData.user.id) + .eq("environment", env) + .in("status", ["active", "trialing", "past_due"]) + .order("created_at", { ascending: false }) + .limit(1) + .maybeSingle(); + + if (!sub?.stripe_subscription_id) { + return new Response( + JSON.stringify({ error: "Inget aktivt abonnemang hittades." }), + { status: 404, headers: { ...corsHeaders, "Content-Type": "application/json" } }, + ); + } + + const stripe = createStripeClient(env); + const stripeSub = await stripe.subscriptions.retrieve(sub.stripe_subscription_id); + const item = stripeSub.items?.data?.[0]; + if (!item) { + return new Response(JSON.stringify({ error: "Kunde inte hitta prisrad" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } + + const productId = + typeof item.price.product === "string" ? item.price.product : item.price.product.id; + const newUnitAmount = Math.round(newKg * WEEKS_PER_MONTH * PRICE_PER_KG_ORE); + + await stripe.subscriptions.update(sub.stripe_subscription_id, { + items: [ + { + id: item.id, + price_data: { + currency: "sek", + product: productId, + recurring: { interval: "month" }, + unit_amount: newUnitAmount, + } as any, + quantity: 1, + }, + ], + proration_behavior: "create_prorations", + metadata: { + ...(stripeSub.metadata ?? {}), + kg_per_week: String(newKg), + }, + description: `Kakabonnemang – ${newKg} kg/vecka`, + } as any); + + // Uppdatera vår tabell direkt (webhook uppdaterar också, detta ger snabb UI-återkoppling) + await supabase + .from("subscriptions") + .update({ + kg_per_week: newKg, + monthly_amount: newUnitAmount / 100, + updated_at: new Date().toISOString(), + }) + .eq("id", sub.id); + + return new Response( + JSON.stringify({ + success: true, + new_kg_per_week: newKg, + new_monthly_amount_exkl_moms: newUnitAmount / 100, + }), + { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }, + ); + } catch (e: any) { + console.error("update-subscription-quantity error:", e); + return new Response(JSON.stringify({ error: e?.message ?? "Kunde inte uppdatera abonnemang" }), { + status: 500, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); + } +}); From bc971a1a4d3e8738a390c27738763457ee8ac458 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:22:44 +0000 Subject: [PATCH 2/3] Changes Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com> --- src/App.tsx | 2 + src/pages/ChangeSubscription.tsx | 216 +++++++++++++++++++++++++++++++ supabase/config.toml | 3 + 3 files changed, 221 insertions(+) create mode 100644 src/pages/ChangeSubscription.tsx diff --git a/src/App.tsx b/src/App.tsx index ba9379f..3912121 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,6 +13,7 @@ import Contact from "./pages/Contact"; import Admin from "./pages/Admin"; import FAQ from "./pages/FAQ"; import OrderConfirmation from "./pages/OrderConfirmation"; +import ChangeSubscription from "./pages/ChangeSubscription"; import { PaymentTestModeBanner } from "@/components/PaymentTestModeBanner"; const queryClient = new QueryClient(); @@ -31,6 +32,7 @@ function AppContent() { } /> } /> } /> + } /> {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> diff --git a/src/pages/ChangeSubscription.tsx b/src/pages/ChangeSubscription.tsx new file mode 100644 index 0000000..c1bfd66 --- /dev/null +++ b/src/pages/ChangeSubscription.tsx @@ -0,0 +1,216 @@ +import { useEffect, useMemo, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { supabase } from "@/integrations/supabase/client"; +import { useAuth } from "@/hooks/useAuth"; +import { useToast } from "@/hooks/use-toast"; +import { getStripeEnvironment, hasPaymentsConfigured } from "@/lib/stripe"; +import Header from "@/components/Header"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft, Loader2 } from "lucide-react"; + +const PRICE_PER_KG_EXKL = 325; +const VAT_RATE = 0.25; +const PRICE_PER_KG_INK = PRICE_PER_KG_EXKL * (1 + VAT_RATE); +const WEEKS_PER_MONTH = 4; + +const ChangeSubscription = () => { + const navigate = useNavigate(); + const { user, loading: authLoading } = useAuth(); + const { toast } = useToast(); + const [currentKg, setCurrentKg] = useState(null); + const [newKg, setNewKg] = useState(10); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!authLoading && !user) navigate("/"); + }, [authLoading, user, navigate]); + + useEffect(() => { + (async () => { + if (!user) return; + const env = hasPaymentsConfigured() ? getStripeEnvironment() : "sandbox"; + const { data } = await supabase + .from("subscriptions") + .select("kg_per_week, status") + .eq("user_id", user.id) + .eq("environment", env) + .in("status", ["active", "trialing", "past_due"]) + .order("created_at", { ascending: false }) + .limit(1) + .maybeSingle(); + + if (!data) { + toast({ + title: "Inget aktivt abonnemang", + description: "Du behöver ett aktivt abonnemang för att ändra mängd.", + variant: "destructive", + }); + navigate("/account"); + return; + } + const kg = Number(data.kg_per_week ?? 10); + setCurrentKg(kg); + setNewKg(kg); + setLoading(false); + })(); + }, [user, navigate, toast]); + + const newMonthlyInk = useMemo( + () => newKg * WEEKS_PER_MONTH * PRICE_PER_KG_INK, + [newKg], + ); + const currentMonthlyInk = useMemo( + () => (currentKg ?? 0) * WEEKS_PER_MONTH * PRICE_PER_KG_INK, + [currentKg], + ); + const diff = newMonthlyInk - currentMonthlyInk; + const unchanged = currentKg !== null && newKg === currentKg; + + const handleSave = async () => { + if (!hasPaymentsConfigured()) { + toast({ title: "Betalningar inte konfigurerade", variant: "destructive" }); + return; + } + setSaving(true); + try { + const { data, error } = await supabase.functions.invoke("update-subscription-quantity", { + body: { new_kg_per_week: newKg, environment: getStripeEnvironment() }, + }); + if (error) throw error; + if ((data as any)?.error) throw new Error((data as any).error); + toast({ + title: "Abonnemang uppdaterat", + description: `Ny mängd: ${newKg} kg/vecka. Prisdifferens justeras på nästa faktura.`, + }); + navigate("/account"); + } catch (e: any) { + toast({ + title: "Kunde inte uppdatera", + description: e?.message ?? String(e), + variant: "destructive", + }); + } finally { + setSaving(false); + } + }; + + if (authLoading || loading) { + return ( +
+
+
+ +
+
+ ); + } + + return ( +
+
+
+
+ + +
+
+

+ Ändra abonnemang +

+

+ Justera antal kg kakor per vecka +

+
+ +
+
+
+ Ny mängd +
+
+ {newKg} kg +
+
per vecka
+
+ +
+ setNewKg(parseInt(e.target.value, 10))} + className="calculator-slider w-full" + aria-label="Kg per vecka" + /> +
+ 1 kg + 100 kg +
+
+ +
+
+ Nuvarande månadskostnad + + {Math.round(currentMonthlyInk).toLocaleString("sv-SE")} kr + +
+
+ Ny månadskostnad + + {Math.round(newMonthlyInk).toLocaleString("sv-SE")} kr + +
+ {!unchanged && ( +
+ Skillnad + 0 ? "text-foreground" : "text-primary" + }`} + > + {diff > 0 ? "+" : ""} + {Math.round(diff).toLocaleString("sv-SE")} kr/mån + +
+ )} +

+ ink moms · prisdifferens proportioneras på nästa faktura +

+
+ + +
+
+
+
+ ); +}; + +export default ChangeSubscription; diff --git a/supabase/config.toml b/supabase/config.toml index d8caa1e..7724f20 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -20,3 +20,6 @@ verify_jwt = false [functions.create-portal-session] verify_jwt = false + +[functions.update-subscription-quantity] +verify_jwt = false From 12714ac354625fc48cbe01f73d8887ebdaed80b4 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 11 Jul 2026 18:22:53 +0000 Subject: [PATCH 3/3] Changes Co-authored-by: wolfoftyreso-debug <250630591+wolfoftyreso-debug@users.noreply.github.com> --- src/pages/Account.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pages/Account.tsx b/src/pages/Account.tsx index 08dfcbf..ace1dc2 100644 --- a/src/pages/Account.tsx +++ b/src/pages/Account.tsx @@ -413,6 +413,16 @@ const Account = () => {

)} + +