update pos consumer module

This commit is contained in:
2026-03-29 18:06:41 +03:30
parent 63fa2bc67e
commit c870a43e35
53 changed files with 2145 additions and 671 deletions
@@ -1,6 +1,6 @@
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 { PosInfo } from '@/common/decorators/posInfo.decorator'
import type { IPosPayload } from '@/common/models/posPayload.model'
import { Controller, Get } from '@nestjs/common'
import { GoodCategoriesService } from './good-categories.service'
@Controller('pos/good_categories')
@@ -8,17 +8,17 @@ export class GoodCategoriesController {
constructor(private readonly goodCategoriesService: GoodCategoriesService) {}
@Get()
findAll(@reqTokenPayload() account) {
return this.goodCategoriesService.findAll(account.complex_id)
findAll(@PosInfo() { complex_id, guild_id }: IPosPayload) {
return this.goodCategoriesService.findAll(complex_id, guild_id)
}
@Get(':id')
findOne(@Param('id') id: string, @reqTokenPayload() account) {
return this.goodCategoriesService.findOne(id, account.complex_id)
}
// @Get(':id')
// findOne(@Param('id') id: string, @PosInfo() { complex_id, guild_id }: IPosPayload) {
// return this.goodCategoriesService.findOne(id, account.complex_id)
// }
@Post()
create(@Body() data: CreateGoodCategoryDto, @reqTokenPayload() account) {
return this.goodCategoriesService.create(data, account.complex_id)
}
// @Post()
// create(@Body() data: CreateGoodCategoryDto, @reqTokenPayload() account) {
// return this.goodCategoriesService.create(data, account.complex_id)
// }
}
@@ -1,26 +1,40 @@
import { GoodCategorySelect } from '@/generated/prisma/models'
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) {
private readonly defaultSelect: GoodCategorySelect = {
id: true,
name: true,
image_url: true,
}
async findAll(complex_id: string, guild_id: string) {
const categories = await this.prisma.goodCategory.findMany({
where: {
complex_id,
guild_id,
},
include: {
select: {
...this.defaultSelect,
_count: {
select: {
goods: true,
goods: {
where: {
OR: [
{ is_default_guild_good: true },
{
complex_id,
},
],
},
},
},
},
},
omit: {
complex_id: true,
},
})
return ResponseMapper.list(
@@ -34,18 +48,18 @@ export class GoodCategoriesService {
)
}
findOne(categoryId: string, complex_id: string) {
return {}
}
// findOne(categoryId: string, complex_id: string) {
// return {}
// }
create(data: CreateGoodCategoryDto, complex_id: string) {
const category = this.prisma.goodCategory.create({
data: {
...data,
complex_id,
},
})
// create(data: CreateGoodCategoryDto, complex_id: string) {
// const category = this.prisma.goodCategory.create({
// data: {
// ...data,
// complex_id,
// },
// })
return ResponseMapper.create(category)
}
// return ResponseMapper.create(category)
// }
}
+11 -11
View File
@@ -1,6 +1,6 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { reqTokenPayload } from '../../auth/current-user.decorator'
import { CreateGoodDto } from './dto/create-good.dto'
import { PosInfo } from '@/common/decorators/posInfo.decorator'
import type { IPosPayload } from '@/common/models/posPayload.model'
import { Controller, Get, Param } from '@nestjs/common'
import { GoodsService } from './goods.service'
@Controller('pos/goods')
@@ -8,17 +8,17 @@ export class GoodsController {
constructor(private readonly goodsService: GoodsService) {}
@Get()
findAll(@reqTokenPayload() account) {
return this.goodsService.findAll(account.complex_id)
findAll(@PosInfo() { complex_id, guild_id }: IPosPayload) {
return this.goodsService.findAll(complex_id, guild_id)
}
@Get(':id')
findOne(@Param('id') goodId: string, @reqTokenPayload() account) {
return this.goodsService.findOne(goodId, account.complex_id)
findOne(@PosInfo() { complex_id, guild_id }: IPosPayload, @Param('id') goodId: string) {
return this.goodsService.findOne(goodId, complex_id, guild_id)
}
@Post()
create(@Body() data: CreateGoodDto, @reqTokenPayload() account) {
return this.goodsService.create(data, account.complex_id)
}
// @Post()
// create(@Body() data: CreateGoodDto, @PosInfo() { complex_id, guild_id }: IPosPayload) {
// return this.goodsService.create(data, complex_id, guild_id)
// }
}
+43 -17
View File
@@ -1,3 +1,4 @@
import { GoodSelect } from '@/generated/prisma/models'
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from '../../../common/response/response-mapper'
import { PrismaService } from '../../../prisma/prisma.service'
@@ -7,31 +8,59 @@ import { CreateGoodDto } from './dto/create-good.dto'
export class GoodsService {
constructor(private prisma: PrismaService) {}
async findAll(complex_id: string) {
private readonly defaultSelect: GoodSelect = {
id: true,
name: true,
barcode: true,
created_at: true,
description: true,
sku: true,
local_sku: true,
is_default_guild_good: true,
pricing_model: true,
unit_type: true,
category: {
select: {
id: true,
name: true,
image_url: true,
},
},
}
async findAll(complex_id: string, guild_id: string) {
const goods = await this.prisma.good.findMany({
where: {
complex_id,
},
omit: {
complex_id: true,
deleted_at: true,
OR: [
{
is_default_guild_good: true,
category: {
guild_id,
},
},
{
complex_id,
},
],
},
select: this.defaultSelect,
})
return ResponseMapper.list(goods)
}
async findOne(good_id: string, complex_id: string) {
async findOne(good_id: string, complex_id: string, guild_id: string) {
const good = await this.prisma.good.findUnique({
where: {
complex_id,
id: good_id,
complex: {
id: complex_id,
business_activity: {
guild_id,
},
},
},
omit: {
complex_id: true,
deleted_at: true,
},
select: this.defaultSelect,
})
return ResponseMapper.single(good)
@@ -64,10 +93,7 @@ export class GoodsService {
},
},
},
omit: {
complex_id: true,
deleted_at: true,
},
select: this.defaultSelect,
})
return ResponseMapper.create(good)
+15
View File
@@ -0,0 +1,15 @@
import { PosInfo } from '@/common/decorators/posInfo.decorator'
import { Controller, Get } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { PosService } from './pos.service'
@ApiTags('Pos')
@Controller('pos')
export class PosController {
constructor(private service: PosService) {}
@Get()
async getInfo(@PosInfo('pos_id') pos_id: string) {
return await this.service.getInfo(pos_id)
}
}
+88 -12
View File
@@ -1,21 +1,97 @@
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common'
import { checkAndDecodeJwtToken } from '@/common/utils/jwt-user.util'
import { PrismaService } from '@/prisma/prisma.service'
import { ForbiddenException, Injectable, NestMiddleware } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { NextFunction, Request, Response } from 'express'
@Injectable()
export class PosMiddleware implements NestMiddleware {
use(req: Request, _res: Response, next: NextFunction) {
// const a = reqTokenPayload()
// console.log(a)
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
// Middleware-based admin check (replace with real auth logic)
const isAdminHeader = req.headers['x-admin']
if (isAdminHeader === 'true') {
// mark request if needed downstream
req['isAdminRequest'] = true
async use(req: Request, _res: Response, next: NextFunction) {
const doForbidden = () => {
throw new ForbiddenException('شما دسترسی لازم را ندارید')
}
return next()
try {
const tokenAccount = checkAndDecodeJwtToken(req, this.jwtService)
throw new UnauthorizedException('Admin access required')
// const posId = req.cookies['posId']
const posId = req.headers.pos_id as string
if (tokenAccount?.type !== 'CONSUMER' || !posId || typeof posId !== 'string') {
return doForbidden()
}
const consumerAccount = await this.prisma.consumerAccount.findUnique({
where: {
account_id: tokenAccount.account_id,
},
})
if (!consumerAccount) {
return doForbidden()
}
const complex = await this.prisma.complex.findFirst({
where: {
pos_list: {
some: {
id: posId,
},
},
},
include: {
business_activity: {
include: {
guild: true,
},
},
},
})
if (!complex) {
return doForbidden()
}
// if (account?.role !== ConsumerRole.OWNER) {
// const accountPermissions = await this.prisma.permissionConsumer.findUnique({
// where: {
// account_id: account?.id,
// },
// include: {
// posPermissions: true,
// businessPermissions: true,
// complexPermissions: true,
// },
// })
// if (
// !(
// accountPermissions?.businessPermissions.some(
// permission => permission.business_id,
// ) ||
// accountPermissions?.complexPermissions.some(
// permission => permission.complex_id,
// ) ||
// accountPermissions?.posPermissions.some(permission => permission.pos_id)
// )
// ) {
// return doForbidden()
// }
// }
req.posData = {
pos_id: posId,
complex_id: complex.id,
business_id: complex.business_activity_id,
guild_id: complex.business_activity.guild_id,
consumer_account_id: consumerAccount.id,
}
req.decodedToken = tokenAccount
return next()
} catch (error) {
return doForbidden()
}
}
}
+10 -1
View File
@@ -1,12 +1,17 @@
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { PosConfigModule } from './config/config.module'
import { PosCustomersModule } from './customers/customers.module'
import { PosGoodCategoriesModule } from './good-categories/good-categories.module'
import { PosGoodsModule } from './goods/goods.module'
import { PosController } from './pos.controller'
import { PosMiddleware } from './pos.middleware'
import { PosService } from './pos.service'
import { PosSalesInvoicesModule } from './sales-invoices/sales-invoices.module'
@Module({
controllers: [PosController],
providers: [PosService, JwtService],
imports: [
PosConfigModule,
PosCustomersModule,
@@ -17,11 +22,15 @@ import { PosSalesInvoicesModule } from './sales-invoices/sales-invoices.module'
})
export class PosModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// apply middleware to all routes within this module (scoped under /admin)
consumer.apply(PosMiddleware).forRoutes({
path: '/pos*path',
method: RequestMethod.ALL,
version: '1',
})
consumer.apply(PosMiddleware).forRoutes({
path: '/pos',
method: RequestMethod.ALL,
version: '1',
})
}
}
+56
View File
@@ -0,0 +1,56 @@
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
@Injectable()
export class PosService {
constructor(private readonly prisma: PrismaService) {}
async getInfo(pos_id: string) {
const pos = await this.prisma.pos.findUniqueOrThrow({
where: {
id: pos_id,
},
select: {
name: true,
complex: {
select: {
id: true,
name: true,
tax_id: true,
business_activity: {
select: {
id: true,
name: true,
guild: {
select: {
id: true,
name: true,
code: true,
},
},
},
},
},
},
},
})
const { name, complex } = pos
const { name: complexName, id: complexId, tax_id, business_activity } = complex
const { name: businessName, id: businessId, guild } = business_activity
return ResponseMapper.single({
name,
complex: {
id: complexId,
name: complexName,
tax_id,
},
businessActivity: {
id: businessId,
name: businessName,
},
guild,
})
}
}
@@ -13,6 +13,7 @@ import { CustomerType } from 'generated/prisma/enums'
import { CreateSalesInvoiceItemDto } from '../sales-invoice-items/dto/create-sales-invoice-item.dto'
export class CreateSalesInvoiceDto {
// @TODO: totalAmount must calculated instead of get from api
@IsNumber()
@ApiProperty({ required: true, default: 0 })
total_amount: number
@@ -1,21 +1,33 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsEnum, IsNumber, IsObject, IsOptional, IsString } from 'class-validator'
import type { SaleInvoiceStandardPayload } from 'common/interfaces/sale-invoice-payload'
import type { SaleInvoiceType } from 'common/interfaces/sale-invoice-payload'
import { UnitType } from 'generated/prisma/enums'
export class CreateSalesInvoiceItemDto {
@IsNumber()
@ApiProperty()
@ApiProperty({ required: true })
unit_price: number
@IsNumber()
@ApiProperty()
@ApiProperty({ required: true, default: 1 })
quantity: number
@IsNumber()
@ApiProperty({ required: true })
total_amount: number
@IsNumber()
@ApiProperty({ required: true })
discount_amount: number
@IsString()
@ApiProperty()
@ApiProperty({ required: true })
invoice_id: string
@IsEnum(UnitType)
@ApiProperty({ enum: Object.values(UnitType) })
unit_type: UnitType
@IsOptional()
@IsString()
@ApiProperty({ required: false })
@@ -26,14 +38,6 @@ export class CreateSalesInvoiceItemDto {
@ApiProperty({ required: false })
service_id?: string
@IsNumber()
@ApiProperty({ required: true, default: 1 })
quantity: number
@IsEnum(UnitType)
@ApiProperty({ enum: Object.values(UnitType) })
unit_type: UnitType
// @IsEnum(SalesInvoiceItemPricingModel)
// @ApiProperty({ enum: Object.values(SalesInvoiceItemPricingModel) })
// @IsOptional()
@@ -42,7 +46,7 @@ export class CreateSalesInvoiceItemDto {
@IsOptional()
@IsObject()
@ApiProperty({ required: false, default: {} })
payload?: SaleInvoiceStandardPayload
payload: SaleInvoiceType
@IsOptional()
@IsString()
@@ -1,6 +1,7 @@
import { Body, Controller, Get, Param, Post } from '@nestjs/common'
import { reqTokenPayload } from '@/modules/auth/current-user.decorator'
import { PosInfo } from '@/common/decorators/posInfo.decorator'
import type { IPosPayload } from '@/common/models/posPayload.model'
import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto'
import { SalesInvoicesService } from './sales-invoices.service'
@@ -19,7 +20,7 @@ export class SalesInvoicesController {
}
@Post()
create(@Body() data: CreateSalesInvoiceDto, @reqTokenPayload() account) {
return this.salesInvoicesService.create(data, account)
create(@Body() data: CreateSalesInvoiceDto, @PosInfo() posInfo: IPosPayload) {
return this.salesInvoicesService.create(data, posInfo)
}
}
@@ -1,3 +1,4 @@
import { IPosPayload } from '@/common/models/posPayload.model'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
@@ -7,12 +8,6 @@ import { CreateSalesInvoiceDto } from './dto/create-sales-invoice.dto'
// Define type guard for CustomerIndividual
function isCustomerIndividual(customer: any): customer is CustomerIndividual {
console.log(
customer &&
typeof customer.first_name === 'string' &&
typeof customer.last_name === 'string',
)
return (
customer &&
typeof customer.first_name === 'string' &&
@@ -43,9 +38,11 @@ export class SalesInvoicesService {
return {}
}
async create(data: CreateSalesInvoiceDto, account) {
async create(data: CreateSalesInvoiceDto, posInfo: IPosPayload) {
data.invoice_date = new Date(data.invoice_date).toISOString() as any
const { complex_id, pos_id, consumer_account_id } = posInfo
const salesInvoice = await this.prisma.$transaction(async $tx => {
const payments = Object.entries(data.payments)
.filter(([_, value]) => value > 0 && PaymentMethodType[_.toUpperCase()])
@@ -58,6 +55,7 @@ export class SalesInvoicesService {
})
const totalPayments = payments.reduce((sum, payment) => sum + payment.amount, 0)
if (totalPayments !== data.total_amount) {
throw new Error('مبلغ پرداختی باید برابر با مبلغ کل فاکتور باشد.')
}
@@ -72,7 +70,7 @@ export class SalesInvoicesService {
let customerIndividual = await $tx.customerIndividual.findUnique({
where: {
complex_id_national_id: {
complex_id: account.complex_id,
complex_id,
national_id: customer.national_id,
},
},
@@ -82,7 +80,7 @@ export class SalesInvoicesService {
await $tx.customerIndividual.update({
where: {
complex_id_national_id: {
complex_id: account.complex_id,
complex_id,
national_id: customer.national_id,
},
},
@@ -99,8 +97,7 @@ export class SalesInvoicesService {
const createdCustomer = await $tx.customer.create({
data: {
type: CustomerType.INDIVIDUAL,
complex_id: account.complex_id,
customerIndividuals: {
customer_individuals: {
connect: {
customer_id: customerIndividual.customer_id,
},
@@ -116,7 +113,7 @@ export class SalesInvoicesService {
let customerLegal = await $tx.customerLegal.findUnique({
where: {
complex_id_registration_number: {
complex_id: account.complex_id,
complex_id,
registration_number: customer.registration_number,
},
},
@@ -126,7 +123,7 @@ export class SalesInvoicesService {
await $tx.customerLegal.update({
where: {
complex_id_registration_number: {
complex_id: account.complex_id,
complex_id,
registration_number: customer.registration_number,
},
},
@@ -143,8 +140,8 @@ export class SalesInvoicesService {
const createdCustomer = await $tx.customer.create({
data: {
type: CustomerType.LEGAL,
complex_id: account.complex_id,
customerIndividuals: {
customer_individuals: {
connect: {
customer_id: customerLegal.customer_id,
},
@@ -156,16 +153,15 @@ export class SalesInvoicesService {
newCustomerId = createdCustomer.id
}
}
} else if (data.customer_type === CustomerType.UNKNOWN) {
}
const salesInvoice = await $tx.salesInvoice.create({
data: {
...invoiceData,
account_id: account.account_id,
customer_id: newCustomerId,
total_amount: data.total_amount,
code: 'INV-' + Date.now(),
complex_id: account.complex_id,
items: {
createMany: {
data: data.items.map(item => ({
@@ -173,17 +169,33 @@ export class SalesInvoicesService {
quantity: item.quantity,
unit_price: item.unit_price,
total_amount: item.total_amount,
payload: item.payload,
payload: JSON.parse(JSON.stringify(item.payload)),
unit_type: item.unit_type,
// pricing_model: item.pricingModel,
})),
},
},
unknown_customer: {},
payments: {
createMany: {
data: payments,
},
},
// customer: {
// connect: {
// id: newCustomerId,
// },
// },
account: {
connect: {
id: consumer_account_id,
},
},
pos: {
connect: {
id: pos_id,
},
},
},
})