ledgerone_backend/write-rules-tax.mjs
2026-03-14 08:51:16 -04:00

415 lines
14 KiB
JavaScript

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<string, unknown>;
actions: Record<string, unknown>;
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<string, unknown>;
actions?: Record<string, unknown>;
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<string, unknown>,
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<string, unknown>;
const actions = rule.actions as Record<string, unknown>;
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<string, { category: string; count: number }>();
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<string, unknown>;
actions: Record<string, unknown>;
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<string, unknown>;
actions?: Record<string, unknown>;
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<string, unknown>,
) {
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<string, unknown> },
) {
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<string, unknown>;
}
`);
console.log("rules + tax files written");