feat: add config module with controller and service

- Implemented ConfigController for handling config-related requests.
- Created ConfigService for business logic related to configurations.
- Added CreateConfigDto for validating configuration data.

feat: add good categories module with controller and service

- Implemented GoodCategoriesController for managing good categories.
- Created GoodCategoriesService for business logic related to good categories.
- Added CreateGoodCategoryDto for validating good category data.

feat: add goods module with controller and service

- Implemented GoodsController for managing goods.
- Created GoodsService for business logic related to goods.
- Added CreateGoodDto for validating good data.

feat: add profile module with controller and service

- Implemented ProfileController for managing user profiles.
- Created ProfileService for business logic related to profiles.
- Added CreateProfileDto for profile data structure.

feat: add sales invoice payments module with controller and service

- Implemented SalesInvoicePaymentsController for managing invoice payments.
- Created SalesInvoicePaymentsService for business logic related to payments.
- Added CreateSalesInvoicePaymentDto for validating payment data.

feat: add service categories module with controller and service

- Implemented ServiceCategoriesController for managing service categories.
- Created ServiceCategoriesService for business logic related to service categories.
- Added CreateServiceCategoryDto for validating service category data.

feat: add services module with controller and service

- Implemented ServicesController for managing services.
- Created ServicesService for business logic related to services.
- Added CreateServiceDto for validating service data.

feat: add trigger logs module with controller and service

- Implemented TriggerLogsController for managing trigger logs.
- Created TriggerLogsService for business logic related to trigger logs.
- Added CreateTriggerLogDto for validating trigger log data.

feat: add uploaders module for handling file uploads

