feat: enhance business activities and sale invoices handling with new query constants and pagination support

This commit is contained in:
2026-05-16 14:49:23 +03:30
parent 83e7c26133
commit 5baf5bfea6
14 changed files with 154 additions and 41 deletions
+9 -9
View File
@@ -1,7 +1,7 @@
export enum GoldKarat { export enum GoldKarat {
KARAT_18 = '18', KARAT_18 = 'KARAT_18',
KARAT_21 = '21', KARAT_21 = 'KARAT_21',
KARAT_24 = '24', KARAT_24 = 'KARAT_24',
} }
export enum TspProviderRequestType { export enum TspProviderRequestType {
@@ -16,16 +16,16 @@ export enum SKUGuildType {
} }
export const UploadedFileTypes = { export const UploadedFileTypes = {
GOOD: 'good', GOOD: 'GOOD',
SERVICE: 'service', SERVICE: 'SERVICE',
PROFILE_AVATAR: 'profile_avatar', PROFILE_AVATAR: 'PROFILE_AVATAR',
PARTNER_LOGO: 'partner_logo', PARTNER_LOGO: 'PARTNER_LOGO',
} }
export type UploadedFileTypes = (typeof UploadedFileTypes)[keyof typeof UploadedFileTypes] export type UploadedFileTypes = (typeof UploadedFileTypes)[keyof typeof UploadedFileTypes]
export const TspProviderCustomerType = { export const TspProviderCustomerType = {
UNKNOWN: 'Unknown', UNKNOWN: 'UNKNOWN',
KNOWN: 'Known', KNOWN: 'KNOWN',
} }
export type TspProviderCustomerType = export type TspProviderCustomerType =
@@ -0,0 +1,56 @@
import { BusinessActivitySelect } from '@/generated/prisma/models'
export const summarySelect: BusinessActivitySelect = {
id: true,
name: true,
economic_code: true,
created_at: true,
fiscal_id: true,
invoice_number_sequence: true,
partner_token: true,
guild: {
select: {
id: true,
code: true,
name: true,
},
},
license_activation: {
select: {
id: true,
starts_at: true,
expires_at: true,
license: {
select: {
activation: {
select: {
_count: {
select: {
account_allocations: true,
},
},
},
},
},
},
},
},
}
export const select: BusinessActivitySelect = {
...summarySelect,
}
export const mappedData = (businessActivity: any) => {
const { license_activation, ...rest } = businessActivity
const { license, ...license_activation_rest } = license_activation
const { _count } = license.activation
return {
...rest,
license_info: {
...license_activation_rest,
accounts_limit: _count.account_allocations,
},
}
}
+2
View File
@@ -1,3 +1,4 @@
import * as BUSINESS_ACTIVITIES from './businessActivities'
import * as CONSUMER from './consumer' import * as CONSUMER from './consumer'
import * as LICENSE_ACTIVATION from './licenseActivation' import * as LICENSE_ACTIVATION from './licenseActivation'
import * as POS from './pos' import * as POS from './pos'
@@ -8,4 +9,5 @@ export const QUERY_CONSTANTS = {
CONSUMER, CONSUMER,
LICENSE_ACTIVATION, LICENSE_ACTIVATION,
SALE_INVOICE, SALE_INVOICE,
BUSINESS_ACTIVITIES,
} }
@@ -1,3 +1,4 @@
import { QUERY_CONSTANTS } from '@/common/queryConstants'
import { BusinessActivitySelect } from '@/generated/prisma/models' import { BusinessActivitySelect } from '@/generated/prisma/models'
import { PrismaService } from '@/prisma/prisma.service' import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common' import { Injectable } from '@nestjs/common'
@@ -7,45 +8,35 @@ export class BusinessActivitiesQueryService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
readonly baseSelect: BusinessActivitySelect = { readonly baseSelect: BusinessActivitySelect = {
id: true, ...QUERY_CONSTANTS.BUSINESS_ACTIVITIES.summarySelect,
name: true,
economic_code: true,
created_at: true,
partner_token: true,
fiscal_id: true,
guild: {
select: {
id: true,
code: true,
name: true,
},
},
} }
findAllByConsumer(consumer_id: string, select: BusinessActivitySelect) { async findAllByConsumer(consumer_id: string, select: BusinessActivitySelect) {
return this.prisma.businessActivity.findMany({ const businessActivities = await this.prisma.businessActivity.findMany({
where: { consumer_id }, where: { consumer_id },
select, select,
}) })
return businessActivities.map(QUERY_CONSTANTS.BUSINESS_ACTIVITIES.mappedData)
} }
findOneByConsumer( async findOneByConsumer(
consumer_id: string, consumer_id: string,
id: string, id: string,
select: BusinessActivitySelect, select: BusinessActivitySelect,
orThrow = false, orThrow = false,
) { ) {
if (orThrow) { // if (orThrow) {
return this.prisma.businessActivity.findUniqueOrThrow({ // return await this.prisma.businessActivity.findUniqueOrThrow({
// where: { consumer_id, id },
// select,
// })
// }
const businessActivity = await this.prisma.businessActivity.findUnique({
where: { consumer_id, id }, where: { consumer_id, id },
select, select,
}) })
}
return this.prisma.businessActivity.findUnique({ return QUERY_CONSTANTS.BUSINESS_ACTIVITIES.mappedData(businessActivity)
where: { consumer_id, id },
select,
})
} }
} }
@@ -151,7 +151,7 @@ export class SharedSaleInvoiceCreateService {
const terminalInfo = rawPayments.terminals as TerminalPaymentInfo | undefined const terminalInfo = rawPayments.terminals as TerminalPaymentInfo | undefined
const payments: NormalizedPayment[] = Object.entries(rawPayments) const payments: NormalizedPayment[] = Object.entries(rawPayments)
.filter(([key]) => key !== 'terminals') .filter(([key, value]) => key !== 'terminals' && value && Number(value) > 0)
.map(([key, value]) => ({ .map(([key, value]) => ({
method: paymentMethodMap[key.toLowerCase()], method: paymentMethodMap[key.toLowerCase()],
amount: typeof value === 'number' ? value : Number(value || 0), amount: typeof value === 'number' ? value : Number(value || 0),
+2
View File
@@ -12,6 +12,8 @@ export function translateEnumValue(
enumKey: keyof typeof translates.enums, enumKey: keyof typeof translates.enums,
value: string | null | undefined, value: string | null | undefined,
): EnumTranslatedValue { ): EnumTranslatedValue {
console.log(enumKey, value)
const enumsRegistry = translates.enums as EnumsRegistry const enumsRegistry = translates.enums as EnumsRegistry
const enumMap = enumsRegistry[String(enumKey)] const enumMap = enumsRegistry[String(enumKey)]
+10
View File
@@ -63,6 +63,9 @@ async function bootstrap() {
.map(s => s.trim()) .map(s => s.trim())
.filter(Boolean) .filter(Boolean)
: [] : []
console.log('envOrigins', envOrigins)
const allowedOrigins = (envOrigins.length ? envOrigins : defaultOrigins).filter(Boolean) const allowedOrigins = (envOrigins.length ? envOrigins : defaultOrigins).filter(Boolean)
const isOriginAllowed = (origin: string) => { const isOriginAllowed = (origin: string) => {
@@ -74,6 +77,9 @@ async function bootstrap() {
const rootDomain = pattern.slice(2) const rootDomain = pattern.slice(2)
return hostname === rootDomain || hostname.endsWith(`.${rootDomain}`) return hostname === rootDomain || hostname.endsWith(`.${rootDomain}`)
} }
console.log('origin', origin, 'pattern', pattern, 'hostname', hostname)
console.log(hostname === pattern || origin === pattern)
return hostname === pattern || origin === pattern return hostname === pattern || origin === pattern
}) })
} catch { } catch {
@@ -84,14 +90,18 @@ async function bootstrap() {
app.enableCors({ app.enableCors({
origin: (origin, callback) => { origin: (origin, callback) => {
if (!origin) { if (!origin) {
console.log('1')
callback(null, true) callback(null, true)
return return
} }
if (isOriginAllowed(origin)) { if (isOriginAllowed(origin)) {
console.log('2')
callback(null, true) callback(null, true)
return return
} }
console.log('3')
callback(new Error('Not allowed by CORS'), false) callback(new Error('Not allowed by CORS'), false)
}, },
@@ -23,12 +23,14 @@ export class GoodsService {
select: { select: {
id: true, id: true,
name: true, name: true,
code: true,
}, },
}, },
measure_unit: { measure_unit: {
select: { select: {
id: true, id: true,
name: true, name: true,
code: true,
}, },
}, },
category: { category: {
+2
View File
@@ -59,6 +59,8 @@ export class AuthService {
include: accountInclude, include: accountInclude,
}) })
console.log('account', account)
if (!account) { if (!account) {
throw new NotFoundException('اطلاعات ورودی شما اشتباه است') throw new NotFoundException('اطلاعات ورودی شما اشتباه است')
} }
@@ -71,7 +71,6 @@ export class BusinessActivitiesService {
consumer_id, consumer_id,
id, id,
this.defaultSelect, this.defaultSelect,
true,
) )
return ResponseMapper.single(this.prepareBusinessActivityResponse(businessActivity)) return ResponseMapper.single(this.prepareBusinessActivityResponse(businessActivity))
} }
@@ -0,0 +1,18 @@
import { ApiPropertyOptional } from '@nestjs/swagger'
import { Type } from 'class-transformer'
import { IsOptional, Max, Min } from 'class-validator'
export class ConsumerSaleInvoicesFilterDto {
@ApiPropertyOptional({ type: Number, example: 1 })
@Type(() => Number)
@Min(1)
@IsOptional()
page?: number
@ApiPropertyOptional({ type: Number, example: 10, description: 'Max value is 50.' })
@Type(() => Number)
@Min(1)
@Max(50)
@IsOptional()
perPage?: number
}
@@ -1,8 +1,9 @@
import { PosInfo } from '@/common/decorators/posInfo.decorator' import { PosInfo } from '@/common/decorators/posInfo.decorator'
import { TokenAccount } from '@/common/decorators/tokenInfo.decorator' import { TokenAccount } from '@/common/decorators/tokenInfo.decorator'
import type { IPosPayload } from '@/common/models' import type { IPosPayload } from '@/common/models'
import { Body, Controller, Get, Param, Post } from '@nestjs/common' import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger' import { ApiTags } from '@nestjs/swagger'
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
import type { import type {
SaleInvoicesServiceFindAllResponseDto, SaleInvoicesServiceFindAllResponseDto,
SaleInvoicesServiceFindOneResponseDto, SaleInvoicesServiceFindOneResponseDto,
@@ -18,8 +19,15 @@ export class StatisticsController {
@Get('') @Get('')
async findAll( async findAll(
@TokenAccount('userId') consumerId: string, @TokenAccount('userId') consumerId: string,
@Query('page') page = '1',
@Query('perPage') perPage = '10',
): Promise<SaleInvoicesServiceFindAllResponseDto> { ): Promise<SaleInvoicesServiceFindAllResponseDto> {
return this.service.findAll(consumerId) const filter: ConsumerSaleInvoicesFilterDto = {
page: Number(page) || 1,
perPage: Number(perPage) || 10,
}
return this.service.findAll(consumerId, filter)
} }
@Get(':id') @Get(':id')
@@ -12,6 +12,7 @@ import { PrismaService } from '@/prisma/prisma.service'
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common' import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common'
import { ResponseMapper } from 'common/response/response-mapper' import { ResponseMapper } from 'common/response/response-mapper'
import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service' import { SalesInvoiceTspService } from '../../tspProviders/sales-invoice-tsp.service'
import { ConsumerSaleInvoicesFilterDto } from './dto/saleInvoices-filter.dto'
@Injectable() @Injectable()
export class SaleInvoicesService { export class SaleInvoicesService {
@@ -21,6 +22,9 @@ export class SaleInvoicesService {
private sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService, private sharedSaleInvoiceActionsService: SharedSaleInvoiceActionsService,
) {} ) {}
private readonly defaultPerPage = 10
private readonly maxPerPage = 50
private readonly defaultSelect: SalesInvoiceSelect = { private readonly defaultSelect: SalesInvoiceSelect = {
pos: { pos: {
select: { select: {
@@ -54,27 +58,45 @@ export class SaleInvoicesService {
} }
} }
async findAll(consumer_id: string, page = 1, perPage = 10) { async findAll(consumer_id: string, filter: ConsumerSaleInvoicesFilterDto) {
const invoicesWhere: SalesInvoiceWhereInput = { const invoicesWhere: SalesInvoiceWhereInput = {
consumer_account: { consumer_account: {
consumer_id, consumer_id,
}, },
} }
const normalizedPageValue = Number(filter.page ?? 1)
const normalizedPage = Number.isFinite(normalizedPageValue)
? Math.max(1, Math.floor(normalizedPageValue))
: 1
const requestedPerPageValue = Number(filter.perPage ?? this.defaultPerPage)
const requestedPerPage = Number.isFinite(requestedPerPageValue)
? Math.max(1, Math.floor(requestedPerPageValue))
: this.defaultPerPage
const normalizedPerPage = Math.min(requestedPerPage, this.maxPerPage)
const [invoices, total] = await this.prisma.$transaction(async tx => [ const [invoices, total] = await this.prisma.$transaction(async tx => [
await tx.salesInvoice.findMany({ await tx.salesInvoice.findMany({
where: invoicesWhere, where: invoicesWhere,
orderBy: {
created_at: 'desc',
},
skip: (normalizedPage - 1) * normalizedPerPage,
take: normalizedPerPage,
select: { select: {
...QUERY_CONSTANTS.SALE_INVOICE.summarySelect, ...QUERY_CONSTANTS.SALE_INVOICE.summarySelect,
...this.defaultSelect, ...this.defaultSelect,
}, },
skip: (page - 1) * perPage,
take: perPage,
}), }),
await tx.salesInvoice.count({ where: invoicesWhere }), await tx.salesInvoice.count({ where: invoicesWhere }),
]) ])
const mappedInvoices = invoices.map(this.invoiceMapper) const mappedInvoices = invoices.map(this.invoiceMapper)
return ResponseMapper.paginate(mappedInvoices, { page, perPage, total }) return ResponseMapper.paginate(mappedInvoices, {
page: normalizedPage,
perPage: normalizedPerPage,
total,
})
} }
async findOne(consumer_id: string, id: string) { async findOne(consumer_id: string, id: string) {
+1
View File
@@ -18,6 +18,7 @@ export class GoodsService {
select: { select: {
id: true, id: true,
name: true, name: true,
code: true,
}, },
}, },
local_sku: true, local_sku: true,