This commit is contained in:
2026-02-04 13:49:07 +03:30
parent 5fd6611aca
commit de14d531e1
222 changed files with 7053 additions and 57956 deletions
+41
View File
@@ -0,0 +1,41 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { ResponseMapper } from '../common/response/response-mapper'
import { AuthService } from './auth.service'
import { CurrentUser } from './current-user.decorator'
import { RefreshTokenDto } from './dto/refresh-token.dto'
import { SendCodeDto } from './dto/send-code.dto'
import { VerifyCodeDto } from './dto/verify-code.dto'
import { JwtAuthGuard } from './jwt-auth.guard'
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private readonly auth: AuthService) {}
@Post('send-code')
async sendCode(@Body() dto: SendCodeDto) {
return this.auth.sendCode(dto)
}
@Post('login')
async login(@Body() dto: VerifyCodeDto) {
return this.auth.loginWithCode(dto)
}
@Post('refresh')
async refresh(@Body() dto: RefreshTokenDto) {
return this.auth.refreshToken(dto)
}
@Post('logout')
async logout(@Body() dto: RefreshTokenDto) {
return this.auth.logout(dto.refreshToken)
}
@Get('me')
@UseGuards(JwtAuthGuard)
async me(@CurrentUser() user: any) {
return ResponseMapper.single(user)
}
}
+22
View File
@@ -0,0 +1,22 @@
import { Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import 'dotenv/config'
import { env } from 'prisma/config'
import { PrismaModule } from '../prisma/prisma.module'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { JwtAuthGuard } from './jwt-auth.guard'
@Module({
imports: [
PrismaModule,
JwtModule.register({
secret: env('JWT_SECRET') || 'secret',
signOptions: { expiresIn: Number(env('JWT_EXPIRES_IN')) || '15m' },
}),
],
providers: [AuthService, JwtAuthGuard],
controllers: [AuthController],
exports: [AuthService, JwtAuthGuard],
})
export class AuthModule {}
+126
View File
@@ -0,0 +1,126 @@
import {
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { createHash, randomBytes } from 'crypto'
import 'dotenv/config'
import { env } from 'prisma/config'
import { ResponseMapper } from '../common/response/response-mapper'
import { PrismaService } from '../prisma/prisma.service'
import { RefreshTokenDto } from './dto/refresh-token.dto'
import { SendCodeDto } from './dto/send-code.dto'
import { VerifyCodeDto } from './dto/verify-code.dto'
@Injectable()
export class AuthService {
private readonly OTP_TTL_MINUTES = 2
private readonly ACCESS_TOKEN_EXP = Number(env('JWT_EXPIRES_IN')) || '15m'
private readonly REFRESH_TOKEN_EXP_DAYS = 30
constructor(
private prisma: PrismaService,
private jwt: JwtService,
) {}
private generateCode() {
// For development you may want fixed code via env, otherwise random 5-digit
const staticCode = process.env.OTP_STATIC_CODE
if (staticCode) return staticCode
return Math.floor(10000 + Math.random() * 90000).toString()
}
async sendCode(dto: SendCodeDto) {
const { mobileNumber } = dto
const user = await this.prisma.user.findFirst({ where: { mobileNumber } })
if (!user) {
throw new NotFoundException('کاربری با این شماره تماس یافت نشد')
}
const code = this.generateCode()
const expiresAt = new Date(Date.now() + this.OTP_TTL_MINUTES * 60 * 1000)
await this.prisma.otpCode.create({
data: {
mobileNumber,
code,
expiresAt,
},
})
// TODO: integrate SMS provider. For now return the code for dev.
return {
ok: true,
}
}
async loginWithCode(dto: VerifyCodeDto) {
const { mobileNumber, code } = dto
const user = await this.prisma.user.findFirst({
where: { mobileNumber },
include: {
role: true,
},
})
if (!user) {
throw new NotFoundException('کاربری با این شماره تماس یافت نشد')
}
const otp = await this.prisma.otpCode.findFirst({
where: { mobileNumber, used: false },
})
if (!otp) throw new BadRequestException('اطلاعات ورودی درست نیست')
if (otp.used) throw new BadRequestException('کد قبلن استفاده شده است')
if (otp.expiresAt.getTime() < Date.now())
throw new BadRequestException('کد وارد شده منقضی شده است')
if (otp.code !== code) throw new BadRequestException('کد وارد شده اشتباه است')
// mark used
await this.prisma.otpCode.update({ where: { id: otp.id }, data: { used: true } })
const accessToken = this.jwt.sign(
{ sub: user.id, mobileNumber },
{ expiresIn: this.ACCESS_TOKEN_EXP },
)
const refreshTokenPlain = randomBytes(48).toString('hex')
const tokenHash = createHash('sha256').update(refreshTokenPlain).digest('hex')
const expiresAt = new Date(
Date.now() + this.REFRESH_TOKEN_EXP_DAYS * 24 * 60 * 60 * 1000,
)
await this.prisma.refreshToken.create({
data: { tokenHash, userId: user.id, expiresAt },
})
return ResponseMapper.single({ accessToken, refreshToken: refreshTokenPlain, user })
}
async refreshToken(dto: RefreshTokenDto) {
const { refreshToken } = dto
const tokenHash = createHash('sha256').update(refreshToken).digest('hex')
const record = await this.prisma.refreshToken.findFirst({ where: { tokenHash } })
if (!record || record.revoked || record.expiresAt.getTime() < Date.now()) {
throw new UnauthorizedException('Invalid refresh token')
}
const user = await this.prisma.user.findUnique({ where: { id: record.userId } })
if (!user) throw new UnauthorizedException('User not found')
const accessToken = this.jwt.sign(
{ sub: user.id, mobileNumber: user.mobileNumber },
{ expiresIn: this.ACCESS_TOKEN_EXP },
)
return { accessToken }
}
async logout(refreshToken: string) {
const tokenHash = createHash('sha256').update(refreshToken).digest('hex')
await this.prisma.refreshToken.updateMany({
where: { tokenHash },
data: { revoked: true },
})
return { ok: true }
}
}
@@ -0,0 +1,8 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest()
return req.user
},
)
+12
View File
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class LoginDto {
@ApiProperty({ description: 'Mobile number used as username', example: '09123456789' })
@IsString()
mobileNumber: string
@ApiProperty({ description: 'OTP password (one-time password)', example: '12345' })
@IsString()
password: string
}
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class RefreshTokenDto {
@ApiProperty()
@IsString()
refreshToken: string
}
+8
View File
@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class SendCodeDto {
@ApiProperty({ example: '09123456789' })
@IsString()
mobileNumber: string
}
+12
View File
@@ -0,0 +1,12 @@
import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class VerifyCodeDto {
@ApiProperty({ example: '09123456789' })
@IsString()
mobileNumber: string
@ApiProperty({ example: '12345' })
@IsString()
code: string
}
+35
View File
@@ -0,0 +1,35 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Request } from 'express'
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwt: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest<Request>()
const auth = req.headers.authorization
if (!auth) throw new UnauthorizedException('Missing Authorization header')
const parts = auth.split(' ')
if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new UnauthorizedException('Invalid Authorization header format')
}
const token = parts[1]
try {
const payload = this.jwt.verify(token, {
secret: process.env.JWT_SECRET || 'secret',
})
;(req as any).user = payload
return true
} catch (err) {
throw new UnauthorizedException('Invalid or expired token')
}
}
}
@@ -1,39 +0,0 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { BankAccountsService } from './bank-accounts.service'
import { CreateBankAccountDto } from './dto/create-bank-account.dto'
import { UpdateBankAccountDto } from './dto/update-bank-account.dto'
@Controller('bank-accounts')
export class BankAccountsController {
constructor(private readonly bankAccountsService: BankAccountsService) {}
@Post()
create(@Body() dto: CreateBankAccountDto) {
return this.bankAccountsService.create(dto)
}
@Get()
findAll() {
return this.bankAccountsService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.bankAccountsService.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateBankAccountDto) {
return this.bankAccountsService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.bankAccountsService.remove(Number(id))
}
@Get(':id/transactions')
getTransactions(@Param('id') id: string) {
return this.bankAccountsService.getTransactions(Number(id))
}
}
@@ -1,13 +0,0 @@
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, BankAccountsWorkflow],
exports: [BankAccountsWorkflow],
})
export class BankAccountsModule {}
@@ -1,153 +0,0 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { BankTransactionRefType } from '../../generated/prisma/enums'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class BankAccountsService {
constructor(private prisma: PrismaService) {}
private readonly bankAccountQuery = {
include: {
branch: {
select: {
id: true,
name: true,
code: true,
bank: {
select: { id: true, name: true, shortName: true },
},
},
},
bankAccountTransactions: {
take: 1,
select: { id: true, createdAt: true, balanceAfter: true },
orderBy: { createdAt: 'desc' as const },
},
},
omit: {
branchId: true,
},
}
async create(data: any) {
const item = await this.prisma.bankAccount.create({ data })
return ResponseMapper.create(item)
}
async findAll() {
const items = await this.prisma.bankAccount.findMany(this.bankAccountQuery)
const mappedData = items.map(({ bankAccountTransactions, ...rest }) => ({
...rest,
currentBalance: Number(bankAccountTransactions[0]?.balanceAfter || 0),
}))
return ResponseMapper.list(mappedData)
}
async findOne(id: number) {
const item = await this.prisma.bankAccount.findUniqueOrThrow({
...this.bankAccountQuery,
where: { id },
})
const { bankAccountTransactions, ...rest } = item
return ResponseMapper.single({
...rest,
currentBalance: Number(bankAccountTransactions[0]?.balanceAfter || 0),
})
}
async update(id: number, data: any) {
const item = await this.prisma.bankAccount.update({ where: { id }, data })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.bankAccount.delete({ where: { id } })
return ResponseMapper.single(item)
}
async getTransactions(bankAccountId: number, page = 1, pageSize = 50) {
const query = {
where: { bankAccountId },
orderBy: { createdAt: 'desc' as const },
skip: (page - 1) * pageSize,
take: pageSize,
}
const [items, count] = await this.prisma.$transaction([
this.prisma.bankAccountTransaction.findMany({
...query,
omit: { bankAccountId: true },
}),
this.prisma.bankAccountTransaction.count({ where: { bankAccountId } }),
])
const PosReferenceIds = [] as number[]
const purchaseReferenceIds = [] as number[]
items.forEach(({ referenceType, referenceId }) => {
if (referenceId && referenceType) {
if (
referenceType === BankTransactionRefType.POS_SALE ||
referenceType === BankTransactionRefType.POS_REFUND
) {
PosReferenceIds.push(referenceId)
} else if (
referenceType === BankTransactionRefType.PURCHASE_PAYMENT ||
referenceType === BankTransactionRefType.PURCHASE_REFUND
) {
purchaseReferenceIds.push(referenceId)
}
}
})
const posRecords = await this.prisma.salesInvoice.findMany({
where: { id: { in: PosReferenceIds } },
select: {
id: true,
code: true,
totalAmount: true,
posAccount: {
select: { id: true, name: true },
},
customer: {
select: { id: true, firstName: true, lastName: true },
},
},
})
const purchaseRecords = await this.prisma.purchaseReceipt.findMany({
where: { id: { in: purchaseReferenceIds } },
select: {
id: true,
code: true,
totalAmount: true,
supplier: {
select: { id: true, firstName: true, lastName: true },
},
},
})
const mappedItems = items.map(tx => {
let reference = null as any
if (tx.referenceId && tx.referenceType) {
if (
tx.referenceType === BankTransactionRefType.POS_SALE ||
tx.referenceType === BankTransactionRefType.POS_REFUND
) {
reference = posRecords.find(r => r.id === tx.referenceId) || null
} else if (
tx.referenceType === BankTransactionRefType.PURCHASE_PAYMENT ||
tx.referenceType === BankTransactionRefType.PURCHASE_REFUND
) {
reference = purchaseRecords.find(r => r.id === tx.referenceId) || null
}
}
return { ...tx, reference }
})
return ResponseMapper.paginate(mappedItems, count, page, pageSize)
}
}
@@ -1,39 +0,0 @@
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.bankAccountTransaction.findFirst({
where: {
bankAccountId: payload.bankAccountId,
},
})
const balance = item ? Number(item?.balanceAfter) : 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,
},
})
}
}
@@ -1,22 +0,0 @@
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,27 +0,0 @@
import { IsInt, IsOptional, IsString } from 'class-validator'
export class CreateBankAccountDto {
@IsString()
accountNumber: string
@IsString()
name: string
@IsString()
iban: string
@IsInt()
branchId: number
@IsOptional()
@IsString()
createdAt?: string
@IsOptional()
@IsString()
updatedAt?: string
@IsOptional()
@IsString()
deletedAt?: string
}
@@ -1,4 +0,0 @@
import { PartialType } from '@nestjs/swagger'
import { CreateBankAccountDto } from './create-bank-account.dto'
export class UpdateBankAccountDto extends PartialType(CreateBankAccountDto) {}
@@ -1,34 +0,0 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { BankBranchesService } from './bank-branches.service'
import { CreateBankBranchDto } from './dto/create-bank-branches.dto'
import { UpdateBankBranchDto } from './dto/update-bank-branches.dto'
@Controller('bank-branches')
export class BankBranchesController {
constructor(private readonly bankBranchesService: BankBranchesService) {}
@Post()
create(@Body() dto: CreateBankBranchDto) {
return this.bankBranchesService.create(dto)
}
@Get()
findAll() {
return this.bankBranchesService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.bankBranchesService.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateBankBranchDto) {
return this.bankBranchesService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.bankBranchesService.remove(Number(id))
}
}
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { BankBranchesController } from './bank-branches.controller'
import { BankBranchesService } from './bank-branches.service'
@Module({
imports: [PrismaModule],
controllers: [BankBranchesController],
providers: [BankBranchesService],
})
export class BankBranchesModule {}
@@ -1,12 +0,0 @@
import { IsInt, IsString } from 'class-validator'
export class CreateBankBranchDto {
@IsString()
name: string
@IsString()
code: string
@IsInt()
bankId: number
}
-12
View File
@@ -1,12 +0,0 @@
import { Controller, Get } from '@nestjs/common'
import { BanksService } from './banks.service'
@Controller('banks')
export class BanksController {
constructor(private readonly banksService: BanksService) {}
@Get()
findAll() {
return this.banksService.findAll()
}
}
-11
View File
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { BanksController } from './banks.controller'
import { BanksService } from './banks.service'
@Module({
imports: [PrismaModule],
controllers: [BanksController],
providers: [BanksService],
})
export class BanksModule {}
-15
View File
@@ -1,15 +0,0 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class BanksService {
constructor(private prisma: PrismaService) {}
async findAll() {
const items = await this.prisma.bank.findMany({
select: { id: true, name: true, shortName: true },
})
return ResponseMapper.list(items)
}
}
-27
View File
@@ -1,27 +0,0 @@
import { Controller, Get, Query } from '@nestjs/common'
import { ApiQuery } from '@nestjs/swagger'
import { CardexService } from './cardex.service'
@Controller('cardex')
export class CardexController {
constructor(private readonly service: CardexService) {}
@Get('')
@ApiQuery({ name: 'startDate', required: true, type: Date })
@ApiQuery({ name: 'endDate', required: false, type: Date })
@ApiQuery({ name: 'inventoryId', required: false, type: Number })
@ApiQuery({ name: 'productId', required: false, type: Number })
async getStock(
@Query('startDate') startDate: Date,
@Query('endDate') endDate: Date,
@Query('inventoryId') inventoryId: number,
@Query('productId') productId: number,
) {
return this.service.getCardex({
startDate,
endDate,
inventoryId,
productId,
})
}
}
-11
View File
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { CardexController } from './cardex.controller'
import { CardexService } from './cardex.service'
@Module({
imports: [PrismaModule],
controllers: [CardexController],
providers: [CardexService],
})
export class CardexModule {}
-54
View File
@@ -1,54 +0,0 @@
import { Injectable } from '@nestjs/common'
import dayjs from 'dayjs'
import { ResponseMapper } from '../../common/response/response-mapper'
import { Prisma } from '../../generated/prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { ReadCardexDto } from './dto/read-cardex.dto'
@Injectable()
export class CardexService {
constructor(private prisma: PrismaService) {}
async getCardex({ startDate, endDate, inventoryId, productId }: ReadCardexDto) {
if (!endDate) {
endDate = dayjs(startDate).add(1, 'months').toDate()
}
const where: Prisma.StockMovementWhereInput = {}
if (inventoryId !== undefined) where.inventoryId = inventoryId
if (productId !== undefined) where.productId = productId
if (startDate || endDate) {
where.createdAt = {}
if (startDate) where.createdAt.gte = new Date(startDate)
if (endDate) where.createdAt.lte = new Date(endDate)
}
const items = await this.prisma.stockMovement.findMany({
where,
include: {
product: { select: { id: true, name: true, sku: true } },
inventory: { select: { id: true, name: true } },
counterInventory: {
select: { id: true, name: true },
},
supplier: {
select: { id: true, firstName: true, lastName: true },
},
customer: {
select: { id: true, firstName: true, lastName: true },
},
},
omit: {
productId: true,
inventoryId: true,
counterInventoryId: true,
supplierId: true,
customerId: true,
},
orderBy: { createdAt: 'desc' },
})
return ResponseMapper.list(items)
}
}
-23
View File
@@ -1,23 +0,0 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { IsDateString, IsNumber, IsOptional } from 'class-validator'
export class ReadCardexDto {
@ApiPropertyOptional()
@IsDateString()
startDate: Date
@ApiPropertyOptional()
@IsOptional()
@IsDateString()
endDate?: Date
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
inventoryId?: number
@ApiPropertyOptional()
@IsOptional()
@IsNumber()
productId?: number
}
@@ -0,0 +1,34 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CustomersService } from './customers.service'
import { CreateCustomerDto } from './dto/create-customer.dto'
import { UpdateCustomerDto } from './dto/update-customer.dto'
@Controller('customers')
export class CustomersController {
constructor(private readonly customersService: CustomersService) {}
@Post()
create(@Body() dto: CreateCustomerDto) {
return this.customersService.create(dto)
}
@Get()
findAll() {
return this.customersService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.customersService.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateCustomerDto) {
return this.customersService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.customersService.remove(Number(id))
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { CustomersController } from './customers.controller'
import { CustomersService } from './customers.service'
@Module({
imports: [PrismaModule],
controllers: [CustomersController],
providers: [CustomersService],
})
export class CustomersModule {}
@@ -3,37 +3,31 @@ import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class BankBranchesService {
export class CustomersService {
constructor(private prisma: PrismaService) {}
async create(data: any) {
const item = await this.prisma.bankBranch.create({ data })
const item = await this.prisma.customer.create({ data })
return ResponseMapper.create(item)
}
async findAll() {
const items = await this.prisma.bankBranch.findMany({
include: {
bank: {
select: { id: true, name: true, shortName: true },
},
},
})
const items = await this.prisma.customer.findMany()
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.bankBranch.findUnique({ where: { id } })
const item = await this.prisma.customer.findUnique({ where: { id } })
if (!item) return null
return ResponseMapper.single(item)
}
async update(id: number, data: any) {
const item = await this.prisma.bankBranch.update({ where: { id }, data })
const item = await this.prisma.customer.update({ where: { id }, data })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.bankBranch.delete({ where: { id } })
const item = await this.prisma.customer.delete({ where: { id } })
return ResponseMapper.single(item)
}
}
@@ -1,6 +1,6 @@
import { IsEmail, IsOptional, IsString } from 'class-validator'
export class CreateSupplierDto {
export class CreateCustomerDto {
@IsString()
firstName: string
@@ -1,7 +1,7 @@
import { Type } from 'class-transformer'
import { IsBoolean, IsEmail, IsOptional, IsString } from 'class-validator'
export class UpdateSupplierDto {
export class UpdateCustomerDto {
@IsOptional()
@IsString()
firstName?: string
@@ -1,6 +0,0 @@
import { IsNumber } from 'class-validator'
export class UpdateInventoryBankAccountDto {
@IsNumber()
bankAccountId: number
}
@@ -1,38 +0,0 @@
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)
}
}
@@ -1,11 +0,0 @@
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 {}
@@ -1,76 +0,0 @@
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)
}
}
@@ -1,13 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { IsOptional, IsString } from 'class-validator'
export class CreateInventoryDto {
@ApiProperty()
@IsString()
name: string
@ApiPropertyOptional()
@IsOptional()
@IsString()
location?: string
}
@@ -1,21 +0,0 @@
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
}
@@ -1,59 +0,0 @@
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))
}
@Get(':id/cardex')
getCardex(@Param('id') id: string) {
return this.inventoriesService.getCardex(Number(id))
}
}
@@ -1,211 +0,0 @@
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', 'referenceType'],
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,
AND: {
referenceId: group.referenceId,
AND: { referenceType: group.referenceType },
},
},
include: {
product: true,
counterInventory: {
select: { id: true, name: true },
},
},
})
let info = null as any
const mapped = movements.map(movement => {
const { id, quantity, unitPrice, totalCost, avgCost, product } = movement
if (info === null) {
info = {
date: movement.createdAt,
type: movement.type,
quantity: Number(movement.quantity),
unitPrice: Number(movement.unitPrice),
totalCost: Number(movement.totalCost),
referenceType: movement.referenceType,
referenceId: movement.referenceId,
counterInventory: movement.counterInventory,
createdAt: movement.createdAt,
}
} else {
info.quantity += Number(movement.quantity)
info.unitPrice += Number(movement.unitPrice)
info.totalCost += Number(movement.totalCost)
}
return {
id,
quantity: Number(quantity),
unitPrice: Number(unitPrice),
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 getCardex(inventoryId: number) {
const movements = await this.prisma.stockMovement.findMany({
where: { inventoryId },
orderBy: { createdAt: 'asc' },
include: {
customer: { select: { id: true, firstName: true, lastName: true } },
supplier: { select: { id: true, firstName: true, lastName: true } },
counterInventory: {
select: { id: true, name: true },
},
product: {
select: { id: true, name: true, sku: true },
},
},
omit: {
inventoryId: true,
productId: true,
customerId: true,
supplierId: true,
counterInventoryId: true,
},
})
return ResponseMapper.list(movements)
}
async getProductCardex(inventoryId: number, productId: number) {
const movements = await this.prisma.stockMovement.findMany({
where: { productId, inventoryId },
orderBy: { createdAt: 'asc' },
include: {
customer: { select: { id: true, firstName: true, lastName: true } },
supplier: { select: { id: true, firstName: true, lastName: true } },
counterInventory: {
select: { id: true, name: true },
},
},
})
return ResponseMapper.list(movements)
}
async getInventoryBankAccounts(inventoryId: number) {
const items = await this.prisma.bankAccount.findMany({
where: { inventoryBankAccounts: { some: { inventoryId } } },
})
return ResponseMapper.list(items)
}
}
@@ -1,5 +0,0 @@
export interface IInventoryResponse {
name: string
location?: string
isActive: boolean
}
@@ -1,13 +0,0 @@
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 {}
@@ -1,16 +0,0 @@
import { IsInt, IsOptional, IsString } from 'class-validator'
export class CreatePosAccountDto {
@IsString()
name: string
@IsString()
code: string
@IsOptional()
@IsString()
description?: string
@IsInt()
bankAccountId: number
}
@@ -1,8 +0,0 @@
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
}
@@ -1,45 +0,0 @@
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))
}
}
@@ -1,11 +0,0 @@
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 {}
@@ -1,157 +0,0 @@
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: {
inventoryBankAccount: {
include: {
bankAccount: {
select: {
id: true,
name: true,
accountNumber: true,
branch: {
select: {
id: true,
name: true,
bank: { select: { id: true, name: true } },
},
},
},
},
},
},
salesInvoices: {
where: {
createdAt: { gte: new Date(new Date().setHours(0, 0, 0, 0)) },
},
select: {
id: true,
code: true,
totalAmount: true,
createdAt: true,
},
},
},
omit: {
inventoryId: true,
bankAccountId: true,
},
})
return ResponseMapper.list(
items.map(item => {
const { inventoryBankAccount, salesInvoices, ...rest } = item
return {
...rest,
bankAccount: inventoryBankAccount?.bankAccount,
todaySalesInfo: {
salesCount: salesInvoices.length,
salesTotal: salesInvoices.reduce(
(acc, si) => acc + Number(si.totalAmount),
0,
),
},
}
}),
)
}
async findOne(inventoryId: number, id: number) {
const item = await this.prisma.posAccount.findFirst({
where: {
id,
inventoryId,
},
include: {
inventoryBankAccount: {
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 ? { ...item, bankAccount: item.inventoryBankAccount?.bankAccount } : null,
)
}
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)
}
}
-21
View File
@@ -1,21 +0,0 @@
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
}
@@ -1,37 +0,0 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { ArrayMinSize, IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
export class CreateOrderDto {
@ApiPropertyOptional()
@IsOptional()
@IsInt()
@Type(() => Number)
customerId?: number
@ApiPropertyOptional()
@IsOptional()
@IsString()
description?: string
@ApiProperty()
@ArrayMinSize(1)
items: CreateOrderItemDto[]
}
export class CreateOrderItemDto {
@ApiProperty()
@IsInt()
@Type(() => Number)
productId: number
@ApiProperty()
@IsNumber()
@Type(() => Number)
count: number
@ApiProperty()
@IsNumber()
@Type(() => Number)
unitPrice: number
}
@@ -1,44 +0,0 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { OrderStatus } from '../../../generated/prisma/enums'
import { CreateOrderDto } from './dto/create-order-item.dto'
import { PosOrdersService } from './orders.service'
@Controller('pos/:posId/orders')
export class PosOrdersController {
constructor(private readonly posOrdersService: PosOrdersService) {}
@Get('')
get(@Param('posId') posId: string) {
return this.posOrdersService.getOrders(Number(posId))
}
@Get('/held')
getHeld(@Param('posId') posId: string) {
return this.posOrdersService.getOrders(Number(posId), OrderStatus.PENDING)
}
@Post('')
async createOrder(@Param('posId') posId: string, @Body() dto: CreateOrderDto) {
return this.posOrdersService.createOrder(Number(posId), dto)
}
@Get('/:orderId')
getOrderDetails(@Param('posId') posId: string, @Param('orderId') orderId: string) {
return this.posOrdersService.getOrderDetails(Number(posId), Number(orderId))
}
@Post('/:orderId/reject')
async rejectOrder(@Param('posId') posId: string, @Param('orderId') orderId: string) {
return this.posOrdersService.rejectOrder(Number(posId), Number(orderId))
}
@Post('/:orderId/done')
async completeOrder(@Param('posId') posId: string, @Param('orderId') orderId: string) {
return this.posOrdersService.completeOrder(Number(posId), Number(orderId))
}
@Post('/:orderId/cancel')
async cancelOrder(@Param('posId') posId: string, @Param('orderId') orderId: string) {
return this.posOrdersService.cancelOrder(Number(posId), Number(orderId))
}
}
-13
View File
@@ -1,13 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../../prisma/prisma.module'
import { SalesInvoicesModule } from '../../sales-invoices/sales-invoices.module'
import { PosOrdersController } from './orders.controller'
import { PosOrdersService } from './orders.service'
import { PosOrdersWorkflow } from './orders.workflow'
@Module({
imports: [PrismaModule, SalesInvoicesModule],
controllers: [PosOrdersController],
providers: [PosOrdersService, PosOrdersWorkflow],
})
export class PosOrdersModule {}
-145
View File
@@ -1,145 +0,0 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { Prisma } from '../../../generated/prisma/client'
import { OrderStatus } from '../../../generated/prisma/enums'
import { OrderCreateInput } from '../../../generated/prisma/models'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreateOrderDto } from './dto/create-order-item.dto'
import { PosOrdersWorkflow } from './orders.workflow'
@Injectable()
export class PosOrdersService {
constructor(
private prisma: PrismaService,
private posWorkflow: PosOrdersWorkflow,
) {}
async createOrder(posId: number, data: CreateOrderDto) {
const { customerId, items, ...rest } = data
const lastCode = await this.prisma.order
.findFirst({
where: { posAccountId: posId },
orderBy: { orderNumber: 'desc' },
select: { orderNumber: true },
})
.then(si => si?.orderNumber || '0')
const newCode = String(Number(lastCode) + 1)
const totalAmount = data.items.reduce(
(acc, item) => acc + item.count * item.unitPrice,
0,
)
const preparedOrder: OrderCreateInput = {
...rest,
orderNumber: newCode,
posAccount: { connect: { id: posId } },
customer: customerId ? { connect: { id: customerId } } : undefined,
totalAmount,
orderItems: {
create: items.map(item => ({
product: { connect: { id: Number(item.productId) } },
quantity: item.count,
unitPrice: item.unitPrice,
totalAmount: item.count * item.unitPrice,
})),
},
}
const item = await this.prisma.$transaction(async tx => {
const order = await tx.order.create({ data: preparedOrder })
await this.posWorkflow.onCreateOrder(tx, order.id, posId)
})
return ResponseMapper.create(item)
}
async createAndConfirmOrder(posId: number, data: CreateOrderDto) {}
async rejectOrder(posAccountId: number, orderId: number) {
const item = await this.prisma.$transaction(async tx => {
const order = await tx.order.update({
where: { id: orderId, posAccountId },
data: { status: OrderStatus.REJECTED },
})
await this.posWorkflow.onRejectOrder(tx, orderId)
return order
})
return ResponseMapper.single(item)
}
async completeOrder(posAccountId: number, orderId: number) {
const item = await this.prisma.$transaction(async tx => {
const order = await tx.order.update({
where: { id: orderId, posAccountId },
data: { status: OrderStatus.DONE },
})
await this.posWorkflow.onConfirmOrder(tx, orderId)
return order
})
return ResponseMapper.single(item)
}
async cancelOrder(posAccountId: number, orderId: number) {
const item = await this.prisma.$transaction(async tx => {
const order = await tx.order.update({
where: { id: orderId, posAccountId },
data: { status: OrderStatus.CANCELED },
})
await this.posWorkflow.onCancelOrder(tx, orderId)
return order
})
return ResponseMapper.single(item)
}
async getOrders(posId: number, status?: OrderStatus) {
const where: Prisma.OrderWhereInput = {
posAccountId: posId,
status,
}
if (status) {
where.status = status
}
const items = await this.prisma.order.findMany({
where,
include: {
customer: {
select: {
id: true,
firstName: true,
lastName: true,
},
},
orderItems: {
include: {
product: true,
},
},
},
orderBy: { createdAt: 'desc' },
})
return ResponseMapper.list(items)
}
async getOrderDetails(posAccountId: number, orderId: number) {
const item = await this.prisma.order.findUniqueOrThrow({
where: { id: orderId, posAccountId },
include: {
customer: {
select: {
id: true,
firstName: true,
lastName: true,
},
},
orderItems: {
include: {
product: true,
},
},
},
})
return ResponseMapper.single(item)
}
}
-87
View File
@@ -1,87 +0,0 @@
import { Injectable } from '@nestjs/common'
import { Prisma } from '../../../generated/prisma/client'
import { SalesInvoicesWorkflow } from '../../sales-invoices/sales-invoices.workflow'
@Injectable()
export class PosOrdersWorkflow {
constructor(private salesInvoicesWorkflow: SalesInvoicesWorkflow) {}
async onCreateOrder(tx: Prisma.TransactionClient, orderId: number, posId: number) {
const { inventoryId } = await tx.posAccount.findUniqueOrThrow({
where: { id: posId },
select: { inventoryId: true },
})
const orderItems = await tx.orderItem.findMany({
where: { orderId },
select: {
product: true,
quantity: true,
},
})
await tx.stockReservation.createMany({
data: orderItems.map(item => ({
orderId,
productId: item.product.id,
quantity: item.quantity,
inventoryId,
})),
})
}
async onRejectOrder(tx: Prisma.TransactionClient, orderId: number) {
const orderItemIds = await tx.orderItem
.findMany({
where: { orderId },
select: {
productId: true,
},
})
.then(items => items.map(i => i.productId))
await tx.stockReservation.deleteMany({
where: {
orderId,
productId: { in: orderItemIds },
},
})
}
async onConfirmOrder(tx: Prisma.TransactionClient, orderId: number) {
const orderItems = await tx.orderItem.findMany({
where: { orderId },
select: {
product: true,
quantity: true,
},
})
await tx.stockReservation.deleteMany({
where: {
orderId,
productId: { in: orderItems.map(i => i.product.id) },
},
})
await this.salesInvoicesWorkflow.onCreateByOrder(tx, orderId)
}
async onCancelOrder(tx: Prisma.TransactionClient, orderId: number) {
const orderItemIds = await tx.orderItem
.findMany({
where: { orderId },
select: {
productId: true,
},
})
.then(items => items.map(i => i.productId))
await tx.stockReservation.deleteMany({
where: {
orderId,
productId: { in: orderItemIds },
},
})
}
}
-34
View File
@@ -1,34 +0,0 @@
import { Controller, Get, Param, Query } from '@nestjs/common'
import { PosService } from './pos.service'
@Controller('pos')
export class PosController {
constructor(private readonly posService: PosService) {}
@Get('/:posId')
getInfo(@Param('posId') posId: string) {
return this.posService.getInfo(Number(posId))
}
@Get('/:posId/stock')
async getStock(
@Param('posId') posId: string,
@Query('isAvailable') isAvailable?: string,
@Query('page') page?: string,
@Query('pageSize') pageSize?: string,
@Query('q') q?: string,
) {
const options = {
isAvailable: isAvailable ? isAvailable === 'true' : undefined,
page: page ? Number(page) : undefined,
pageSize: pageSize ? Number(pageSize) : undefined,
q,
}
return this.posService.getStock(Number(posId), options)
}
@Get('/:posId/product-categories')
async getProductCategories(@Param('posId') posId: string) {
return this.posService.getProductCategories(Number(posId))
}
}
-12
View File
@@ -1,12 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { PosOrdersModule } from './orders/orders.module'
import { PosController } from './pos.controller'
import { PosService } from './pos.service'
@Module({
imports: [PrismaModule, PosOrdersModule],
controllers: [PosController],
providers: [PosService],
})
export class PosModule {}
-154
View File
@@ -1,154 +0,0 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class PosService {
constructor(private prisma: PrismaService) {}
async getInfo(posId: number) {
const info = await this.prisma.posAccount.findUniqueOrThrow({
where: { id: posId },
include: {
inventoryBankAccount: {
include: {
bankAccount: {
select: {
id: true,
name: true,
accountNumber: true,
branch: {
select: {
id: true,
name: true,
bank: { select: { id: true, name: true } },
},
},
},
},
inventory: {
select: { id: true, name: true },
},
},
},
},
})
const { inventoryBankAccount, ...rest } = info
return ResponseMapper.single({
...rest,
bankAccount: inventoryBankAccount?.bankAccount,
inventory: inventoryBankAccount?.inventory,
})
}
async getStock(
posId: number,
options: {
isAvailable?: boolean
page?: number
pageSize?: number
q?: string
productIds?: number[]
} = {},
) {
const { isAvailable, page = 1, pageSize = 50, q, productIds } = options
const pos = await this.prisma.posAccount.findUniqueOrThrow({
where: { id: posId },
select: { inventoryId: true },
})
const inventoryId = pos.inventoryId
let sql = `
SELECT sb.productId, sb.quantity - COALESCE(SUM(sr.quantity), 0) as availableQuantity, p.id, p.name, p.sku, p.description, p.salePrice, p.categoryId
FROM Stock_Balance sb
LEFT JOIN Stock_Reservations sr ON sb.productId = sr.productId AND sb.inventoryId = sr.inventoryId
LEFT JOIN Products p ON sb.productId = p.id
WHERE sb.inventoryId = ?
`
const params: any[] = [inventoryId]
if (productIds && productIds.length > 0) {
sql += ` AND sb.productId IN (${productIds.map(() => '?').join(',')})`
params.push(...productIds)
}
if (q) {
sql += ` AND (p.name LIKE ? OR p.sku LIKE ?)`
params.push(`%${q}%`, `%${q}%`)
}
sql += ` GROUP BY sb.productId`
if (isAvailable !== undefined) {
sql += isAvailable
? ` HAVING availableQuantity > 0`
: ` HAVING availableQuantity <= 0`
}
sql += ` ORDER BY sb.quantity DESC`
const items = await this.prisma.$queryRawUnsafe(sql, ...params)
const mapped = (items as any[]).map(item => ({
id: item.productId,
quantity: Number(item.availableQuantity),
product: {
id: item.id,
name: item.name,
sku: item.sku,
description: item.description,
salePrice: item.salePrice,
categoryId: item.categoryId,
},
}))
const count = mapped.length
const paginated = mapped.slice((page - 1) * pageSize, page * pageSize)
return ResponseMapper.paginate(paginated, count, page, pageSize)
}
async getProductCategories(posId: number) {
const pos = await this.prisma.posAccount.findUniqueOrThrow({
where: { id: posId },
select: { inventoryId: true },
})
const inventoryId = pos.inventoryId
const balances = await this.prisma.stockBalance.findMany({
where: { inventoryId },
select: {
product: { select: { id: true, category: { select: { id: true, name: true } } } },
quantity: true,
},
})
const map = new Map<
number,
{ id: number; name: string; totalQuantity: number; productCount: number }
>()
for (const b of balances) {
const product = b.product
const cat = product?.category
if (!cat) continue // skip products without category
const existing = map.get(cat.id)
if (existing) {
existing.totalQuantity += Number(b.quantity)
existing.productCount += 1
} else {
map.set(cat.id, {
id: cat.id,
name: cat.name,
totalQuantity: Number(b.quantity),
productCount: 1,
})
}
}
const categories = Array.from(map.values())
return ResponseMapper.list(categories)
}
}
@@ -0,0 +1,14 @@
import { IsOptional, IsString } from 'class-validator'
export class CreateProductCategoryDto {
@IsString()
name: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
imageUrl?: string
}
@@ -1,6 +1,6 @@
import { IsOptional, IsString } from 'class-validator'
export class UpdateBankBranchDto {
export class UpdateProductCategoryDto {
@IsOptional()
@IsString()
name?: string
@@ -0,0 +1,34 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CreateProductCategoryDto } from './dto/create-product-category.dto'
import { UpdateProductCategoryDto } from './dto/update-product-category.dto'
import { ProductCategoriesService } from './product-categories.service'
@Controller('product-categories')
export class ProductCategoriesController {
constructor(private readonly productCategoriesService: ProductCategoriesService) {}
@Post()
create(@Body() dto: CreateProductCategoryDto) {
return this.productCategoriesService.create(dto)
}
@Get()
findAll() {
return this.productCategoriesService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.productCategoriesService.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateProductCategoryDto) {
return this.productCategoriesService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.productCategoriesService.remove(Number(id))
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { ProductCategoriesController } from './product-categories.controller'
import { ProductCategoriesService } from './product-categories.service'
@Module({
imports: [PrismaModule],
controllers: [ProductCategoriesController],
providers: [ProductCategoriesService],
})
export class ProductCategoriesModule {}
@@ -0,0 +1,33 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class ProductCategoriesService {
constructor(private prisma: PrismaService) {}
async create(data: any) {
const item = await this.prisma.goodCategory.create({ data })
return ResponseMapper.create(item)
}
async findAll() {
const items = await this.prisma.goodCategory.findMany()
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.goodCategory.findUnique({ where: { id } })
if (!item) return null
return ResponseMapper.single(item)
}
async update(id: number, data: any) {
const item = await this.prisma.goodCategory.update({ where: { id }, data })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.goodCategory.delete({ where: { id } })
return ResponseMapper.single(item)
}
}
@@ -1,15 +1,17 @@
import { Type } from 'class-transformer'
import { IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
import { IsInt, IsOptional, IsString } from 'class-validator'
export class CreateProductDto {
@IsString()
name: string
export class UpdatePurchaseReceiptDto {
@IsOptional()
@IsString()
code?: string
sku?: string
@IsOptional()
@Type(() => Number)
@IsNumber()
totalAmount?: number
@IsString()
barcode?: string
@IsOptional()
@IsString()
@@ -18,10 +20,14 @@ export class UpdatePurchaseReceiptDto {
@IsOptional()
@Type(() => Number)
@IsInt()
supplierId?: number | null
brandId?: number
@IsOptional()
@Type(() => Number)
@IsInt()
inventoryId?: number | null
categoryId?: number
@IsOptional()
@Type(() => Number)
minimumStockAlertLevel?: number
}
@@ -0,0 +1,35 @@
import { Type } from 'class-transformer'
import { IsInt, IsOptional, IsString } from 'class-validator'
export class UpdateProductDto {
@IsOptional()
@IsString()
name?: string
@IsOptional()
@IsString()
sku?: string
@IsOptional()
@IsString()
barcode?: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@Type(() => Number)
@IsInt()
brandId?: number
@IsOptional()
@Type(() => Number)
@IsInt()
categoryId?: number
@IsOptional()
@Type(() => Number)
@IsInt()
minimumStockAlertLevel?: number
}
@@ -0,0 +1,37 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'
import { ApiQuery } from '@nestjs/swagger'
import { CreateProductDto } from './dto/create-product.dto'
import { UpdateProductDto } from './dto/update-product.dto'
import { ProductsService } from './products.service'
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) {}
@Post()
create(@Body() dto: CreateProductDto) {
return this.productsService.create(dto)
}
@Get()
@ApiQuery({ name: 'page', required: false, type: Number, default: 1 })
@ApiQuery({ name: 'pageSize', required: false, type: Number, default: 30 })
findAll(@Query('page') page: number = 1, @Query('pageSize') pageSize: number = 30) {
return this.productsService.findAll(page, pageSize)
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.productsService.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateProductDto) {
return this.productsService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.productsService.remove(Number(id))
}
}
+11
View File
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { ProductsController } from './products.controller'
import { ProductsService } from './products.service'
@Module({
imports: [PrismaModule],
controllers: [ProductsController],
providers: [ProductsService],
})
export class ProductsModule {}
+131
View File
@@ -0,0 +1,131 @@
import { Injectable } from '@nestjs/common'
import { ShortEntity } from '../../common/interfaces/response-models'
import { ResponseMapper } from '../../common/response/response-mapper'
import { Prisma } from '../../generated/prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateProductDto } from './dto/create-product.dto'
import { UpdateProductDto } from './dto/update-product.dto'
@Injectable()
export class ProductsService {
constructor(private prisma: PrismaService) {}
async create(data: CreateProductDto) {
const { brandId, categoryId, ...rest } = data
const dataToCreate = {
...rest,
brand: brandId ? { connect: { id: Number(brandId) } } : undefined,
category: categoryId ? { connect: { id: Number(categoryId) } } : undefined,
}
const item = await this.prisma.product.create({ data: dataToCreate })
return ResponseMapper.create(item)
}
async findAll(page = 1, pageSize = 20) {
const query: Prisma.ProductFindManyArgs = {}
const [items, count] = await this.prisma.$transaction([
this.prisma.product.findMany({
include: { brand: true, category: true, stockBalances: true },
skip: (page - 1) * pageSize,
take: pageSize,
}),
this.prisma.product.count(),
])
const mapped = items.map(p => {
const { brandId, categoryId, ...rest } = p as any
const brand: ShortEntity | null = p.brand
? { id: p.brand.id, name: p.brand.name }
: null
const category: ShortEntity | null = p.category
? { id: p.category.id, name: p.category.name }
: null
const stock =
p.stockBalances && p.stockBalances.length > 0
? p.stockBalances.reduce((acc, sb) => acc + Number(sb.quantity), 0)
: 0
return {
...rest,
salePrice: Number(p.salePrice),
minimumStockAlertLevel: Number(p.minimumStockAlertLevel),
brand,
category,
stock,
}
})
return ResponseMapper.paginate(mapped, count, page, pageSize)
}
async findOne(id: number) {
const product = await this.prisma.product.findUniqueOrThrow({
where: { id },
include: {
brand: {
select: { id: true, name: true },
},
category: {
select: { id: true, name: true },
},
stockBalances: {
select: {
quantity: true,
avgCost: true,
inventory: { select: { id: true, name: true } },
},
},
salesInvoiceItems: {
select: { id: true },
},
},
omit: {
brandId: true,
categoryId: true,
},
})
const { salesInvoiceItems, ...rest } = product
return ResponseMapper.single({
...rest,
salePrice: Number(product.salePrice),
minimumStockAlertLevel: Number(product.minimumStockAlertLevel),
stock: product.stockBalances?.reduce((acc, sb) => acc + Number(sb.quantity), 0),
avgCost:
product.stockBalances?.reduce((acc, sb) => acc + Number(sb.avgCost), 0) /
product.stockBalances.length,
salesCount: product.salesInvoiceItems.length,
})
}
async update(id: number, data: UpdateProductDto) {
const { brandId, categoryId, ...rest } = data
const dataToUpdate = {
...rest,
brand:
brandId === null
? { disconnect: true }
: brandId
? { connect: { id: Number(brandId) } }
: undefined,
category:
categoryId === null
? { disconnect: true }
: categoryId
? { connect: { id: Number(categoryId) } }
: undefined,
}
const item = await this.prisma.product.update({ where: { id }, data: dataToUpdate })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.product.delete({ where: { id } })
return ResponseMapper.single(item)
}
}
@@ -1,32 +0,0 @@
import { Type } from 'class-transformer'
import { ArrayMinSize, IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
import { CreatePurchaseReceiptItemDto } from '../../purchase-receipt-items/dto/create-purchase-receipt-item.dto'
export class CreatePurchaseReceiptDto {
@IsString()
code: string
@Type(() => Number)
@IsNumber()
totalAmount: number
@Type(() => Number)
@IsNumber()
paidAmount: number
@IsOptional()
@IsString()
description?: string
@Type(() => Number)
@IsInt()
supplierId: number
@Type(() => Number)
@IsInt()
inventoryId: number
@Type(() => Array)
@ArrayMinSize(1)
items: Array<Omit<CreatePurchaseReceiptItemDto, 'receiptId'>>
}
@@ -1,34 +0,0 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CreatePurchaseReceiptDto } from './dto/create-purchase-receipt.dto'
import { UpdatePurchaseReceiptDto } from './dto/update-purchase-receipt.dto'
import { PurchaseReceiptsService } from './purchase-receipts.service'
@Controller('purchase-receipts')
export class PurchaseReceiptsController {
constructor(private readonly service: PurchaseReceiptsService) {}
@Post()
create(@Body() dto: CreatePurchaseReceiptDto) {
return this.service.create(dto)
}
@Get()
findAll() {
return this.service.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.service.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdatePurchaseReceiptDto) {
return this.service.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.service.remove(Number(id))
}
}
@@ -1,114 +0,0 @@
import { Injectable } from '@nestjs/common'
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,
private purchaseReceiptWorkflow: PurchaseReceiptWorkflow,
) {}
async create(dto: CreatePurchaseReceiptDto) {
const data: Prisma.PurchaseReceiptCreateInput = {
code: dto.code,
totalAmount: new Prisma.Decimal(dto.totalAmount),
description: dto.description ?? null,
supplier: { connect: { id: dto.supplierId } },
inventory: { connect: { id: dto.inventoryId } },
items: dto.items?.length
? {
create: dto.items.map((item, idx) => {
const mapped = {
count: new Prisma.Decimal(item.count ?? 0),
unitPrice: new Prisma.Decimal(item.unitPrice ?? 0),
totalAmount: new Prisma.Decimal(item.totalAmount ?? 0),
description: item.description ?? null,
product: { connect: { id: item.productId } },
}
return mapped
}),
}
: undefined,
}
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)
})
}
async findAll() {
const items = await this.prisma.purchaseReceipt.findMany({
include: { supplier: true, inventory: true },
})
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.purchaseReceipt.findUnique({
where: { id },
include: {
supplier: {
select: {
id: true,
firstName: true,
lastName: true,
},
},
inventory: {
select: {
id: true,
name: true,
},
},
items: true,
},
})
if (!item) return null
return ResponseMapper.single(item)
}
async update(id: number, data: any) {
const payload: any = { ...data }
if (Object.prototype.hasOwnProperty.call(payload, 'supplierId')) {
if (payload.supplierId === null) payload.supplier = { disconnect: true }
else payload.supplier = { connect: { id: Number(payload.supplierId) } }
delete payload.supplierId
}
if (Object.prototype.hasOwnProperty.call(payload, 'inventoryId')) {
if (payload.inventoryId === null) payload.inventory = { disconnect: true }
else payload.inventory = { connect: { id: Number(payload.inventoryId) } }
delete payload.inventoryId
}
const item = await this.prisma.purchaseReceipt.update({
where: { id },
data: payload,
})
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.purchaseReceipt.delete({ where: { id } })
return ResponseMapper.single(item)
}
}
@@ -1,23 +0,0 @@
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,41 +0,0 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CreatePurchaseReceiptItemDto } from './dto/create-purchase-receipt-item.dto'
import { UpdatePurchaseReceiptItemDto } from './dto/update-purchase-receipt-item.dto'
import { PurchaseReceiptItemsService } from './purchase-receipt-items.service'
@Controller('purchase-receipts/:receiptId/items')
export class PurchaseReceiptItemsController {
constructor(private readonly service: PurchaseReceiptItemsService) {}
@Post()
create(
@Param('receiptId') receiptId: string,
@Body() dto: CreatePurchaseReceiptItemDto,
) {
return this.service.createForReceipt(Number(receiptId), dto)
}
@Get()
findAll(@Param('receiptId') receiptId: string) {
return this.service.findAllForReceipt(Number(receiptId))
}
@Get(':id')
findOne(@Param('receiptId') receiptId: string, @Param('id') id: string) {
return this.service.findOneForReceipt(Number(receiptId), Number(id))
}
@Patch(':id')
update(
@Param('receiptId') receiptId: string,
@Param('id') id: string,
@Body() dto: UpdatePurchaseReceiptItemDto,
) {
return this.service.updateForReceipt(Number(receiptId), Number(id), dto)
}
@Delete(':id')
remove(@Param('receiptId') receiptId: string, @Param('id') id: string) {
return this.service.removeForReceipt(Number(receiptId), Number(id))
}
}
@@ -1,11 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../../prisma/prisma.module'
import { PurchaseReceiptItemsController } from './purchase-receipt-items.controller'
import { PurchaseReceiptItemsService } from './purchase-receipt-items.service'
@Module({
imports: [PrismaModule],
controllers: [PurchaseReceiptItemsController],
providers: [PurchaseReceiptItemsService],
})
export class PurchaseReceiptItemsModule {}
@@ -1,100 +0,0 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
import { CreatePurchaseReceiptItemDto } from './dto/create-purchase-receipt-item.dto'
@Injectable()
export class PurchaseReceiptItemsService {
constructor(private prisma: PrismaService) {}
async create(dto: CreatePurchaseReceiptItemDto) {
const payload: any = { ...dto }
if (Object.prototype.hasOwnProperty.call(payload, 'productId')) {
payload.product = { connect: { id: Number(payload.productId) } }
delete payload.productId
}
if (Object.prototype.hasOwnProperty.call(payload, 'receiptId')) {
payload.receipt = { connect: { id: Number(payload.receiptId) } }
delete payload.receiptId
}
payload.unitPrice = String(payload.unitPrice)
payload.count = String(payload.count)
payload.totalAmount = String(payload.totalAmount)
const item = await this.prisma.purchaseReceiptItem.create({ data: payload })
return ResponseMapper.create(item)
}
async findAll(receiptId: number) {
const where = { receiptId: Number(receiptId) }
const items = await this.prisma.purchaseReceiptItem.findMany({
where,
include: { product: true },
})
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.purchaseReceiptItem.findUnique({
where: { id },
include: { product: true, receipt: true },
})
if (!item) return null
return ResponseMapper.single(item)
}
async createForReceipt(receiptId: number, dto: CreatePurchaseReceiptItemDto) {
const payload: any = { ...dto, receiptId }
return this.create(payload)
}
async findAllForReceipt(receiptId: number) {
return this.findAll(receiptId)
}
async findOneForReceipt(receiptId: number, id: number) {
const item = await this.prisma.purchaseReceiptItem.findFirst({
where: { id, receiptId },
include: { product: true, receipt: true },
})
if (!item) return null
return ResponseMapper.single(item)
}
async update(id: number, data: any) {
const payload: any = { ...data }
if (Object.prototype.hasOwnProperty.call(payload, 'productId')) {
payload.product = { connect: { id: Number(payload.productId) } }
delete payload.productId
}
if (Object.prototype.hasOwnProperty.call(payload, 'receiptId')) {
payload.receipt = { connect: { id: Number(payload.receiptId) } }
delete payload.receiptId
}
const item = await this.prisma.purchaseReceiptItem.update({
where: { id },
data: payload,
})
return ResponseMapper.update(item)
}
async updateForReceipt(receiptId: number, id: number, data: any) {
const existing = await this.prisma.purchaseReceiptItem.findFirst({
where: { id, receiptId },
})
if (!existing) return null
return this.update(id, data)
}
async remove(id: number) {
const item = await this.prisma.purchaseReceiptItem.delete({ where: { id } })
return ResponseMapper.single(item)
}
async removeForReceipt(receiptId: number, id: number) {
const existing = await this.prisma.purchaseReceiptItem.findFirst({
where: { id, receiptId },
})
if (!existing) return null
return this.remove(id)
}
}
@@ -1,30 +0,0 @@
import { IsEnum, IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
import { PaymentMethodType, PaymentType } from '../../../../generated/prisma/enums'
export class CreatePurchaseReceiptPaymentDto {
@IsNumber()
amount: number
@IsEnum(PaymentMethodType)
paymentMethod: PaymentMethodType
@IsEnum(PaymentType)
type: PaymentType
@IsInt()
bankAccountId: number
@IsInt()
receiptId: number
@IsString()
payedAt: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
createdAt?: string
}
@@ -1,6 +0,0 @@
import { PartialType } from '@nestjs/swagger'
import { CreatePurchaseReceiptPaymentDto } from './create-purchase-receipt-payment.dto'
export class UpdatePurchaseReceiptPaymentDto extends PartialType(
CreatePurchaseReceiptPaymentDto,
) {}
@@ -1,36 +0,0 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CreatePurchaseReceiptPaymentDto } from './dto/create-purchase-receipt-payment.dto'
import { UpdatePurchaseReceiptPaymentDto } from './dto/update-purchase-receipt-payment.dto'
import { PurchaseReceiptPaymentsService } from './purchase-receipt-payments.service'
@Controller('purchase-receipt-payments')
export class PurchaseReceiptPaymentsController {
constructor(
private readonly purchaseReceiptPaymentsService: PurchaseReceiptPaymentsService,
) {}
@Post()
create(@Body() dto: CreatePurchaseReceiptPaymentDto) {
return this.purchaseReceiptPaymentsService.create(dto)
}
@Get()
findAll() {
return this.purchaseReceiptPaymentsService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.purchaseReceiptPaymentsService.findOne(Number(id))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdatePurchaseReceiptPaymentDto) {
return this.purchaseReceiptPaymentsService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.purchaseReceiptPaymentsService.remove(Number(id))
}
}
@@ -1,15 +0,0 @@
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,80 +0,0 @@
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 { CreatePurchaseReceiptPaymentDto } from './dto/create-purchase-receipt-payment.dto'
import { PurchaseReceiptPaymentsWorkflow } from './purchase-receipt-payments.workflow'
@Injectable()
export class PurchaseReceiptPaymentsService {
constructor(
private prisma: PrismaService,
private purchaseReceiptPaymentsWorkflow: PurchaseReceiptPaymentsWorkflow,
) {}
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() {
const items = await this.prisma.purchaseReceiptPayments.findMany({
include: {
receipt: {
select: {
id: true,
code: true,
totalAmount: true,
paidAmount: true,
status: true,
supplier: {
select: { id: true, firstName: true, lastName: true },
},
},
},
},
})
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.purchaseReceiptPayments.findUnique({
where: { id },
include: {
receipt: {
select: {
id: true,
code: true,
totalAmount: true,
paidAmount: true,
status: true,
supplier: {
select: { id: true, firstName: true, lastName: true },
},
},
},
},
})
if (!item) return null
return ResponseMapper.single(item)
}
async update(id: number, data: any) {
const item = await this.prisma.purchaseReceiptPayments.update({ where: { id }, data })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.purchaseReceiptPayments.delete({ where: { id } })
return ResponseMapper.single(item)
}
}
@@ -1,58 +0,0 @@
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,
},
})
}
}
@@ -1,21 +0,0 @@
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 {}
@@ -1,25 +1,24 @@
import { Type } from 'class-transformer'
import { IsNotEmpty, IsNumber, Min } from 'class-validator'
import { IsInt, IsNumber } from 'class-validator'
export class CreatePurchaseReceiptItemDto {
export class CreateSalesInvoiceItemDto {
@Type(() => Number)
@IsNumber()
@Min(0)
count: number
@Type(() => Number)
@IsNumber()
@Min(0)
unitPrice: number
@Type(() => Number)
@IsNumber()
@Min(0)
totalAmount: number
@Type(() => Number)
@IsNotEmpty()
productId: number
@IsInt()
invoiceId: number
description?: string
@Type(() => Number)
@IsInt()
productId: number
}
@@ -1,7 +1,7 @@
import { Type } from 'class-transformer'
import { IsInt, IsNumber, IsOptional } from 'class-validator'
export class UpdatePurchaseReceiptItemDto {
export class UpdateSalesInvoiceItemDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
@@ -20,7 +20,7 @@ export class UpdatePurchaseReceiptItemDto {
@IsOptional()
@Type(() => Number)
@IsInt()
receiptId?: number
invoiceId?: number
@IsOptional()
@Type(() => Number)
@@ -0,0 +1,38 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CreateSalesInvoiceItemDto } from './dto/create-sales-invoice-item.dto'
import { UpdateSalesInvoiceItemDto } from './dto/update-sales-invoice-item.dto'
import { SalesInvoiceItemsService } from './sales-invoice-items.service'
@Controller('sales-invoices/:invoiceId/items')
export class SalesInvoiceItemsController {
constructor(private readonly service: SalesInvoiceItemsService) {}
@Post()
create(@Param('invoiceId') invoiceId: string, @Body() dto: CreateSalesInvoiceItemDto) {
return this.service.createForInvoice(Number(invoiceId), dto)
}
@Get()
findAll(@Param('invoiceId') invoiceId: string) {
return this.service.findAllForInvoice(Number(invoiceId))
}
@Get(':id')
findOne(@Param('invoiceId') invoiceId: string, @Param('id') id: string) {
return this.service.findOneForInvoice(Number(invoiceId), Number(id))
}
@Patch(':id')
update(
@Param('invoiceId') invoiceId: string,
@Param('id') id: string,
@Body() dto: UpdateSalesInvoiceItemDto,
) {
return this.service.updateForInvoice(Number(invoiceId), Number(id), dto)
}
@Delete(':id')
remove(@Param('invoiceId') invoiceId: string, @Param('id') id: string) {
return this.service.removeForInvoice(Number(invoiceId), Number(id))
}
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { SalesInvoiceItemsController } from './sales-invoice-items.controller'
import { SalesInvoiceItemsService } from './sales-invoice-items.service'
@Module({
imports: [PrismaModule],
controllers: [SalesInvoiceItemsController],
providers: [SalesInvoiceItemsService],
})
export class SalesInvoiceItemsModule {}
@@ -0,0 +1,98 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateSalesInvoiceItemDto } from './dto/create-sales-invoice-item.dto'
@Injectable()
export class SalesInvoiceItemsService {
constructor(private prisma: PrismaService) {}
async create(dto: CreateSalesInvoiceItemDto) {
const payload: any = { ...dto }
if (Object.prototype.hasOwnProperty.call(payload, 'invoiceId')) {
payload.invoice = { connect: { id: Number(payload.invoiceId) } }
delete payload.invoiceId
}
if (Object.prototype.hasOwnProperty.call(payload, 'productId')) {
payload.product = { connect: { id: Number(payload.productId) } }
delete payload.productId
}
const item = await this.prisma.salesInvoiceItem.create({ data: payload })
return ResponseMapper.create(item)
}
async findAll(invoiceId?: number) {
const where = invoiceId ? { invoiceId: Number(invoiceId) } : undefined
const items = await this.prisma.salesInvoiceItem.findMany({
where,
include: { product: true },
})
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.salesInvoiceItem.findUnique({
where: { id },
include: { product: true, invoice: true },
})
if (!item) return null
return ResponseMapper.single(item)
}
async createForInvoice(invoiceId: number, dto: CreateSalesInvoiceItemDto) {
const payload: any = { ...dto, invoiceId }
return this.create(payload)
}
async findAllForInvoice(invoiceId: number) {
return this.findAll(invoiceId)
}
async findOneForInvoice(invoiceId: number, id: number) {
const item = await this.prisma.salesInvoiceItem.findFirst({
where: { id, invoiceId },
include: { product: true, invoice: true },
})
if (!item) return null
return ResponseMapper.single(item)
}
async update(id: number, data: any) {
const payload: any = { ...data }
if (Object.prototype.hasOwnProperty.call(payload, 'productId')) {
payload.product = { connect: { id: Number(payload.productId) } }
delete payload.productId
}
if (Object.prototype.hasOwnProperty.call(payload, 'invoiceId')) {
payload.invoice = { connect: { id: Number(payload.invoiceId) } }
delete payload.invoiceId
}
const item = await this.prisma.salesInvoiceItem.update({
where: { id },
data: payload,
})
return ResponseMapper.update(item)
}
async updateForInvoice(invoiceId: number, id: number, data: any) {
const existing = await this.prisma.salesInvoiceItem.findFirst({
where: { id, invoiceId },
})
if (!existing) return null
return this.update(id, data)
}
async remove(id: number) {
const item = await this.prisma.salesInvoiceItem.delete({ where: { id } })
return ResponseMapper.single(item)
}
async removeForInvoice(invoiceId: number, id: number) {
const existing = await this.prisma.salesInvoiceItem.findFirst({
where: { id, invoiceId },
})
if (!existing) return null
return this.remove(id)
}
}
@@ -0,0 +1,17 @@
import { Controller, Get } from '@nestjs/common'
import { StockBalanceService } from './stock-balance.service'
@Controller('stock-balance')
export class StockBalanceController {
constructor(private readonly service: StockBalanceService) {}
@Get()
findAll() {
return this.service.findAll()
}
// @Get(':itemId')
// findOne(@Param('itemId') itemId: string) {
// return this.service.findOne(Number(itemId))
// }
}
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { StockBalanceController } from './stock-balance.controller'
import { StockBalanceService } from './stock-balance.service'
@Module({
imports: [PrismaModule],
controllers: [StockBalanceController],
providers: [StockBalanceService],
})
export class StockBalanceModule {}
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class StockBalanceService {
constructor(private prisma: PrismaService) {}
async findAll() {
const items = await this.prisma.stockBalance.findMany()
return ResponseMapper.list(items)
}
// async findOne(productId: number) {
// const item = await this.prisma.stockBalance.findUnique({ where: { productId: productId } })
// if (!item) return null
// return ResponseMapper.single(item)
// }
}
@@ -1,34 +0,0 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { CreateSupplierDto } from './dto/create-supplier.dto'
import { UpdateSupplierDto } from './dto/update-supplier.dto'
import { SuppliersService } from './suppliers.service'
@Controller('suppliers')
export class SuppliersController {
constructor(private readonly suppliersService: SuppliersService) {}
@Post()
create(@Body() dto: CreateSupplierDto) {
return this.suppliersService.create(dto)
}
@Get()
findAll() {
return this.suppliersService.findAll()
}
@Get(':supplierId')
findOne(@Param('supplierId') supplierId: string) {
return this.suppliersService.findOne(Number(supplierId))
}
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateSupplierDto) {
return this.suppliersService.update(Number(id), dto)
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.suppliersService.remove(Number(id))
}
}
@@ -1,49 +0,0 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
@Injectable()
export class SuppliersService {
constructor(private prisma: PrismaService) {}
async create(data: any) {
const item = await this.prisma.supplier.create({ data })
return ResponseMapper.create(item)
}
async findAll() {
const items = await this.prisma.supplier.findMany()
return ResponseMapper.list(items)
}
async findOne(id: number) {
const item = await this.prisma.supplier.findUnique({ where: { id } })
if (!item) return null
const receiptsInfo = await this.prisma.purchaseReceipt.aggregate({
where: {
supplierId: id,
},
_count: true,
_sum: {
totalAmount: true,
},
})
return ResponseMapper.single({
...item,
receiptsOverview: {
totalPayment: receiptsInfo._sum.totalAmount,
count: receiptsInfo._count,
},
})
}
async update(id: number, data: any) {
const item = await this.prisma.supplier.update({ where: { id }, data })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.supplier.delete({ where: { id } })
return ResponseMapper.single(item)
}
}
@@ -1,34 +0,0 @@
import {
IsDateString,
IsEnum,
IsInt,
IsNumber,
IsOptional,
IsString,
} from 'class-validator'
import { PaymentMethodType, PaymentType } from '../../../../generated/prisma/enums'
export class CreateReceiptPaymentDto {
@IsNumber()
amount: number
@IsEnum(PaymentMethodType)
paymentMethod: PaymentMethodType
@IsEnum(PaymentType)
type: PaymentType
@IsInt()
bankAccountId: number
@IsDateString()
payedAt: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
createdAt?: string
}
@@ -1,47 +0,0 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { CreateReceiptPaymentDto } from './dto/create-receipt-payment.dto'
import { SupplierInvoicesService } from './invoices.service'
@Controller('suppliers/:supplierId/invoices')
export class SupplierInvoicesController {
constructor(private readonly supplierInvoicesService: SupplierInvoicesService) {}
@Get()
getInvoices(@Param('supplierId') supplierId: string) {
return this.supplierInvoicesService.findAll(Number(supplierId))
}
@Get(':invoiceId')
getInvoice(
@Param('supplierId') supplierId: string,
@Param('invoiceId') invoiceId: string,
) {
return this.supplierInvoicesService.findOne(Number(supplierId), Number(invoiceId))
}
@Post(':invoiceId/pay')
createPayment(
@Param('supplierId') supplierId: string,
@Param('invoiceId') invoiceId: string,
@Body() data: CreateReceiptPaymentDto,
) {
return this.supplierInvoicesService.createPayment(
Number(supplierId),
Number(invoiceId),
data,
)
}
@Get(':invoiceId/payments')
getInvoicePayments(
@Param('supplierId') supplierId: string,
@Param('invoiceId') invoiceId: string,
) {
return this.supplierInvoicesService.findPayments(Number(invoiceId))
}
@Get('payments')
getPayments(@Param('supplierId') supplierId: string) {
return this.supplierInvoicesService.findPayments(Number(supplierId))
}
}
@@ -1,14 +0,0 @@
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, PurchaseReceiptPaymentsModule, SupplierLedgerModule],
controllers: [SupplierInvoicesController],
providers: [SupplierInvoicesService, SupplierInvoicesWorkflow],
})
export class SupplierInvoicesModule {}
@@ -1,196 +0,0 @@
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,
private supplierInvoicesWorkflow: SupplierInvoicesWorkflow,
) {}
async findAll(supplierId: number) {
const items = await this.prisma.purchaseReceipt.findMany({
where: {
supplierId,
},
include: {
inventory: {
select: {
id: true,
name: true,
},
},
items: {
select: {
id: true,
count: true,
unitPrice: true,
totalAmount: true,
product: {
select: {
id: true,
name: true,
sku: true,
},
},
},
},
payments: {
select: {
id: true,
amount: true,
},
},
},
omit: {
inventoryId: true,
supplierId: true,
},
})
return ResponseMapper.list(items)
}
async findOne(supplierId: number, invoiceId: number) {
const invoice = await this.prisma.purchaseReceipt.findUnique({
where: {
id: invoiceId,
supplierId,
},
include: {
inventory: {
select: {
id: true,
name: true,
},
},
items: true,
},
omit: {
inventoryId: true,
supplierId: true,
},
})
return ResponseMapper.single(invoice)
}
async findPayments(invoiceId: number) {
const items = await this.prisma.purchaseReceiptPayments.findMany({
where: {
receipt: {
id: invoiceId,
},
},
include: {
receipt: {
include: {
inventory: {
select: {
id: true,
name: true,
},
},
},
},
bankAccount: {
select: {
branch: {
select: {
id: true,
name: true,
bank: {
select: {
id: true,
name: true,
shortName: true,
},
},
},
},
accountNumber: true,
cardNumber: true,
name: true,
id: true,
iban: true,
},
},
},
omit: {
receiptId: true,
},
orderBy: {
payedAt: 'desc',
},
})
return ResponseMapper.list(items)
}
async findAllPayments(supplierId: number) {
const items = await this.prisma.purchaseReceiptPayments.findMany({
where: {
receipt: {
supplierId,
},
},
include: {
receipt: {
select: {
id: true,
totalAmount: true,
status: true,
paidAmount: true,
},
},
},
omit: {
receiptId: true,
},
})
return ResponseMapper.list(items)
}
async createPayment(
supplierId: number,
invoiceId: number,
data: CreateReceiptPaymentDto,
) {
const { bankAccountId, ...rest } = data
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,
},
},
},
include: {
receipt: true,
},
omit: {
receiptId: true,
},
})
return ResponseMapper.create(payment)
})
}
}
@@ -1,36 +0,0 @@
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',
)
}
}
@@ -1,12 +0,0 @@
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))
}
}
@@ -1,13 +0,0 @@
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 {}
@@ -1,23 +0,0 @@
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)
}
}
@@ -1,57 +0,0 @@
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,
},
})
}
}
-13
View File
@@ -1,13 +0,0 @@
import { Module } from '@nestjs/common'
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, SupplierLedgerModule],
controllers: [SuppliersController],
providers: [SuppliersService],
})
export class SuppliersModule {}
@@ -0,0 +1,21 @@
import { Injectable } from '@nestjs/common'
import { File } from 'multer'
import { UploaderService } from './uploader.service'
@Injectable()
export class ImageUploaderService {
constructor(private readonly uploaderService: UploaderService) {}
async uploadImage(
file: File,
accountId: string,
type: string,
): Promise<string> {
return this.uploaderService.uploadFile(
file.buffer,
file.originalname,
accountId,
type,
)
}
}

Some files were not shown because too many files have changed in this diff Show More