refactor(inventories): restructure inventory and pos account modules, remove deprecated DTOs and controllers

- Removed create and update DTOs for inventory.
- Deleted inventories controller and service.
- Introduced new inventory bank accounts module with controller and service.
- Added new pos accounts module with controller and service.
- Updated Prisma models to reflect changes in bank account relationships.
- Adjusted PosAccount and SalesInvoice models for stricter type definitions.
- Implemented new response mapping for inventory and pos account services.
This commit is contained in:
2025-12-26 01:20:03 +03:30
parent cbe51b9343
commit b4f226fb3a
29 changed files with 466 additions and 637 deletions
@@ -0,0 +1,6 @@
import { IsNumber } from 'class-validator'
export class UpdateInventoryBankAccountDto {
@IsNumber()
bankAccountId: number
}
@@ -0,0 +1,38 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { CreateBankAccountDto } from '../../bank-accounts/dto/create-bank-account.dto'
import { UpdateInventoryBankAccountDto } from './dto/update-inventory-bank-account.dto'
import { InventoryBankAccountsService } from './inventory-bank-accounts.service'
@Controller('inventories/:inventoryId/bank-accounts')
export class InventoryBankAccountsController {
constructor(private readonly bankAccountsService: InventoryBankAccountsService) {}
@Post()
create(
@Param('inventoryId') inventoryId: string,
@Body() bankAccount: CreateBankAccountDto,
) {
return this.bankAccountsService.create(Number(inventoryId), bankAccount)
}
@Get()
findAll(@Param('inventoryId') inventoryId: string) {
return this.bankAccountsService.findAll(Number(inventoryId))
}
@Post('/assign')
assign(
@Param('inventoryId') inventoryId: string,
@Body() dto: UpdateInventoryBankAccountDto,
) {
return this.bankAccountsService.assign(Number(inventoryId), dto.bankAccountId)
}
@Post('/unassign')
unassign(
@Param('inventoryId') inventoryId: string,
@Body() dto: UpdateInventoryBankAccountDto,
) {
return this.bankAccountsService.unassign(Number(inventoryId), dto.bankAccountId)
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../../prisma/prisma.module'
import { InventoryBankAccountsController } from './inventory-bank-accounts.controller'
import { InventoryBankAccountsService } from './inventory-bank-accounts.service'
@Module({
imports: [PrismaModule],
controllers: [InventoryBankAccountsController],
providers: [InventoryBankAccountsService],
})
export class InventoryBankAccountsModule {}
@@ -0,0 +1,76 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreateBankAccountDto } from '../../bank-accounts/dto/create-bank-account.dto'
@Injectable()
export class InventoryBankAccountsService {
constructor(private prisma: PrismaService) {}
async create(inventoryId: number, bankAccount: CreateBankAccountDto) {
return this.prisma.bankAccount
.create({
data: bankAccount,
})
.then(async res => {
await this.prisma.inventoryBankAccount.create({
data: {
inventoryId,
bankAccountId: res.id,
},
})
return ResponseMapper.create(res)
})
}
async findAll(inventoryId: number) {
const items = await this.prisma.inventoryBankAccount.findMany({
where: {
inventoryId,
},
include: {
bankAccount: {
include: {
branch: {
include: {
bank: {
select: { id: true, name: true, shortName: true },
},
},
},
},
},
posAccounts: {
select: { id: true, name: true, code: true },
},
},
omit: {
bankAccountId: true,
inventoryId: true,
},
})
return ResponseMapper.list(
items.map(item => ({ posAccounts: item.posAccounts, ...item.bankAccount })),
)
}
async assign(inventoryId: number, bankAccountId: number) {
const item = await this.prisma.inventoryBankAccount.create({
data: {
inventoryId,
bankAccountId,
},
})
return ResponseMapper.create(item)
}
async unassign(inventoryId: number, bankAccountId: number) {
const item = await this.prisma.inventoryBankAccount.deleteMany({
where: {
inventoryId,
bankAccountId,
},
})
return ResponseMapper.single(item)
}
}
@@ -0,0 +1,13 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { IsOptional, IsString } from 'class-validator'
export class CreateInventoryDto {
@ApiProperty()
@IsString()
name: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
location?: string
}
@@ -0,0 +1,21 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsBoolean, IsOptional, IsString } from 'class-validator'
export class UpdateInventoryDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
name?: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
location?: string
@ApiPropertyOptional()
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isActive?: boolean
}
@@ -0,0 +1,54 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'
import { ApiQuery } from '@nestjs/swagger'
import { MovementType } from '../../../generated/prisma/enums'
import { CreateInventoryDto } from './dto/create-inventory.dto'
import { UpdateInventoryDto } from './dto/update-inventory.dto'
import { InventoriesService } from './inventories.service'
@Controller('inventories')
export class InventoriesController {
constructor(private readonly inventoriesService: InventoriesService) {}
@Post()
create(@Body() dto: CreateInventoryDto) {
return this.inventoriesService.create(dto)
}
@Get()
@ApiQuery({ name: 'isPointOfSale', required: false, type: Boolean })
findAll(@Query('isPointOfSale') isPointOfSale?: boolean) {
return this.inventoriesService.findAll(isPointOfSale)
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.inventoriesService.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateInventoryDto) {
return this.inventoriesService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.inventoriesService.remove(Number(id))
}
@Get(':id/movements')
@ApiQuery({ name: 'type', required: false, enum: MovementType })
findInventoryMovements(@Param('id') id: string, @Query('type') type?: MovementType) {
return this.inventoriesService.findInventoryMovements(Number(id), type ?? undefined)
}
@Get(':id/stock')
@ApiQuery({ name: 'isAvailable', required: false, type: Boolean })
getStock(@Param('id') id: string, @Query('isAvailable') isAvailable?: boolean) {
return this.inventoriesService.getStock(Number(id), isAvailable ?? undefined)
}
@Get(':id/products/:productId/cardex')
getProductCardex(@Param('id') id: string, @Param('productId') productId: string) {
return this.inventoriesService.getProductCardex(Number(id), Number(productId))
}
}
@@ -0,0 +1,195 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { MovementType } from '../../../generated/prisma/enums'
import { PrismaService } from '../../../prisma/prisma.service'
@Injectable()
export class InventoriesService {
constructor(private prisma: PrismaService) {}
async create(data: any) {
const item = await this.prisma.inventory.create({ data })
return ResponseMapper.create(item)
}
async findAll(isPointOfSale?: boolean) {
const items = await this.prisma.inventory.findMany({
where: isPointOfSale !== undefined ? { isPointOfSale } : undefined,
})
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.inventory.findUnique({
where: { id },
include: {
stockBalances: {
where: { quantity: { gt: 0 } },
select: { quantity: true, totalCost: true },
},
},
})
if (!item) return null
const { stockBalances, ...rest } = item
return ResponseMapper.single({
...rest,
availableProductTypes: stockBalances.length,
availableProductCount: stockBalances.reduce(
(acc, sb) => acc + Number(sb.quantity),
0,
),
availableProductsCost: stockBalances.reduce(
(acc, sb) => acc + Number(sb.totalCost),
0,
),
})
}
async update(id: number, data: any) {
const item = await this.prisma.inventory.update({ where: { id }, data })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.inventory.delete({ where: { id } })
return ResponseMapper.single(item)
}
async findInventoryMovements(
inventoryId: number,
type?: MovementType,
page = 1,
pageSize = 10,
) {
const groups = await this.prisma.stockMovement.groupBy({
by: ['referenceId'],
where: { inventoryId, type },
_count: { id: true },
orderBy: { referenceId: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
})
const result = await Promise.all(
groups.map(async group => {
const movements = await this.prisma.stockMovement.findMany({
where: { inventoryId, referenceId: group.referenceId, type },
include: {
product: true,
counterInventory: {
select: { id: true, name: true },
},
},
})
let info = null as any
const mapped = movements.map(movement => {
const { id, quantity, fee, totalCost, avgCost, product } = movement
if (info === null) {
info = {
date: movement.createdAt,
type: movement.type,
quantity: Number(movement.quantity),
fee: Number(movement.fee),
totalCost: Number(movement.totalCost),
referenceType: movement.referenceType,
referenceId: movement.referenceId,
counterInventory: movement.counterInventory,
createdAt: movement.createdAt,
}
} else {
info.quantity += Number(movement.quantity)
info.fee += Number(movement.fee)
info.totalCost += Number(movement.totalCost)
}
return {
id,
quantity: Number(quantity),
fee: Number(fee),
totalCost: Number(totalCost),
avgCost: Number(avgCost),
product,
}
})
return {
receiptId: group.referenceId,
count: group._count.id,
info,
movements: mapped,
}
}),
)
return ResponseMapper.list(result)
}
async getStock(inventoryId: number, isAvailable?: boolean, page = 1, pageSize = 10) {
const items = await this.prisma.stockBalance.findMany({
where: {
inventoryId,
quantity:
isAvailable === true
? { gt: 0 }
: isAvailable === false
? { lte: 0 }
: undefined,
},
include: {
product: true,
},
orderBy: {
updatedAt: 'desc',
},
skip: (page - 1) * pageSize,
take: pageSize,
})
const mapped = items.map(item => ({
id: item.id,
quantity: Number(item.quantity),
avgCost: Number(item.avgCost),
product: item.product,
}))
return ResponseMapper.list(mapped)
}
async getProductCardex(inventoryId: number, productId: number) {
const movements = await this.prisma.stockMovement.findMany({
where: { productId, inventoryId },
orderBy: { createdAt: 'asc' },
include: { inventory: true, supplier: true },
})
const mapped = movements.map(movement => ({
id: movement.id,
type: movement.type,
quantity: Number(movement.quantity),
fee: Number(movement.fee),
totalCost: Number(movement.totalCost),
avgCost: Number(movement.avgCost),
referenceType: movement.referenceType,
referenceId: movement.referenceId,
createdAt: movement.createdAt,
remainedInStock: movement.remainedInStock,
inventory: {
id: movement.inventory.id,
name: movement.inventory.name,
},
supplier: movement.supplier
? {
id: movement.supplier.id,
name: movement.supplier.firstName + ' ' + movement.supplier.lastName,
}
: null,
}))
return ResponseMapper.list(mapped)
}
async getInventoryBankAccounts(inventoryId: number) {
const items = await this.prisma.bankAccount.findMany({
where: { inventoryBankAccounts: { some: { inventoryId } } },
})
return ResponseMapper.list(items)
}
}
@@ -0,0 +1,5 @@
export interface IInventoryResponse {
name: string
location?: string
isActive: boolean
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { InventoryBankAccountsModule } from './bank-accounts/inventory-bank-accounts.module'
import { InventoriesController } from './index/inventories.controller'
import { InventoriesService } from './index/inventories.service'
import { PosAccountsModule } from './pos-accounts/pos-accounts.module'
@Module({
imports: [PrismaModule, InventoryBankAccountsModule, PosAccountsModule],
controllers: [InventoriesController],
providers: [InventoriesService],
})
export class InventoriesModule {}
@@ -0,0 +1,16 @@
import { IsInt, IsOptional, IsString } from 'class-validator'
export class CreatePosAccountDto {
@IsString()
name: string
@IsString()
code: string
@IsOptional()
@IsString()
description?: string
@IsInt()
bankAccountId: number
}
@@ -0,0 +1,8 @@
import { PartialType } from '@nestjs/swagger'
import { IsInt } from 'class-validator'
import { CreatePosAccountDto } from './create-pos-accounts.dto'
export class UpdatePosAccountDto extends PartialType(CreatePosAccountDto) {
@IsInt()
bankAccountId: number
}
@@ -0,0 +1,45 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CreatePosAccountDto } from './dto/create-pos-accounts.dto'
import { UpdatePosAccountDto } from './dto/update-pos-accounts.dto'
import { PosAccountsService } from './pos-accounts.service'
@Controller('inventories/:inventoryId/pos-accounts')
export class PosAccountsController {
constructor(private readonly posAccountsService: PosAccountsService) {}
@Post()
create(
@Param('inventoryId') inventoryId: string,
@Body() createPosAccountDto: CreatePosAccountDto,
) {
return this.posAccountsService.create(Number(inventoryId), createPosAccountDto)
}
@Get()
findAll(@Param('inventoryId') inventoryId: string) {
return this.posAccountsService.findAll(Number(inventoryId))
}
@Get(':id')
findOne(@Param('inventoryId') inventoryId: string, @Param('id') id: string) {
return this.posAccountsService.findOne(Number(inventoryId), Number(id))
}
@Patch(':id')
update(
@Param('inventoryId') inventoryId: string,
@Param('id') id: string,
@Body() updatePosAccountDto: UpdatePosAccountDto,
) {
return this.posAccountsService.update(
Number(inventoryId),
Number(id),
updatePosAccountDto,
)
}
@Delete(':id')
remove(@Param('inventoryId') inventoryId: string, @Param('id') id: string) {
return this.posAccountsService.remove(Number(inventoryId), Number(id))
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../../prisma/prisma.module'
import { PosAccountsController } from './pos-accounts.controller'
import { PosAccountsService } from './pos-accounts.service'
@Module({
imports: [PrismaModule],
controllers: [PosAccountsController],
providers: [PosAccountsService],
})
export class PosAccountsModule {}
@@ -0,0 +1,120 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreatePosAccountDto } from './dto/create-pos-accounts.dto'
import { UpdatePosAccountDto } from './dto/update-pos-accounts.dto'
@Injectable()
export class PosAccountsService {
constructor(private prisma: PrismaService) {}
private async validateBankAccountInventoryLink(
inventoryId: number,
bankAccountId: number,
) {
const link = await this.prisma.inventoryBankAccount.findUnique({
where: {
inventoryId_bankAccountId: {
inventoryId,
bankAccountId,
},
},
})
if (!link) {
throw new Error('حساب بانکی به این انبار متصل نیست')
}
}
async create(inventoryId: number, dto: CreatePosAccountDto) {
if (dto.bankAccountId) {
await this.validateBankAccountInventoryLink(inventoryId, dto.bankAccountId)
}
const item = await this.prisma.posAccount.create({
data: {
...dto,
inventoryId,
},
})
return ResponseMapper.create(item)
}
async findAll(inventoryId: number) {
const items = await this.prisma.posAccount.findMany({
where: {
inventoryId,
},
include: {
bankAccount: {
select: {
id: true,
name: true,
accountNumber: true,
branch: {
select: {
id: true,
name: true,
bank: { select: { id: true, name: true } },
},
},
},
},
},
omit: {
inventoryId: true,
bankAccountId: true,
},
})
return ResponseMapper.list(items)
}
async findOne(inventoryId: number, id: number) {
const item = await this.prisma.posAccount.findFirst({
where: {
id,
inventoryId,
},
include: {
bankAccount: {
select: {
id: true,
name: true,
accountNumber: true,
branch: {
select: {
id: true,
name: true,
bank: { select: { id: true, name: true } },
},
},
},
},
},
omit: {
inventoryId: true,
bankAccountId: true,
},
})
return ResponseMapper.single(item)
}
async update(inventoryId: number, id: number, dto: UpdatePosAccountDto) {
await this.validateBankAccountInventoryLink(inventoryId, dto.bankAccountId)
const item = await this.prisma.posAccount.update({
where: {
id,
},
data: dto,
})
return ResponseMapper.update(item)
}
async remove(inventoryId: number, id: number) {
const item = await this.prisma.posAccount.delete({
where: {
id,
},
})
return ResponseMapper.delete(item)
}
}