From 746c822778f63de772ffa03cb1f461bd34ac8a44 Mon Sep 17 00:00:00 2001 From: morteza-mortezai Date: Mon, 22 Dec 2025 00:10:17 +0330 Subject: [PATCH] revenue chart --- .../controllers/payments.controller.ts | 12 +- src/modules/payments/dto/payment-chart.dto.ts | 41 ++++++ src/modules/payments/events/payment.events.ts | 8 +- .../payments/services/payments.service.ts | 132 +++++++++++++++++- 4 files changed, 187 insertions(+), 6 deletions(-) create mode 100644 src/modules/payments/dto/payment-chart.dto.ts diff --git a/src/modules/payments/controllers/payments.controller.ts b/src/modules/payments/controllers/payments.controller.ts index c706b44..5641125 100644 --- a/src/modules/payments/controllers/payments.controller.ts +++ b/src/modules/payments/controllers/payments.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards } from '@nestjs/common'; +import { Controller, Get, Post, Body, Patch, Param, Delete, UseGuards, Query } from '@nestjs/common'; import { PaymentsService } from '../services/payments.service'; import { ApiTags, @@ -14,6 +14,7 @@ import { PaymentMethodService } from '../services/payment-method.service'; import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto'; import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto'; import { VerifyPaymentDto } from '../dto/verify-payment.dto'; +import { PaymentChartDto } from '../dto/payment-chart.dto'; import { RestId, UserId } from 'src/common/decorators'; import { AuthGuard } from 'src/modules/auth/guards/auth.guard'; import { API_HEADER_SLUG } from 'src/common/constants'; @@ -121,4 +122,13 @@ export class PaymentsController { verifyCashPayment(@Param('paymentId') paymentId: string) { return this.paymentMethodService.remove(paymentId); } + + @UseGuards(AdminAuthGuard) + @ApiBearerAuth() + @Get('admin/payments/chart') + @ApiOperation({ summary: 'Get payment chart data with date and period filters' }) + @ApiHeader(API_HEADER_SLUG) + getPaymentChart(@Query() query: PaymentChartDto, @RestId() restId: string) { + return this.paymentsService.getChartData(query, restId); + } } diff --git a/src/modules/payments/dto/payment-chart.dto.ts b/src/modules/payments/dto/payment-chart.dto.ts new file mode 100644 index 0000000..ae1f87b --- /dev/null +++ b/src/modules/payments/dto/payment-chart.dto.ts @@ -0,0 +1,41 @@ +import { IsOptional, IsString, IsEnum, IsDateString } from 'class-validator'; +import { ApiPropertyOptional } from '@nestjs/swagger'; + +export enum ChartPeriodEnum { + Daily = 'daily', + Weekly = 'weekly', + Monthly = 'monthly', +} + +export class PaymentChartDto { + @ApiPropertyOptional({ + description: 'Start date for filtering (ISO date string)', + type: String, + format: 'date-time', + example: '2024-01-01T00:00:00Z', + }) + @IsOptional() + @IsDateString() + startDate?: string; + + @ApiPropertyOptional({ + description: 'End date for filtering (ISO date string)', + type: String, + format: 'date-time', + example: '2024-12-31T23:59:59Z', + }) + @IsOptional() + @IsDateString() + endDate?: string; + + @ApiPropertyOptional({ + description: 'Period type for grouping data', + enum: ChartPeriodEnum, + default: ChartPeriodEnum.Daily, + example: ChartPeriodEnum.Daily, + }) + @IsOptional() + @IsEnum(ChartPeriodEnum) + type?: ChartPeriodEnum = ChartPeriodEnum.Daily; +} + diff --git a/src/modules/payments/events/payment.events.ts b/src/modules/payments/events/payment.events.ts index ccb8b2e..c43e9c5 100644 --- a/src/modules/payments/events/payment.events.ts +++ b/src/modules/payments/events/payment.events.ts @@ -1,13 +1,13 @@ import { PaymentMethodEnum } from "../interface/payment"; - export class paymentSucceedEvent { +export class paymentSucceedEvent { constructor( public readonly orderId: string, public readonly restaurantId: string, public readonly orderNumber: string, public readonly paymentMethod: PaymentMethodEnum, - public readonly total:number - ) {} + public readonly total: number + ) { } } - + diff --git a/src/modules/payments/services/payments.service.ts b/src/modules/payments/services/payments.service.ts index 5640bdb..0e505ff 100644 --- a/src/modules/payments/services/payments.service.ts +++ b/src/modules/payments/services/payments.service.ts @@ -8,6 +8,7 @@ import { GatewayManager } from '../gateways/gateway.manager'; import { UserWallet } from 'src/modules/users/entities/user-wallet.entity'; import { OrderPaymentContext } from '../interface/payment'; import { OrderStatus } from 'src/modules/orders/interface/order.interface'; +import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto'; @Injectable() export class PaymentsService { @@ -16,7 +17,7 @@ export class PaymentsService { constructor( private readonly em: EntityManager, private readonly gatewayManager: GatewayManager, - ) {} + ) { } async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> { const ctx = await this.loadAndValidateOrder(orderId); @@ -288,4 +289,133 @@ export class PaymentsService { await em.flush(); return payment; } + + async getChartData(dto: PaymentChartDto, restaurantId: string): Promise> { + const { startDate, endDate, type = ChartPeriodEnum.Daily } = dto; + + // Set default date range if not provided + const start = startDate ? new Date(startDate) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000); // Default: last 30 days + const end = endDate ? new Date(endDate) : new Date(); + + let dateGrouping: string; + let dateSelect: string; + + switch (type) { + case ChartPeriodEnum.Weekly: + dateGrouping = "DATE_TRUNC('week', p.paid_at::timestamp)"; + dateSelect = "TO_CHAR(DATE_TRUNC('week', p.paid_at::timestamp), 'YYYY-MM-DD')"; + break; + case ChartPeriodEnum.Monthly: + dateGrouping = "DATE_TRUNC('month', p.paid_at::timestamp)"; + dateSelect = "TO_CHAR(DATE_TRUNC('month', p.paid_at::timestamp), 'YYYY-MM-DD')"; + break; + case ChartPeriodEnum.Daily: + default: + dateGrouping = "DATE_TRUNC('day', p.paid_at::timestamp)"; + dateSelect = "TO_CHAR(DATE_TRUNC('day', p.paid_at::timestamp), 'YYYY-MM-DD')"; + } + + const conditions: string[] = [`p.status = 'paid'`, 'p.paid_at IS NOT NULL']; + const params: any[] = []; + + conditions.push(`p.paid_at::timestamp >= ?`); + params.push(start); + + conditions.push(`p.paid_at::timestamp <= ?`); + params.push(end); + + if (restaurantId) { + conditions.push(`o.restaurant_id = ?`); + params.push(restaurantId); + } + + const whereClause = conditions.join(' AND '); + + const query = ` + SELECT + ${dateSelect} as date, + COALESCE(SUM(CASE WHEN p.method = 'Cash' THEN p.amount ELSE 0 END), 0)::numeric as cash, + COALESCE(SUM(CASE WHEN p.method = 'Online' THEN p.amount ELSE 0 END), 0)::numeric as online + FROM payments p + INNER JOIN orders o ON p.order_id = o.id + WHERE ${whereClause} + GROUP BY ${dateGrouping} + ORDER BY ${dateGrouping} ASC + `; + + const result = await this.em.execute(query, params); + + // Create a map of existing data + const dataMap = new Map(); + result.forEach((row: any) => { + dataMap.set(row.date, { + cash: Number(row.cash), + online: Number(row.online), + }); + }); + + // Generate all dates in the range based on period type + const allDates = this.generateDateRange(start, end, type); + + // Return array with all dates, filling missing ones with zeros + return allDates.map(date => ({ + date, + cash: dataMap.get(date)?.cash ?? 0, + online: dataMap.get(date)?.online ?? 0, + })); + } + + private generateDateRange(start: Date, end: Date, type: ChartPeriodEnum): string[] { + const dates: string[] = []; + const current = new Date(start); + const endDate = new Date(end); + + // Normalize start date based on period type + switch (type) { + case ChartPeriodEnum.Weekly: + // Start of week (Monday) - PostgreSQL DATE_TRUNC('week') uses ISO week (Monday as first day) + const dayOfWeek = current.getDay(); + const diff = current.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); + current.setDate(diff); + current.setHours(0, 0, 0, 0); + // Also normalize end date to include the week containing it + const endDayOfWeek = endDate.getDay(); + const endDiff = endDate.getDate() - endDayOfWeek + (endDayOfWeek === 0 ? -6 : 1); + endDate.setDate(endDiff); + endDate.setHours(23, 59, 59, 999); + break; + case ChartPeriodEnum.Monthly: + // Start of month + current.setDate(1); + current.setHours(0, 0, 0, 0); + // Also normalize end date to include the month containing it + endDate.setDate(1); + endDate.setHours(23, 59, 59, 999); + break; + case ChartPeriodEnum.Daily: + default: + current.setHours(0, 0, 0, 0); + endDate.setHours(23, 59, 59, 999); + } + + while (current <= endDate) { + const dateStr = current.toISOString().split('T')[0]; + dates.push(dateStr); + + // Increment based on period type + switch (type) { + case ChartPeriodEnum.Weekly: + current.setDate(current.getDate() + 7); + break; + case ChartPeriodEnum.Monthly: + current.setMonth(current.getMonth() + 1); + break; + case ChartPeriodEnum.Daily: + default: + current.setDate(current.getDate() + 1); + } + } + + return dates; + } }