refactor: restructure purchase receipts module and related workflows

- Removed old DTOs for creating and updating purchase receipts.
- Updated purchase receipts controller and service to use new DTOs and workflows.
- Introduced transaction helper for managing database transactions.
- Added new workflows for handling purchase receipt payments and supplier ledgers.
- Implemented new logic for managing purchase receipt items and payments.
- Enhanced error handling for payment processing in workflows.
- Updated supplier ledger management to reflect changes in purchase receipts.
This commit is contained in:
2026-01-05 10:07:23 +03:30
parent a2db2daa70
commit fda190f902
38 changed files with 617 additions and 486 deletions
+1 -5
View File
@@ -11,7 +11,7 @@ import { BanksModule } from './modules/banks/banks.module'
import { CardexModule } from './modules/cardex/cardex.module'
import { InventoriesModule } from './modules/inventories/inventories.module'
import { PosModule } from './modules/pos/pos.module'
import { PurchaseReceiptPaymentsModule } from './modules/purchase-receipt-payments/purchase-receipt-payments.module'
import { PurchaseReceiptsModule } from './modules/purchase-receipts/purchase-receipts.module'
import { StatisticsModule } from './modules/statistics/statistics.module'
import { SuppliersModule } from './modules/suppliers/suppliers.module'
import { PrismaModule } from './prisma/prisma.module'
@@ -19,8 +19,6 @@ import { ProductBrandsModule } from './product-brands/product-brands.module'
import { ProductCategoriesModule } from './product-categories/product-categories.module'
import { ProductVariantsModule } from './product-variants/product-variants.module'
import { ProductsModule } from './products/products.module'
import { PurchaseReceiptItemsModule } from './purchase-receipt-items/purchase-receipt-items.module'
import { PurchaseReceiptsModule } from './purchase-receipts/purchase-receipts.module'
import { RolesModule } from './roles/roles.module'
import { SalesInvoiceItemsModule } from './sales-invoice-items/sales-invoice-items.module'
import { SalesInvoicesModule } from './sales-invoices/sales-invoices.module'
@@ -43,8 +41,6 @@ import { UsersModule } from './users/users.module'
CustomersModule,
InventoriesModule,
PurchaseReceiptsModule,
PurchaseReceiptItemsModule,
PurchaseReceiptPaymentsModule,
SalesInvoicesModule,
SalesInvoiceItemsModule,
InventoryTransfersModule,
+11
View File
@@ -0,0 +1,11 @@
// src/common/database/transaction.helper.ts
import { PrismaService } from '../../prisma/prisma.service'
export async function withTransaction<T>(
prisma: PrismaService,
fn: (tx: PrismaService) => Promise<T>,
): Promise<T> {
return prisma.$transaction(async tx => {
return fn(tx as PrismaService)
})
}
@@ -2,10 +2,12 @@ import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { BankAccountsController } from './bank-accounts.controller'
import { BankAccountsService } from './bank-accounts.service'
import { BankAccountsWorkflow } from './bank-accounts.workflow'
@Module({
imports: [PrismaModule],
controllers: [BankAccountsController],
providers: [BankAccountsService],
providers: [BankAccountsService, BankAccountsWorkflow],
exports: [BankAccountsWorkflow],
})
export class BankAccountsModule {}
@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common'
import { withTransaction } from '../../common/database/transaction.helper'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@@ -6,8 +7,16 @@ import { PrismaService } from '../../prisma/prisma.service'
export class BankAccountsService {
constructor(private prisma: PrismaService) {}
async create(data: any) {
const item = await this.prisma.bankAccount.create({ data })
return ResponseMapper.create(item)
return withTransaction(this.prisma, async tx => {
const item = await tx.bankAccount.create({ data })
await tx.bankAccountBalance.create({
data: {
bankAccountId: item.id,
balance: 0,
},
})
return ResponseMapper.create(item)
})
}
async findAll() {
@@ -0,0 +1,65 @@
import { Injectable } from '@nestjs/common'
import { Prisma } from '../../generated/prisma/client'
import { AddTransactionToBankAccountDto } from './dto/add-transaction.dto'
@Injectable()
export class BankAccountsWorkflow {
async addTransaction(
tx: Prisma.TransactionClient,
payload: AddTransactionToBankAccountDto,
) {
const item = await tx.bankAccountBalance.findUnique({
where: {
bankAccountId: payload.bankAccountId,
},
})
const balance = item ? Number(item.balance) : 0
const newBalance = item
? payload.type === 'DEPOSIT'
? balance + payload.amount
: balance - payload.amount
: payload.type === 'DEPOSIT'
? payload.amount
: -payload.amount
await tx.bankAccountTransaction.create({
data: {
bankAccount: { connect: { id: payload.bankAccountId } },
amount: payload.amount,
type: payload.type,
balanceAfter: newBalance,
referenceId: payload.referenceId,
referenceType: payload.referenceType,
},
})
console.log('first')
const bankAccountBalanceItem = await tx.bankAccountBalance.findUnique({
where: {
bankAccountId: payload.bankAccountId,
},
})
if (!bankAccountBalanceItem) {
return await tx.bankAccountBalance.create({
data: {
bankAccount: { connect: { id: payload.bankAccountId } },
balance: newBalance,
},
})
}
return await tx.bankAccountBalance.update({
where: {
bankAccountId: payload.bankAccountId,
},
data: {
balance: newBalance,
},
})
}
}
@@ -0,0 +1,22 @@
import { IsEnum, IsInt } from 'class-validator'
import {
BankAccountTransactionType,
BankTransactionRefType,
} from '../../../generated/prisma/enums'
export class AddTransactionToBankAccountDto {
@IsInt()
bankAccountId: number
@IsInt()
amount: number
@IsInt()
referenceId: number
@IsEnum(BankTransactionRefType)
referenceType: BankTransactionRefType
@IsEnum(BankAccountTransactionType)
type: BankAccountTransactionType
}
@@ -1,12 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { PurchaseReceiptPaymentsController } from './purchase-receipt-payments.controller'
import { PurchaseReceiptPaymentsService } from './purchase-receipt-payments.service'
@Module({
imports: [PrismaModule],
controllers: [PurchaseReceiptPaymentsController],
providers: [PurchaseReceiptPaymentsService],
})
export class PurchaseReceiptPaymentsModule {}
@@ -1,12 +1,17 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../common/response/response-mapper'
import { Prisma } from '../generated/prisma/client'
import { PrismaService } from '../prisma/prisma.service'
import { withTransaction } from '../../../common/database/transaction.helper'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { Prisma } from '../../../generated/prisma/client'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreatePurchaseReceiptDto } from './dto/create-purchase-receipt.dto'
import { PurchaseReceiptWorkflow } from './purchase-receipts.workflow'
@Injectable()
export class PurchaseReceiptsService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private purchaseReceiptWorkflow: PurchaseReceiptWorkflow,
) {}
async create(dto: CreatePurchaseReceiptDto) {
const data: Prisma.PurchaseReceiptCreateInput = {
@@ -31,15 +36,24 @@ export class PurchaseReceiptsService {
}
: undefined,
}
const item = await this.prisma.purchaseReceipt.create({
data,
include: { items: true },
omit: {
supplierId: true,
inventoryId: true,
},
return withTransaction(this.prisma, async tx => {
const item = await tx.purchaseReceipt.create({
data,
include: { items: true },
omit: {
supplierId: true,
inventoryId: true,
},
})
await this.purchaseReceiptWorkflow.onCreatePurchaseReceipt(
tx,
dto.supplierId,
dto.totalAmount,
item.id,
)
return ResponseMapper.create(item)
})
return ResponseMapper.create(item)
}
async findAll() {
@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common'
import { Prisma } from '../../../generated/prisma/client'
import { SupplierLedgerWorkflow } from '../../suppliers/ledger/supplier-ledger.workflow'
@Injectable()
export class PurchaseReceiptWorkflow {
constructor(private supplierLedgerWorkflow: SupplierLedgerWorkflow) {}
async onCreatePurchaseReceipt(
tx: Prisma.TransactionClient,
supplierId: number,
amount: number,
sourceId: number,
) {
await this.supplierLedgerWorkflow.updateLedgerBalance(
tx,
supplierId,
amount,
sourceId,
'PURCHASE',
)
}
}
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../prisma/prisma.module'
import { PrismaModule } from '../../../prisma/prisma.module'
import { PurchaseReceiptItemsController } from './purchase-receipt-items.controller'
import { PurchaseReceiptItemsService } from './purchase-receipt-items.service'
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../common/response/response-mapper'
import { PrismaService } from '../prisma/prisma.service'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreatePurchaseReceiptItemDto } from './dto/create-purchase-receipt-item.dto'
@Injectable()
@@ -1,5 +1,5 @@
import { IsEnum, IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
import { PaymentMethodType, PaymentType } from '../../../generated/prisma/enums'
import { PaymentMethodType, PaymentType } from '../../../../generated/prisma/enums'
export class CreatePurchaseReceiptPaymentDto {
@IsNumber()
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../../prisma/prisma.module'
import { BankAccountsModule } from '../../bank-accounts/bank-accounts.module'
import { PurchaseReceiptPaymentsController } from './purchase-receipt-payments.controller'
import { PurchaseReceiptPaymentsService } from './purchase-receipt-payments.service'
import { PurchaseReceiptPaymentsWorkflow } from './purchase-receipt-payments.workflow'
@Module({
imports: [PrismaModule, BankAccountsModule],
controllers: [PurchaseReceiptPaymentsController],
providers: [PurchaseReceiptPaymentsService, PurchaseReceiptPaymentsWorkflow],
exports: [PurchaseReceiptPaymentsWorkflow],
})
export class PurchaseReceiptPaymentsModule {}
@@ -1,14 +1,29 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
import { withTransaction } from '../../../common/database/transaction.helper'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreatePurchaseReceiptPaymentDto } from './dto/create-purchase-receipt-payment.dto'
import { PurchaseReceiptPaymentsWorkflow } from './purchase-receipt-payments.workflow'
@Injectable()
export class PurchaseReceiptPaymentsService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private purchaseReceiptPaymentsWorkflow: PurchaseReceiptPaymentsWorkflow,
) {}
async create(data: any) {
const item = await this.prisma.purchaseReceiptPayments.create({ data })
return ResponseMapper.create(item)
async create(data: CreatePurchaseReceiptPaymentDto) {
return withTransaction(this.prisma, async tx => {
const item = await tx.purchaseReceiptPayments.create({ data })
await this.purchaseReceiptPaymentsWorkflow.onAddPayment(
tx,
data.bankAccountId,
data.receiptId,
data.amount,
data.type,
)
return ResponseMapper.create(item)
})
}
async findAll() {
@@ -0,0 +1,58 @@
import { Injectable } from '@nestjs/common'
import { Prisma } from '../../../generated/prisma/client'
import { PaymentType } from '../../../generated/prisma/enums'
import { BankAccountsWorkflow } from '../../bank-accounts/bank-accounts.workflow'
@Injectable()
export class PurchaseReceiptPaymentsWorkflow {
constructor(private bankAccountsWorkflow: BankAccountsWorkflow) {}
async onAddPayment(
tx: Prisma.TransactionClient,
bankAccountId: number,
receiptId: number,
amount: number,
type: PaymentType,
) {
const { paidAmount, totalAmount } = await tx.purchaseReceipt.findUniqueOrThrow({
where: { id: receiptId },
})
if (type === 'PAYMENT' && Number(paidAmount) + Number(amount) > Number(totalAmount)) {
throw new Error('مجموع مبلغ پرداختی بیشتر از مبلغ فاکتور است.')
}
if (type === 'REFUND' && Number(paidAmount) - Number(amount) < 0) {
throw new Error(
'مجموع مبلغ بازپرداختی بیشتر از مبلغ پرداخت‌ شده در این فاکتور است.',
)
}
await this.bankAccountsWorkflow.addTransaction(tx, {
bankAccountId: bankAccountId,
amount: amount,
type: type === 'PAYMENT' ? 'WITHDRAWAL' : 'DEPOSIT',
referenceId: receiptId,
referenceType: type === 'PAYMENT' ? 'PURCHASE_PAYMENT' : 'PURCHASE_REFUND',
})
const newPaidAmount =
type === 'PAYMENT'
? Number(paidAmount) + Number(amount)
: Number(paidAmount) - Number(amount)
const newStatus =
newPaidAmount === 0
? 'UNPAID'
: newPaidAmount >= Number(totalAmount)
? 'PAID'
: 'PARTIALLY_PAID'
await tx.purchaseReceipt.update({
where: { id: receiptId },
data: {
paidAmount: newPaidAmount,
status: newStatus,
},
})
}
}
@@ -0,0 +1,21 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { SupplierLedgerModule } from '../suppliers/ledger/supplier-ledger.module'
import { PurchaseReceiptsController } from './index/purchase-receipts.controller'
import { PurchaseReceiptsService } from './index/purchase-receipts.service'
import { PurchaseReceiptWorkflow } from './index/purchase-receipts.workflow'
import { PurchaseReceiptItemsModule } from './purchase-receipt-items/purchase-receipt-items.module'
import { PurchaseReceiptPaymentsModule } from './purchase-receipt-payments/purchase-receipt-payments.module'
@Module({
imports: [
PrismaModule,
PurchaseReceiptItemsModule,
PurchaseReceiptPaymentsModule,
SupplierLedgerModule,
],
controllers: [PurchaseReceiptsController],
providers: [PurchaseReceiptsService, PurchaseReceiptWorkflow],
exports: [PurchaseReceiptWorkflow],
})
export class PurchaseReceiptsModule {}
@@ -22,11 +22,6 @@ export class SuppliersController {
return this.suppliersService.findOne(Number(supplierId))
}
@Get(':supplierId/ledger')
getLedger(@Param('supplierId') supplierId: string) {
return this.suppliersService.getLedger(Number(supplierId))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateSupplierDto) {
return this.suppliersService.update(Number(id), dto)
@@ -37,21 +37,6 @@ export class SuppliersService {
})
}
async getLedger(supplierId: number) {
const items = await this.prisma.supplierLedger.findMany({
where: {
supplierId,
},
omit: {
supplierId: true,
},
orderBy: {
createdAt: 'desc',
},
})
return ResponseMapper.list(items)
}
async update(id: number, data: any) {
const item = await this.prisma.supplier.update({ where: { id }, data })
return ResponseMapper.update(item)
@@ -21,10 +21,15 @@ export class SupplierInvoicesController {
@Post(':invoiceId/pay')
createPayment(
@Param('supplierId') supplierId: string,
@Param('invoiceId') invoiceId: string,
@Body() data: CreateReceiptPaymentDto,
) {
return this.supplierInvoicesService.createPayment(Number(invoiceId), data)
return this.supplierInvoicesService.createPayment(
Number(supplierId),
Number(invoiceId),
data,
)
}
@Get(':invoiceId/payments')
@@ -1,11 +1,14 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../../prisma/prisma.module'
import { PurchaseReceiptPaymentsModule } from '../../purchase-receipts/purchase-receipt-payments/purchase-receipt-payments.module'
import { SupplierLedgerModule } from '../ledger/supplier-ledger.module'
import { SupplierInvoicesController } from './invoices.controller'
import { SupplierInvoicesService } from './invoices.service'
import { SupplierInvoicesWorkflow } from './invoices.workflow'
@Module({
imports: [PrismaModule],
imports: [PrismaModule, PurchaseReceiptPaymentsModule, SupplierLedgerModule],
controllers: [SupplierInvoicesController],
providers: [SupplierInvoicesService],
providers: [SupplierInvoicesService, SupplierInvoicesWorkflow],
})
export class SupplierInvoicesModule {}
@@ -1,11 +1,16 @@
import { Injectable } from '@nestjs/common'
import { withTransaction } from '../../../common/database/transaction.helper'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreateReceiptPaymentDto } from './dto/create-receipt-payment.dto'
import { SupplierInvoicesWorkflow } from './invoices.workflow'
@Injectable()
export class SupplierInvoicesService {
constructor(private prisma: PrismaService) {}
constructor(
private prisma: PrismaService,
private supplierInvoicesWorkflow: SupplierInvoicesWorkflow,
) {}
async findAll(supplierId: number) {
const items = await this.prisma.purchaseReceipt.findMany({
@@ -147,32 +152,45 @@ export class SupplierInvoicesService {
return ResponseMapper.list(items)
}
async createPayment(invoiceId: number, data: CreateReceiptPaymentDto) {
async createPayment(
supplierId: number,
invoiceId: number,
data: CreateReceiptPaymentDto,
) {
const { bankAccountId, ...rest } = data
console.log(data)
const payment = await this.prisma.purchaseReceiptPayments.create({
data: {
...rest,
payedAt: new Date(data.payedAt),
receipt: {
connect: {
id: invoiceId,
return withTransaction(this.prisma, async tx => {
await this.supplierInvoicesWorkflow.onPaymentCreated(
tx,
supplierId,
data.amount,
invoiceId,
bankAccountId,
)
const payment = await tx.purchaseReceiptPayments.create({
data: {
...rest,
payedAt: new Date(data.payedAt),
receipt: {
connect: {
id: invoiceId,
},
},
bankAccount: {
connect: {
id: bankAccountId,
},
},
},
bankAccount: {
connect: {
id: bankAccountId,
},
include: {
receipt: true,
},
},
include: {
receipt: true,
},
omit: {
receiptId: true,
},
omit: {
receiptId: true,
},
})
return ResponseMapper.create(payment)
})
return ResponseMapper.create(payment)
}
}
@@ -0,0 +1,36 @@
import { Injectable } from '@nestjs/common'
import { Prisma } from '../../../generated/prisma/client'
import { PurchaseReceiptPaymentsWorkflow } from '../../purchase-receipts/purchase-receipt-payments/purchase-receipt-payments.workflow'
import { SupplierLedgerWorkflow } from '../ledger/supplier-ledger.workflow'
@Injectable()
export class SupplierInvoicesWorkflow {
constructor(
private supplierLedgerWorkflow: SupplierLedgerWorkflow,
private purchaseReceiptPaymentWorkflow: PurchaseReceiptPaymentsWorkflow,
) {}
async onPaymentCreated(
tx: Prisma.TransactionClient,
supplierId: number,
amount: number,
invoiceId: number,
bankAccountId: number,
) {
await this.supplierLedgerWorkflow.updateLedgerBalance(
tx,
supplierId,
amount,
invoiceId,
'PAYMENT',
)
await this.purchaseReceiptPaymentWorkflow.onAddPayment(
tx,
bankAccountId,
invoiceId,
amount,
'PAYMENT',
)
}
}
@@ -0,0 +1,12 @@
import { Controller, Get, Param } from '@nestjs/common'
import { SupplierLedgerService } from './supplier-ledger.service'
@Controller('suppliers/:supplierId/ledger')
export class SupplierLedgerController {
constructor(private readonly supplierLedgerService: SupplierLedgerService) {}
@Get('')
getLedger(@Param('supplierId') supplierId: string) {
return this.supplierLedgerService.findAll(Number(supplierId))
}
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../../prisma/prisma.module'
import { SupplierLedgerController } from './supplier-ledger.controller'
import { SupplierLedgerService } from './supplier-ledger.service'
import { SupplierLedgerWorkflow } from './supplier-ledger.workflow'
@Module({
imports: [PrismaModule],
controllers: [SupplierLedgerController],
providers: [SupplierLedgerService, SupplierLedgerWorkflow],
exports: [SupplierLedgerWorkflow],
})
export class SupplierLedgerModule {}
@@ -0,0 +1,23 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
@Injectable()
export class SupplierLedgerService {
constructor(private prisma: PrismaService) {}
async findAll(supplierId: number) {
const items = await this.prisma.supplierLedger.findMany({
where: {
supplierId,
},
omit: {
supplierId: true,
},
orderBy: {
createdAt: 'desc',
},
})
return ResponseMapper.list(items)
}
}
@@ -0,0 +1,57 @@
import { Injectable } from '@nestjs/common'
import { Prisma } from '../../../generated/prisma/client'
import { LedgerSourceType } from '../../../generated/prisma/enums'
@Injectable()
export class SupplierLedgerWorkflow {
async updateLedgerBalance(
tx: Prisma.TransactionClient,
supplierId: number,
amount: number,
sourceId: number,
sourceType: LedgerSourceType,
) {
const lastSupplierLedgerBalance = await tx.supplierLedger
.findFirstOrThrow({
where: { supplierId },
orderBy: { createdAt: 'desc' },
})
.then(sl => (sl ? sl.balance : 0))
let newBalance = Number(lastSupplierLedgerBalance)
let credit = 0
let debit = 0
let description = ''
switch (sourceType) {
case 'PURCHASE':
newBalance += amount
credit = amount
description = `خرید به مبلغ ${amount} بابت خرید شماره ${sourceId}`
break
case 'REFUND':
case 'PAYMENT':
newBalance -= amount
debit = amount
description = `پرداخت به مبلغ ${amount} بابت پرداخت شماره ${sourceId}`
break
case 'ADJUSTMENT':
newBalance = amount
description = `اصلاح حساب به مبلغ ${amount} بابت پرداخت شماره ${sourceId}`
break
}
return await tx.supplierLedger.create({
data: {
supplierId,
debit,
credit,
balance: newBalance,
sourceId,
sourceType,
description,
},
})
}
}
+2 -1
View File
@@ -3,9 +3,10 @@ import { PrismaModule } from '../../prisma/prisma.module'
import { SuppliersController } from './index/suppliers.controller'
import { SuppliersService } from './index/suppliers.service'
import { SupplierInvoicesModule } from './invoices/invoices.module'
import { SupplierLedgerModule } from './ledger/supplier-ledger.module'
@Module({
imports: [PrismaModule, SupplierInvoicesModule],
imports: [PrismaModule, SupplierInvoicesModule, SupplierLedgerModule],
controllers: [SuppliersController],
providers: [SuppliersService],
})
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../prisma/prisma.module'
import { PurchaseReceiptsController } from './purchase-receipts.controller'
import { PurchaseReceiptsService } from './purchase-receipts.service'
@Module({
imports: [PrismaModule],
controllers: [PurchaseReceiptsController],
providers: [PurchaseReceiptsService],
})
export class PurchaseReceiptsModule {}