feat(statistics): implement statistics module with controller and service for POS invoices
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
import dayjs, { Dayjs } from 'dayjs'
|
||||
import 'dayjs/locale/fa'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import jalaliPlugin from 'jalaliday/dayjs'
|
||||
|
||||
dayjs.extend(jalaliPlugin)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export function toJalali(date: string | number | Date | Dayjs): Date {
|
||||
return dayjs(date).calendar('jalali').toDate()
|
||||
}
|
||||
|
||||
export function toGregorian(date: string | number | Date | Dayjs): Date {
|
||||
return dayjs(date).calendar('gregory').toDate()
|
||||
}
|
||||
|
||||
export function getCurrentJalaliSeasonStart(
|
||||
baseDate: string | number | Date | Dayjs = dayjs(),
|
||||
): Date {
|
||||
const date = dayjs(baseDate).calendar('jalali')
|
||||
const month = date.month() + 1
|
||||
const seasonStartMonth = Math.floor((month - 1) / 3) * 3 + 1
|
||||
return date.month(seasonStartMonth - 1).startOf('month').startOf('day').toDate()
|
||||
}
|
||||
|
||||
export function getCurrentJalaliSeasonEnd(
|
||||
baseDate: string | number | Date | Dayjs = dayjs(),
|
||||
): Date {
|
||||
return dayjs(getCurrentJalaliSeasonStart(baseDate))
|
||||
.add(2, 'month')
|
||||
.endOf('month')
|
||||
.endOf('day')
|
||||
.toDate()
|
||||
}
|
||||
|
||||
export function getCurrentGregorianSeasonStart(
|
||||
baseDate: string | number | Date | Dayjs = dayjs(),
|
||||
): Date {
|
||||
const date = dayjs(baseDate).calendar('gregory')
|
||||
const month = date.month() + 1
|
||||
const seasonStartMonth = Math.floor((month - 1) / 3) * 3 + 1
|
||||
return date.month(seasonStartMonth - 1).startOf('month').startOf('day').toDate()
|
||||
}
|
||||
|
||||
export function getCurrentGregorianSeasonEnd(
|
||||
baseDate: string | number | Date | Dayjs = dayjs(),
|
||||
): Date {
|
||||
return dayjs(getCurrentGregorianSeasonStart(baseDate))
|
||||
.add(2, 'month')
|
||||
.endOf('month')
|
||||
.endOf('day')
|
||||
.toDate()
|
||||
}
|
||||
|
||||
export function getCurrentJalaliSeasonRange(
|
||||
baseDate: string | number | Date | Dayjs = dayjs(),
|
||||
): { start: Date; end: Date } {
|
||||
return {
|
||||
start: getCurrentJalaliSeasonStart(baseDate),
|
||||
end: getCurrentJalaliSeasonEnd(baseDate),
|
||||
}
|
||||
}
|
||||
|
||||
export function getCurrentGregorianSeasonRange(
|
||||
baseDate: string | number | Date | Dayjs = dayjs(),
|
||||
): { start: Date; end: Date } {
|
||||
return {
|
||||
start: getCurrentGregorianSeasonStart(baseDate),
|
||||
end: getCurrentGregorianSeasonEnd(baseDate),
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
toJalali,
|
||||
toGregorian,
|
||||
getCurrentJalaliSeasonStart,
|
||||
getCurrentJalaliSeasonEnd,
|
||||
getCurrentGregorianSeasonStart,
|
||||
getCurrentGregorianSeasonEnd,
|
||||
getCurrentJalaliSeasonRange,
|
||||
getCurrentGregorianSeasonRange,
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './date-formatter.utils'
|
||||
export * from './enum-translator.util'
|
||||
export * from './field-validator.util'
|
||||
export * from './http-client.util'
|
||||
|
||||
@@ -14,4 +14,50 @@ export class PosKeyMaker {
|
||||
static goodListByGuildPattern(guildId: string): string {
|
||||
return `pos:goods:list:guildId:${guildId}:ba:*`
|
||||
}
|
||||
|
||||
static statisticInvoicesPattern(businessActivityId: string): string {
|
||||
return `pos:statistics:invoices:ba:${businessActivityId}:*`
|
||||
}
|
||||
static statisticInvoices(
|
||||
businessActivityId: string,
|
||||
posId: string,
|
||||
from: string,
|
||||
): string {
|
||||
return `pos:statistics:invoices:ba:${businessActivityId}:pos:${posId}:from:${from}:all`
|
||||
}
|
||||
static statisticInvoicesNotSent(
|
||||
businessActivityId: string,
|
||||
posId: string,
|
||||
from: string,
|
||||
): string {
|
||||
return `pos:statistics:invoices:ba:${businessActivityId}:pos:${posId}:from:${from}:not-sent`
|
||||
}
|
||||
static statisticInvoicesSuccess(
|
||||
businessActivityId: string,
|
||||
posId: string,
|
||||
from: string,
|
||||
): string {
|
||||
return `pos:statistics:invoices:ba:${businessActivityId}:pos:${posId}:from:${from}:success`
|
||||
}
|
||||
static statisticInvoicesFailure(
|
||||
businessActivityId: string,
|
||||
posId: string,
|
||||
from: string,
|
||||
): string {
|
||||
return `pos:statistics:invoices:ba:${businessActivityId}:pos:${posId}:from:${from}:failure`
|
||||
}
|
||||
static statisticInvoicesPending(
|
||||
businessActivityId: string,
|
||||
posId: string,
|
||||
from: string,
|
||||
): string {
|
||||
return `pos:statistics:invoices:ba:${businessActivityId}:pos:${posId}:from:${from}:pending`
|
||||
}
|
||||
static statisticInvoicesCredit(
|
||||
businessActivityId: string,
|
||||
posId: string,
|
||||
from: string,
|
||||
): string {
|
||||
return `pos:statistics:invoices:ba:${businessActivityId}:pos:${posId}:from:${from}:credit`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user