161 lines
4.7 KiB
TypeScript
161 lines
4.7 KiB
TypeScript
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<Actor, 'userId' | 'role'>,
|
|
): Promise<CommissionEntryDto[]> {
|
|
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<Actor, 'userId' | 'role'>,
|
|
): Prisma.CommissionEntryWhereInput {
|
|
if (actor.role === 'owner' || actor.role === 'admin') return {};
|
|
return { userId: actor.userId };
|
|
}
|
|
|
|
private canSeeInvoice(
|
|
actor: Pick<Actor, 'userId' | 'role'>,
|
|
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(),
|
|
};
|
|
}
|
|
}
|