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() {
+ Justera antal kg kakor per vecka
+
+ ink moms · prisdifferens proportioneras på nästa faktura
+
+ Ändra abonnemang
+
+