|
|
|
@@ -13,11 +13,20 @@ function dec(value: Prisma.Decimal | number | string | null | undefined) {
|
|
|
|
|
function money(value: Prisma.Decimal | number | string) {
|
|
|
|
|
return new Prisma.Decimal(value).toFixed(2);
|
|
|
|
|
}
|
|
|
|
|
function reasonText(value?: string | null) {
|
|
|
|
|
const text = value?.trim() ?? '';
|
|
|
|
|
return text ? text.slice(0, 500) : null;
|
|
|
|
|
}
|
|
|
|
|
function parseDate(iso?: string | null) {
|
|
|
|
|
if (!iso) return null;
|
|
|
|
|
const d = new Date(iso);
|
|
|
|
|
return Number.isNaN(d.getTime()) ? null : d;
|
|
|
|
|
}
|
|
|
|
|
function payrollStatus(payable: Prisma.Decimal, paid: Prisma.Decimal) {
|
|
|
|
|
if (paid.lessThanOrEqualTo(0)) return 'open';
|
|
|
|
|
if (paid.greaterThanOrEqualTo(payable)) return 'settled';
|
|
|
|
|
return 'partial';
|
|
|
|
|
}
|
|
|
|
|
function periodKey(date: Date) {
|
|
|
|
|
return date.toISOString().slice(0, 10);
|
|
|
|
|
}
|
|
|
|
@@ -62,8 +71,11 @@ export class TreasuryOpsService {
|
|
|
|
|
period: string;
|
|
|
|
|
baseAmount: string | number;
|
|
|
|
|
benefits?: string | number;
|
|
|
|
|
benefitsReason?: string | null;
|
|
|
|
|
bonus?: string | number;
|
|
|
|
|
bonusReason?: string | null;
|
|
|
|
|
deductions?: string | number;
|
|
|
|
|
deductionsReason?: string | null;
|
|
|
|
|
dueDate?: string;
|
|
|
|
|
notes?: string | null;
|
|
|
|
|
},
|
|
|
|
@@ -88,8 +100,11 @@ export class TreasuryOpsService {
|
|
|
|
|
period: input.period.trim(),
|
|
|
|
|
baseAmount: base,
|
|
|
|
|
benefits,
|
|
|
|
|
benefitsReason: benefits.greaterThan(0) ? reasonText(input.benefitsReason) : null,
|
|
|
|
|
bonus,
|
|
|
|
|
bonusReason: bonus.greaterThan(0) ? reasonText(input.bonusReason) : null,
|
|
|
|
|
deductions,
|
|
|
|
|
deductionsReason: deductions.greaterThan(0) ? reasonText(input.deductionsReason) : null,
|
|
|
|
|
payable,
|
|
|
|
|
dueDate: parseDate(input.dueDate) ?? new Date(),
|
|
|
|
|
notes: input.notes?.trim() || null,
|
|
|
|
@@ -137,7 +152,7 @@ export class TreasuryOpsService {
|
|
|
|
|
if (!account) throw new BadRequestException('حساب پرداخت پیدا نشد.');
|
|
|
|
|
const balance = await this.balance(account.id);
|
|
|
|
|
if (balance.lessThan(amount)) throw new BadRequestException('موجودی حساب کافی نیست.');
|
|
|
|
|
const status = paid.plus(amount).equals(row.payable) ? 'settled' : 'partial';
|
|
|
|
|
const status = payrollStatus(row.payable, paid.plus(amount));
|
|
|
|
|
await this.prisma.$transaction([
|
|
|
|
|
this.prisma.financialMovement.create({
|
|
|
|
|
data: {
|
|
|
|
@@ -158,6 +173,123 @@ export class TreasuryOpsService {
|
|
|
|
|
return this.listPayroll(actor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updatePayroll(
|
|
|
|
|
actor: Actor,
|
|
|
|
|
id: string,
|
|
|
|
|
input: {
|
|
|
|
|
userId: string;
|
|
|
|
|
period: string;
|
|
|
|
|
baseAmount: string | number;
|
|
|
|
|
benefits?: string | number;
|
|
|
|
|
benefitsReason?: string | null;
|
|
|
|
|
bonus?: string | number;
|
|
|
|
|
bonusReason?: string | null;
|
|
|
|
|
deductions?: string | number;
|
|
|
|
|
deductionsReason?: string | null;
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
const row = await this.prisma.financialPayroll.findFirst({
|
|
|
|
|
where: { id, organizationId: actor.organizationId },
|
|
|
|
|
include: { movements: true },
|
|
|
|
|
});
|
|
|
|
|
if (!row || row.voidedAt || row.status === 'void') {
|
|
|
|
|
throw new NotFoundException('رکورد حقوق برای ویرایش پیدا نشد.');
|
|
|
|
|
}
|
|
|
|
|
const base = dec(input.baseAmount);
|
|
|
|
|
const benefits = dec(input.benefits ?? 0);
|
|
|
|
|
const bonus = dec(input.bonus ?? 0);
|
|
|
|
|
const deductions = dec(input.deductions ?? 0);
|
|
|
|
|
const payable = base.plus(benefits).plus(bonus).minus(deductions);
|
|
|
|
|
if (!input.userId || !input.period?.trim()) {
|
|
|
|
|
throw new BadRequestException('پرسنل و دوره حقوق لازم است.');
|
|
|
|
|
}
|
|
|
|
|
if (!payable.greaterThan(0)) throw new BadRequestException('مبلغ قابل پرداخت باید بزرگتر از صفر باشد.');
|
|
|
|
|
const paid = this.paidOf(row.movements);
|
|
|
|
|
if (payable.lessThan(paid)) {
|
|
|
|
|
throw new BadRequestException('مبلغ قابل پرداخت از پرداختهای ثبتشده کمتر است.');
|
|
|
|
|
}
|
|
|
|
|
const member = await this.prisma.membership.findFirst({
|
|
|
|
|
where: { organizationId: actor.organizationId, userId: input.userId, isActive: true },
|
|
|
|
|
});
|
|
|
|
|
if (!member) throw new BadRequestException('این کاربر عضو محیط نیست.');
|
|
|
|
|
try {
|
|
|
|
|
await this.prisma.financialPayroll.update({
|
|
|
|
|
where: { id: row.id },
|
|
|
|
|
data: {
|
|
|
|
|
userId: input.userId,
|
|
|
|
|
period: input.period.trim(),
|
|
|
|
|
baseAmount: base,
|
|
|
|
|
benefits,
|
|
|
|
|
benefitsReason: benefits.greaterThan(0) ? reasonText(input.benefitsReason) : null,
|
|
|
|
|
bonus,
|
|
|
|
|
bonusReason: bonus.greaterThan(0) ? reasonText(input.bonusReason) : null,
|
|
|
|
|
deductions,
|
|
|
|
|
deductionsReason: deductions.greaterThan(0) ? reasonText(input.deductionsReason) : null,
|
|
|
|
|
payable,
|
|
|
|
|
status: payrollStatus(payable, paid),
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
|
|
|
|
|
throw new BadRequestException('برای این پرسنل در این دوره حقوق ثبت شده است.');
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
return this.listPayroll(actor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updatePayrollPayment(
|
|
|
|
|
actor: Actor,
|
|
|
|
|
payrollId: string,
|
|
|
|
|
movementId: string,
|
|
|
|
|
input: { amount: string | number; accountId: string; occurredAt?: string },
|
|
|
|
|
) {
|
|
|
|
|
const movement = await this.prisma.financialMovement.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
id: movementId,
|
|
|
|
|
payrollId,
|
|
|
|
|
organizationId: actor.organizationId,
|
|
|
|
|
voidedAt: null,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const payroll = await this.prisma.financialPayroll.findFirst({
|
|
|
|
|
where: { id: payrollId, organizationId: actor.organizationId },
|
|
|
|
|
include: { movements: true },
|
|
|
|
|
});
|
|
|
|
|
if (!movement || !payroll || payroll.voidedAt || payroll.status === 'void') {
|
|
|
|
|
throw new NotFoundException('پرداخت حقوق برای ویرایش پیدا نشد.');
|
|
|
|
|
}
|
|
|
|
|
const amount = dec(input.amount);
|
|
|
|
|
const otherPaid = this.paidOf(payroll.movements.filter((item) => item.id !== movement.id));
|
|
|
|
|
if (!amount.greaterThan(0) || otherPaid.plus(amount).greaterThan(payroll.payable)) {
|
|
|
|
|
throw new BadRequestException('مبلغ پرداخت حقوق نامعتبر است.');
|
|
|
|
|
}
|
|
|
|
|
const account = await this.prisma.financialAccount.findFirst({
|
|
|
|
|
where: { id: input.accountId, organizationId: actor.organizationId },
|
|
|
|
|
});
|
|
|
|
|
if (!account || (!account.isActive && account.id !== movement.accountId)) {
|
|
|
|
|
throw new BadRequestException('حساب پرداخت پیدا نشد.');
|
|
|
|
|
}
|
|
|
|
|
const available = await this.balance(account.id);
|
|
|
|
|
const room = account.id === movement.accountId ? available.plus(movement.amount) : available;
|
|
|
|
|
if (room.lessThan(amount)) throw new BadRequestException('موجودی حساب کافی نیست.');
|
|
|
|
|
await this.prisma.$transaction([
|
|
|
|
|
this.prisma.financialMovement.update({
|
|
|
|
|
where: { id: movement.id },
|
|
|
|
|
data: {
|
|
|
|
|
accountId: account.id,
|
|
|
|
|
amount,
|
|
|
|
|
occurredAt: parseDate(input.occurredAt) ?? movement.occurredAt,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
this.prisma.financialPayroll.update({
|
|
|
|
|
where: { id: payroll.id },
|
|
|
|
|
data: { status: payrollStatus(payroll.payable, otherPaid.plus(amount)) },
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
return this.listPayroll(actor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listRecurring(actor: Actor) {
|
|
|
|
|
const rows = await this.prisma.financialRecurringItem.findMany({
|
|
|
|
|
where: { organizationId: actor.organizationId },
|
|
|
|
@@ -495,8 +627,11 @@ export class TreasuryOpsService {
|
|
|
|
|
period: string;
|
|
|
|
|
baseAmount: Prisma.Decimal;
|
|
|
|
|
benefits: Prisma.Decimal;
|
|
|
|
|
benefitsReason?: string | null;
|
|
|
|
|
bonus: Prisma.Decimal;
|
|
|
|
|
bonusReason?: string | null;
|
|
|
|
|
deductions: Prisma.Decimal;
|
|
|
|
|
deductionsReason?: string | null;
|
|
|
|
|
payable: Prisma.Decimal;
|
|
|
|
|
dueDate: Date;
|
|
|
|
|
status: string;
|
|
|
|
@@ -504,6 +639,7 @@ export class TreasuryOpsService {
|
|
|
|
|
id: string;
|
|
|
|
|
amount: Prisma.Decimal;
|
|
|
|
|
voidedAt: Date | null;
|
|
|
|
|
accountId: string;
|
|
|
|
|
occurredAt: Date;
|
|
|
|
|
account?: { name: string } | null;
|
|
|
|
|
}>;
|
|
|
|
@@ -519,8 +655,11 @@ export class TreasuryOpsService {
|
|
|
|
|
period: row.period,
|
|
|
|
|
baseAmount: money(row.baseAmount),
|
|
|
|
|
benefits: money(row.benefits),
|
|
|
|
|
benefitsReason: row.benefitsReason?.trim() || null,
|
|
|
|
|
bonus: money(row.bonus),
|
|
|
|
|
bonusReason: row.bonusReason?.trim() || null,
|
|
|
|
|
deductions: money(row.deductions),
|
|
|
|
|
deductionsReason: row.deductionsReason?.trim() || null,
|
|
|
|
|
payable: money(row.payable),
|
|
|
|
|
paidAmount: money(paid),
|
|
|
|
|
remaining: money(left.isNegative() ? 0 : left),
|
|
|
|
@@ -532,6 +671,7 @@ export class TreasuryOpsService {
|
|
|
|
|
id: item.id,
|
|
|
|
|
amount: money(item.amount),
|
|
|
|
|
occurredAt: item.occurredAt.toISOString(),
|
|
|
|
|
accountId: item.accountId,
|
|
|
|
|
accountName: item.account?.name ?? '',
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|