import { writeFileSync } from "fs"; // ─── rules.service.ts (Prisma) ─────────────────────────────────────────────── writeFileSync("src/rules/rules.service.ts", `import { BadRequestException, Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; @Injectable() export class RulesService { constructor(private readonly prisma: PrismaService) {} async list(userId: string) { return this.prisma.rule.findMany({ where: { userId, isActive: true }, orderBy: { priority: "asc" }, }); } async create( userId: string, payload: { name: string; priority?: number; conditions: Record; actions: Record; isActive?: boolean; }, ) { const count = await this.prisma.rule.count({ where: { userId } }); return this.prisma.rule.create({ data: { userId, name: payload.name ?? "Untitled rule", priority: payload.priority ?? count + 1, conditions: payload.conditions as Prisma.InputJsonValue, actions: payload.actions as Prisma.InputJsonValue, isActive: payload.isActive !== false, }, }); } async update( userId: string, id: string, payload: { name?: string; priority?: number; conditions?: Record; actions?: Record; isActive?: boolean; }, ) { const existing = await this.prisma.rule.findFirst({ where: { id, userId } }); if (!existing) throw new BadRequestException("Rule not found."); return this.prisma.rule.update({ where: { id }, data: { ...(payload.name !== undefined && { name: payload.name }), ...(payload.priority !== undefined && { priority: payload.priority }), ...(payload.conditions !== undefined && { conditions: payload.conditions as Prisma.InputJsonValue }), ...(payload.actions !== undefined && { actions: payload.actions as Prisma.InputJsonValue }), ...(payload.isActive !== undefined && { isActive: payload.isActive }), }, }); } private matchesRule( conditions: Record, tx: { description: string; amount: number | string }, ): boolean { const textContains = typeof conditions.textContains === "string" ? conditions.textContains : ""; const amountGt = typeof conditions.amountGreaterThan === "number" ? conditions.amountGreaterThan : null; const amountLt = typeof conditions.amountLessThan === "number" ? conditions.amountLessThan : null; if (textContains && !tx.description.toLowerCase().includes(textContains.toLowerCase())) { return false; } const amount = Number(tx.amount); if (amountGt !== null && amount <= amountGt) return false; if (amountLt !== null && amount >= amountLt) return false; return true; } async execute(userId: string, id: string) { const rule = await this.prisma.rule.findFirst({ where: { id, userId } }); if (!rule || !rule.isActive) return { id, status: "skipped" }; const conditions = rule.conditions as Record; const actions = rule.actions as Record; const transactions = await this.prisma.transactionRaw.findMany({ where: { account: { userId } }, include: { derived: true }, }); let applied = 0; for (const tx of transactions) { if (!this.matchesRule(conditions, { description: tx.description, amount: tx.amount })) { continue; } await this.prisma.transactionDerived.upsert({ where: { rawTransactionId: tx.id }, update: { userCategory: typeof actions.setCategory === "string" ? actions.setCategory : tx.derived?.userCategory ?? null, isHidden: typeof actions.setHidden === "boolean" ? actions.setHidden : tx.derived?.isHidden ?? false, modifiedAt: new Date(), modifiedBy: "rule", }, create: { rawTransactionId: tx.id, userCategory: typeof actions.setCategory === "string" ? actions.setCategory : null, isHidden: typeof actions.setHidden === "boolean" ? actions.setHidden : false, modifiedAt: new Date(), modifiedBy: "rule", }, }); await this.prisma.ruleExecution.create({ data: { ruleId: rule.id, transactionId: tx.id, result: { applied: true } as Prisma.InputJsonValue, }, }); applied += 1; } return { id: rule.id, status: "completed", applied }; } async suggest(userId: string) { const derived = await this.prisma.transactionDerived.findMany({ where: { raw: { account: { userId } }, userCategory: { not: null }, }, include: { raw: { select: { description: true } } }, take: 200, }); const bucket = new Map(); for (const item of derived) { const key = item.raw.description.toLowerCase(); const category = item.userCategory ?? "Uncategorized"; const entry = bucket.get(key) ?? { category, count: 0 }; entry.count += 1; bucket.set(key, entry); } return Array.from(bucket.entries()) .filter(([, value]) => value.count >= 2) .slice(0, 5) .map(([description, value], index) => ({ id: \`suggestion_\${index + 1}\`, name: \`Auto: \${value.category}\`, conditions: { textContains: description }, actions: { setCategory: value.category }, confidence: Math.min(0.95, 0.5 + value.count * 0.1), })); } } `); // ─── rules.controller.ts ───────────────────────────────────────────────────── writeFileSync("src/rules/rules.controller.ts", `import { Body, Controller, Get, Param, Post, Put } from "@nestjs/common"; import { ok } from "../common/response"; import { RulesService } from "./rules.service"; import { CurrentUser } from "../common/decorators/current-user.decorator"; @Controller("rules") export class RulesController { constructor(private readonly rulesService: RulesService) {} @Get() async list(@CurrentUser() userId: string) { const data = await this.rulesService.list(userId); return ok(data); } @Post() async create( @CurrentUser() userId: string, @Body() payload: { name: string; priority?: number; conditions: Record; actions: Record; isActive?: boolean; }, ) { const data = await this.rulesService.create(userId, payload); return ok(data); } @Put(":id") async update( @CurrentUser() userId: string, @Param("id") id: string, @Body() payload: { name?: string; priority?: number; conditions?: Record; actions?: Record; isActive?: boolean; }, ) { const data = await this.rulesService.update(userId, id, payload); return ok(data); } @Post(":id/execute") async execute(@CurrentUser() userId: string, @Param("id") id: string) { const data = await this.rulesService.execute(userId, id); return ok(data); } @Get("suggestions") async suggestions(@CurrentUser() userId: string) { const data = await this.rulesService.suggest(userId); return ok(data); } } `); // ─── rules.module.ts ───────────────────────────────────────────────────────── writeFileSync("src/rules/rules.module.ts", `import { Module } from "@nestjs/common"; import { RulesController } from "./rules.controller"; import { RulesService } from "./rules.service"; @Module({ controllers: [RulesController], providers: [RulesService], }) export class RulesModule {} `); // ─── tax.service.ts (Prisma) ───────────────────────────────────────────────── writeFileSync("src/tax/tax.service.ts", `import { BadRequestException, Injectable } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { PrismaService } from "../prisma/prisma.service"; import { CreateTaxReturnDto } from "./dto/create-return.dto"; import { UpdateTaxReturnDto } from "./dto/update-return.dto"; @Injectable() export class TaxService { constructor(private readonly prisma: PrismaService) {} async listReturns(userId: string) { return this.prisma.taxReturn.findMany({ where: { userId }, include: { documents: true }, orderBy: { createdAt: "desc" }, }); } async createReturn(userId: string, payload: CreateTaxReturnDto) { return this.prisma.taxReturn.create({ data: { userId, taxYear: payload.taxYear, filingType: payload.filingType, jurisdictions: payload.jurisdictions as Prisma.InputJsonValue, status: "draft", summary: {}, }, }); } async updateReturn(userId: string, id: string, payload: UpdateTaxReturnDto) { const existing = await this.prisma.taxReturn.findFirst({ where: { id, userId } }); if (!existing) throw new BadRequestException("Tax return not found."); return this.prisma.taxReturn.update({ where: { id }, data: { ...(payload.status !== undefined && { status: payload.status }), ...(payload.summary !== undefined && { summary: payload.summary as Prisma.InputJsonValue }), }, }); } async addDocument( userId: string, returnId: string, docType: string, metadata: Record, ) { const taxReturn = await this.prisma.taxReturn.findFirst({ where: { id: returnId, userId } }); if (!taxReturn) throw new BadRequestException("Tax return not found."); return this.prisma.taxDocument.create({ data: { taxReturnId: returnId, docType, metadata: metadata as Prisma.InputJsonValue, }, }); } async exportReturn(userId: string, id: string) { const taxReturn = await this.prisma.taxReturn.findFirst({ where: { id, userId }, include: { documents: true }, }); if (!taxReturn) throw new BadRequestException("Tax return not found."); await this.prisma.taxReturn.update({ where: { id }, data: { status: "exported" }, }); return { return: { ...taxReturn, status: "exported" }, documents: taxReturn.documents }; } } `); // ─── tax.controller.ts ─────────────────────────────────────────────────────── writeFileSync("src/tax/tax.controller.ts", `import { Body, Controller, Get, Param, Patch, Post } from "@nestjs/common"; import { ok } from "../common/response"; import { CreateTaxReturnDto } from "./dto/create-return.dto"; import { UpdateTaxReturnDto } from "./dto/update-return.dto"; import { TaxService } from "./tax.service"; import { CurrentUser } from "../common/decorators/current-user.decorator"; @Controller("tax") export class TaxController { constructor(private readonly taxService: TaxService) {} @Get("returns") async listReturns(@CurrentUser() userId: string) { const data = await this.taxService.listReturns(userId); return ok(data); } @Post("returns") async createReturn(@CurrentUser() userId: string, @Body() payload: CreateTaxReturnDto) { const data = await this.taxService.createReturn(userId, payload); return ok(data); } @Patch("returns/:id") async updateReturn( @CurrentUser() userId: string, @Param("id") id: string, @Body() payload: UpdateTaxReturnDto, ) { const data = await this.taxService.updateReturn(userId, id, payload); return ok(data); } @Post("returns/:id/documents") async addDocument( @CurrentUser() userId: string, @Param("id") id: string, @Body() payload: { docType: string; metadata?: Record }, ) { const data = await this.taxService.addDocument(userId, id, payload.docType, payload.metadata ?? {}); return ok(data); } @Post("returns/:id/export") async exportReturn(@CurrentUser() userId: string, @Param("id") id: string) { const data = await this.taxService.exportReturn(userId, id); return ok(data); } } `); // ─── tax.module.ts ─────────────────────────────────────────────────────────── writeFileSync("src/tax/tax.module.ts", `import { Module } from "@nestjs/common"; import { TaxController } from "./tax.controller"; import { TaxService } from "./tax.service"; @Module({ controllers: [TaxController], providers: [TaxService], }) export class TaxModule {} `); // ─── tax DTOs (updated to class-validator) ─────────────────────────────────── writeFileSync("src/tax/dto/create-return.dto.ts", `import { IsArray, IsIn, IsInt, IsString } from "class-validator"; export class CreateTaxReturnDto { @IsInt() taxYear!: number; @IsString() @IsIn(["individual", "business"]) filingType!: "individual" | "business"; @IsArray() @IsString({ each: true }) jurisdictions!: string[]; } `); writeFileSync("src/tax/dto/update-return.dto.ts", `import { IsIn, IsObject, IsOptional, IsString } from "class-validator"; export class UpdateTaxReturnDto { @IsOptional() @IsString() @IsIn(["draft", "ready", "exported"]) status?: "draft" | "ready" | "exported"; @IsOptional() @IsObject() summary?: Record; } `); console.log("rules + tax files written");