- Implemented UploadersController for managing file uploads.
- Created ImageUploaderService for image upload logic.
- Created UploaderService for file handling and storage.
This commit is contained in:
2026-02-12 20:31:04 +03:30
parent de14d531e1
commit a073af76fe
116 changed files with 6108 additions and 3977 deletions
+34 -26
View File
@@ -1,41 +1,49 @@
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common'
import { Controller, Get } 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'
import { reqTokenPayload } from './current-user.decorator'
@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)
}
// @Public()
// @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)
}
// @Public()
// @Post('login')
// async login(@Body() dto: VerifyCodeDto, @Res({ passthrough: true }) res) {
// const result = await this.auth.loginWithCode(dto)
// // Set accessToken as httpOnly cookie
// // @ts-ignore
// if (result?.data?.accessToken) {
// // @ts-ignore
// res.cookie('accessToken', result.data.accessToken, {
// httpOnly: true,
// secure: process.env.NODE_ENV === 'production',
// sameSite: 'lax',
// maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days
// })
// }
// return result
// }
@Post('refresh')
async refresh(@Body() dto: RefreshTokenDto) {
return this.auth.refreshToken(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)
}
// @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)
async me(@reqTokenPayload() user: any) {
return this.auth.me(user.username)
}
}
+3 -4
View File
@@ -2,10 +2,9 @@ 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 { PrismaModule } from '../../prisma/prisma.module'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { JwtAuthGuard } from './jwt-auth.guard'
@Module({
imports: [
@@ -15,8 +14,8 @@ import { JwtAuthGuard } from './jwt-auth.guard'
signOptions: { expiresIn: Number(env('JWT_EXPIRES_IN')) || '15m' },
}),
],
providers: [AuthService, JwtAuthGuard],
providers: [AuthService],
controllers: [AuthController],
exports: [AuthService, JwtAuthGuard],
exports: [AuthService, JwtModule],
})
export class AuthModule {}
+151 -110
View File
@@ -1,126 +1,167 @@
import {
BadRequestException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common'
import { Injectable } 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'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@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
// private readonly OTP_TTL_MINUTES = 2
// private readonly ACCESS_TOKEN_EXP = Number(env('JWT_EXPIRES_IN')) || '150m'
// 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()
}
// 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)
// // @TODO: we must detect context of request to verify user need to logged in as which role (admin, user, etc) and generate token with that role.
// async sendCode(dto: SendCodeDto) {
// const { mobile_number } = dto
// const account = await this.prisma.account.findFirst({
// where: {
// user: {
// mobile_number,
// },
// },
// })
// console.log(mobile_number)
await this.prisma.otpCode.create({
data: {
mobileNumber,
code,
expiresAt,
},
// if (!account) {
// throw new NotFoundException('کاربری با این شماره تماس یافت نشد')
// }
// const existingCode = await this.prisma.verificationCode.findFirst({
// where: { account_id: account.id, expires_at: { gt: new Date() } },
// })
// if (existingCode) {
// throw new BadRequestException('کد برای شما ارسال شده است')
// }
// const code = this.generateCode()
// const expires_at = new Date(Date.now() + this.OTP_TTL_MINUTES * 60 * 1000)
// await this.prisma.verificationCode.create({
// data: {
// account_id: account.id,
// code,
// expires_at,
// },
// })
// // TODO: integrate SMS provider. For now return the code for dev.
// return {
// ok: true,
// }
// }
// async loginWithCode(dto: VerifyCodeDto) {
// const { mobile_number, code } = dto
// const account = await this.prisma.account.findFirst({
// where: { user: { mobile_number } },
// include: {
// user: true,
// },
// })
// if (!account) {
// throw new NotFoundException('کاربری با این شماره تماس یافت نشد')
// }
// const otp = await this.prisma.verificationCode.findFirst({
// where: { account_id: account.id },
// })
// if (!otp) throw new BadRequestException('اطلاعات ورودی درست نیست')
// // if (otp.expires_at) throw new BadRequestException('کد قبلن استفاده شده است')
// if (otp.expires_at.getTime() < Date.now())
// throw new BadRequestException('کد وارد شده منقضی شده است')
// if (otp.code !== code) throw new BadRequestException('کد وارد شده اشتباه است')
// // mark used
// const signTokenData = {
// sub: account.user.id,
// mobile_number: account.user.mobile_number,
// type: account.type,
// username: account.username,
// }
// if (account.pos_id) {
// const pos = await this.prisma.pos.findUnique({
// where: { id: account.pos_id },
// include: {
// complex: true,
// licenses: true,
// },
// })
// if (!pos) {
// throw new NotFoundException('متاسفانه کاربر شما یافت نشد')
// }
// Object.assign(signTokenData, {
// pos_id: pos.id,
// pos_name: pos.serial,
// complex: pos.complex,
// license: pos.licenses[0],
// })
// }
// console.log(signTokenData)
// const accessToken = this.jwt.sign(signTokenData, { expiresIn: this.ACCESS_TOKEN_EXP })
// const refreshTokenPlain = randomBytes(48).toString('hex')
// const tokenHash = createHash('sha256').update(refreshTokenPlain).digest('hex')
// const expires_at = new Date(
// Date.now() + this.REFRESH_TOKEN_EXP_DAYS * 24 * 60 * 60 * 1000,
// )
// await this.prisma.token.upsert({
// where: { type_account_id: { type: TokenType.ACCESS, account_id: account.id } },
// update: { token: tokenHash, expires_at },
// create: {
// token: tokenHash,
// account_id: account.id,
// expires_at,
// type: TokenType.ACCESS,
// },
// })
// await this.prisma.verificationCode.delete({ where: { id: otp.id } })
// return ResponseMapper.single({
// accessToken,
// refreshToken: refreshTokenPlain,
// account,
// })
// }
// // 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() {
// // await this.prisma.token.deleteMany({
// // where: { tokenHash },
// // data: { revoked: true },
// // })
// // return { ok: true }
// // }
async me(username: string) {
return ResponseMapper.single({
username,
})
// 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 }
}
}
+3 -2
View File
@@ -1,8 +1,9 @@
import { createParamDecorator, ExecutionContext } from '@nestjs/common'
import { AccessTokenPayload } from '../../common/models/token-model'
export const CurrentUser = createParamDecorator(
export const reqTokenPayload = createParamDecorator(
(data: unknown, ctx: ExecutionContext) => {
const req = ctx.switchToHttp().getRequest()
return req.user
return req.dataPayload as AccessTokenPayload
},
)
+2 -2
View File
@@ -2,11 +2,11 @@ import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class LoginDto {
@ApiProperty({ description: 'Mobile number used as username', example: '09123456789' })
@ApiProperty({ description: 'Mobile number used as username', example: '09120258156' })
@IsString()
mobileNumber: string
@ApiProperty({ description: 'OTP password (one-time password)', example: '12345' })
@ApiProperty({ description: 'OTP password (one-time password)', example: '11111' })
@IsString()
password: string
}
+2 -2
View File
@@ -2,7 +2,7 @@ import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class SendCodeDto {
@ApiProperty({ example: '09123456789' })
@ApiProperty({ example: '09120258156' })
@IsString()
mobileNumber: string
mobile_number: string
}
+3 -3
View File
@@ -2,11 +2,11 @@ import { ApiProperty } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class VerifyCodeDto {
@ApiProperty({ example: '09123456789' })
@ApiProperty({ example: '09120258156' })
@IsString()
mobileNumber: string
mobile_number: string
@ApiProperty({ example: '12345' })
@ApiProperty({ example: '11111' })
@IsString()
code: string
}
+37 -10
View File
@@ -4,31 +4,58 @@ import {
Injectable,
UnauthorizedException,
} from '@nestjs/common'
import { Reflector } from '@nestjs/core'
import { JwtService } from '@nestjs/jwt'
import { Request } from 'express'
import { IS_PUBLIC_KEY } from '../../common/decorators/public.decorator'
import { IWithJWTPayloadRequest } from '../../common/models/token-model'
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwt: JwtService) {}
constructor(
private jwt: JwtService,
private reflector: Reflector,
) {}
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 isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
])
if (isPublic) return true
const parts = auth.split(' ')
if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new UnauthorizedException('Invalid Authorization header format')
const req = context.switchToHttp().getRequest<IWithJWTPayloadRequest>()
// Log headers to inspect whether Authorization is present
// Try token from cookie first, then from Authorization header (Bearer)
let token: string | undefined = req.cookies?.accessToken
if (!token) {
const authHeader = (req.headers.authorization || req.headers.Authorization) as
| string
| undefined
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.slice(7)
}
}
const token = parts[1]
if (!token) throw new UnauthorizedException('Missing access token')
try {
const payload = this.jwt.verify(token, {
secret: process.env.JWT_SECRET || 'secret',
})
;(req as any).user = payload
console.log(payload)
if (payload.type !== 'POS')
throw new UnauthorizedException('Invalid or expired token')
// Set the typed dataPayload to the request
req.dataPayload = payload
return true
} catch (err) {
console.log(err)
throw new UnauthorizedException('Invalid or expired token')
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { ConfigService } from './config.service'
import { CreateConfigDto } from './dto/create-config.dto'
@Controller('config')
export class ConfigController {
constructor(private readonly configService: ConfigService) {}
@Get(':id')
findOne(@Param('id') uuid: string) {
return this.configService.findOne(uuid)
}
@Post()
create(@Body() data: CreateConfigDto) {
return this.configService.create(data)
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { ConfigController } from './config.controller'
import { ConfigService } from './config.service'
@Module({
controllers: [ConfigController],
providers: [ConfigService],
})
export class ConfigModule {}
+38
View File
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common'
import { Public } from '../../common/decorators/public.decorator'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateConfigDto } from './dto/create-config.dto'
@Injectable()
export class ConfigService {
constructor(private prisma: PrismaService) {}
@Public()
async findOne(uuid: string) {
const config = await this.prisma.device.findUnique({
where: {
uuid,
},
})
return ResponseMapper.single(config)
}
@Public()
async create(data: CreateConfigDto) {
const config = await this.prisma.device.upsert({
where: {
uuid: data.uuid,
},
update: {
...data,
},
create: {
...data,
},
})
return ResponseMapper.create(config)
}
}
@@ -0,0 +1,56 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsOptional, IsString } from 'class-validator'
export class CreateConfigDto {
@ApiProperty()
@IsString()
app_version: string
@ApiProperty()
@IsString()
build_number: string
@ApiProperty()
@IsString()
uuid: string
@ApiProperty({ required: false })
@IsString()
@IsOptional()
fcm_token?: string
@ApiProperty()
@IsString()
platform: string
@ApiProperty()
@IsString()
brand: string
@ApiProperty()
@IsString()
model: string
@ApiProperty()
@IsString()
device: string
@ApiProperty()
@IsString()
os_version: string
@ApiProperty()
@IsString()
sdk_version: string
@ApiProperty()
@IsString()
release_number: string
@ApiProperty({ required: false })
@IsString()
@IsOptional()
browser_name: string
}
export class UpdateConfigDto extends PartialType(CreateConfigDto) {}
+7 -19
View File
@@ -1,34 +1,22 @@
import { Body, Controller, Delete, Get, Param, Patch, Post } from '@nestjs/common'
import { Body, Controller, Get, Param, 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)
}
constructor(private readonly customerService: CustomersService) {}
@Get()
findAll() {
return this.customersService.findAll()
return this.customerService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.customersService.findOne(Number(id))
return this.customerService.findOne(+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))
@Post()
create(@Body() data: any) {
return this.customerService.create(data)
}
}
@@ -1,10 +1,8 @@
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],
})
+9 -23
View File
@@ -1,33 +1,19 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class CustomersService {
constructor(private prisma: PrismaService) {}
async create(data: any) {
const item = await this.prisma.customer.create({ data })
return ResponseMapper.create(item)
findAll() {
// TODO: Implement fetching all customers
return []
}
async findAll() {
const items = await this.prisma.customer.findMany()
return ResponseMapper.list(items)
findOne(id: number) {
// TODO: Implement fetching a single customer
return {}
}
async findOne(id: number) {
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.customer.update({ where: { id }, data })
return ResponseMapper.update(item)
}
async remove(id: number) {
const item = await this.prisma.customer.delete({ where: { id } })
return ResponseMapper.single(item)
create(data: any) {
// TODO: Implement customer creation
return data
}
}
@@ -1,33 +1,33 @@
import { IsEmail, IsOptional, IsString } from 'class-validator'
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsBoolean, IsEmail, IsOptional, IsString } from 'class-validator'
export class CreateCustomerDto {
@IsString()
firstName: string
@ApiProperty()
first_name: string
@IsString()
lastName: string
@ApiProperty()
last_name: string
@IsOptional()
@IsEmail()
@ApiProperty({ required: false })
email?: string
@IsOptional()
@IsString()
mobileNumber?: string
@ApiProperty()
mobile_number: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
address?: string
@IsOptional()
@IsString()
city?: string
@IsOptional()
@IsString()
state?: string
@IsOptional()
@IsString()
country?: string
@IsBoolean()
@ApiProperty({ required: false })
is_active?: boolean
}
export class UpdateCustomerDto extends PartialType(CreateCustomerDto) {}
@@ -1,41 +0,0 @@
import { Type } from 'class-transformer'
import { IsBoolean, IsEmail, IsOptional, IsString } from 'class-validator'
export class UpdateCustomerDto {
@IsOptional()
@IsString()
firstName?: string
@IsOptional()
@IsString()
lastName?: string
@IsOptional()
@IsEmail()
email?: string
@IsOptional()
@IsString()
mobileNumber?: string
@IsOptional()
@IsString()
address?: string
@IsOptional()
@IsString()
city?: string
@IsOptional()
@IsString()
state?: string
@IsOptional()
@IsString()
country?: string
@IsOptional()
@Type(() => Boolean)
@IsBoolean()
isActive?: boolean
}
@@ -0,0 +1,20 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsOptional, IsString } from 'class-validator'
export class CreateGoodCategoryDto {
@IsString()
@ApiProperty()
name: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
description?: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
image_url?: string
}
export class UpdateGoodCategoryDto extends PartialType(CreateGoodCategoryDto) {}
@@ -0,0 +1,24 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { reqTokenPayload } from '../auth/current-user.decorator'
import { CreateGoodCategoryDto } from './dto/create-good-category.dto'
import { GoodCategoriesService } from './good-categories.service'
@Controller('good-categories')
export class GoodCategoriesController {
constructor(private readonly goodCategoriesService: GoodCategoriesService) {}
@Get()
findAll(@reqTokenPayload() account) {
return this.goodCategoriesService.findAll(account.complex_id)
}
@Get(':id')
findOne(@Param('id') id: string, @reqTokenPayload() account) {
return this.goodCategoriesService.findOne(id, account.complex_id)
}
@Post()
create(@Body() data: CreateGoodCategoryDto, @reqTokenPayload() account) {
return this.goodCategoriesService.create(data, account.complex_id)
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { GoodCategoriesController } from './good-categories.controller'
import { GoodCategoriesService } from './good-categories.service'
@Module({
controllers: [GoodCategoriesController],
providers: [GoodCategoriesService],
})
export class GoodCategoriesModule {}
@@ -0,0 +1,42 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateGoodCategoryDto } from './dto/create-good-category.dto'
@Injectable()
export class GoodCategoriesService {
constructor(private prisma: PrismaService) {}
async findAll(complex_id: string) {
// console.log(account)
const categories = await this.prisma.goodCategory.findMany({
where: {
complex_id,
},
include: {
goods: {
include: {
_count: true,
},
},
},
})
return ResponseMapper.list(categories)
}
findOne(categoryId: string, complex_id: string) {
return {}
}
create(data: CreateGoodCategoryDto, complex_id: string) {
const category = this.prisma.goodCategory.create({
data: {
...data,
complex_id,
account_id: '',
},
})
return ResponseMapper.create(category)
}
}
+38
View File
@@ -0,0 +1,38 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsNumber, IsOptional, IsString } from 'class-validator'
export class CreateGoodDto {
@IsString()
@ApiProperty()
name: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
description?: string
@IsString()
@ApiProperty()
sku: string
@IsString()
@ApiProperty()
local_sku: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
barcode?: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
category_id?: string
@IsOptional()
@IsNumber()
@ApiProperty({ required: false })
base_sale_price?: number
}
export class UpdateGoodDto extends PartialType(CreateGoodDto) {}
+24
View File
@@ -0,0 +1,24 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { reqTokenPayload } from '../auth/current-user.decorator'
import { CreateGoodDto } from './dto/create-good.dto'
import { GoodsService } from './goods.service'
@Controller('goods')
export class GoodsController {
constructor(private readonly goodsService: GoodsService) {}
@Get()
findAll(@reqTokenPayload() account) {
return this.goodsService.findAll(account.complex_id)
}
@Get(':id')
findOne(@Param('id') goodId: string, @reqTokenPayload() account) {
return this.goodsService.findOne(goodId, account.complex_id)
}
@Post()
create(@Body() data: CreateGoodDto, @reqTokenPayload() account) {
return this.goodsService.create(data, account.complex_id)
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { GoodsController } from './goods.controller'
import { GoodsService } from './goods.service'
@Module({
controllers: [GoodsController],
providers: [GoodsService],
})
export class GoodsModule {}
+66
View File
@@ -0,0 +1,66 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
import { CreateGoodDto } from './dto/create-good.dto'
@Injectable()
export class GoodsService {
constructor(private prisma: PrismaService) {}
async findAll(complex_id: string) {
const goods = await this.prisma.good.findMany({
where: {
complex_id,
},
omit: {
account_id: true,
complex_id: true,
deleted_at: true,
},
})
return ResponseMapper.list(goods)
}
async findOne(good_id: string, complex_id: string) {
const good = await this.prisma.good.findUnique({
where: {
complex_id,
id: good_id,
},
omit: {
account_id: true,
complex_id: true,
deleted_at: true,
},
})
return ResponseMapper.single(good)
}
async create(data: CreateGoodDto, complex_id: string) {
const { category_id, ...rest } = data
const dataToCreate = {
...rest,
complex_id,
account_id: '',
category: {
connect: {
id: category_id,
},
},
}
const good = await this.prisma.good.create({
data: dataToCreate,
omit: {
account_id: true,
complex_id: true,
deleted_at: true,
},
})
return ResponseMapper.create(good)
}
}
@@ -1,14 +0,0 @@
import { IsOptional, IsString } from 'class-validator'
export class CreateProductCategoryDto {
@IsString()
name: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
imageUrl?: string
}
@@ -1,15 +0,0 @@
import { IsOptional, IsString } from 'class-validator'
export class UpdateProductCategoryDto {
@IsOptional()
@IsString()
name?: string
@IsOptional()
@IsString()
description?: string
@IsOptional()
@IsString()
imageUrl?: string
}
@@ -1,34 +0,0 @@
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))
}
}
@@ -1,11 +0,0 @@
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 {}
@@ -1,33 +0,0 @@
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,33 +0,0 @@
import { Type } from 'class-transformer'
import { IsInt, IsOptional, IsString } from 'class-validator'
export class CreateProductDto {
@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)
minimumStockAlertLevel?: number
}
@@ -1,35 +0,0 @@
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
}
@@ -1,37 +0,0 @@
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
@@ -1,11 +0,0 @@
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
@@ -1,131 +0,0 @@
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)
}
}
@@ -0,0 +1,5 @@
import { PartialType } from '@nestjs/swagger'
export class CreateProfileDto {}
export class UpdateProfileDto extends PartialType(CreateProfileDto) {}
+13
View File
@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common'
import { reqTokenPayload } from '../auth/current-user.decorator'
import { ProfileService } from './profile.service'
@Controller('profile')
export class ProfileController {
constructor(private readonly profileService: ProfileService) {}
@Get('me')
findOne(@reqTokenPayload() account) {
return this.profileService.findOne(account)
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { ProfileController } from './profile.controller'
import { ProfileService } from './profile.service'
@Module({
controllers: [ProfileController],
providers: [ProfileService],
})
export class ProfileModule {}
+15
View File
@@ -0,0 +1,15 @@
import { Injectable } from '@nestjs/common'
import { Public } from '../../common/decorators/public.decorator'
import type { AccessTokenPayload } from '../../common/models/token-model'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class ProfileService {
constructor(private prisma: PrismaService) {}
@Public()
async findOne(account: AccessTokenPayload) {
return ResponseMapper.single(account)
}
}
@@ -1,24 +1,32 @@
import { Type } from 'class-transformer'
import { IsInt, IsNumber } from 'class-validator'
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsNumber, IsOptional, IsString } from 'class-validator'
export class CreateSalesInvoiceItemDto {
@Type(() => Number)
@IsNumber()
@ApiProperty()
count: number
@Type(() => Number)
@IsNumber()
unitPrice: number
@ApiProperty()
unit_price: number
@Type(() => Number)
@IsNumber()
totalAmount: number
@ApiProperty()
total_amount: number
@Type(() => Number)
@IsInt()
invoiceId: number
@IsString()
@ApiProperty()
invoice_id: string
@Type(() => Number)
@IsInt()
productId: number
@IsOptional()
@IsString()
@ApiProperty({ required: false })
good_id?: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
service_id?: string
}
export class UpdateSalesInvoiceItemDto extends PartialType(CreateSalesInvoiceItemDto) {}
@@ -1,29 +0,0 @@
import { Type } from 'class-transformer'
import { IsInt, IsNumber, IsOptional } from 'class-validator'
export class UpdateSalesInvoiceItemDto {
@IsOptional()
@Type(() => Number)
@IsNumber()
count?: number
@IsOptional()
@Type(() => Number)
@IsNumber()
unitPrice?: number
@IsOptional()
@Type(() => Number)
@IsNumber()
totalAmount?: number
@IsOptional()
@Type(() => Number)
@IsInt()
invoiceId?: number
@IsOptional()
@Type(() => Number)
@IsInt()
productId?: number
}
@@ -1,38 +1,22 @@
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 { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { SalesInvoiceItemsService } from './sales-invoice-items.service'
@Controller('sales-invoices/:invoiceId/items')
@Controller('sales-invoice-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)
}
constructor(private readonly salesInvoiceItemsService: SalesInvoiceItemsService) {}
@Get()
findAll(@Param('invoiceId') invoiceId: string) {
return this.service.findAllForInvoice(Number(invoiceId))
findAll() {
return this.salesInvoiceItemsService.findAll()
}
@Get(':id')
findOne(@Param('invoiceId') invoiceId: string, @Param('id') id: string) {
return this.service.findOneForInvoice(Number(invoiceId), Number(id))
findOne(@Param('id') id: string) {
return this.salesInvoiceItemsService.findOne(+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))
@Post()
create(@Body() data: any) {
return this.salesInvoiceItemsService.create(data)
}
}
@@ -1,10 +1,8 @@
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],
})
@@ -1,98 +1,19 @@
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)
findAll() {
// TODO: Implement fetching all sales invoice items
return []
}
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)
findOne(id: number) {
// TODO: Implement fetching a single sales invoice item
return {}
}
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)
create(data: any) {
// TODO: Implement sales invoice item creation
return data
}
}
@@ -0,0 +1,32 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsDateString, IsEnum, IsNumber, IsString } from 'class-validator'
enum PaymentMethodType {
CASH = 'CASH',
CARD = 'CARD',
BANK = 'BANK',
CHECK = 'CHECK',
OTHER = 'OTHER',
}
export class CreateSalesInvoicePaymentDto {
@ApiProperty()
@IsString()
invoice_id: string
@ApiProperty()
@IsNumber()
amount: number
@ApiProperty({ enum: PaymentMethodType })
@IsEnum(PaymentMethodType)
payment_method: PaymentMethodType
@ApiProperty()
@IsDateString()
paid_at: string
}
export class UpdateSalesInvoicePaymentDto extends PartialType(
CreateSalesInvoicePaymentDto,
) {}
@@ -0,0 +1,24 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { SalesInvoicePaymentsService } from './sales-invoice-payments.service'
@Controller('sales-invoice-payments')
export class SalesInvoicePaymentsController {
constructor(
private readonly salesInvoicePaymentsService: SalesInvoicePaymentsService,
) {}
@Get()
findAll() {
return this.salesInvoicePaymentsService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.salesInvoicePaymentsService.findOne(+id)
}
@Post()
create(@Body() data: any) {
return this.salesInvoicePaymentsService.create(data)
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { SalesInvoicePaymentsController } from './sales-invoice-payments.controller'
import { SalesInvoicePaymentsService } from './sales-invoice-payments.service'
@Module({
controllers: [SalesInvoicePaymentsController],
providers: [SalesInvoicePaymentsService],
})
export class SalesInvoicePaymentsModule {}
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class SalesInvoicePaymentsService {
findAll() {
// TODO: Implement fetching all sales invoice payments
return []
}
findOne(id: number) {
// TODO: Implement fetching a single sales invoice payment
return {}
}
create(data: any) {
// TODO: Implement sales invoice payment creation
return data
}
}
@@ -1,20 +1,24 @@
import { Type } from 'class-transformer'
import { IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsNumber, IsOptional, IsString } from 'class-validator'
export class CreateSalesInvoiceDto {
@IsString()
@ApiProperty()
code: string
@Type(() => Number)
@IsNumber()
totalAmount: number
@ApiProperty()
total_amount: number
@IsOptional()
@IsString()
@ApiProperty({ required: false })
description?: string
@IsOptional()
@Type(() => Number)
@IsInt()
customerId?: number
@IsString()
@ApiProperty({ required: false })
customer_id?: string
}
export class UpdateSalesInvoiceDto extends PartialType(CreateSalesInvoiceDto) {}
@@ -1,22 +0,0 @@
import { Type } from 'class-transformer'
import { IsInt, IsNumber, IsOptional, IsString } from 'class-validator'
export class UpdateSalesInvoiceDto {
@IsOptional()
@IsString()
code?: string
@IsOptional()
@Type(() => Number)
@IsNumber()
totalAmount?: number
@IsOptional()
@IsString()
description?: string
@IsOptional()
@Type(() => Number)
@IsInt()
customerId?: number | null
}
@@ -1,32 +1,22 @@
import { Controller, Delete, Get, Param } from '@nestjs/common'
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { SalesInvoicesService } from './sales-invoices.service'
@Controller('sales-invoices')
export class SalesInvoicesController {
constructor(private readonly service: SalesInvoicesService) {}
// @Post()
// create(@Body() dto: CreateSalesInvoiceDto) {
// return this.service.create(dto)
// }
constructor(private readonly salesInvoicesService: SalesInvoicesService) {}
@Get()
findAll() {
return this.service.findAll()
return this.salesInvoicesService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.service.findOne(Number(id))
return this.salesInvoicesService.findOne(+id)
}
// @Patch(':id')
// update(@Param('id') id: string, @Body() dto: UpdateSalesInvoiceDto) {
// return this.service.update(Number(id), dto)
// }
@Delete(':id')
remove(@Param('id') id: string) {
return this.service.remove(Number(id))
@Post()
create(@Body() data: any) {
return this.salesInvoicesService.create(data)
}
}
@@ -1,13 +1,9 @@
import { Module } from '@nestjs/common'
import { PrismaModule } from '../../prisma/prisma.module'
import { SalesInvoicesController } from './sales-invoices.controller'
import { SalesInvoicesService } from './sales-invoices.service'
import { SalesInvoicesWorkflow } from './sales-invoices.workflow'
@Module({
imports: [PrismaModule],
controllers: [SalesInvoicesController],
providers: [SalesInvoicesService, SalesInvoicesWorkflow],
exports: [SalesInvoicesWorkflow],
providers: [SalesInvoicesService],
})
export class SalesInvoicesModule {}
@@ -1,49 +1,19 @@
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class SalesInvoicesService {
constructor(private prisma: PrismaService) {}
// async create(dto: CreateSalesInvoiceDto) {
// const payload: any = { ...dto }
// if (Object.prototype.hasOwnProperty.call(payload, 'customerId')) {
// payload.customer = { connect: { id: Number(payload.customerId) } }
// delete payload.customerId
// }
// const item = await this.prisma.salesInvoice.create({ data: payload })
// return ResponseMapper.create(item)
// }
async findAll() {
const items = await this.prisma.salesInvoice.findMany({ include: { customer: true } })
return ResponseMapper.list(items)
findAll() {
// TODO: Implement fetching all sales invoices
return []
}
async findOne(id: number) {
const item = await this.prisma.salesInvoice.findUnique({
where: { id },
include: { items: true, customer: true },
})
if (!item) return null
return ResponseMapper.single(item)
findOne(id: number) {
// TODO: Implement fetching a single sales invoice
return {}
}
// async update(id: number, data: any) {
// const payload: any = { ...data }
// if (Object.prototype.hasOwnProperty.call(payload, 'customerId')) {
// if (payload.customerId === null) payload.customer = { disconnect: true }
// else payload.customer = { connect: { id: Number(payload.customerId) } }
// delete payload.customerId
// }
// const item = await this.prisma.salesInvoice.update({ where: { id }, data: payload })
// return ResponseMapper.update(item)
// }
async remove(id: number) {
const item = await this.prisma.salesInvoice.delete({ where: { id } })
return ResponseMapper.single(item)
create(data: any) {
// TODO: Implement sales invoice creation
return data
}
}
@@ -1,38 +0,0 @@
import {
MovementReferenceType,
MovementType,
Prisma,
} from '../../generated/prisma/client'
export class SalesInvoicesWorkflow {
constructor() {}
async onCreateByOrder(tx: Prisma.TransactionClient, orderId: number) {
const { posAccount } = await tx.order.findUniqueOrThrow({
where: { id: orderId },
select: { posAccount: { select: { inventoryId: true, id: true } } },
})
const posInventoryId = posAccount.inventoryId
const orderItems = await tx.orderItem.findMany({
where: {
orderId,
},
})
await tx.stockMovement.createMany({
data: orderItems.map(item => ({
productId: item.productId,
orderId,
type: MovementType.OUT,
referenceType: MovementReferenceType.SALES,
referenceId: String(orderId),
quantity: item.quantity,
inventoryId: posInventoryId,
unitPrice: item.unitPrice,
totalCost: item.totalAmount,
avgCost: item.unitPrice,
})),
})
}
}
@@ -0,0 +1,20 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsOptional, IsString } from 'class-validator'
export class CreateServiceCategoryDto {
@IsString()
@ApiProperty()
name: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
description?: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
imageUrl?: string
}
export class UpdateServiceCategoryDto extends PartialType(CreateServiceCategoryDto) {}
@@ -0,0 +1,22 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { ServiceCategoriesService } from './service-categories.service'
@Controller('service-categories')
export class ServiceCategoriesController {
constructor(private readonly serviceCategoriesService: ServiceCategoriesService) {}
@Get()
findAll() {
return this.serviceCategoriesService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.serviceCategoriesService.findOne(+id)
}
@Post()
create(@Body() data: any) {
return this.serviceCategoriesService.create(data)
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { ServiceCategoriesController } from './service-categories.controller'
import { ServiceCategoriesService } from './service-categories.service'
@Module({
controllers: [ServiceCategoriesController],
providers: [ServiceCategoriesService],
})
export class ServiceCategoriesModule {}
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class ServiceCategoriesService {
findAll() {
// TODO: Implement fetching all service categories
return []
}
findOne(id: number) {
// TODO: Implement fetching a single service category
return {}
}
create(data: any) {
// TODO: Implement service category creation
return data
}
}
@@ -0,0 +1,34 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsNumber, IsOptional, IsString } from 'class-validator'
export class CreateServiceDto {
@IsString()
@ApiProperty()
name: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
description?: string
@IsString()
@ApiProperty()
sku: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
barcode?: string
@IsOptional()
@IsString()
@ApiProperty({ required: false })
categoryId?: string
@IsOptional()
@IsNumber()
@ApiProperty({ required: false })
baseSalePrice?: number
}
export class UpdateServiceDto extends PartialType(CreateServiceDto) {}
@@ -0,0 +1,22 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { ServicesService } from './services.service'
@Controller('services')
export class ServicesController {
constructor(private readonly servicesService: ServicesService) {}
@Get()
findAll() {
return this.servicesService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.servicesService.findOne(+id)
}
@Post()
create(@Body() data: any) {
return this.servicesService.create(data)
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { ServicesController } from './services.controller'
import { ServicesService } from './services.service'
@Module({
controllers: [ServicesController],
providers: [ServicesService],
})
export class ServicesModule {}
+19
View File
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class ServicesService {
findAll() {
// TODO: Implement fetching all services
return []
}
findOne(id: number) {
// TODO: Implement fetching a single service
return {}
}
create(data: any) {
// TODO: Implement service creation
return data
}
}
@@ -1,17 +0,0 @@
SELECT p.id
FROM Products p
WHERE (
SELECT COALESCE(SUM(sb.quantity), 0)
FROM Stock_Balance sb
WHERE
sb.productId = p.id
) < p.minimumStockAlertLevel
ORDER BY (
p.minimumStockAlertLevel - (
SELECT COALESCE(SUM(sb.quantity), 0)
FROM Stock_Balance sb
WHERE
sb.productId = p.id
)
) DESC
LIMIT 10
@@ -1,27 +0,0 @@
import { Controller, Get } from '@nestjs/common'
import { StatisticsService } from './statistics.service'
@Controller('statistics')
export class StatisticsController {
constructor(private readonly statisticsService: StatisticsService) {}
@Get('top-last-sales')
getTopLastSales() {
return this.statisticsService.getTopLastSales()
}
@Get('top-alert-stocks')
getTopAlertStocks() {
return this.statisticsService.getTopAlertStocks()
}
@Get('top-supplier-debts')
getTopSupplierDebts() {
return this.statisticsService.getTopSupplierDebts()
}
@Get('top-sales')
getTopSellProducts() {
return this.statisticsService.getTopSellProducts()
}
}
@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common'
import { PrismaService } from '../../prisma/prisma.service'
import { StatisticsController } from './statistics.controller'
import { StatisticsService } from './statistics.service'
@Module({
controllers: [StatisticsController],
providers: [StatisticsService, PrismaService],
})
export class StatisticsModule {}
@@ -1,184 +0,0 @@
import { Injectable } from '@nestjs/common'
import * as fs from 'fs'
import * as path from 'path'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class StatisticsService {
constructor(private prisma: PrismaService) {}
private readSqlFile(filename: string): string {
const filePath = path.join(__dirname, 'queries', filename)
return fs.readFileSync(filePath, 'utf8')
}
async getTopLastSales() {
const sales = await this.prisma.salesInvoice.findMany({
take: 10,
orderBy: { createdAt: 'desc' },
include: {
customer: { select: { id: true, firstName: true, lastName: true } },
posAccount: { select: { id: true, name: true } },
items: {
include: {
product: { select: { id: true, name: true } },
},
},
},
})
const mapped = sales.map(sale => ({
...sale,
totalAmount: Number(sale.totalAmount),
itemsCount: sale.items.length,
}))
return ResponseMapper.list(mapped)
}
async getTopAlertStocks() {
const productIds = await this.prisma.$queryRaw<{ id: number }[]>`
SELECT p.id
FROM Products p
WHERE (SELECT COALESCE(SUM(sb.quantity), 0) FROM Stock_Balance sb WHERE sb.productId = p.id) < p.minimumStockAlertLevel
-- ORDER BY (p.minimumStockAlertLevel - (SELECT COALESCE(SUM(sb.quantity), 0) FROM stock_balance sb WHERE sb.productId = p.id)) DESC
LIMIT 10
`
const ids = productIds.map(p => p.id)
if (ids.length === 0) {
return ResponseMapper.list([])
}
const products = await this.prisma.product.findMany({
where: { id: { in: ids } },
select: {
id: true,
name: true,
sku: true,
minimumStockAlertLevel: true,
stockBalances: {
select: {
quantity: true,
inventory: { select: { id: true, name: true } },
},
},
category: { select: { id: true, name: true } },
brand: { select: { id: true, name: true } },
},
})
const mappedData = products.map(product => ({
...product,
stock: product.stockBalances.reduce((acc, sb) => acc + Number(sb.quantity), 0),
deficit:
Number(product.minimumStockAlertLevel) -
product.stockBalances.reduce((acc, sb) => acc + Number(sb.quantity), 0),
}))
return ResponseMapper.list(mappedData)
}
async getTopSupplierDebts() {
// Get unpaid receipts
const receipts = await this.prisma.purchaseReceipt.findMany({
where: {
status: { not: 'PAID' },
},
select: {
id: true,
totalAmount: true,
paidAmount: true,
supplierId: true,
},
})
const debtMap = new Map<
number,
{ supplierId: number; totalDebt: number; unpaidReceipts: number }
>()
for (const receipt of receipts) {
const debt = Number(receipt.totalAmount) - Number(receipt.paidAmount)
if (debt > 0) {
const existing = debtMap.get(receipt.supplierId)
if (existing) {
existing.totalDebt += debt
existing.unpaidReceipts += 1
} else {
debtMap.set(receipt.supplierId, {
supplierId: receipt.supplierId,
totalDebt: debt,
unpaidReceipts: 1,
})
}
}
}
const supplierIds = Array.from(debtMap.keys())
const suppliers = await this.prisma.supplier.findMany({
where: { id: { in: supplierIds } },
select: { id: true, firstName: true, lastName: true },
})
const supplierMap = new Map(
suppliers.map(s => [s.id, `${s.firstName} ${s.lastName}`.trim()]),
)
const mapped = Array.from(debtMap.values())
.map(item => ({
id: item.supplierId,
name: supplierMap.get(item.supplierId) || 'Unknown',
totalDebt: item.totalDebt,
unpaidReceipts: item.unpaidReceipts,
}))
.sort((a, b) => b.totalDebt - a.totalDebt)
.slice(0, 10)
return ResponseMapper.list(mapped)
}
async getTopSellProducts() {
const productData = await this.prisma.$queryRaw<any[]>`
SELECT p.id, p.name, p.sku, COALESCE(SUM(sii.count), 0) as totalSold
FROM Products p
LEFT JOIN Sales_Invoice_Items sii ON p.id = sii.productId
GROUP BY p.id
HAVING totalSold > 0
ORDER BY totalSold DESC
LIMIT 10
`
const ids = productData.map(p => p.id)
if (ids.length === 0) {
return ResponseMapper.list([])
}
const products = await this.prisma.product.findMany({
where: { id: { in: ids } },
select: {
id: true,
category: { select: { id: true, name: true } },
brand: { select: { id: true, name: true } },
},
})
const productMap = new Map(
products.map(p => [p.id, { category: p.category, brand: p.brand }]),
)
const mapped = productData.map(product => ({
id: product.id,
name: product.name,
sku: product.sku,
totalSold: Number(product.totalSold),
category: productMap.get(product.id)?.category,
brand: productMap.get(product.id)?.brand,
}))
return ResponseMapper.list(mapped)
}
}
@@ -1,17 +0,0 @@
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))
// }
}
@@ -1,11 +0,0 @@
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 {}
@@ -1,19 +0,0 @@
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)
// }
}
@@ -0,0 +1,14 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class CreateTriggerLogDto {
@ApiProperty()
@IsString()
message: string
@ApiProperty()
@IsString()
name: string
}
export class UpdateTriggerLogDto extends PartialType(CreateTriggerLogDto) {}
@@ -0,0 +1,22 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { TriggerLogsService } from './trigger-logs.service'
@Controller('trigger-logs')
export class TriggerLogsController {
constructor(private readonly triggerLogsService: TriggerLogsService) {}
@Get()
findAll() {
return this.triggerLogsService.findAll()
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.triggerLogsService.findOne(+id)
}
@Post()
create(@Body() data: any) {
return this.triggerLogsService.create(data)
}
}
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common'
import { TriggerLogsController } from './trigger-logs.controller'
import { TriggerLogsService } from './trigger-logs.service'
@Module({
controllers: [TriggerLogsController],
providers: [TriggerLogsService],
})
export class TriggerLogsModule {}
@@ -0,0 +1,19 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class TriggerLogsService {
findAll() {
// TODO: Implement fetching all trigger logs
return []
}
findOne(id: number) {
// TODO: Implement fetching a single trigger log
return {}
}
create(data: any) {
// TODO: Implement trigger log creation
return data
}
}
@@ -1,5 +1,4 @@
import { Injectable } from '@nestjs/common'
import { File } from 'multer'
import { UploaderService } from './uploader.service'
@Injectable()
@@ -7,14 +6,14 @@ export class ImageUploaderService {
constructor(private readonly uploaderService: UploaderService) {}
async uploadImage(
file: File,
accountId: string,
file: Express.Multer.File,
account_id: string,
type: string,
): Promise<string> {
return this.uploaderService.uploadFile(
file.buffer,
file.originalname,
accountId,
account_id,
type,
)
}
@@ -2,8 +2,8 @@ import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/c
import { FileInterceptor } from '@nestjs/platform-express'
import { ImageUploaderService } from './image-uploader.service'
@Controller('uploads')
export class UploadsController {
@Controller('uploaders')
export class UploadersController {
constructor(private readonly imageUploaderService: ImageUploaderService) {}
@Post('image')
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common'
import { ImageUploaderService } from './image-uploader.service'
import { UploaderService } from './uploader.service'
import { UploadsController } from './uploads.controller'
import { UploadersController } from './uploaders.controller'
@Module({
providers: [UploaderService, ImageUploaderService],
controllers: [UploadsController],
controllers: [UploadersController],
exports: [UploaderService, ImageUploaderService],
})
export class UploadsModule {}
export class UploadersModule {}