diff --git a/plan importer willys.md.txt b/plan importer willys.md.txt new file mode 100644 index 0000000..712bf1d --- /dev/null +++ b/plan importer willys.md.txt @@ -0,0 +1,946 @@ +Här är en **komplett plan i markdown-format** som du kan spara som en fil (t.ex. `premium_recipe_feature_plan.md`) och ladda upp i **VS Code-chatten** för att implementera **AI som en premium-funktion** för receptgenerering. Filen är strukturerad för att vara **lätt att följa** och innehåller alla nödvändiga steg, kodsnuttar och förklaringar. + +--- + +```markdown +# 📌 Plan: Implementera AI som Premium-Funktion för Receptgenerering + +--- +## **🎯 Översikt** +Denna plan beskriver hur du implementerar **AI som en premium-funktion** i din app, där: +- **Gratis-användare** får **grundläggande recept** (genererade från mallar). +- **Premium-användare** får **AI-genererade recept** (mer kreativa, personliga och intelligenta). +- Systemet är **modulärt** och kan enkelt utökas. + +--- + +--- + +## **📋 Förutsättningar** +1. **Node.js/TypeScript** (backend). +2. **Prisma** (databas). +3. **pdf-parse** och **Tesseract.js** (PDF-extrahering). +4. **Mistral API-nyckel** (för AI-funktioner). +5. **Frontend-ramverk** (t.ex. React, Next.js, eller liknande). + +--- + +--- + +## **🛠️ Databasschema (Prisma)** +Lägg till fält för att spåra **premium-status** i din `user`-tabell: + +```prisma +model User { + id Int @id @default(autoincrement()) + // ... andra fält ... + is_premium Boolean @default(false) + premium_expiry_date DateTime? +} +``` + +**SQL för att lägga till fält om tabellen redan finns:** +```sql +ALTER TABLE "User" ADD COLUMN "is_premium" BOOLEAN DEFAULT false; +ALTER TABLE "User" ADD COLUMN "premium_expiry_date" TIMESTAMP; +``` + +--- + +--- + +## **📁 Filstruktur** +``` +src/ +├── services/ +│ ├── pdfTextExtractor.ts # Extraherar text från PDF (pdf-parse + OCR) +│ ├── willysParser.ts # Parsar text till strukturerad data (regex) +│ ├── inventoryMatcher.ts # Matchar produkter med inventory (fuzzy matching) +│ └── recipeGenerator.ts # Genererar recept (AI för premium, mallar för gratis) +├── api/ +│ ├── pdfImport.ts # API-endpoint för PDF-import +│ └── premium.ts # API-endpoint för premium-uppgradering +└── types/ + └── recipeTypes.ts # Typer för recept och produkter +``` + +--- + +--- + +## **📄 1. `pdfTextExtractor.ts` – Extrahera text från PDF** +Använder `pdf-parse` som primär metod och `Tesseract.js` som fallback för OCR. + +```typescript +import * as fs from 'fs'; +import * as pdf from 'pdf-parse'; +import Tesseract from 'tesseract.js'; + +/** + * Extraherar text från en PDF-fil, med fallback till OCR om nödvändigt. + * @param pdfPath Sökväg till PDF-filen. + * @returns Extraherad text. + */ +export async function extractTextFromPDF(pdfPath: string): Promise { + try { + const dataBuffer = fs.readFileSync(pdfPath); + const data = await pdf(dataBuffer); + if (data.text.trim()) return data.text; + } catch (error) { + console.warn('pdf-parse misslyckades, försöker med OCR...', error); + } + + try { + const { data: { text } } = await Tesseract.recognize(pdfPath, 'swe', { + logger: (m) => console.log(m), + }); + return text; + } catch (error) { + console.error('OCR misslyckades:', error); + throw new Error('Kunde inte extrahera text från PDF:en.'); + } +} +``` + +--- + +--- + +## **📄 2. `willysParser.ts` – Parsa text till strukturerad data** +Använder **regex** för att extrahera produktnamn, priser, jämförpriser, etc. från Willys veckoblad. + +```typescript +/** + * Parsar text från Willys veckoblad till strukturerad data. + * @param text Den extraherade texten. + * @returns Strukturerad data (JSON-array). + */ +export function parseWillysText(text: string): any[] { + const lines = text.split('\n'); + const products: any[] = []; + let currentProduct: any = {}; + let currentCategory: string | null = null; + + // Regex-mönster + const productLineRegex = /^(.*?)\s*•\s*(.*?)\s*•\s*(.*?)\s*•?\s*Jämförpris\s*([\d:]+)\s*kr\/([a-z]+)/i; + const simpleProductLineRegex = /^(.*?)\s*•\s*(.*?)\s*•\s*(.*?)$/i; + const priceLineRegex = /^([\d:]+)\s*(Per\s*(förp|kg|st|l|))?/i; + const offerLineRegex = /^(Max \d+ (köp|förp)\/hushåll|Lägsta 30-dgrspris [\d:]+ kr)/i; + const categoryLineRegex = /^(Fisk|Kött|Mejeri|Grönsaker|Frukt|Dryck|Bröd|Pasta|Ris)/i; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + // Matcha kategori + const categoryMatch = trimmedLine.match(categoryLineRegex); + if (categoryMatch) { + currentCategory = categoryMatch[1]; + continue; + } + + // Matcha produktrad (med jämförpris) + const productMatch = trimmedLine.match(productLineRegex); + if (productMatch) { + if (Object.keys(currentProduct).length > 0) { + currentProduct.category = currentCategory; + products.push(currentProduct); + currentProduct = {}; + } + currentProduct = { + name: productMatch[1].trim(), + weight: productMatch[2].trim(), + origin: productMatch[3].trim(), + comparisonPrice: `${productMatch[4].replace(':', '.')} kr/${productMatch[5]}`, + category: currentCategory, + }; + continue; + } + + // Matcha enkel produktrad + const simpleProductMatch = trimmedLine.match(simpleProductLineRegex); + if (simpleProductMatch) { + if (Object.keys(currentProduct).length > 0) { + currentProduct.category = currentCategory; + products.push(currentProduct); + currentProduct = {}; + } + currentProduct = { + name: simpleProductMatch[1].trim(), + weight: simpleProductMatch[2].trim(), + origin: simpleProductMatch[3].trim(), + category: currentCategory, + }; + continue; + } + + // Matcha pris + const priceMatch = trimmedLine.match(priceLineRegex); + if (priceMatch) { + currentProduct.price = `${priceMatch[1].replace(':', '.')} kr/${priceMatch[2]?.trim() || 'förp'}`; + continue; + } + + // Matcha erbjudande + const offerMatch = trimmedLine.match(offerLineRegex); + if (offerMatch) { + currentProduct.offer = offerMatch[1]; + continue; + } + } + + if (Object.keys(currentProduct).length > 0) { + currentProduct.category = currentCategory; + products.push(currentProduct); + } + + return products; +} +``` + +--- + +--- + +## **📄 3. `inventoryMatcher.ts` – Matcha produkter med inventory** +Använder **fuzzy matching** för att matcha produktnamn från PDF:en med användarens inventory. + +```typescript +import { stringSimilarity } from 'string-similarity'; + +/** + * Matchar produkter från PDF:en med användarens inventory. + * @param products Produkter från PDF:en. + * @param userId Användarens ID. + * @returns Matchade produkter med inventory-status. + */ +export async function matchProductsWithInventory(products: any[], userId: number) { + const inventory = await prisma.userInventory.findMany({ + where: { userId }, + }); + + const inventoryNames = inventory.map((item) => ({ + name: item.name.toLowerCase(), + id: item.id, + quantity: item.quantity, + category: item.category, + })); + + return products.map((product) => { + const productName = product.name.toLowerCase(); + const bestMatch = inventoryNames.reduce( + (best, item) => { + const similarity = stringSimilarity.compareTwoStrings(productName, item.name); + return similarity > best.similarity ? { ...item, similarity } : best; + }, + { name: '', similarity: 0, id: null, quantity: 0, category: null } + ); + + const inventoryItem = bestMatch.similarity > 0.6 + ? inventory.find((item) => item.id === bestMatch.id) + : null; + + return { + ...product, + inInventory: !!inventoryItem, + inventoryId: inventoryItem?.id || null, + inventoryQuantity: inventoryItem?.quantity || 0, + inventoryMatchSimilarity: bestMatch.similarity, + }; + }); +} +``` + +--- + +--- + +## **📄 4. `recipeGenerator.ts` – Generera recept (AI eller mallar)** +Modulär funktion som använder **AI för premium-användare** och **mallar för gratis-användare**. + +```typescript +import { MistralClient } from '@mistralai/mistralai'; +import { prisma } from '../prisma'; // Antas att Prisma är konfigurerat + +const mistral = new MistralClient({ apiKey: process.env.MISTRAL_API_KEY }); + +/** + * Genererar recept baserat på matchade produkter. + * @param matchedProducts Produkter matchade med inventory. + * @param userId Användarens ID. + * @param userPreferences Användarens preferenser. + * @returns Genererade recept. + */ +export async function generateRecipes( + matchedProducts: any[], + userId: number, + userPreferences: any = {} +): Promise { + const user = await prisma.user.findUnique({ where: { id: userId } }); + const isPremium = user?.is_premium || false; + + if (isPremium) { + return generateRecipesWithAI(matchedProducts, userPreferences); + } else { + return generateRecipesFromTemplates(matchedProducts); + } +} + +/** + * Genererar AI-recept för premium-användare. + */ +async function generateRecipesWithAI(matchedProducts: any[], userPreferences: any): Promise { + const relevantProducts = matchedProducts.filter((p) => p.inInventory || p.offer); + if (relevantProducts.length === 0) return []; + + const prompt = ` + Du är en kock som skapar kostnadseffektiva, läckra och personliga recept. + Skapa **3 recept** baserat på följande produkter: + ${JSON.stringify(relevantProducts, null, 2)} + + Användarens preferenser: + - Diet: ${userPreferences.diet || 'Inga restriktioner'} + - Köksstil: ${userPreferences.cuisine || 'Nordisk'} + - Tid: ${userPreferences.time || 'Under 30 minuter'} + + Recepten ska: + 1. Använda produkter från inventory eller på erbjudande. + 2. Vara lätt att laga. + 3. Inkludera näringsinformation (uppskattad). + 4. Vara på svenska. + + Returnera recepten i JSON-format: + { + "recipes": [ + { + "title": "Receptets titel", + "description": "Beskrivning", + "ingredients": [ + {"name": "Produktnamn", "quantity": "Mängd", "fromInventory": true/false, "onOffer": true/false} + ], + "instructions": ["Steg 1", "Steg 2"], + "cost": "Uppskattad kostnad", + "time": "Tillagningstid", + "nutritionalInfo": {"calories": "Kalorier", "protein": "Protein (g)"} + } + ] + } + `; + + try { + const response = await mistral.chat({ + model: 'mistral-small-latest', + messages: [{ role: 'user', content: prompt }], + temperature: 0.7, + }); + + const jsonString = response.choices[0].message.content + .replace(/```json|```/g, '') + .trim(); + + const parsed = JSON.parse(jsonString); + return parsed.recipes || []; + } catch (error) { + console.error('AI-generering misslyckades:', error); + return generateRecipesFromTemplates(matchedProducts); + } +} + +/** + * Genererar recept från mallar för gratis-användare. + */ +function generateRecipesFromTemplates(matchedProducts: any[]): any[] { + const recipes: any[] = []; + const relevantProducts = matchedProducts.filter((p) => p.inInventory || p.offer); + if (relevantProducts.length === 0) return recipes; + + const productsByCategory: Record = {}; + relevantProducts.forEach((product) => { + const category = product.category || 'Okänt'; + if (!productsByCategory[category]) productsByCategory[category] = []; + productsByCategory[category].push(product); + }); + + const templates: Record = { + Fisk: [ + { + title: 'Lax med potatis och grönsaker', + ingredients: [ + { name: 'Lax', category: 'Fisk', required: true }, + { name: 'Potatis', category: 'Grönsaker', required: true }, + ], + instructions: [ + 'Koka potatisen i 15 minuter.', + 'Stek laxen i 5 minuter på varje sida.', + 'Servera med grönsaker.', + ], + time: '25 minuter', + }, + ], + Kött: [ + { + title: 'Köttbullar med potatismos', + ingredients: [ + { name: 'Köttfärs', category: 'Kött', required: true }, + { name: 'Potatis', category: 'Grönsaker', required: true }, + ], + instructions: [ + 'Blanda köttfärs med salt och peppar.', + 'Stek köttbullarna i en panna.', + 'Koka och mosa potatisen.', + ], + time: '30 minuter', + }, + ], + }; + + for (const [category, categoryTemplates] of Object.entries(templates)) { + const categoryProducts = productsByCategory[category] || []; + for (const template of categoryTemplates) { + const missingIngredients = template.ingredients.filter( + (ing: any) => ing.required && !categoryProducts.some((p) => + p.name.toLowerCase().includes(ing.name.toLowerCase()) + ) + ); + if (missingIngredients.length === 0) { + recipes.push({ + title: template.title, + description: `Ett enkelt ${category.toLowerCase()}-recept.`, + ingredients: template.ingredients.map((ing: any) => { + const matchedProduct = categoryProducts.find((p) => + p.name.toLowerCase().includes(ing.name.toLowerCase()) + ); + return { + name: matchedProduct?.name || ing.name, + quantity: matchedProduct?.weight || '1 portion', + fromInventory: matchedProduct?.inInventory || false, + onOffer: matchedProduct?.offer ? true : false, + }; + }), + instructions: template.instructions, + time: template.time, + cost: calculateRecipeCost(template.ingredients, categoryProducts), + }); + } + } + } + + return recipes; +} + +function calculateRecipeCost(ingredients: any[], products: any[]): string { + let totalCost = 0; + for (const ing of ingredients) { + const matchedProduct = products.find((p) => + p.name.toLowerCase().includes(ing.name.toLowerCase()) + ); + if (matchedProduct?.price) { + const price = parseFloat(matchedProduct.price.replace(/[^\d.]/g, '')); + totalCost += price; + } + } + return `Ca ${totalCost.toFixed(2)} kr`; +} +``` + +--- + +--- + +## **📄 5. `pdfImportService.ts` – Fullständigt importflöde** +Haneterar hela flödet från PDF-import till receptgenerering. + +```typescript +import { extractTextFromPDF } from './pdfTextExtractor'; +import { parseWillysText } from './willysParser'; +import { matchProductsWithInventory } from './inventoryMatcher'; +import { generateRecipes } from './recipeGenerator'; +import { prisma } from '../prisma'; + +/** + * Importerar en PDF och genererar recept. + * @param pdfPath Sökväg till PDF-filen. + * @param userId Användarens ID. + * @param userPreferences Användarens preferenser. + * @returns Resultat med produkter och recept. + */ +export async function importPDFAndGenerateRecipes( + pdfPath: string, + userId: number, + userPreferences: any = {} +) { + try { + // 1. Extrahera text + const text = await extractTextFromPDF(pdfPath); + + // 2. Parsa texten + const products = parseWillysText(text); + + // 3. Matcha med inventory + const matchedProducts = await matchProductsWithInventory(products, userId); + + // 4. Generera recept + const recipes = await generateRecipes(matchedProducts, userId, userPreferences); + + // 5. Spara recepten + const savedRecipes = []; + for (const recipe of recipes) { + const savedRecipe = await prisma.encryptedData.create({ + data: { + encrypted_data: JSON.stringify(recipe), + owner_id: userId, + created_at: new Date(), + is_generated: true, + source: user.is_premium ? 'AI (Premium)' : 'Mallar (Gratis)', + }, + }); + savedRecipes.push(savedRecipe); + } + + return { + success: true, + products: matchedProducts, + recipes: savedRecipes, + isPremium: await isUserPremium(userId), + }; + } catch (error) { + console.error('Fel vid import:', error); + return { success: false, error: error instanceof Error ? error.message : 'Okänt fel' }; + } +} + +/** + * Kontrollerar om en användare är premium. + */ +async function isUserPremium(userId: number): Promise { + const user = await prisma.user.findUnique({ where: { id: userId } }); + if (!user) return false; + + if (user.premium_expiry_date && new Date(user.premium_expiry_date) < new Date()) { + await prisma.user.update({ + where: { id: userId }, + data: { is_premium: false }, + }); + return false; + } + + return user.is_premium; +} +``` + +--- + +--- + +## **📄 6. API-Endpoints** +### **🔹 PDF-import (`/api/import/pdf`)** +```typescript +import express from 'express'; +import multer from 'multer'; +import { importPDFAndGenerateRecipes } from '../services/pdfImportService'; + +const router = express.Router(); +const upload = multer({ dest: 'uploads/' }); + +router.post('/import/pdf', upload.single('pdf'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'Ingen PDF-fil uppladdad.' }); + } + + const userId = req.user.id; + const userPreferences = req.body.preferences || {}; + + const result = await importPDFAndGenerateRecipes( + req.file.path, + userId, + userPreferences + ); + + fs.unlinkSync(req.file.path); // Rensa upp + + if (!result.success) { + return res.status(500).json({ error: result.error }); + } + + res.json(result); + } catch (error) { + console.error('Fel vid PDF-import:', error); + res.status(500).json({ error: 'Kunde inte importera PDF:en.' }); + } +}); + +export default router; +``` + +### **🔹 Premium-uppgradering (`/api/user/upgrade`)** +```typescript +router.post('/upgrade-premium', async (req, res) => { + try { + const userId = req.user.id; + // Antas att betalning är verifierad (t.ex. via Stripe) + const expiryDate = new Date(); + expiryDate.setMonth(expiryDate.getMonth() + 1); // 1 månad premium + + await prisma.user.update({ + where: { id: userId }, + data: { + is_premium: true, + premium_expiry_date: expiryDate, + }, + }); + + res.json({ success: true, premiumExpiryDate: expiryDate }); + } catch (error) { + console.error('Fel vid uppgradering:', error); + res.status(500).json({ error: 'Kunde inte uppgradera till premium.' }); + } +}); +``` + +--- + +--- + +## **📄 7. Frontend-Integrering (Exempel: React)** +### **🔹 Visa premium-status** +```tsx +import { useState, useEffect } from 'react'; + +function PremiumStatus({ user }) { + const [isPremium, setIsPremium] = useState(user.is_premium); + + useEffect(() => { + setIsPremium(user.is_premium); + }, [user]); + + return ( +
+ {isPremium ? ( +
+ ✅ Premium (gäller till: {new Date(user.premium_expiry_date).toLocaleDateString('sv-SE')}) +

Du har tillgång till AI-genererade recept!

+
+ ) : ( +
+ 🔒 Uppgradera till premium för AI-recept! +
+ )} +
+ ); +} +``` + +### **🔹 PDF-import-formulär** +```tsx +import { useState } from 'react'; +import axios from 'axios'; + +function PDFImportForm({ user }) { + const [file, setFile] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(null); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!file) return; + + setIsLoading(true); + const formData = new FormData(); + formData.append('pdf', file); + formData.append('preferences', JSON.stringify({ + diet: 'Inga restriktioner', + cuisine: 'Nordisk', + })); + + try { + const response = await axios.post('/api/import/pdf', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + setResult(response.data); + } catch (error) { + console.error('Fel vid uppladdning:', error); + } finally { + setIsLoading(false); + } + }; + + return ( +
+

Importera veckoblad

+
+ setFile(e.target.files?.[0] || null)} + required + /> + +
+ + {result && ( +
+

Resultat

+

Hittade {result.products?.length || 0} produkter.

+

Genererade {result.recipes?.length || 0} recept.

+ {result.recipes?.map((recipe: any, index: number) => ( +
+

{recipe.title}

+

{recipe.description}

+
    + {recipe.ingredients?.map((ing: any, i: number) => ( +
  • + {ing.quantity} {ing.name} {ing.onOffer && '🔥'} +
  • + ))} +
+
+ ))} +
+ )} +
+ ); +} +``` + +--- + +--- + +## **📌 Miljövariabler (`.env`)** +```env +# Mistral API-nyckel (för premium-funktioner) +MISTRAL_API_KEY=din_api_nyckel_här + +# Databas-URL +DATABASE_URL=postgresql://user:password@localhost:5432/db_name + +# Session-hemlighet (för autentisering) +SESSION_SECRET=din_hemliga_nyckel_här +``` + +--- + +--- + +## **📌 Paket som behövs (`package.json`)** +```json +{ + "dependencies": { + "express": "^4.18.2", + "multer": "^1.4.5-lts.1", + "pdf-parse": "^1.1.1", + "tesseract.js": "^4.0.2", + "@mistralai/mistralai": "^0.0.1", + "@prisma/client": "^5.0.0", + "prisma": "^5.0.0", + "string-similarity": "^4.0.4", + "typescript": "^5.0.0" + } +} +``` + +--- +--- +## **📌 Steg-för-steg Implementeringsguide** + +### **1. Förbered databasen** +- Kör migrationskommandot för att lägga till `is_premium` och `premium_expiry_date` i `User`-tabellen: + ```bash + npx prisma migrate dev --name add_premium_fields + ``` + +### **2. Installera beroenden** +```bash +npm install express multer pdf-parse tesseract.js @mistralai/mistralai string-similarity +npm install --save-dev typescript @types/node @types/express @types/multer prisma +``` + +### **3. Konfigurera Prisma** +- Skapa eller uppdatera `schema.prisma` med `User`-modellen (se ovan). +- Kör `npx prisma generate` för att generera Prisma-klienten. + +### **4. Skapa filerna** +- Skapa mappen `src/services/` och lägg till filerna: + - `pdfTextExtractor.ts` + - `willysParser.ts` + - `inventoryMatcher.ts` + - `recipeGenerator.ts` + - `pdfImportService.ts` +- Skapa mappen `src/api/` och lägg till: + - `pdfImport.ts` + - `premium.ts` + +### **5. Konfigurera Express-server** +- Skapa en `server.ts` för att starta din Express-server: + ```typescript + import express from 'express'; + import pdfImportRouter from './api/pdfImport'; + import premiumRouter from './api/premium'; + + const app = express(); + app.use(express.json()); + + // API-endpoints + app.use('/api/import', pdfImportRouter); + app.use('/api/user', premiumRouter); + + const PORT = process.env.PORT || 3000; + app.listen(PORT, () => { + console.log(`Server körs på port ${PORT}`); + }); + ``` + +### **6. Testa flödet** +1. **Ladda upp en PDF** (t.ex. Willys veckoblad) via `/api/import/pdf`. +2. **Kontrollera att recept genereras** (mallar för gratis, AI för premium). +3. **Testa premium-uppgraderingen** via `/api/user/upgrade`. + +--- +--- +## **📌 Säkerhetsöverväganden** +1. **API-nycklar**: + - Lagra `MISTRAL_API_KEY` i miljövariabler (aldrig i koden). +2. **Premium-status**: + - Kontrollera alltid `is_premium` innan AI-anrop. +3. **Filuppladdning**: + - Validera filtypen (endast PDF). + - Begränsa filstorleken (t.ex. max 10MB). +4. **Autentisering**: + - Se till att användaren är inloggad innan de kan ladda upp PDF:er eller uppgradera. + +--- +--- +## **📌 Felsökning** +| Problem | Lösning | +|---------|---------| +| **PDF:en läses inte in** | Kontrollera att filen är en giltig PDF. Använd `Tesseract.js` som fallback. | +| **Regex matchar inte** | Justera regex-mönstren baserat på PDF:ens struktur. | +| **AI-generering misslyckas** | Fallback till mallar. Kontrollera API-nyckeln. | +| **Premium-status uppdateras inte** | Kontrollera att `is_premium` och `premium_expiry_date` uppdateras korrekt. | +| **Inga recept genereras** | Kontrollera att produkterna matchas korrekt med inventory. | + +--- +--- +## **🚀 Nästa steg** +1. **Testa med riktiga PDF:er** (t.ex. Willys veckoblad). +2. **Justera regex-mönstren** för att fånga fler produktdetaljer. +3. **Lägg till fler receptmallar** för olika kategorier. +4. **Implementera en betalningslösning** (t.ex. Stripe) för premium-uppgraderingar. +5. **Monitorera användningen** av premium-funktioner. + +--- +--- +## **📌 Exempel på betalningsintegrering (Stripe)** +Om du vill lägga till **verklig betalning** för premium-uppgraderingar, kan du använda **Stripe**: + +### **1. Installera Stripe** +```bash +npm install stripe @stripe/stripe-js +``` + +### **2. Skapa en Stripe-endpoint** +```typescript +import Stripe from 'stripe'; + +const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { + apiVersion: '2023-10-16', +}); + +router.post('/create-checkout-session', async (req, res) => { + const userId = req.user.id; + + const session = await stripe.checkout.sessions.create({ + payment_method_types: ['card'], + line_items: [ + { + price_data: { + currency: 'sek', + product_data: { + name: 'Premium-medlemskap (1 månad)', + }, + unit_amount: 9900, // 99 kr + }, + quantity: 1, + }, + ], + mode: 'payment', + success_url: `${process.env.FRONTEND_URL}/success?session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${process.env.FRONTEND_URL}/cancel`, + metadata: { userId }, + }); + + res.json({ url: session.url }); +}); +``` + +### **3. Hantera framgångsrik betalning** +```typescript +router.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => { + const sig = req.headers['stripe-signature'] as string; + const event = stripe.webhooks.constructEvent( + req.body, + sig, + process.env.STRIPE_WEBHOOK_SECRET! + ); + + if (event.type === 'checkout.session.completed') { + const session = event.data.object as Stripe.Checkout.Session; + const userId = parseInt(session.metadata.userId); + + const expiryDate = new Date(); + expiryDate.setMonth(expiryDate.getMonth() + 1); + + await prisma.user.update({ + where: { id: userId }, + data: { + is_premium: true, + premium_expiry_date: expiryDate, + }, + }); + } + + res.json({ received: true }); +}); +``` + +--- +--- +## **📌 Sammanfattning** +Denna plan ger dig en **komplett lösning** för att: +1. **Importera PDF:er** (t.ex. Willys veckoblad). +2. **Extrahera och strukturera data** (med regex och OCR). +3. **Matcha produkter med inventory** (fuzzy matching). +4. **Generera recept**: + - **Gratis**: Med mallar. + - **Premium**: Med AI (Mistral). +5. **Hantera premium-uppgraderingar** (med Stripe). + +--- +--- +## **📥 Hur du använder denna plan i VS Code** +1. **Spara denna text** som `premium_recipe_feature_plan.md` i din projektmapp. +2. **Öppna filen i VS Code**. +3. **Använd VS Code-chatten** (t.ex. med **Mistral Vibe** eller **Continue**) för att: + - **Fråga om förtydliganden** för specifika steg. + - **Be om hjälp** med att implementera enskilda filer. + - **Debugga** om något inte fungerar. +4. **Kopiera och klistra in kodsnuttarna** i dina egna filer. + +--- +--- +## **💡 Tips för VS Code** +- **Använd `Ctrl+Shift+P`** för att öppna kommandopaletten och köra `Prisma: Generate Client`. +- **Installera ESLint** för att fånga syntaxfel tidigt. +- **Använd Git** för att spåra dina ändringar: + ```bash + git init + git add . + git commit -m "Lade till premium-receptfunktion" + ``` +``` + +--- +**Spara denna text som `premium_recipe_feature_plan.md` och ladda upp den i VS Code-chatten när du behöver hjälp med implementeringen!** 🚀 \ No newline at end of file