consumer pos

This commit is contained in:
2026-04-14 15:56:49 +03:30
parent 59da9585c4
commit d098ef10e1
21 changed files with 1651 additions and 75 deletions
+6
View File
@@ -17,3 +17,9 @@ DB_ROOT_PASSWORD="root_password"
NODE_ENV="production" NODE_ENV="production"
ARVANCLOUD_ENDPOINT=https://s3.ir-thr-at1.arvanstorage.com
ARVANCLOUD_ACCESS_KEY_ID=YOUR_ACCESS_KEY
ARVANCLOUD_SECRET_ACCESS_KEY=YOUR_SECRET_KEY
ARVANCLOUD_BUCKET=your-bucket-name
ARVANCLOUD_REGION=ir-thr-at1
+1
View File
@@ -13,6 +13,7 @@
}, },
"editor.formatOnSave": true, "editor.formatOnSave": true,
"cSpell.words": [ "cSpell.words": [
"ARVANCLOUD",
"autoincrement", "autoincrement",
"Cardex", "Cardex",
"fkey", "fkey",
+2
View File
@@ -1,7 +1,9 @@
{ {
"author": "", "author": "",
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.1029.0",
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/config": "^4.0.4",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2", "@nestjs/jwt": "^11.0.2",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
+1372 -54
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -11,7 +11,7 @@ import { PosModule } from './modules/pos/pos.module'
import { SalesInvoiceItemsModule } from './modules/pos/sales-invoices/sales-invoice-items/sales-invoice-items.module' import { SalesInvoiceItemsModule } from './modules/pos/sales-invoices/sales-invoice-items/sales-invoice-items.module'
import { SalesInvoicePaymentsModule } from './modules/pos/sales-invoices/sales-invoice-payments/sales-invoice-payments.module' import { SalesInvoicePaymentsModule } from './modules/pos/sales-invoices/sales-invoice-payments/sales-invoice-payments.module'
import { TriggerLogsModule } from './modules/trigger-logs/trigger-logs.module' import { TriggerLogsModule } from './modules/trigger-logs/trigger-logs.module'
import { UploadersModule } from './modules/uploaders/uploaders.module' import { UploaderModule } from './modules/uploaders/uploader.module'
import { PrismaModule } from './prisma/prisma.module' import { PrismaModule } from './prisma/prisma.module'
@Module({ @Module({
@@ -27,7 +27,7 @@ import { PrismaModule } from './prisma/prisma.module'
SalesInvoiceItemsModule, SalesInvoiceItemsModule,
SalesInvoicePaymentsModule, SalesInvoicePaymentsModule,
TriggerLogsModule, TriggerLogsModule,
UploadersModule, UploaderModule,
], ],
controllers: [AppController], controllers: [AppController],
providers: [AppService], providers: [AppService],
+1
View File
@@ -1,3 +1,4 @@
export * from './consumerPayload.model' export * from './consumerPayload.model'
export * from './posPayload.model' export * from './posPayload.model'
export * from './tokenPayload.model' export * from './tokenPayload.model'
export * from './uploadedFileTypes.model'
@@ -0,0 +1 @@
export type TUploadedFileTypes = 'good' | 'service' | 'profile_avatar'
+5
View File
@@ -0,0 +1,5 @@
import * as pos from './pos'
export const QUERY_CONSTANTS = {
pos,
}
+42
View File
@@ -0,0 +1,42 @@
import { PosSelect } from '@/generated/prisma/models'
export const select: PosSelect = {
id: true,
name: true,
model: true,
serial: true,
status: true,
created_at: true,
pos_type: true,
provider: {
select: {
id: true,
name: true,
},
},
device: {
select: {
name: true,
id: true,
brand: {
select: {
id: true,
name: true,
},
},
},
},
complex: {
select: {
id: true,
name: true,
business_activity: {
select: {
id: true,
name: true,
},
},
},
},
}
+2
View File
@@ -6,6 +6,7 @@ import { ConsumerController } from './consumer.controller'
import { ConsumerMiddleware } from './consumer.middleware' import { ConsumerMiddleware } from './consumer.middleware'
import { ConsumerService } from './consumer.service' import { ConsumerService } from './consumer.service'
import { ConsumerCustomersModule } from './customers/customers.module' import { ConsumerCustomersModule } from './customers/customers.module'
import { ConsumerPosesModule } from './poses/poses.module'
import { ConsumerStatisticsModule } from './statistics/statistics.module' import { ConsumerStatisticsModule } from './statistics/statistics.module'
@Module({ @Module({
@@ -15,6 +16,7 @@ import { ConsumerStatisticsModule } from './statistics/statistics.module'
ConsumerBusinessActivitiesModule, ConsumerBusinessActivitiesModule,
ConsumerCustomersModule, ConsumerCustomersModule,
ConsumerStatisticsModule, ConsumerStatisticsModule,
ConsumerPosesModule,
], ],
providers: [JwtService, ConsumerService], providers: [JwtService, ConsumerService],
}) })
@@ -0,0 +1,10 @@
import { ApiProperty, PartialType } from '@nestjs/swagger'
import { IsString } from 'class-validator'
export class CreateBusinessActivityDto {
@IsString()
@ApiProperty({ required: true })
name: string
}
export class UpdateBusinessActivityDto extends PartialType(CreateBusinessActivityDto) {}
@@ -0,0 +1,28 @@
import { TokenAccount } from '@/common/decorators/tokenInfo.decorator'
import { Body, Controller, Get, Param, Patch } from '@nestjs/common'
import { UpdateBusinessActivityDto } from './dto/poses.dto'
import { PosesService } from './poses.service'
@Controller('consumer/poses')
export class PosesController {
constructor(private readonly service: PosesService) {}
@Get()
async findAll(@TokenAccount('userId') userId: string) {
return this.service.findAll(userId)
}
@Get(':id')
async findOne(@TokenAccount('userId') userId: string, @Param('id') id: string) {
return this.service.findOne(userId, id)
}
@Patch(':id')
async update(
@TokenAccount('userId') userId: string,
@Param('id') id: string,
@Body() data: UpdateBusinessActivityDto,
) {
return this.service.update(userId, id, data)
}
}
@@ -0,0 +1,11 @@
import { PrismaModule } from '@/prisma/prisma.module'
import { Module } from '@nestjs/common'
import { PosesController } from './poses.controller'
import { PosesService } from './poses.service'
@Module({
imports: [PrismaModule],
controllers: [PosesController],
providers: [PosesService],
})
export class ConsumerPosesModule {}
@@ -0,0 +1,43 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper'
@Injectable()
export class PosesService {
constructor(private readonly prisma: PrismaService) {}
private readonly defaultSelect = QUERY_CONSTANTS.pos.select
private readonly defaultWhere = (consumer_id: string) => ({
complex: {
business_activity: {
consumer_id,
},
},
})
async findAll(consumer_id: string) {
const poses = await this.prisma.pos.findMany({
where: this.defaultWhere(consumer_id),
select: this.defaultSelect,
})
return ResponseMapper.list(poses)
}
async findOne(consumer_id: string, id: string) {
const pos = await this.prisma.pos.findUniqueOrThrow({
where: { ...this.defaultWhere(consumer_id), id },
select: this.defaultSelect,
})
return ResponseMapper.single(pos)
}
async update(consumer_id: string, id: string, data: any) {
const pos = await this.prisma.pos.update({
where: { ...this.defaultWhere(consumer_id), id },
data,
select: this.defaultSelect,
})
return ResponseMapper.update(pos)
}
}
+10
View File
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { StorageService } from './storage.service'
@Module({
imports: [ConfigModule.forRoot()],
providers: [StorageService],
exports: [StorageService],
})
export class StorageModule {}
+50
View File
@@ -0,0 +1,50 @@
import { DeleteObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
import { Injectable } from '@nestjs/common'
import { ConfigService } from '@nestjs/config'
import { randomUUID } from 'crypto'
@Injectable()
export class StorageService {
private readonly s3: S3Client
private readonly bucket: string
private readonly endpoint: string
constructor(private readonly config: ConfigService) {
;((this.endpoint = process.env.ARVANCLOUD_ENDPOINT!),
(this.bucket = process.env.ARVANCLOUD_BUCKET!),
(this.s3 = new S3Client({
region: process.env.ARVANCLOUD_REGION!,
endpoint: this.endpoint!,
credentials: {
accessKeyId: process.env.ARVANCLOUD_ACCESS_KEY_ID!,
secretAccessKey: process.env.ARVANCLOUD_SECRET_ACCESS_KEY!,
},
forcePathStyle: true,
})))
}
async uploadFile(file: Express.Multer.File, folder = 'uploads') {
const key = `${folder}/${randomUUID()}-${file.originalname}`
await this.s3.send(
new PutObjectCommand({
Bucket: this.bucket,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
}),
)
// ArvanCloud uses public URLs like:
const publicUrl = `${this.endpoint}/${this.bucket}/${key}`
return { key, url: publicUrl }
}
async deleteFile(key: string) {
await this.s3.send(
new DeleteObjectCommand({
Bucket: this.bucket,
Key: key,
}),
)
}
}
@@ -1,3 +1,4 @@
import { TUploadedFileTypes } from '@/common/models'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { UploaderService } from './uploader.service' import { UploaderService } from './uploader.service'
@@ -7,14 +8,8 @@ export class ImageUploaderService {
async uploadImage( async uploadImage(
file: Express.Multer.File, file: Express.Multer.File,
account_id: string, type: TUploadedFileTypes,
type: string,
): Promise<string> { ): Promise<string> {
return this.uploaderService.uploadFile( return this.uploaderService.uploadFile(file.buffer, file.originalname, type)
file.buffer,
file.originalname,
account_id,
type,
)
} }
} }
@@ -1,9 +1,10 @@
import type { TUploadedFileTypes } from '@/common/models'
import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common' import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common'
import { FileInterceptor } from '@nestjs/platform-express' import { FileInterceptor } from '@nestjs/platform-express'
import { ImageUploaderService } from './image-uploader.service' import { ImageUploaderService } from './image-uploader.service'
@Controller('uploaders') @Controller('uploader')
export class UploadersController { export class UploaderController {
constructor(private readonly imageUploaderService: ImageUploaderService) {} constructor(private readonly imageUploaderService: ImageUploaderService) {}
@Post('image') @Post('image')
@@ -11,9 +12,9 @@ export class UploadersController {
async uploadImage( async uploadImage(
@UploadedFile() file: Express.Multer.File, @UploadedFile() file: Express.Multer.File,
@Body('accountId') accountId: string, @Body('accountId') accountId: string,
@Body('type') type: string, @Body('type') type: TUploadedFileTypes,
) { ) {
const url = await this.imageUploaderService.uploadImage(file, accountId, type) const url = await this.imageUploaderService.uploadImage(file, type)
return { url } return { url }
} }
} }
@@ -0,0 +1,50 @@
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 {
constructor(
private prisma: PrismaService,
private jwtService: JwtService,
) {}
async use(req: Request, _res: Response, next: NextFunction) {
const doForbidden = () => {
throw new ForbiddenException('شما دسترسی لازم را ندارید')
}
try {
const tokenAccount = checkAndDecodeJwtToken(req, this.jwtService)
const { type, account_id } = tokenAccount!
if (type === 'ADMIN' || type === 'CONSUMER') {
const account = await this.prisma.account.findFirst({
where: {
OR: [
{
admin_account: {
id: account_id,
},
},
{
consumer_account: {
id: account_id,
},
},
],
},
})
if (account) return next()
}
throw doForbidden()
} catch (error) {
if (error && typeof error === 'object') {
throw error
}
return doForbidden()
}
}
}
@@ -1,11 +1,11 @@
import { Module } from '@nestjs/common' import { Module } from '@nestjs/common'
import { ImageUploaderService } from './image-uploader.service' import { ImageUploaderService } from './image-uploader.service'
import { UploaderController } from './uploader.controller'
import { UploaderService } from './uploader.service' import { UploaderService } from './uploader.service'
import { UploadersController } from './uploaders.controller'
@Module({ @Module({
providers: [UploaderService, ImageUploaderService], providers: [UploaderService, ImageUploaderService],
controllers: [UploadersController], controllers: [UploaderController],
exports: [UploaderService, ImageUploaderService], exports: [UploaderService, ImageUploaderService],
}) })
export class UploadersModule {} export class UploaderModule {}
+4 -4
View File
@@ -1,3 +1,4 @@
import { TUploadedFileTypes } from '@/common/models'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import { join } from 'path' import { join } from 'path'
@@ -10,16 +11,15 @@ export class UploaderService {
async uploadFile( async uploadFile(
buffer: Buffer, buffer: Buffer,
originalname: string, originalname: string,
accountId: string, type: TUploadedFileTypes,
type: string,
): Promise<string> { ): Promise<string> {
const ext = originalname.split('.').pop() const ext = originalname.split('.').pop()
const filename = `${uuidv4()}.${ext}` const filename = `${uuidv4()}.${ext}`
const dirPath = join(this.uploadDir, accountId, type) const dirPath = join(this.uploadDir, type)
const filePath = join(dirPath, filename) const filePath = join(dirPath, filename)
await fs.mkdir(dirPath, { recursive: true }) await fs.mkdir(dirPath, { recursive: true })
await fs.writeFile(filePath, buffer) await fs.writeFile(filePath, buffer)
// Return a URL path, e.g. /uploads/{accountId}/{type}/{filename} // Return a URL path, e.g. /uploads/{accountId}/{type}/{filename}
return `/uploads/${accountId}/${type}/${filename}` return `/uploads/${type}/${filename}`
} }
} }