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:
35
apps/api/src/plans/plans.controller.ts
Normal file
35
apps/api/src/plans/plans.controller.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Controller, Get, Post, Patch, Param, Body, UseGuards } from "@nestjs/common";
|
||||
import { PlansService } from "./plans.service";
|
||||
import { Public } from "../common/decorators/public.decorator";
|
||||
import { Roles } from "../common/decorators/roles.decorator";
|
||||
import { RolesGuard } from "../common/guards/roles.guard";
|
||||
|
||||
@Controller("plans")
|
||||
export class PlansController {
|
||||
constructor(private plansService: PlansService) {}
|
||||
|
||||
@Get()
|
||||
@Public()
|
||||
async findAll() {
|
||||
return this.plansService.findAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles("admin")
|
||||
async create(
|
||||
@Body() body: { name: string; brandCount: number; priceMonthly: number; priceYearly: number },
|
||||
) {
|
||||
return this.plansService.create(body);
|
||||
}
|
||||
|
||||
@Patch(":id")
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles("admin")
|
||||
async update(
|
||||
@Param("id") id: string,
|
||||
@Body() body: { name?: string; brandCount?: number; priceMonthly?: number; priceYearly?: number; isActive?: boolean },
|
||||
) {
|
||||
return this.plansService.update(id, body);
|
||||
}
|
||||
}
|
||||
10
apps/api/src/plans/plans.module.ts
Normal file
10
apps/api/src/plans/plans.module.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PlansController } from "./plans.controller";
|
||||
import { PlansService } from "./plans.service";
|
||||
|
||||
@Module({
|
||||
controllers: [PlansController],
|
||||
providers: [PlansService],
|
||||
exports: [PlansService],
|
||||
})
|
||||
export class PlansModule {}
|
||||
40
apps/api/src/plans/plans.service.ts
Normal file
40
apps/api/src/plans/plans.service.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { DATABASE, Database } from "../database/database.provider";
|
||||
import { plans } from "../database/schema/core";
|
||||
|
||||
@Injectable()
|
||||
export class PlansService {
|
||||
constructor(@Inject(DATABASE) private db: Database) {}
|
||||
|
||||
async findAll(activeOnly = true) {
|
||||
if (activeOnly) {
|
||||
return this.db.select().from(plans).where(eq(plans.isActive, true)).orderBy(plans.priceMonthly);
|
||||
}
|
||||
return this.db.select().from(plans).orderBy(plans.priceMonthly);
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
const result = await this.db.select().from(plans).where(eq(plans.id, id)).limit(1);
|
||||
if (result.length === 0) throw new NotFoundException("Plan not found");
|
||||
return result[0];
|
||||
}
|
||||
|
||||
async create(data: { name: string; brandCount: number; priceMonthly: number; priceYearly: number }) {
|
||||
const [plan] = await this.db.insert(plans).values(data).returning();
|
||||
return plan;
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
data: { name?: string; brandCount?: number; priceMonthly?: number; priceYearly?: number; isActive?: boolean },
|
||||
) {
|
||||
const [plan] = await this.db
|
||||
.update(plans)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(eq(plans.id, id))
|
||||
.returning();
|
||||
if (!plan) throw new NotFoundException("Plan not found");
|
||||
return plan;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user