revenue chart

This commit is contained in:
2025-12-22 00:10:17 +03:30
parent 2f409e5c8c
commit 746c822778
4 changed files with 187 additions and 6 deletions
@@ -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);
}
}
@@ -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;
}
@@ -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
) { }
}
@@ -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<Array<{ date: string; cash: number; online: number }>> {
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<string, { cash: number; online: number }>();
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;
}
}