Files
microservice-importer/plan importer willys.md.txt
nilsjohan cd830b9de8
Test Suite / test (24.15.0) (push) Has been cancelled
Upload files to "/"
2026-05-18 16:38:17 +02:00

946 lines
28 KiB
Plaintext
Raw Permalink 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.
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<string> {
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<any[]> {
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<any[]> {
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<string, any[]> = {};
relevantProducts.forEach((product) => {
const category = product.category || 'Okänt';
if (!productsByCategory[category]) productsByCategory[category] = [];
productsByCategory[category].push(product);
});
const templates: Record<string, any[]> = {
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<boolean> {
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 (
<div className="premium-status">
{isPremium ? (
<div className="premium-badge">
✅ Premium (gäller till: {new Date(user.premium_expiry_date).toLocaleDateString('sv-SE')})
<p>Du har tillgång till AI-genererade recept!</p>
</div>
) : (
<div className="upgrade-prompt">
🔒 <a href="/upgrade">Uppgradera till premium</a> för AI-recept!
</div>
)}
</div>
);
}
```
### **🔹 PDF-import-formulär**
```tsx
import { useState } from 'react';
import axios from 'axios';
function PDFImportForm({ user }) {
const [file, setFile] = useState<File | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [result, setResult] = useState<any>(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 (
<div>
<h2>Importera veckoblad</h2>
<form onSubmit={handleSubmit}>
<input
type="file"
accept=".pdf"
onChange={(e) => setFile(e.target.files?.[0] || null)}
required
/>
<button type="submit" disabled={isLoading}>
{isLoading ? 'Laddar upp...' : 'Importera'}
</button>
</form>
{result && (
<div>
<h3>Resultat</h3>
<p>Hittade {result.products?.length || 0} produkter.</p>
<p>Genererade {result.recipes?.length || 0} recept.</p>
{result.recipes?.map((recipe: any, index: number) => (
<div key={index} className="recipe-card">
<h4>{recipe.title}</h4>
<p>{recipe.description}</p>
<ul>
{recipe.ingredients?.map((ing: any, i: number) => (
<li key={i}>
{ing.quantity} {ing.name} {ing.onOffer && '🔥'}
</li>
))}
</ul>
</div>
))}
</div>
)}
</div>
);
}
```
---
---
## **📌 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!** 🚀