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:
295
apps/api/prisma/schema.prisma
Normal file
295
apps/api/prisma/schema.prisma
Normal 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
139
apps/api/prisma/seed.ts
Normal 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();
|
||||
});
|
||||
Reference in New Issue
Block a user