feat: sase.tr v2 full application implementation
Complete rewrite of sase.tr VIN lookup platform with modern stack: Backend (NestJS 10 + Drizzle ORM + PostgreSQL + Redis + BullMQ): - 34 DB models (core + PL24 + EMEX schemas) - Auth via Better Auth (email/password + social) - Brands, Plans, Subscriptions, Payments (iyzico + EFT) - VIN decode orchestration (Corgi + PL24 + EMEX + NHTSA) - Interactive schema viewer backend (MinIO storage) - EMEX scraping integration (Puppeteer + BullMQ workers) - Translation module (EN→TR automotive dictionary) - Admin dashboard API (stats, user mgmt, payment approval) - Rate limiting, Helmet security, file upload validation Frontend (Next.js 15 + Tailwind v4 + shadcn/ui + TanStack Query + Zustand): - 20 routes: auth, dashboard, VIN search, schema viewer, admin - Interactive schema viewer with zoom/pan/hotspot highlighting - Subscription management with brand selector - Payment flow (iyzico 3D Secure + EFT with receipt upload) - i18n support (TR/EN) - Error boundaries, loading skeletons, 404 page Infrastructure: - 85 tests (52 backend + 33 frontend, Vitest) - CI/CD (GitHub Actions: lint, typecheck, test, build, deploy) - Zero-downtime deploy script (PM2) - Env validation script Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
22
apps/api/src/parts/parts.controller.ts
Normal file
22
apps/api/src/parts/parts.controller.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Controller, Get, Param, Query } from "@nestjs/common";
|
||||
import { PartsService } from "./parts.service";
|
||||
|
||||
@Controller("parts")
|
||||
export class PartsController {
|
||||
constructor(private partsService: PartsService) {}
|
||||
|
||||
@Get("category/:categoryId")
|
||||
async getByCategory(@Param("categoryId") categoryId: string) {
|
||||
return this.partsService.getByCategory(categoryId);
|
||||
}
|
||||
|
||||
@Get("search")
|
||||
async searchByOem(@Query("oem") oem: string) {
|
||||
return this.partsService.searchByOem(oem);
|
||||
}
|
||||
|
||||
@Get(":id")
|
||||
async getById(@Param("id") id: string) {
|
||||
return this.partsService.getById(id);
|
||||
}
|
||||
}
|
||||
12
apps/api/src/parts/parts.module.ts
Normal file
12
apps/api/src/parts/parts.module.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PartsController } from "./parts.controller";
|
||||
import { PartsService } from "./parts.service";
|
||||
import { PL24Module } from "../integrations/pl24/pl24.module";
|
||||
|
||||
@Module({
|
||||
imports: [PL24Module],
|
||||
controllers: [PartsController],
|
||||
providers: [PartsService],
|
||||
exports: [PartsService],
|
||||
})
|
||||
export class PartsModule {}
|
||||
107
apps/api/src/parts/parts.service.ts
Normal file
107
apps/api/src/parts/parts.service.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { eq, like } from "drizzle-orm";
|
||||
import { DATABASE, Database } from "../database/database.provider";
|
||||
import { parts, categories, vehicles } from "../database/schema/core";
|
||||
import { PL24Service } from "../integrations/pl24/pl24.service";
|
||||
|
||||
@Injectable()
|
||||
export class PartsService {
|
||||
constructor(
|
||||
@Inject(DATABASE) private db: Database,
|
||||
private pl24Service: PL24Service,
|
||||
) {}
|
||||
|
||||
async getByCategory(categoryId: string) {
|
||||
// Check DB first
|
||||
let dbParts = await this.db
|
||||
.select()
|
||||
.from(parts)
|
||||
.where(eq(parts.categoryId, categoryId));
|
||||
|
||||
if (dbParts.length > 0) return dbParts;
|
||||
|
||||
// Fetch from PL24 on-demand
|
||||
const [category] = await this.db
|
||||
.select()
|
||||
.from(categories)
|
||||
.where(eq(categories.id, categoryId))
|
||||
.limit(1);
|
||||
|
||||
if (!category) throw new NotFoundException("Category not found");
|
||||
|
||||
const [vehicle] = await this.db
|
||||
.select()
|
||||
.from(vehicles)
|
||||
.where(eq(vehicles.id, category.vehicleId))
|
||||
.limit(1);
|
||||
|
||||
if (!vehicle) throw new NotFoundException("Vehicle not found");
|
||||
|
||||
const rawData = vehicle.rawData as any;
|
||||
const pl24VehicleId = rawData?.vehicleId;
|
||||
const groupId = category.externalId;
|
||||
|
||||
if (pl24VehicleId && groupId && vehicle.brandName) {
|
||||
const pl24Parts = await this.pl24Service.getParts(pl24VehicleId, groupId, vehicle.brandName);
|
||||
|
||||
if (pl24Parts.length > 0) {
|
||||
const insertData = pl24Parts.flatMap((p) =>
|
||||
p.oemCodes.length > 0
|
||||
? p.oemCodes.map((oem) => ({
|
||||
vehicleId: vehicle.id,
|
||||
categoryId,
|
||||
oemCode: oem,
|
||||
name: p.name,
|
||||
nameOriginal: p.name,
|
||||
description: p.description || null,
|
||||
quantity: p.quantity || null,
|
||||
position: p.position || null,
|
||||
hotspotIndex: p.hotspotIndex ?? null,
|
||||
source: "pl24" as const,
|
||||
}))
|
||||
: [
|
||||
{
|
||||
vehicleId: vehicle.id,
|
||||
categoryId,
|
||||
oemCode: "N/A",
|
||||
name: p.name,
|
||||
nameOriginal: p.name,
|
||||
description: p.description || null,
|
||||
quantity: p.quantity || null,
|
||||
position: p.position || null,
|
||||
hotspotIndex: p.hotspotIndex ?? null,
|
||||
source: "pl24" as const,
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
dbParts = await this.db.insert(parts).values(insertData).returning();
|
||||
}
|
||||
}
|
||||
|
||||
return dbParts;
|
||||
}
|
||||
|
||||
async searchByOem(oemCode: string) {
|
||||
return this.db
|
||||
.select({
|
||||
part: parts,
|
||||
vehicle: {
|
||||
vin: vehicles.vin,
|
||||
brandName: vehicles.brandName,
|
||||
model: vehicles.model,
|
||||
year: vehicles.year,
|
||||
},
|
||||
})
|
||||
.from(parts)
|
||||
.innerJoin(vehicles, eq(parts.vehicleId, vehicles.id))
|
||||
.where(like(parts.oemCode, `%${oemCode}%`))
|
||||
.limit(50);
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
const [part] = await this.db.select().from(parts).where(eq(parts.id, id)).limit(1);
|
||||
if (!part) throw new NotFoundException("Part not found");
|
||||
return part;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user