revenue chart
This commit is contained in:
@@ -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 { PaymentsService } from '../services/payments.service';
|
||||||
import {
|
import {
|
||||||
ApiTags,
|
ApiTags,
|
||||||
@@ -14,6 +14,7 @@ import { PaymentMethodService } from '../services/payment-method.service';
|
|||||||
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
|
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
|
||||||
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
|
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
|
||||||
import { VerifyPaymentDto } from '../dto/verify-payment.dto';
|
import { VerifyPaymentDto } from '../dto/verify-payment.dto';
|
||||||
|
import { PaymentChartDto } from '../dto/payment-chart.dto';
|
||||||
import { RestId, UserId } from 'src/common/decorators';
|
import { RestId, UserId } from 'src/common/decorators';
|
||||||
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||||
import { API_HEADER_SLUG } from 'src/common/constants';
|
import { API_HEADER_SLUG } from 'src/common/constants';
|
||||||
@@ -121,4 +122,13 @@ export class PaymentsController {
|
|||||||
verifyCashPayment(@Param('paymentId') paymentId: string) {
|
verifyCashPayment(@Param('paymentId') paymentId: string) {
|
||||||
return this.paymentMethodService.remove(paymentId);
|
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";
|
import { PaymentMethodEnum } from "../interface/payment";
|
||||||
|
|
||||||
export class paymentSucceedEvent {
|
export class paymentSucceedEvent {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly orderId: string,
|
public readonly orderId: string,
|
||||||
public readonly restaurantId: string,
|
public readonly restaurantId: string,
|
||||||
public readonly orderNumber: string,
|
public readonly orderNumber: string,
|
||||||
public readonly paymentMethod: PaymentMethodEnum,
|
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 { UserWallet } from 'src/modules/users/entities/user-wallet.entity';
|
||||||
import { OrderPaymentContext } from '../interface/payment';
|
import { OrderPaymentContext } from '../interface/payment';
|
||||||
import { OrderStatus } from 'src/modules/orders/interface/order.interface';
|
import { OrderStatus } from 'src/modules/orders/interface/order.interface';
|
||||||
|
import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentsService {
|
export class PaymentsService {
|
||||||
@@ -16,7 +17,7 @@ export class PaymentsService {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly gatewayManager: GatewayManager,
|
private readonly gatewayManager: GatewayManager,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {
|
async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {
|
||||||
const ctx = await this.loadAndValidateOrder(orderId);
|
const ctx = await this.loadAndValidateOrder(orderId);
|
||||||
@@ -288,4 +289,133 @@ export class PaymentsService {
|
|||||||
await em.flush();
|
await em.flush();
|
||||||
return payment;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user