feat(cardex): implement cardex controller, service, and DTO for stock movements

feat(pos): add POS controller and service for order creation and stock retrieval
refactor(pos): enhance stock and product category retrieval with pagination and mapping
This commit is contained in:
2025-12-21 19:09:41 +03:30
parent d514267f58
commit c6a86719dd
42 changed files with 3769 additions and 119 deletions
+5 -1
View File
@@ -1,11 +1,13 @@
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { AuthModule } from './auth/auth.module'
import { CustomersModule } from './customers/customers.module'
import { InventoriesModule } from './inventories/inventories.module'
import { InventoryTransferItemsModule } from './inventory-transfer-items/inventory-transfer-items.module'
import { InventoryTransfersModule } from './inventory-transfers/inventory-transfers.module'
import { PosModule } from './pos/pos.module'
import { CardexModule } from './modules/cardex/cardex.module'
import { PosModule } from './modules/pos/pos.module'
import { PrismaModule } from './prisma/prisma.module'
import { ProductBrandsModule } from './product-brands/product-brands.module'
import { ProductCategoriesModule } from './product-categories/product-categories.module'
@@ -28,6 +30,7 @@ import { UsersModule } from './users/users.module'
imports: [
PrismaModule,
UsersModule,
AuthModule,
RolesModule,
ProductsModule,
ProductVariantsModule,
@@ -48,6 +51,7 @@ import { UsersModule } from './users/users.module'
StockAdjustmentsModule,
StockBalanceModule,
PosModule,
CardexModule,
],
controllers: [AppController],
providers: [AppService],
+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 }
}
}
+8
View File
@@ -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
}
+8
View File
@@ -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')
}
}
}
+10
View File
@@ -107,6 +107,16 @@ export type StockMovement = Prisma.StockMovementModel
*
*/
export type StockBalance = Prisma.StockBalanceModel
/**
* Model OtpCode
*
*/
export type OtpCode = Prisma.OtpCodeModel
/**
* Model RefreshToken
*
*/
export type RefreshToken = Prisma.RefreshTokenModel
/**
* Model InventoryTransfer
*
+10
View File
@@ -127,6 +127,16 @@ export type StockMovement = Prisma.StockMovementModel
*
*/
export type StockBalance = Prisma.StockBalanceModel
/**
* Model OtpCode
*
*/
export type OtpCode = Prisma.OtpCodeModel
/**
* Model RefreshToken
*
*/
export type RefreshToken = Prisma.RefreshTokenModel
/**
* Model InventoryTransfer
*
File diff suppressed because one or more lines are too long
@@ -402,6 +402,8 @@ export const ModelName = {
SalesInvoiceItem: 'SalesInvoiceItem',
StockMovement: 'StockMovement',
StockBalance: 'StockBalance',
OtpCode: 'OtpCode',
RefreshToken: 'RefreshToken',
InventoryTransfer: 'InventoryTransfer',
InventoryTransferItem: 'InventoryTransferItem',
StockAdjustment: 'StockAdjustment',
@@ -424,7 +426,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "role" | "productVariant" | "product" | "productBrand" | "productCategory" | "supplier" | "customer" | "inventory" | "store" | "productCharge" | "order" | "purchaseReceipt" | "purchaseReceiptItem" | "salesInvoice" | "salesInvoiceItem" | "stockMovement" | "stockBalance" | "inventoryTransfer" | "inventoryTransferItem" | "stockAdjustment" | "triggerLog" | "inventory_overview" | "stock_cardex" | "stock_view"
modelProps: "user" | "role" | "productVariant" | "product" | "productBrand" | "productCategory" | "supplier" | "customer" | "inventory" | "store" | "productCharge" | "order" | "purchaseReceipt" | "purchaseReceiptItem" | "salesInvoice" | "salesInvoiceItem" | "stockMovement" | "stockBalance" | "otpCode" | "refreshToken" | "inventoryTransfer" | "inventoryTransferItem" | "stockAdjustment" | "triggerLog" | "inventory_overview" | "stock_cardex" | "stock_view"
txIsolationLevel: TransactionIsolationLevel
}
model: {
@@ -1616,6 +1618,138 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
}
}
}
OtpCode: {
payload: Prisma.$OtpCodePayload<ExtArgs>
fields: Prisma.OtpCodeFieldRefs
operations: {
findUnique: {
args: Prisma.OtpCodeFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload> | null
}
findUniqueOrThrow: {
args: Prisma.OtpCodeFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
findFirst: {
args: Prisma.OtpCodeFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload> | null
}
findFirstOrThrow: {
args: Prisma.OtpCodeFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
findMany: {
args: Prisma.OtpCodeFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>[]
}
create: {
args: Prisma.OtpCodeCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
createMany: {
args: Prisma.OtpCodeCreateManyArgs<ExtArgs>
result: BatchPayload
}
delete: {
args: Prisma.OtpCodeDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
update: {
args: Prisma.OtpCodeUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
deleteMany: {
args: Prisma.OtpCodeDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.OtpCodeUpdateManyArgs<ExtArgs>
result: BatchPayload
}
upsert: {
args: Prisma.OtpCodeUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$OtpCodePayload>
}
aggregate: {
args: Prisma.OtpCodeAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateOtpCode>
}
groupBy: {
args: Prisma.OtpCodeGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OtpCodeGroupByOutputType>[]
}
count: {
args: Prisma.OtpCodeCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.OtpCodeCountAggregateOutputType> | number
}
}
}
RefreshToken: {
payload: Prisma.$RefreshTokenPayload<ExtArgs>
fields: Prisma.RefreshTokenFieldRefs
operations: {
findUnique: {
args: Prisma.RefreshTokenFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload> | null
}
findUniqueOrThrow: {
args: Prisma.RefreshTokenFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
findFirst: {
args: Prisma.RefreshTokenFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload> | null
}
findFirstOrThrow: {
args: Prisma.RefreshTokenFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
findMany: {
args: Prisma.RefreshTokenFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>[]
}
create: {
args: Prisma.RefreshTokenCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
createMany: {
args: Prisma.RefreshTokenCreateManyArgs<ExtArgs>
result: BatchPayload
}
delete: {
args: Prisma.RefreshTokenDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
update: {
args: Prisma.RefreshTokenUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
deleteMany: {
args: Prisma.RefreshTokenDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.RefreshTokenUpdateManyArgs<ExtArgs>
result: BatchPayload
}
upsert: {
args: Prisma.RefreshTokenUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$RefreshTokenPayload>
}
aggregate: {
args: Prisma.RefreshTokenAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateRefreshToken>
}
groupBy: {
args: Prisma.RefreshTokenGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RefreshTokenGroupByOutputType>[]
}
count: {
args: Prisma.RefreshTokenCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.RefreshTokenCountAggregateOutputType> | number
}
}
}
InventoryTransfer: {
payload: Prisma.$InventoryTransferPayload<ExtArgs>
fields: Prisma.InventoryTransferFieldRefs
@@ -2289,6 +2423,30 @@ export const StockBalanceScalarFieldEnum = {
export type StockBalanceScalarFieldEnum = (typeof StockBalanceScalarFieldEnum)[keyof typeof StockBalanceScalarFieldEnum]
export const OtpCodeScalarFieldEnum = {
id: 'id',
mobileNumber: 'mobileNumber',
code: 'code',
used: 'used',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type OtpCodeScalarFieldEnum = (typeof OtpCodeScalarFieldEnum)[keyof typeof OtpCodeScalarFieldEnum]
export const RefreshTokenScalarFieldEnum = {
id: 'id',
tokenHash: 'tokenHash',
userId: 'userId',
revoked: 'revoked',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type RefreshTokenScalarFieldEnum = (typeof RefreshTokenScalarFieldEnum)[keyof typeof RefreshTokenScalarFieldEnum]
export const InventoryTransferScalarFieldEnum = {
id: 'id',
code: 'code',
@@ -2553,6 +2711,21 @@ export const StockMovementOrderByRelevanceFieldEnum = {
export type StockMovementOrderByRelevanceFieldEnum = (typeof StockMovementOrderByRelevanceFieldEnum)[keyof typeof StockMovementOrderByRelevanceFieldEnum]
export const OtpCodeOrderByRelevanceFieldEnum = {
mobileNumber: 'mobileNumber',
code: 'code'
} as const
export type OtpCodeOrderByRelevanceFieldEnum = (typeof OtpCodeOrderByRelevanceFieldEnum)[keyof typeof OtpCodeOrderByRelevanceFieldEnum]
export const RefreshTokenOrderByRelevanceFieldEnum = {
tokenHash: 'tokenHash'
} as const
export type RefreshTokenOrderByRelevanceFieldEnum = (typeof RefreshTokenOrderByRelevanceFieldEnum)[keyof typeof RefreshTokenOrderByRelevanceFieldEnum]
export const InventoryTransferOrderByRelevanceFieldEnum = {
code: 'code',
description: 'description'
@@ -2778,6 +2951,8 @@ export type GlobalOmitConfig = {
salesInvoiceItem?: Prisma.SalesInvoiceItemOmit
stockMovement?: Prisma.StockMovementOmit
stockBalance?: Prisma.StockBalanceOmit
otpCode?: Prisma.OtpCodeOmit
refreshToken?: Prisma.RefreshTokenOmit
inventoryTransfer?: Prisma.InventoryTransferOmit
inventoryTransferItem?: Prisma.InventoryTransferItemOmit
stockAdjustment?: Prisma.StockAdjustmentOmit
@@ -69,6 +69,8 @@ export const ModelName = {
SalesInvoiceItem: 'SalesInvoiceItem',
StockMovement: 'StockMovement',
StockBalance: 'StockBalance',
OtpCode: 'OtpCode',
RefreshToken: 'RefreshToken',
InventoryTransfer: 'InventoryTransfer',
InventoryTransferItem: 'InventoryTransferItem',
StockAdjustment: 'StockAdjustment',
@@ -374,6 +376,30 @@ export const StockBalanceScalarFieldEnum = {
export type StockBalanceScalarFieldEnum = (typeof StockBalanceScalarFieldEnum)[keyof typeof StockBalanceScalarFieldEnum]
export const OtpCodeScalarFieldEnum = {
id: 'id',
mobileNumber: 'mobileNumber',
code: 'code',
used: 'used',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type OtpCodeScalarFieldEnum = (typeof OtpCodeScalarFieldEnum)[keyof typeof OtpCodeScalarFieldEnum]
export const RefreshTokenScalarFieldEnum = {
id: 'id',
tokenHash: 'tokenHash',
userId: 'userId',
revoked: 'revoked',
expiresAt: 'expiresAt',
createdAt: 'createdAt'
} as const
export type RefreshTokenScalarFieldEnum = (typeof RefreshTokenScalarFieldEnum)[keyof typeof RefreshTokenScalarFieldEnum]
export const InventoryTransferScalarFieldEnum = {
id: 'id',
code: 'code',
@@ -638,6 +664,21 @@ export const StockMovementOrderByRelevanceFieldEnum = {
export type StockMovementOrderByRelevanceFieldEnum = (typeof StockMovementOrderByRelevanceFieldEnum)[keyof typeof StockMovementOrderByRelevanceFieldEnum]
export const OtpCodeOrderByRelevanceFieldEnum = {
mobileNumber: 'mobileNumber',
code: 'code'
} as const
export type OtpCodeOrderByRelevanceFieldEnum = (typeof OtpCodeOrderByRelevanceFieldEnum)[keyof typeof OtpCodeOrderByRelevanceFieldEnum]
export const RefreshTokenOrderByRelevanceFieldEnum = {
tokenHash: 'tokenHash'
} as const
export type RefreshTokenOrderByRelevanceFieldEnum = (typeof RefreshTokenOrderByRelevanceFieldEnum)[keyof typeof RefreshTokenOrderByRelevanceFieldEnum]
export const InventoryTransferOrderByRelevanceFieldEnum = {
code: 'code',
description: 'description'
+2
View File
@@ -26,6 +26,8 @@ export type * from './models/SalesInvoice.js'
export type * from './models/SalesInvoiceItem.js'
export type * from './models/StockMovement.js'
export type * from './models/StockBalance.js'
export type * from './models/OtpCode.js'
export type * from './models/RefreshToken.js'
export type * from './models/InventoryTransfer.js'
export type * from './models/InventoryTransferItem.js'
export type * from './models/StockAdjustment.js'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -245,17 +245,17 @@ export type RoleOrderByWithRelationInput = {
export type RoleWhereUniqueInput = Prisma.AtLeast<{
id?: number
name?: string
AND?: Prisma.RoleWhereInput | Prisma.RoleWhereInput[]
OR?: Prisma.RoleWhereInput[]
NOT?: Prisma.RoleWhereInput | Prisma.RoleWhereInput[]
name?: Prisma.StringFilter<"Role"> | string
description?: Prisma.StringNullableFilter<"Role"> | string | null
permissions?: Prisma.JsonNullableFilter<"Role">
createdAt?: Prisma.DateTimeFilter<"Role"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"Role"> | Date | string
deletedAt?: Prisma.DateTimeNullableFilter<"Role"> | Date | string | null
users?: Prisma.UserListRelationFilter
}, "id">
}, "id" | "name">
export type RoleOrderByWithAggregationInput = {
id?: Prisma.SortOrder
+151
View File
@@ -253,6 +253,7 @@ export type UserWhereInput = {
deletedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.XOR<Prisma.RoleScalarRelationFilter, Prisma.RoleWhereInput>
refreshTokens?: Prisma.RefreshTokenListRelationFilter
}
export type UserOrderByWithRelationInput = {
@@ -266,6 +267,7 @@ export type UserOrderByWithRelationInput = {
deletedAt?: Prisma.SortOrderInput | Prisma.SortOrder
updatedAt?: Prisma.SortOrder
role?: Prisma.RoleOrderByWithRelationInput
refreshTokens?: Prisma.RefreshTokenOrderByRelationAggregateInput
_relevance?: Prisma.UserOrderByRelevanceInput
}
@@ -283,6 +285,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
deletedAt?: Prisma.DateTimeNullableFilter<"User"> | Date | string | null
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
role?: Prisma.XOR<Prisma.RoleScalarRelationFilter, Prisma.RoleWhereInput>
refreshTokens?: Prisma.RefreshTokenListRelationFilter
}, "id" | "mobileNumber">
export type UserOrderByWithAggregationInput = {
@@ -326,6 +329,7 @@ export type UserCreateInput = {
deletedAt?: Date | string | null
updatedAt?: Date | string
role: Prisma.RoleCreateNestedOneWithoutUsersInput
refreshTokens?: Prisma.RefreshTokenCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateInput = {
@@ -338,6 +342,7 @@ export type UserUncheckedCreateInput = {
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedCreateNestedManyWithoutUserInput
}
export type UserUpdateInput = {
@@ -349,6 +354,7 @@ export type UserUpdateInput = {
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.RoleUpdateOneRequiredWithoutUsersNestedInput
refreshTokens?: Prisma.RefreshTokenUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateInput = {
@@ -361,6 +367,7 @@ export type UserUncheckedUpdateInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateManyInput = {
@@ -459,6 +466,11 @@ export type UserOrderByRelationAggregateInput = {
_count?: Prisma.SortOrder
}
export type UserScalarRelationFilter = {
is?: Prisma.UserWhereInput
isNot?: Prisma.UserWhereInput
}
export type StringFieldUpdateOperationsInput = {
set?: string
}
@@ -521,6 +533,20 @@ export type UserUncheckedUpdateManyWithoutRoleNestedInput = {
deleteMany?: Prisma.UserScalarWhereInput | Prisma.UserScalarWhereInput[]
}
export type UserCreateNestedOneWithoutRefreshTokensInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutRefreshTokensInput
connect?: Prisma.UserWhereUniqueInput
}
export type UserUpdateOneRequiredWithoutRefreshTokensNestedInput = {
create?: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
connectOrCreate?: Prisma.UserCreateOrConnectWithoutRefreshTokensInput
upsert?: Prisma.UserUpsertWithoutRefreshTokensInput
connect?: Prisma.UserWhereUniqueInput
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutRefreshTokensInput, Prisma.UserUpdateWithoutRefreshTokensInput>, Prisma.UserUncheckedUpdateWithoutRefreshTokensInput>
}
export type UserCreateWithoutRoleInput = {
mobileNumber: string
password: string
@@ -529,6 +555,7 @@ export type UserCreateWithoutRoleInput = {
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
refreshTokens?: Prisma.RefreshTokenCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutRoleInput = {
@@ -540,6 +567,7 @@ export type UserUncheckedCreateWithoutRoleInput = {
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutRoleInput = {
@@ -583,6 +611,68 @@ export type UserScalarWhereInput = {
updatedAt?: Prisma.DateTimeFilter<"User"> | Date | string
}
export type UserCreateWithoutRefreshTokensInput = {
mobileNumber: string
password: string
firstName: string
lastName: string
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
role: Prisma.RoleCreateNestedOneWithoutUsersInput
}
export type UserUncheckedCreateWithoutRefreshTokensInput = {
id?: number
mobileNumber: string
password: string
firstName: string
lastName: string
roleId: number
createdAt?: Date | string
deletedAt?: Date | string | null
updatedAt?: Date | string
}
export type UserCreateOrConnectWithoutRefreshTokensInput = {
where: Prisma.UserWhereUniqueInput
create: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
}
export type UserUpsertWithoutRefreshTokensInput = {
update: Prisma.XOR<Prisma.UserUpdateWithoutRefreshTokensInput, Prisma.UserUncheckedUpdateWithoutRefreshTokensInput>
create: Prisma.XOR<Prisma.UserCreateWithoutRefreshTokensInput, Prisma.UserUncheckedCreateWithoutRefreshTokensInput>
where?: Prisma.UserWhereInput
}
export type UserUpdateToOneWithWhereWithoutRefreshTokensInput = {
where?: Prisma.UserWhereInput
data: Prisma.XOR<Prisma.UserUpdateWithoutRefreshTokensInput, Prisma.UserUncheckedUpdateWithoutRefreshTokensInput>
}
export type UserUpdateWithoutRefreshTokensInput = {
mobileNumber?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
role?: Prisma.RoleUpdateOneRequiredWithoutUsersNestedInput
}
export type UserUncheckedUpdateWithoutRefreshTokensInput = {
id?: Prisma.IntFieldUpdateOperationsInput | number
mobileNumber?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
firstName?: Prisma.StringFieldUpdateOperationsInput | string
lastName?: Prisma.StringFieldUpdateOperationsInput | string
roleId?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
export type UserCreateManyRoleInput = {
id?: number
mobileNumber: string
@@ -602,6 +692,7 @@ export type UserUpdateWithoutRoleInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refreshTokens?: Prisma.RefreshTokenUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutRoleInput = {
@@ -613,6 +704,7 @@ export type UserUncheckedUpdateWithoutRoleInput = {
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
deletedAt?: Prisma.NullableDateTimeFieldUpdateOperationsInput | Date | string | null
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
refreshTokens?: Prisma.RefreshTokenUncheckedUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateManyWithoutRoleInput = {
@@ -627,6 +719,35 @@ export type UserUncheckedUpdateManyWithoutRoleInput = {
}
/**
* Count Type UserCountOutputType
*/
export type UserCountOutputType = {
refreshTokens: number
}
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
refreshTokens?: boolean | UserCountOutputTypeCountRefreshTokensArgs
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeDefaultArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the UserCountOutputType
*/
select?: Prisma.UserCountOutputTypeSelect<ExtArgs> | null
}
/**
* UserCountOutputType without action
*/
export type UserCountOutputTypeCountRefreshTokensArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
where?: Prisma.RefreshTokenWhereInput
}
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
id?: boolean
@@ -639,6 +760,8 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
deletedAt?: boolean
updatedAt?: boolean
role?: boolean | Prisma.RoleDefaultArgs<ExtArgs>
refreshTokens?: boolean | Prisma.User$refreshTokensArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, ExtArgs["result"]["user"]>
@@ -658,12 +781,15 @@ export type UserSelectScalar = {
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "mobileNumber" | "password" | "firstName" | "lastName" | "roleId" | "createdAt" | "deletedAt" | "updatedAt", ExtArgs["result"]["user"]>
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
role?: boolean | Prisma.RoleDefaultArgs<ExtArgs>
refreshTokens?: boolean | Prisma.User$refreshTokensArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}
export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "User"
objects: {
role: Prisma.$RolePayload<ExtArgs>
refreshTokens: Prisma.$RefreshTokenPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
id: number
@@ -1016,6 +1142,7 @@ readonly fields: UserFieldRefs;
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
readonly [Symbol.toStringTag]: "PrismaPromise"
role<T extends Prisma.RoleDefaultArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.RoleDefaultArgs<ExtArgs>>): Prisma.Prisma__RoleClient<runtime.Types.Result.GetResult<Prisma.$RolePayload<ExtArgs>, T, "findUniqueOrThrow", GlobalOmitOptions> | Null, Null, ExtArgs, GlobalOmitOptions>
refreshTokens<T extends Prisma.User$refreshTokensArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$refreshTokensArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$RefreshTokenPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
/**
* Attaches callbacks for the resolution and/or rejection of the Promise.
* @param onfulfilled The callback to execute when the Promise is resolved.
@@ -1396,6 +1523,30 @@ export type UserDeleteManyArgs<ExtArgs extends runtime.Types.Extensions.Internal
limit?: number
}
/**
* User.refreshTokens
*/
export type User$refreshTokensArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
/**
* Select specific fields to fetch from the RefreshToken
*/
select?: Prisma.RefreshTokenSelect<ExtArgs> | null
/**
* Omit specific fields from the RefreshToken
*/
omit?: Prisma.RefreshTokenOmit<ExtArgs> | null
/**
* Choose, which related nodes to fetch as well
*/
include?: Prisma.RefreshTokenInclude<ExtArgs> | null
where?: Prisma.RefreshTokenWhereInput
orderBy?: Prisma.RefreshTokenOrderByWithRelationInput | Prisma.RefreshTokenOrderByWithRelationInput[]
cursor?: Prisma.RefreshTokenWhereUniqueInput
take?: number
skip?: number
distinct?: Prisma.RefreshTokenScalarFieldEnum | Prisma.RefreshTokenScalarFieldEnum[]
}
/**
* User without action
*/
+27
View File
@@ -0,0 +1,27 @@
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
@@ -0,0 +1,11 @@
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 {}
+56
View File
@@ -0,0 +1,56 @@
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-pos.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: true, inventory: true },
orderBy: { createdAt: 'desc' },
})
const mapped = items.map(i => ({
id: i.id,
type: i.type,
quantity: Number(i.quantity),
fee: Number(i.fee),
totalCost: Number(i.totalCost),
referenceType: i.referenceType,
referenceId: i.referenceId,
createdAt: i.createdAt,
avgCost: Number(i.avgCost),
product: {
name: i.product.name,
sku: i.product.sku,
id: i.productId,
},
inventory: {
name: i.inventory.name,
id: i.inventoryId,
},
}))
return ResponseMapper.list(mapped)
}
}
+23
View File
@@ -0,0 +1,23 @@
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
}
@@ -1,5 +1,5 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../prisma/prisma.module'
import { PrismaModule } from '../../prisma/prisma.module'
import { PosController } from './pos.controller'
import { PosService } from './pos.service'
@@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../common/response/response-mapper'
import { Prisma } from '../generated/prisma/client'
import { PrismaService } from '../prisma/prisma.service'
import { ResponseMapper } from '../../common/response/response-mapper'
import { Prisma } from '../../generated/prisma/client'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateOrderDto } from './dto/create-pos.dto'
@Injectable()
+6 -3
View File
@@ -1,4 +1,5 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
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'
@@ -13,8 +14,10 @@ export class ProductsController {
}
@Get()
findAll() {
return this.productsService.findAll()
@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')
+13 -5
View File
@@ -1,6 +1,7 @@
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'
@@ -28,10 +29,17 @@ export class ProductsService {
return ResponseMapper.create(item)
}
async findAll() {
const items = await this.prisma.product.findMany({
include: { brand: true, category: true, stockBalances: true },
})
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
@@ -48,7 +56,7 @@ export class ProductsService {
return { ...rest, brand, category, stock }
})
return ResponseMapper.list(mapped)
return ResponseMapper.paginate(mapped, count, page, pageSize)
}
async findOne(id: number) {
@@ -23,8 +23,8 @@ export class PurchaseReceiptItemsService {
const item = await this.prisma.purchaseReceiptItem.create({ data: payload })
return ResponseMapper.create(item)
}
async findAll(receiptId?: number) {
const where = receiptId ? { receiptId: Number(receiptId) } : undefined
async findAll(receiptId: number) {
const where = { receiptId: Number(receiptId) }
const items = await this.prisma.purchaseReceiptItem.findMany({
where,
include: { product: true },
@@ -47,7 +47,22 @@ export class PurchaseReceiptsService {
async findOne(id: number) {
const item = await this.prisma.purchaseReceipt.findUnique({
where: { id },
include: { supplier: true, inventory: true, items: true },
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)
+16 -3
View File
@@ -17,9 +17,22 @@ export class SuppliersController {
return this.suppliersService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.suppliersService.findOne(Number(id))
@Get(':supplierId')
findOne(@Param('supplierId') supplierId: string) {
return this.suppliersService.findOne(Number(supplierId))
}
@Get(':supplierId/invoices')
getInvoices(@Param('supplierId') supplierId: string) {
return this.suppliersService.getInvoices(Number(supplierId))
}
@Get(':supplierId/invoices/:invoiceId')
getInvoice(
@Param('supplierId') supplierId: string,
@Param('invoiceId') invoiceId: string,
) {
return this.suppliersService.getInvoice(Number(supplierId), Number(invoiceId))
}
@Patch(':id')
+76 -1
View File
@@ -18,7 +18,82 @@ export class SuppliersService {
async findOne(id: number) {
const item = await this.prisma.supplier.findUnique({ where: { id } })
if (!item) return null
return ResponseMapper.single(item)
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 getInvoices(supplierId: number) {
const items = await this.prisma.purchaseReceipt.findMany({
where: {
supplierId,
},
include: {
inventory: {
select: {
id: true,
name: true,
},
},
items: {
select: {
id: true,
count: true,
fee: true,
total: true,
product: {
select: {
id: true,
name: true,
sku: true,
},
},
},
},
},
omit: {
inventoryId: true,
supplierId: true,
},
})
return ResponseMapper.list(items)
}
async getInvoice(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 update(id: number, data: any) {