feat(statistics): implement statistics module with controller and service for POS invoices

This commit is contained in:
2026-05-23 18:09:41 +03:30
parent 2dc9480170
commit 2c97b7302d
8 changed files with 317 additions and 0 deletions
+68
View File
@@ -16,4 +16,72 @@ export class PosCacheInvalidationService {
async invalidatePosGoodsByGuildPattern(guildId: string): Promise<void> {
this.redisService.deleteByPattern(PosKeyMaker.goodListByGuildPattern(guildId))
}
async invalidatePosStatisticsInvoicesByBusinessActivityId(
businessActivityId: string,
): Promise<void> {
this.redisService.deleteByPattern(
PosKeyMaker.statisticInvoicesPattern(businessActivityId),
)
}
async invalidatePosStatisticsInvoicesAll(
businessActivityId: string,
posId: string,
from: string,
): Promise<void> {
this.redisService.delete(
PosKeyMaker.statisticInvoices(businessActivityId, posId, from),
)
}
async invalidatePosStatisticsInvoicesFailure(
businessActivityId: string,
posId: string,
from: string,
): Promise<void> {
this.redisService.delete(
PosKeyMaker.statisticInvoicesFailure(businessActivityId, posId, from),
)
}
async invalidatePosStatisticsInvoicesSuccess(
businessActivityId: string,
posId: string,
from: string,
): Promise<void> {
this.redisService.delete(
PosKeyMaker.statisticInvoicesSuccess(businessActivityId, posId, from),
)
}
async invalidatePosStatisticsInvoicesPending(
businessActivityId: string,
posId: string,
from: string,
): Promise<void> {
this.redisService.delete(
PosKeyMaker.statisticInvoicesPending(businessActivityId, posId, from),
)
}
async invalidatePosStatisticsInvoicesCredit(
businessActivityId: string,
posId: string,
from: string,
): Promise<void> {
this.redisService.delete(
PosKeyMaker.statisticInvoicesCredit(businessActivityId, posId, from),
)
}
async invalidatePosStatisticsInvoicesNotSent(
businessActivityId: string,
posId: string,
from: string,
): Promise<void> {
this.redisService.delete(
PosKeyMaker.statisticInvoicesNotSent(businessActivityId, posId, from),
)
}
}
+2
View File
@@ -8,6 +8,7 @@ import { PosController } from './pos.controller'
import { PosMiddleware } from './pos.middleware'
import { PosService } from './pos.service'
import { PosSalesInvoicesModule } from './sales-invoices/sales-invoices.module'
import { StatisticsModule } from './statistics/statistics.module'
@Module({
controllers: [PosController],
@@ -18,6 +19,7 @@ import { PosSalesInvoicesModule } from './sales-invoices/sales-invoices.module'
PosGoodsModule,
PosSalesInvoicesModule,
OwnedGoodsModule,
StatisticsModule,
],
})
export class PosModule implements NestModule {
@@ -0,0 +1,20 @@
import { PosInfo } from '@/common/decorators/posInfo.decorator'
import type { IPosPayload } from '@/common/models'
import { Controller, Get, Query } from '@nestjs/common'
import { ApiTags } from '@nestjs/swagger'
import { StatisticsService } from './statistics.service'
@ApiTags('PosStatistics')
@Controller('pos/statistics')
export class StatisticsController {
constructor(private readonly service: StatisticsService) {}
@Get('sale-invoices')
findAll(@PosInfo() PosInfo: IPosPayload, @Query('from_date') from_date?: string) {
return this.service.findAll(
PosInfo.pos_id,
PosInfo.business_id,
from_date ? new Date(from_date) : new Date(),
)
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { StatisticsService } from './statistics.service';
import { StatisticsController } from './statistics.controller';
import { PrismaService } from '@/prisma/prisma.service';
@Module({
controllers: [StatisticsController],
providers: [StatisticsService, PrismaService],
})
export class StatisticsModule {}
@@ -0,0 +1,88 @@
import { ResponseMapper } from '@/common/response/response-mapper'
import { getCurrentJalaliSeasonRange } from '@/common/utils'
import { TspProviderResponseStatus } from '@/generated/prisma/enums'
import { PrismaService } from '@/prisma/prisma.service'
import { Injectable } from '@nestjs/common'
@Injectable()
export class StatisticsService {
constructor(private readonly prisma: PrismaService) {}
async findAll(posId: string, businessId: string, fromDate: Date = new Date()) {
const seasonDate = getCurrentJalaliSeasonRange(fromDate)
const [item] = await this.prisma.$queryRaw<
Array<{
all_amount_sum: number | null
all_count: number | null
success_amount_sum: number | null
success_count: number | null
failure_amount_sum: number | null
failure_count: number | null
pending_amount_sum: number | null
pending_count: number | null
not_sended_amount_sum: number | null
not_sended_count: number | null
}>
>`
SELECT
SUM(si.total_amount) AS all_amount_sum,
COUNT(*) AS all_count,
SUM(CASE WHEN la.status = ${TspProviderResponseStatus.SUCCESS} THEN si.total_amount ELSE 0 END) AS success_amount_sum,
SUM(CASE WHEN la.status = ${TspProviderResponseStatus.SUCCESS} THEN 1 ELSE 0 END) AS success_count,
SUM(CASE WHEN la.status = ${TspProviderResponseStatus.FAILURE} THEN si.total_amount ELSE 0 END) AS failure_amount_sum,
SUM(CASE WHEN la.status = ${TspProviderResponseStatus.FAILURE} THEN 1 ELSE 0 END) AS failure_count,
SUM(CASE WHEN la.status = ${TspProviderResponseStatus.QUEUED} THEN si.total_amount ELSE 0 END) AS pending_amount_sum,
SUM(CASE WHEN la.status = ${TspProviderResponseStatus.QUEUED} THEN 1 ELSE 0 END) AS pending_count,
SUM(CASE WHEN la.status IS NULL THEN si.total_amount ELSE 0 END) AS not_sended_amount_sum,
SUM(CASE WHEN la.status IS NULL THEN 1 ELSE 0 END) AS not_sended_count
FROM sales_invoices si
INNER JOIN poses p ON p.id = si.pos_id
LEFT JOIN (
SELECT a1.invoice_id, a1.status
FROM sale_invoice_tsp_attempts a1
INNER JOIN (
SELECT invoice_id, MAX(attempt_no) AS max_attempt_no
FROM sale_invoice_tsp_attempts
GROUP BY invoice_id
) latest
ON latest.invoice_id = a1.invoice_id
AND latest.max_attempt_no = a1.attempt_no
) la ON la.invoice_id = si.id
WHERE si.invoice_date >= ${seasonDate.start}
AND si.invoice_date <= ${seasonDate.end}
AND si.pos_id = ${posId}
AND p.complex_id IN (
SELECT c.id FROM complexes c WHERE c.business_activity_id = ${businessId}
)
`
return ResponseMapper.single({
all: {
total_amount: Number(item?.all_amount_sum || 0),
total_tax: Number(0),
count: Number(item?.all_count || 0),
},
success: {
total_amount: Number(item?.success_amount_sum || 0),
total_tax: Number(0),
count: Number(item?.success_count || 0),
},
failure: {
total_amount: Number(item?.failure_amount_sum || 0),
total_tax: Number(0),
count: Number(item?.failure_count || 0),
},
pending: {
total_amount: Number(item?.pending_amount_sum || 0),
total_tax: Number(0),
count: Number(item?.pending_count || 0),
},
notSended: {
total_amount: Number(item?.not_sended_amount_sum || 0),
total_tax: Number(0),
count: Number(item?.not_sended_count || 0),
},
})
}
}