189 lines
5.8 KiB
TypeScript
189 lines
5.8 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import type {
|
|
ChequeDto,
|
|
CreateChequeRequest,
|
|
UpdateChequeRequest,
|
|
} from '@mento/shared';
|
|
import { Prisma } from '@prisma/client';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { type ScopedActor, ownerWhere, scopedOwnerIds } from '../common/record-scope';
|
|
import { buildSearchWhere, ci } from '../common/search';
|
|
|
|
const CHEQUE_STATUSES = ['pending', 'cleared', 'bounced'] as const;
|
|
|
|
@Injectable()
|
|
export class ChequesService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
async list(
|
|
organizationId: string,
|
|
actor: ScopedActor,
|
|
opts?: { status?: string; q?: string; direction?: string },
|
|
): Promise<ChequeDto[]> {
|
|
const rows = await this.prisma.cheque.findMany({
|
|
where: {
|
|
organizationId,
|
|
...this.accessWhere(actor),
|
|
...(opts?.status ? { status: opts.status } : {}),
|
|
...(opts?.direction === 'in' || opts?.direction === 'out'
|
|
? { direction: opts.direction }
|
|
: {}),
|
|
...this.searchWhere(opts?.q),
|
|
},
|
|
include: this.include,
|
|
orderBy: [{ dueDate: 'asc' }, { createdAt: 'desc' }],
|
|
take: 300,
|
|
});
|
|
return rows.map((row) => this.toDto(row));
|
|
}
|
|
|
|
private searchWhere(q?: string): Prisma.ChequeWhereInput {
|
|
const search = buildSearchWhere<Prisma.ChequeWhereInput>(q, (t) => [
|
|
{ number: ci(t) },
|
|
{ bank: ci(t) },
|
|
{ invoice: { number: ci(t) } },
|
|
{
|
|
person: {
|
|
OR: [
|
|
{ firstName: ci(t) },
|
|
{ lastName: ci(t) },
|
|
{ mobile: { contains: t } },
|
|
],
|
|
},
|
|
},
|
|
]);
|
|
return search ? { AND: [search] } : {};
|
|
}
|
|
|
|
async create(
|
|
organizationId: string,
|
|
actor: ScopedActor,
|
|
dto: CreateChequeRequest,
|
|
): Promise<ChequeDto> {
|
|
const person = await this.prisma.person.findFirst({
|
|
where: { id: dto.personId, organizationId, ...ownerWhere(actor) },
|
|
select: { id: true },
|
|
});
|
|
if (!person) throw new BadRequestException('شخص معتبر نیست');
|
|
if (!(dto.amount > 0)) {
|
|
throw new BadRequestException('مبلغ چک لازم است');
|
|
}
|
|
if (!dto.number?.trim() || !dto.bank?.trim() || !dto.dueDate) {
|
|
throw new BadRequestException('شماره، بانک و سررسید چک لازم است');
|
|
}
|
|
|
|
const row = await this.prisma.cheque.create({
|
|
data: {
|
|
organizationId,
|
|
personId: person.id,
|
|
ownerId: actor.userId,
|
|
amount: new Prisma.Decimal(dto.amount),
|
|
currency: (dto.currency ?? 'IRR').trim() || 'IRR',
|
|
number: dto.number.trim(),
|
|
bank: dto.bank.trim(),
|
|
dueDate: new Date(dto.dueDate),
|
|
direction: dto.direction === 'out' ? 'out' : 'in',
|
|
status:
|
|
dto.status === 'cleared' || dto.status === 'bounced'
|
|
? dto.status
|
|
: 'pending',
|
|
notes: dto.notes?.trim() ? dto.notes.trim() : null,
|
|
},
|
|
include: this.include,
|
|
});
|
|
return this.toDto(row);
|
|
}
|
|
|
|
async update(
|
|
organizationId: string,
|
|
id: string,
|
|
actor: ScopedActor,
|
|
dto: UpdateChequeRequest,
|
|
): Promise<ChequeDto> {
|
|
const existing = await this.prisma.cheque.findFirst({
|
|
where: { id, organizationId, ...this.accessWhere(actor) },
|
|
select: { id: true },
|
|
});
|
|
if (!existing) throw new NotFoundException('چک پیدا نشد');
|
|
|
|
const data: Prisma.ChequeUncheckedUpdateInput = {};
|
|
if (dto.number !== undefined) data.number = dto.number.trim();
|
|
if (dto.bank !== undefined) data.bank = dto.bank.trim();
|
|
if (dto.dueDate !== undefined) data.dueDate = new Date(dto.dueDate);
|
|
if (dto.status !== undefined) {
|
|
if (!CHEQUE_STATUSES.includes(dto.status as (typeof CHEQUE_STATUSES)[number])) {
|
|
throw new BadRequestException('وضعیت چک معتبر نیست');
|
|
}
|
|
data.status = dto.status;
|
|
}
|
|
if ('notes' in dto) {
|
|
data.notes = dto.notes?.trim() ? dto.notes.trim() : null;
|
|
}
|
|
|
|
const row = await this.prisma.cheque.update({
|
|
where: { id },
|
|
data,
|
|
include: this.include,
|
|
});
|
|
return this.toDto(row);
|
|
}
|
|
|
|
private accessWhere(
|
|
actor: ScopedActor,
|
|
): Prisma.ChequeWhereInput {
|
|
const ids = scopedOwnerIds(actor, 'finance');
|
|
if (ids === null) return {};
|
|
return {
|
|
OR: [{ ownerId: { in: ids } }, { invoice: { ownerId: { in: ids } } }],
|
|
};
|
|
}
|
|
|
|
private include = {
|
|
payment: { select: { id: true, amount: true, currency: true, paidAt: true } },
|
|
person: { select: { firstName: true, lastName: true } },
|
|
invoice: {
|
|
select: {
|
|
id: true,
|
|
number: true,
|
|
title: true,
|
|
person: { select: { firstName: true, lastName: true } },
|
|
company: { select: { name: true } },
|
|
},
|
|
},
|
|
} satisfies Prisma.ChequeInclude;
|
|
|
|
private toDto(
|
|
row: Prisma.ChequeGetPayload<{ include: ChequesService['include'] }>,
|
|
): ChequeDto {
|
|
const personName = row.person
|
|
? `${row.person.firstName} ${row.person.lastName}`.trim()
|
|
: row.invoice?.person
|
|
? `${row.invoice.person.firstName} ${row.invoice.person.lastName}`.trim()
|
|
: row.invoice?.company?.name ?? null;
|
|
return {
|
|
id: row.id,
|
|
paymentId: row.paymentId,
|
|
invoiceId: row.invoiceId,
|
|
invoiceNumber: row.invoice?.number ?? null,
|
|
invoiceTitle: row.invoice?.title ?? null,
|
|
personId: row.personId,
|
|
personName,
|
|
amount: Number(row.amount ?? row.payment?.amount ?? 0),
|
|
currency: row.currency || row.payment?.currency || 'IRR',
|
|
number: row.number,
|
|
bank: row.bank,
|
|
dueDate: row.dueDate.toISOString(),
|
|
status: row.status,
|
|
direction: row.direction === 'out' ? 'out' : 'in',
|
|
notes: row.notes,
|
|
paidAt: (row.payment?.paidAt ?? row.createdAt).toISOString(),
|
|
createdAt: row.createdAt.toISOString(),
|
|
updatedAt: row.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
}
|