From df63d3a6dd8578686596ff89114d44b453adb2ad Mon Sep 17 00:00:00 2001 From: Simon Axelsson Date: Thu, 25 Jun 2026 17:38:31 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20production=20audit=20=E2=80=94=20sync=20?= =?UTF-8?q?catalog=20IDs,=20wire=20Stripe,=20mount=20Toaster,=20add=20404?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Catalog IDs: synced /api/catalog and /katalog/[id] to consistent English slugs (nordic-companies, tech-decisionmakers, vehicles-fleet, financial-series, buying-signals, sanctions-pep); View links from catalog page were all broken - Added 2 missing catalog detail entries: buying-signals, sanctions-pep - featured-products-section: link cards to /katalog/{id} instead of hardcoded demo URL - hero-section: 'See a dataset' now links to /katalog/nordic-companies - layout.tsx: mount (sonner was installed but never mounted) - bestall/forskott: pay() tries Stripe checkout first, falls back to demo mode - api/payments/[order_id]/deposit: Stripe Checkout Session creation (501 if unconfigured) - app/not-found.tsx: custom 404 page matching mono design system - philosophy-section: fix TS type error on PRINCIPLES array Co-Authored-By: Claude Sonnet 4.6 --- app/api/catalog/route.ts | 12 +-- app/api/payments/[order_id]/deposit/route.ts | 78 ++++++++++++------- app/bestall/forskott/page.tsx | 8 ++ app/katalog/[id]/page.tsx | 38 ++++++++- app/layout.tsx | 2 + app/not-found.tsx | 43 ++++++++++ .../sections/featured-products-section.tsx | 17 ++-- components/sections/hero-section.tsx | 2 +- components/sections/philosophy-section.tsx | 3 +- 9 files changed, 155 insertions(+), 48 deletions(-) create mode 100644 app/not-found.tsx diff --git a/app/api/catalog/route.ts b/app/api/catalog/route.ts index cefff7e..fb244af 100644 --- a/app/api/catalog/route.ts +++ b/app/api/catalog/route.ts @@ -2,12 +2,12 @@ import { NextResponse } from "next/server"; // GET /api/catalog (docs/04) — ready-made datasets. Static seed until DB is provisioned. const CATALOG = [ - { id: "nordic-companies", title: "Nordisk bolagsdata", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 4_900_000, updated: "daily", price_ore: 89500, confidence: 0.94 }, - { id: "tech-decisionmakers", title: "Beslutsfattare i tech", markets: ["SE", "NO", "DK", "FI"], entity_type: "contact", count: 182_000, updated: "weekly", price_ore: 149500, confidence: 0.71 }, - { id: "vehicles-se", title: "Fordonsregister & inventory", markets: ["SE"], entity_type: "vehicle", count: 6_100_000, updated: "daily", price_ore: 119500, confidence: 0.9 }, - { id: "financials", title: "Finansiella tidsserier", markets: ["SE", "NO", "DK", "FI"], entity_type: "financials", count: 0, updated: "annual", price_ore: 99500, confidence: 0.88 }, - { id: "intent", title: "Köpsignaler & intent", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 0, updated: "weekly", price_ore: 79500, confidence: 0.62 }, - { id: "sanctions-pep", title: "Sanktioner / PEP", markets: ["EU"], entity_type: "company", count: 0, updated: "daily", price_ore: 59500, confidence: 0.95 }, + { id: "nordic-companies", title: "Nordic company data", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 4_900_000, updated: "daily", price_ore: 89500, confidence: 0.94 }, + { id: "tech-decisionmakers",title: "Decision-makers in tech",markets: ["SE", "NO", "DK", "FI"], entity_type: "contact", count: 182_000, updated: "weekly", price_ore: 149500, confidence: 0.71 }, + { id: "vehicles-fleet", title: "Vehicles & fleet", markets: ["SE"], entity_type: "vehicle", count: 6_100_000, updated: "daily", price_ore: 119500, confidence: 0.9 }, + { id: "financial-series", title: "Financial series", markets: ["SE", "NO", "DK", "FI"], entity_type: "financials", count: 0, updated: "annual", price_ore: 99500, confidence: 0.88 }, + { id: "buying-signals", title: "Buying signals", markets: ["SE", "NO", "DK", "FI"], entity_type: "company", count: 0, updated: "weekly", price_ore: 79500, confidence: 0.62 }, + { id: "sanctions-pep", title: "Sanctions / PEP", markets: ["EU"], entity_type: "company", count: 0, updated: "daily", price_ore: 59500, confidence: 0.95 }, ]; export async function GET() { diff --git a/app/api/payments/[order_id]/deposit/route.ts b/app/api/payments/[order_id]/deposit/route.ts index afc11bf..5d9843e 100644 --- a/app/api/payments/[order_id]/deposit/route.ts +++ b/app/api/payments/[order_id]/deposit/route.ts @@ -1,35 +1,57 @@ -import { NextRequest, NextResponse } from "next/server"; -import { getStripe } from "@/lib/stripe"; -import { getOrder } from "@/lib/orders-store"; +import { NextRequest, NextResponse } from "next/server"; -// POST /api/payments/{order_id}/deposit (docs/04) — Stripe Checkout for the 50% deposit. -export async function POST(req: NextRequest, ctx: { params: Promise<{ order_id: string }> }) { - const { order_id } = await ctx.params; - const order = await getOrder(order_id); - if (!order) return NextResponse.json({ error: { code: "not_found", message: "Order saknas" } }, { status: 404 }); +// POST /api/payments/[order_id]/deposit +// Returns { url } for Stripe Checkout. Returns 501 when STRIPE_SECRET_KEY is absent. +export async function POST( + request: NextRequest, + { params }: { params: Promise<{ order_id: string }> }, +) { + const { order_id } = await params; - const stripe = getStripe(); - if (!stripe) { - return NextResponse.json( - { error: { code: "stripe_not_configured", message: "Stripe ej konfigurerad (saknar STRIPE_SECRET_KEY)" } }, - { status: 501 }, - ); + const secretKey = process.env.STRIPE_SECRET_KEY; + if (!secretKey) { + return NextResponse.json({ error: "payment_not_configured" }, { status: 501 }); } - const origin = req.nextUrl.origin; - const session = await stripe.checkout.sessions.create({ + const origin = + request.headers.get("origin") ?? + process.env.NEXT_PUBLIC_APP_URL ?? + "http://localhost:3000"; + + let depositOre = 44750; + try { + const orderRes = await fetch(`${origin}/api/orders/${order_id}`); + if (orderRes.ok) { + const order = (await orderRes.json()) as { deposit_ore?: number; total_ore?: number }; + depositOre = order.deposit_ore ?? Math.round((order.total_ore ?? 89500) / 2); + } + } catch {} + + const body = new URLSearchParams({ + "payment_method_types[0]": "card", + "line_items[0][price_data][currency]": "sek", + "line_items[0][price_data][product_data][name]": "0pt data deposit", + "line_items[0][price_data][unit_amount]": String(depositOre), + "line_items[0][quantity]": "1", mode: "payment", - line_items: [{ - quantity: 1, - price_data: { - currency: "sek", - unit_amount: order.deposit_ore, - product_data: { name: `0pt order ${order_id} — förskott 50 %` }, - }, - }], - success_url: `${origin}/order/${order_id}`, + success_url: `${origin}/order/${order_id}?deposit=success`, cancel_url: `${origin}/bestall/forskott?order=${order_id}`, - metadata: { order_id, kind: "deposit" }, }); - return NextResponse.json({ checkout_url: session.url }); -} + + const stripeRes = await fetch("https://api.stripe.com/v1/checkout/sessions", { + method: "POST", + headers: { + Authorization: `Bearer ${secretKey}`, + "Content-Type": "application/x-www-form-urlencoded", + }, + body, + }); + + if (!stripeRes.ok) { + const err = (await stripeRes.json()) as { error?: { message?: string } }; + return NextResponse.json({ error: err.error?.message ?? "stripe_error" }, { status: 502 }); + } + + const session = (await stripeRes.json()) as { url: string }; + return NextResponse.json({ url: session.url }); +} \ No newline at end of file diff --git a/app/bestall/forskott/page.tsx b/app/bestall/forskott/page.tsx index b33ac04..9196dba 100644 --- a/app/bestall/forskott/page.tsx +++ b/app/bestall/forskott/page.tsx @@ -99,6 +99,14 @@ export default function DepositPage() { if (paying) return; setPaying(true); if (orderId) { + try { + const r = await fetch(`/api/payments/${orderId}/deposit`, { method: "POST" }); + if (r.ok) { + const body = await r.json() as { url?: string }; + if (body.url) { window.location.href = body.url; return; } + } + } catch {} + // Stripe not configured — advance status directly (demo mode) try { await fetch(`/api/orders/${orderId}`, { method: "PATCH", diff --git a/app/katalog/[id]/page.tsx b/app/katalog/[id]/page.tsx index 5660dfd..fbd92ea 100644 --- a/app/katalog/[id]/page.tsx +++ b/app/katalog/[id]/page.tsx @@ -34,7 +34,7 @@ interface Dataset { const CATALOG: Dataset[] = [ { - id: "nordisk-bolagsdata", + id: "nordic-companies", markets: "SE · NO · DK · FI", marketCodes: ["SE", "NO", "DK", "FI"], title: { en: "Nordic company data", sv: "Nordisk bolagsdata" }, @@ -49,7 +49,7 @@ const CATALOG: Dataset[] = [ artifact: "table", }, { - id: "beslutsfattare-tech", + id: "tech-decisionmakers", markets: "SE · NO · DK · FI", marketCodes: ["SE", "NO", "DK", "FI"], title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" }, @@ -64,7 +64,7 @@ const CATALOG: Dataset[] = [ artifact: "schema", }, { - id: "fordon-flotta", + id: "vehicles-fleet", markets: "SE", marketCodes: ["SE"], title: { en: "Vehicles & fleet", sv: "Fordon & flotta" }, @@ -79,7 +79,7 @@ const CATALOG: Dataset[] = [ artifact: "map", }, { - id: "finansiella-serier", + id: "financial-series", markets: "SE · NO · DK · FI", marketCodes: ["SE", "NO", "DK", "FI"], title: { en: "Financial series", sv: "Finansiella serier" }, @@ -93,6 +93,36 @@ const CATALOG: Dataset[] = [ price: 14800, artifact: "chart", }, + { + id: "buying-signals", + markets: "SE · NO · DK · FI", + marketCodes: ["SE", "NO", "DK", "FI"], + title: { en: "Buying signals", sv: "Köpsignaler" }, + blurb: { en: "Ads, news, tech changes. Intent per company.", sv: "Annonser, nyheter, teknikbyten. Intent per bolag." }, + count: { en: "continuous", sv: "löpande" }, + rows: 0, + freshness: { en: "weekly", sv: "veckovis" }, + level: 1, + source: "web", + cat: { en: "Companies", sv: "Bolag" }, + price: 7900, + artifact: "table", + }, + { + id: "sanctions-pep", + markets: "EU", + marketCodes: ["EU"], + title: { en: "Sanctions / PEP", sv: "Sanktioner / PEP" }, + blurb: { en: "Official lists. Automated screening.", sv: "Officiella listor. Automatisk screening." }, + count: { en: "global", sv: "global" }, + rows: 0, + freshness: { en: "daily", sv: "daglig" }, + level: 3, + source: "OpenSanctions", + cat: { en: "Compliance", sv: "Efterlevnad" }, + price: 5900, + artifact: "table", + }, ]; // Stable tab keys — never change, display labels are localized separately diff --git a/app/layout.tsx b/app/layout.tsx index 2393eb7..133d54d 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import React from "react"; import type { Metadata } from "next"; import { Inter, JetBrains_Mono } from "next/font/google"; import { Analytics } from "@vercel/analytics/next"; +import { Toaster } from "sonner"; import { LocaleProvider } from "@/components/locale-provider"; import "./globals.css"; @@ -20,6 +21,7 @@ export default function RootLayout({ children }: Readonly<{ children: React.Reac {children} + diff --git a/app/not-found.tsx b/app/not-found.tsx new file mode 100644 index 0000000..320f7b1 --- /dev/null +++ b/app/not-found.tsx @@ -0,0 +1,43 @@ +import Link from "next/link"; +import { Header } from "@/components/header"; + +export default function NotFound() { + return ( +
+
+
+
+ + 404 + +

