import { Injectable } from '@nestjs/common'; import type { CommissionEntryDto } from '@mento/shared'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import type { Actor } from '../common/record-scope'; function roundMoney(value: number): number { return Math.round((value + Number.EPSILON) * 100) / 100; } @Injectable() export class CommissionsService { constructor(private readonly prisma: PrismaService) {} async list( organizationId: string, actor: Pick, ): Promise { const rows = await this.prisma.commissionEntry.findMany({ where: { organizationId, ...this.userScope(actor), }, include: { user: { select: { id: true, fullName: true } }, invoice: { select: { id: true, number: true, title: true, ownerId: true } }, }, orderBy: { createdAt: 'desc' }, take: 200, }); return rows .filter((row) => this.canSeeInvoice(actor, row.invoice.ownerId)) .map((row) => this.toDto(row)); } async syncInvoice(organizationId: string, invoiceId: string) { const invoice = await this.prisma.invoice.findFirst({ where: { id: invoiceId, organizationId }, include: { owner: { select: { id: true, commissionPercent: true } }, lineItems: true, commissionEntry: true, }, }); if (!invoice) return; const shouldPost = invoice.kind !== 'credit' && invoice.status !== 'draft' && invoice.status !== 'canceled' && Boolean(invoice.ownerId); if (!shouldPost) { if (invoice.commissionEntry) { await this.prisma.commissionEntry.update({ where: { id: invoice.commissionEntry.id }, data: { status: 'void', amount: 0 }, }); } return; } const subtotal = invoice.lineItems.reduce( (sum, item) => sum + Number(item.quantity) * Number(item.unitPrice), 0, ); const discountAmount = Number(invoice.discountAmount ?? 0); const taxPercent = Number(invoice.taxPercent ?? 0); const taxAmount = roundMoney( Math.max(0, subtotal - discountAmount) * (taxPercent / 100), ); const payable = Math.abs( roundMoney(subtotal - discountAmount + taxAmount), ); const fixed = invoice.commissionFixedAmount; const overridePercent = invoice.commissionPercent; const membership = invoice.ownerId ? await this.prisma.membership.findFirst({ where: { userId: invoice.ownerId, organizationId, isActive: true, }, select: { commissionPercent: true }, }) : null; const defaultPercent = Number(membership?.commissionPercent ?? 0); const percent = fixed != null ? payable > 0 ? roundMoney((Number(fixed) / payable) * 100) : 0 : Number(overridePercent ?? defaultPercent); const amount = fixed != null ? roundMoney(Number(fixed)) : roundMoney((payable * percent) / 100); await this.prisma.commissionEntry.upsert({ where: { invoiceId: invoice.id }, create: { organizationId, userId: invoice.ownerId!, invoiceId: invoice.id, percent: new Prisma.Decimal(percent), amount: new Prisma.Decimal(amount), currency: invoice.currency, status: 'posted', }, update: { userId: invoice.ownerId!, percent: new Prisma.Decimal(percent), amount: new Prisma.Decimal(amount), currency: invoice.currency, status: 'posted', }, }); } private userScope( actor: Pick, ): Prisma.CommissionEntryWhereInput { if (actor.role === 'owner' || actor.role === 'admin') return {}; return { userId: actor.userId }; } private canSeeInvoice( actor: Pick, ownerId: string | null, ) { if (actor.role === 'owner' || actor.role === 'admin') return true; return ownerId === actor.userId; } private toDto(row: { id: string; userId: string; invoiceId: string; percent: Prisma.Decimal; amount: Prisma.Decimal; currency: string; status: string; createdAt: Date; user: { fullName: string }; invoice: { number: string; title: string }; }): CommissionEntryDto { return { id: row.id, userId: row.userId, userName: row.user.fullName, invoiceId: row.invoiceId, invoiceNumber: row.invoice.number, invoiceTitle: row.invoice.title, percent: Number(row.percent), amount: Number(row.amount), currency: row.currency, status: row.status, createdAt: row.createdAt.toISOString(), }; } }