Let cheques be received or paid without an invoice, and remind staff three days before due date.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-11 11:34:49 +03:30
co-authored by Cursor
parent 9a991fc1f1
commit 9beaed96e8
18 changed files with 982 additions and 61 deletions
+91 -13
View File
@@ -3,10 +3,14 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
import type { ChequeDto, UpdateChequeRequest } from '@mento/shared';
import type {
ChequeDto,
CreateChequeRequest,
UpdateChequeRequest,
} from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
import { type Actor, ownerWhere } from '../common/record-scope';
import { type Actor, elevated, ownerWhere } from '../common/record-scope';
const CHEQUE_STATUSES = ['pending', 'cleared', 'bounced'] as const;
@@ -17,13 +21,16 @@ export class ChequesService {
async list(
organizationId: string,
actor: Pick<Actor, 'userId' | 'role'>,
opts?: { status?: string; q?: string },
opts?: { status?: string; q?: string; direction?: string },
): Promise<ChequeDto[]> {
const rows = await this.prisma.cheque.findMany({
where: {
organizationId,
invoice: ownerWhere(actor),
...this.accessWhere(actor),
...(opts?.status ? { status: opts.status } : {}),
...(opts?.direction === 'in' || opts?.direction === 'out'
? { direction: opts.direction }
: {}),
...(opts?.q?.trim()
? {
OR: [
@@ -34,6 +41,24 @@ export class ChequesService {
number: { contains: opts.q.trim(), mode: 'insensitive' },
},
},
{
person: {
OR: [
{
firstName: {
contains: opts.q.trim(),
mode: 'insensitive',
},
},
{
lastName: {
contains: opts.q.trim(),
mode: 'insensitive',
},
},
],
},
},
],
}
: {}),
@@ -45,6 +70,45 @@ export class ChequesService {
return rows.map((row) => this.toDto(row));
}
async create(
organizationId: string,
actor: Pick<Actor, 'userId' | 'role'>,
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,
@@ -52,7 +116,7 @@ export class ChequesService {
dto: UpdateChequeRequest,
): Promise<ChequeDto> {
const existing = await this.prisma.cheque.findFirst({
where: { id, organizationId, invoice: ownerWhere(actor) },
where: { id, organizationId, ...this.accessWhere(actor) },
select: { id: true },
});
if (!existing) throw new NotFoundException('چک پیدا نشد');
@@ -79,8 +143,18 @@ export class ChequesService {
return this.toDto(row);
}
private accessWhere(
actor: Pick<Actor, 'userId' | 'role'>,
): Prisma.ChequeWhereInput {
if (elevated(actor.role)) return {};
return {
OR: [{ ownerId: actor.userId }, { invoice: { ownerId: actor.userId } }],
};
}
private include = {
payment: { select: { id: true, amount: true, currency: true, paidAt: true } },
person: { select: { firstName: true, lastName: true } },
invoice: {
select: {
id: true,
@@ -95,24 +169,28 @@ export class ChequesService {
private toDto(
row: Prisma.ChequeGetPayload<{ include: ChequesService['include'] }>,
): ChequeDto {
const personName = row.invoice.person
? `${row.invoice.person.firstName} ${row.invoice.person.lastName}`.trim()
: row.invoice.company?.name ?? null;
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,
invoiceTitle: row.invoice.title,
invoiceNumber: row.invoice?.number ?? null,
invoiceTitle: row.invoice?.title ?? null,
personId: row.personId,
personName,
amount: Number(row.payment.amount),
currency: row.payment.currency,
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.toISOString(),
paidAt: (row.payment?.paidAt ?? row.createdAt).toISOString(),
createdAt: row.createdAt.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};