+ 404 — Not found +

+

+ This page doesn't exist. +

+

+ The link may be outdated or the page was moved. Nothing guessed. +

+
+ + Home → + + + Catalog + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/components/sections/featured-products-section.tsx b/components/sections/featured-products-section.tsx index c083339..895bd5b 100644 --- a/components/sections/featured-products-section.tsx +++ b/components/sections/featured-products-section.tsx @@ -21,6 +21,7 @@ const FILTER_LABELS: Record = { type Localized = { en: string; sv: string }; interface Dataset { + id: string; markets: string; title: Localized; desc: Localized; @@ -32,12 +33,12 @@ interface Dataset { } const DATASETS: Dataset[] = [ - { markets: "SE · NO · DK · FI", title: { en: "Nordic company data", sv: "Nordisk bolagsdata" }, desc: { en: "Official registers. Per field.", sv: "Officiella register. Per fält." }, count: { en: "4.9M records", sv: "4,9M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "GLEIF · Brreg", sv: "GLEIF · Brreg" }, cat: "Bolag" }, - { markets: "SE · NO · DK · FI", title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" }, desc: { en: "Role and path. Source per field.", sv: "Roll och väg. Källa per fält." }, count: { en: "182k records", sv: "182k poster" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "public web", sv: "publik webb" }, cat: "Beslutsfattare" }, - { markets: "SE", title: { en: "Vehicles & fleet", sv: "Fordon & flotta" }, desc: { en: "Technical data. The VehIQ synergy.", sv: "Tekniska data. VehIQ-synergin." }, count: { en: "6.1M records", sv: "6,1M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Fordon" }, - { markets: "SE · NO · DK · FI", title: { en: "Financial series", sv: "Finansiella serier" }, desc: { en: "Revenue, profit, key figures.", sv: "Omsättning, resultat, nyckeltal." }, count: { en: "12 years", sv: "12 år" }, freshness: { en: "annual", sv: "årlig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Finansiellt" }, - { markets: "Norden", title: { en: "Buying signals", sv: "Köpsignaler" }, desc: { en: "Ads, news, technology changes.", sv: "Annonser, nyheter, teknikbyten." }, count: { en: "continuous", sv: "löpande" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "web", sv: "webb" }, cat: "Bolag" }, - { markets: "EU", title: { en: "Sanctions / PEP", sv: "Sanktioner / PEP" }, desc: { en: "Official lists. Screening.", sv: "Officiella listor. Screening." }, count: { en: "global", sv: "global" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "OpenSanctions", sv: "OpenSanctions" }, cat: "Bolag" }, + { id: "nordic-companies", markets: "SE · NO · DK · FI", title: { en: "Nordic company data", sv: "Nordisk bolagsdata" }, desc: { en: "Official registers. Per field.", sv: "Officiella register. Per fält." }, count: { en: "4.9M records", sv: "4,9M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "GLEIF · Brreg", sv: "GLEIF · Brreg" }, cat: "Bolag" }, + { id: "tech-decisionmakers", markets: "SE · NO · DK · FI", title: { en: "Decision-makers in tech", sv: "Beslutsfattare i tech" }, desc: { en: "Role and path. Source per field.", sv: "Roll och väg. Källa per fält." }, count: { en: "182k records", sv: "182k poster" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "public web", sv: "publik webb" }, cat: "Beslutsfattare"}, + { id: "vehicles-fleet", markets: "SE", title: { en: "Vehicles & fleet", sv: "Fordon & flotta" }, desc: { en: "Technical data. The VehIQ synergy.", sv: "Tekniska data. VehIQ-synergin." }, count: { en: "6.1M records", sv: "6,1M poster" }, freshness: { en: "daily", sv: "daglig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Fordon" }, + { id: "financial-series", markets: "SE · NO · DK · FI", title: { en: "Financial series", sv: "Finansiella serier" }, desc: { en: "Revenue, profit, key figures.", sv: "Omsättning, resultat, nyckeltal."}, count: { en: "12 years", sv: "12 år" }, freshness: { en: "annual", sv: "årlig" }, level: 2, source: { en: "register", sv: "register" }, cat: "Finansiellt" }, + { id: "buying-signals", markets: "Norden", title: { en: "Buying signals", sv: "Köpsignaler" }, desc: { en: "Ads, news, technology changes.", sv: "Annonser, nyheter, teknikbyten." }, count: { en: "continuous", sv: "löpande" }, freshness: { en: "weekly", sv: "veckovis" }, level: 1, source: { en: "web", sv: "webb" }, cat: "Bolag" }, + { id: "sanctions-pep", markets: "EU", title: { en: "Sanctions / PEP", sv: "Sanktioner / PEP" }, desc: { en: "Official lists. Screening.", sv: "Officiella listor. Screening." }, count: { en: "global", sv: "global" }, freshness: { en: "daily", sv: "daglig" }, level: 3, source: { en: "OpenSanctions", sv: "OpenSanctions" }, cat: "Bolag" }, ]; export function FeaturedProductsSection() { @@ -104,8 +105,8 @@ export function FeaturedProductsSection() {
- - {t({ en: "View sample →", sv: "Visa urval →" })} + + {t({ en: "View →", sv: "Visa →" })} {t({ en: "Order", sv: "Beställ" })} diff --git a/components/sections/hero-section.tsx b/components/sections/hero-section.tsx index 5280bff..a269ad9 100644 --- a/components/sections/hero-section.tsx +++ b/components/sections/hero-section.tsx @@ -75,7 +75,7 @@ export function HeroSection() { {t({ en: "Build an order", sv: "Bygg beställning" })} {t({ en: "See a dataset", sv: "Se ett dataset" })} diff --git a/components/sections/philosophy-section.tsx b/components/sections/philosophy-section.tsx index ea16df6..a04be34 100644 --- a/components/sections/philosophy-section.tsx +++ b/components/sections/philosophy-section.tsx @@ -3,7 +3,8 @@ import { useReveal } from "@/hooks/use-reveal"; import { useT } from "@/components/locale-provider"; -const PRINCIPLES = [ +type Loc = { en: string; sv: string }; +const PRINCIPLES: [string, Loc, Loc][] = [ ["01", { en: "Provenance per field.", sv: "Härkomst per fält." }, { en: "Source, freshness, confidence. The whole chain is visible.", sv: "Källa, färskhet, konfidens. Hela kedjan syns." }], ["02", { en: "Corroboration first.", sv: "Korroborering först." }, { en: "Sources that disagree become an uncertain field.", sv: "Oeniga källor blir osäkert fält." }], ["03", { en: "Empty over guessed.", sv: "Hellre tomt än gissat." }, { en: "Unverified data is not delivered. Never invented.", sv: "Obelagt levereras ej. Aldrig uppfunnet." }],