Initial commit: Sase.tr VIN Sorgulama Platformu

Features:
- Next.js 16.1.3 frontend with Turbopack
- NestJS API with Prisma ORM
- EMEX VIN scraper integration
- Turkish translations for automotive parts
- JWT authentication with refresh tokens
- PM2 production deployment

Tech Stack:
- Frontend: Next.js 16.1, React 19, TailwindCSS
- Backend: NestJS, Prisma, MySQL
- Scraping: Puppeteer

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
root
2026-01-17 03:04:30 +01:00
commit b9532a61ee
195 changed files with 31777 additions and 0 deletions

View File

@@ -0,0 +1,295 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// ==================== USERS ====================
model User {
id String @id @default(cuid())
email String @unique
name String?
avatar String?
role Role @default(USER)
provider AuthProvider @default(EMAIL)
providerId String?
passwordHash String?
isActive Boolean @default(true)
subscription UserSubscription?
selectedBrands UserBrand[]
vehicles Vehicle[] @relation("QueriedBy")
queryLogs QueryLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([email])
@@index([role])
@@map("users")
}
enum Role {
USER
MODERATOR
ADMIN
SUPER_ADMIN
}
enum AuthProvider {
EMAIL
GOOGLE
APPLE
FACEBOOK
}
// ==================== BRANDS ====================
model Brand {
id String @id @default(cuid())
code String @unique
name String
logo String?
isActive Boolean @default(true)
sortOrder Int @default(0)
vehicles Vehicle[]
userBrands UserBrand[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("brands")
}
// ==================== SUBSCRIPTIONS ====================
model Plan {
id String @id @default(cuid())
name String
slug String @unique
description String? @db.Text
price Decimal @db.Decimal(10, 2)
currency String @default("TRY")
brandLimit Int
hasFullAccess Boolean @default(false)
features Json
durationDays Int @default(30)
sortOrder Int @default(0)
isActive Boolean @default(true)
isPopular Boolean @default(false)
subscriptions UserSubscription[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("plans")
}
model UserSubscription {
id String @id @default(cuid())
userId String @unique
planId String
status SubscriptionStatus @default(ACTIVE)
currentPeriodStart DateTime @default(now())
currentPeriodEnd DateTime
cancelAtPeriodEnd Boolean @default(false)
cancelledAt DateTime?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
plan Plan @relation(fields: [planId], references: [id])
payments Payment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([currentPeriodEnd])
@@map("user_subscriptions")
}
model UserBrand {
id String @id @default(cuid())
userId String
brandId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
brand Brand @relation(fields: [brandId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, brandId])
@@index([userId])
@@map("user_brands")
}
enum SubscriptionStatus {
ACTIVE
PAST_DUE
CANCELLED
EXPIRED
SUSPENDED
PENDING
}
// ==================== VEHICLES ====================
model Vehicle {
id String @id @default(cuid())
vin String @unique @db.VarChar(17)
brandId String
model String
year Int
series String?
bodyType String?
engineCode String?
engineType String?
engineVolume String?
transmission String?
driveType String?
colorCode String?
rawResponse Json
queriedById String?
queriedBy User? @relation("QueriedBy", fields: [queriedById], references: [id], onDelete: SetNull)
brand Brand @relation(fields: [brandId], references: [id])
categories VehicleCategory[]
parts Part[]
queryLogs QueryLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([vin])
@@index([brandId])
@@map("vehicles")
}
// ==================== CATEGORIES ====================
model Category {
id String @id @default(cuid())
code String @unique
nameEn String
nameTr String
slug String @unique
description String? @db.Text
parentId String?
iconName String?
schemaImageUrl String?
sortOrder Int @default(0)
isActive Boolean @default(true)
parent Category? @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
children Category[] @relation("CategoryTree")
vehicleCategories VehicleCategory[]
parts Part[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([parentId])
@@map("categories")
}
model VehicleCategory {
id String @id @default(cuid())
vehicleId String
categoryId String
partCount Int @default(0)
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([vehicleId, categoryId])
@@index([vehicleId])
@@index([categoryId])
@@map("vehicle_categories")
}
// ==================== PARTS ====================
model Part {
id String @id @default(cuid())
vehicleId String
categoryId String
oemCode String
oemCodes Json?
nameEn String
nameTr String
description String? @db.Text
positionCode String?
positionX Float?
positionY Float?
brandPrices Json @default("[]")
imageUrl String?
notes String? @db.Text
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([oemCode])
@@index([vehicleId])
@@index([categoryId])
@@index([vehicleId, categoryId])
@@map("parts")
}
// ==================== PAYMENTS ====================
model Payment {
id String @id @default(cuid())
subscriptionId String
amount Decimal @db.Decimal(10, 2)
currency String @default("TRY")
status PaymentStatus @default(PENDING)
provider String @default("iyzico")
providerTxId String? @unique
providerData Json?
invoiceNumber String?
invoiceUrl String?
failureReason String?
subscription UserSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status])
@@index([providerTxId])
@@map("payments")
}
enum PaymentStatus {
PENDING
PROCESSING
COMPLETED
FAILED
REFUNDED
CANCELLED
}
// ==================== QUERY LOGS ====================
model QueryLog {
id String @id @default(cuid())
userId String
vehicleId String?
vin String @db.VarChar(17)
responseTime Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
vehicle Vehicle? @relation(fields: [vehicleId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
@@index([userId])
@@index([vin])
@@index([createdAt])
@@index([userId, createdAt])
@@map("query_logs")
}

139
apps/api/prisma/seed.ts Normal file
View File

@@ -0,0 +1,139 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log('Seeding database...');
// Seed Brands
const brands = [
{ code: 'FIAT', name: 'Fiat', sortOrder: 1 },
{ code: 'RENAULT', name: 'Renault', sortOrder: 2 },
{ code: 'VOLKSWAGEN', name: 'Volkswagen', sortOrder: 3 },
{ code: 'BMW', name: 'BMW', sortOrder: 4 },
{ code: 'MERCEDES', name: 'Mercedes-Benz', sortOrder: 5 },
{ code: 'AUDI', name: 'Audi', sortOrder: 6 },
{ code: 'TOYOTA', name: 'Toyota', sortOrder: 7 },
{ code: 'HONDA', name: 'Honda', sortOrder: 8 },
{ code: 'FORD', name: 'Ford', sortOrder: 9 },
{ code: 'OPEL', name: 'Opel', sortOrder: 10 },
{ code: 'HYUNDAI', name: 'Hyundai', sortOrder: 11 },
{ code: 'KIA', name: 'Kia', sortOrder: 12 },
{ code: 'PEUGEOT', name: 'Peugeot', sortOrder: 13 },
{ code: 'CITROEN', name: 'Citroen', sortOrder: 14 },
{ code: 'SKODA', name: 'Skoda', sortOrder: 15 },
{ code: 'SEAT', name: 'SEAT', sortOrder: 16 },
{ code: 'NISSAN', name: 'Nissan', sortOrder: 17 },
{ code: 'MAZDA', name: 'Mazda', sortOrder: 18 },
{ code: 'VOLVO', name: 'Volvo', sortOrder: 19 },
{ code: 'DACIA', name: 'Dacia', sortOrder: 20 },
];
for (const brand of brands) {
await prisma.brand.upsert({
where: { code: brand.code },
update: {},
create: brand,
});
}
console.log(`Seeded ${brands.length} brands`);
// Seed Plans
const plans = [
{
name: 'Baslangic',
slug: 'starter',
description: 'Tek marka icin ideal baslangic paketi',
price: 299,
currency: 'TRY',
brandLimit: 1,
hasFullAccess: false,
features: JSON.stringify(['1 marka erisimi', 'Sinirsiz sorgu', 'Email destek']),
durationDays: 30,
sortOrder: 1,
isPopular: false,
},
{
name: 'Pro',
slug: 'pro',
description: 'Kucuk isletmeler icin ideal paket',
price: 599,
currency: 'TRY',
brandLimit: 3,
hasFullAccess: false,
features: JSON.stringify(['3 marka erisimi', 'Sinirsiz sorgu', 'Oncelikli destek']),
durationDays: 30,
sortOrder: 2,
isPopular: true,
},
{
name: 'Isletme',
slug: 'business',
description: 'Orta olcekli isletmeler icin',
price: 999,
currency: 'TRY',
brandLimit: 10,
hasFullAccess: false,
features: JSON.stringify(['10 marka erisimi', 'Sinirsiz sorgu', 'Telefon destek', 'API erisimi']),
durationDays: 30,
sortOrder: 3,
isPopular: false,
},
{
name: 'Full',
slug: 'full',
description: 'Tum markalara sinirsiz erisim',
price: 1999,
currency: 'TRY',
brandLimit: 0,
hasFullAccess: true,
features: JSON.stringify(['Tum markalar', 'Sinirsiz sorgu', '7/24 destek', 'API erisimi', 'Ozel entegrasyon']),
durationDays: 30,
sortOrder: 4,
isPopular: false,
},
];
for (const plan of plans) {
await prisma.plan.upsert({
where: { slug: plan.slug },
update: {},
create: plan,
});
}
console.log(`Seeded ${plans.length} plans`);
// Seed Categories
const categories = [
{ code: 'ENGINE', nameEn: 'Engine', nameTr: 'Motor', slug: 'motor', iconName: 'engine', sortOrder: 1 },
{ code: 'BRAKE', nameEn: 'Brake System', nameTr: 'Fren Sistemi', slug: 'fren-sistemi', iconName: 'brake', sortOrder: 2 },
{ code: 'SUSPENSION', nameEn: 'Suspension', nameTr: 'Suspansiyon', slug: 'suspansiyon', iconName: 'suspension', sortOrder: 3 },
{ code: 'ELECTRICAL', nameEn: 'Electrical', nameTr: 'Elektrik', slug: 'elektrik', iconName: 'zap', sortOrder: 4 },
{ code: 'BODY', nameEn: 'Body', nameTr: 'Govde', slug: 'govde', iconName: 'car', sortOrder: 5 },
{ code: 'INTERIOR', nameEn: 'Interior', nameTr: 'Ic Mekan', slug: 'ic-mekan', iconName: 'sofa', sortOrder: 6 },
{ code: 'EXTERIOR', nameEn: 'Exterior', nameTr: 'Dis Mekan', slug: 'dis-mekan', iconName: 'sun', sortOrder: 7 },
{ code: 'TRANSMISSION', nameEn: 'Transmission', nameTr: 'Sanziman', slug: 'sanziman', iconName: 'settings', sortOrder: 8 },
{ code: 'EXHAUST', nameEn: 'Exhaust', nameTr: 'Egzoz', slug: 'egzoz', iconName: 'wind', sortOrder: 9 },
{ code: 'COOLING', nameEn: 'Cooling', nameTr: 'Sogutma', slug: 'sogutma', iconName: 'thermometer', sortOrder: 10 },
];
for (const category of categories) {
await prisma.category.upsert({
where: { code: category.code },
update: {},
create: category,
});
}
console.log(`Seeded ${categories.length} categories`);
console.log('Seeding completed!');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});