Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { CartModule } from './cart/cart.module';
import { RecommendationModule } from './recommendation/recommendation.module';
import { ReportsModule } from './reports/reports.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
import { SalesModule } from './sales/sales.module';
import { ActivePrincipleModule } from './active-principle/active-principle.module';

@Module({
Expand Down Expand Up @@ -68,6 +69,7 @@ import { ActivePrincipleModule } from './active-principle/active-principle.modul
CartModule,
RecommendationModule,
ReportsModule,
SalesModule,
ActivePrincipleModule,
],
controllers: [AppController],
Expand Down
21 changes: 21 additions & 0 deletions src/sales/dto/predict-sales.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsInt, IsOptional } from 'class-validator';

export class PredictSalesDTO {
@IsOptional()
days?: string = '7';
}

export class DailySaleDTO {
date: string;
total: number;
}

export class PredictedSaleDTO {
@ApiProperty()
date: string;

@ApiProperty()
@IsInt()
predictedTotal: number;
}
53 changes: 53 additions & 0 deletions src/sales/sales.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common';
import { SalesService } from './sales.service';
import { PredictedSaleDTO, PredictSalesDTO } from './dto/predict-sales.dto';
import { AuthGuard, CustomRequest } from 'src/auth/auth.guard';
import { RolesGuard } from 'src/auth/roles.guard';
import { Roles } from 'src/auth/roles.decorador';
import { UserRole } from 'src/user/entities/user.entity';
import {
ApiBearerAuth,
ApiOperation,
ApiQuery,
ApiResponse,
} from '@nestjs/swagger';

@Controller('sales')
export class SalesController {
constructor(private readonly salesService: SalesService) {}

@UseGuards(AuthGuard, RolesGuard)
@Roles(UserRole.ADMIN, UserRole.BRANCH_ADMIN)
@Get('predict')
@ApiBearerAuth()
@ApiOperation({
summary: 'Predict future daily sales',
description:
'Returns sales predictions starting from today for the number of future days specified.',
})
@ApiQuery({
name: 'days',
required: false,
description: 'Number of future days to predict (default: 7)',
type: Number,
example: 5,
})
@ApiResponse({
status: 200,
description: 'List of predicted daily sales values',
type: [PredictedSaleDTO],
})
async predict(
@Req() req: CustomRequest,
@Query() query: PredictSalesDTO,
): Promise<PredictedSaleDTO[]> {
let branchId: string | undefined;
if (req.user.role === UserRole.BRANCH_ADMIN) {
branchId = req.user.branch.id;
}
const salesData = await this.salesService.getDailySales(branchId);
const dailySales = this.salesService.fillMissingDates(salesData);
const days = query.days ? parseInt(query.days, 10) || 7 : 7;
return await this.salesService.predictNext(days, dailySales);
}
}
13 changes: 13 additions & 0 deletions src/sales/sales.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Order } from 'src/order/entities/order.entity';
import { SalesController } from './sales.controller';
import { SalesService } from './sales.service';
import { AuthModule } from 'src/auth/auth.module';

@Module({
imports: [TypeOrmModule.forFeature([Order]), AuthModule],
controllers: [SalesController],
providers: [SalesService],
})
export class SalesModule {}
103 changes: 103 additions & 0 deletions src/sales/sales.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Order, OrderStatus } from '../order/entities/order.entity';
import { Repository } from 'typeorm';
import * as tf from '@tensorflow/tfjs';
import { DailySaleDTO, PredictedSaleDTO } from './dto/predict-sales.dto';

@Injectable()
export class SalesService {
constructor(
@InjectRepository(Order)
private readonly orderRepository: Repository<Order>,
) {}

async getDailySales(branchId?: string): Promise<DailySaleDTO[]> {
const query = this.orderRepository
.createQueryBuilder('order')
.select('DATE(order.createdAt)', 'date')
.addSelect('SUM(order.totalPrice)', 'total')
.where('order.status = :status', { status: OrderStatus.COMPLETED })
.groupBy('DATE(order.createdAt)')
.orderBy('DATE(order.createdAt)', 'ASC');
if (branchId) {
query.andWhere('order.branchId = :branchId', { branchId });
}
const sales = await query.getRawMany<{ date: string; total: string }>();

return sales.map((row) => ({
date: row.date,
total: parseInt(row.total),
}));
}

fillMissingDates(data: DailySaleDTO[]): DailySaleDTO[] {
if (data.length < 2) return data;

const filled = [];
const start = new Date(data[0].date);
const yesterday = new Date();
yesterday.setHours(0, 0, 0, 0);
yesterday.setDate(yesterday.getDate() - 1);

const dateMap = new Map(
data.map((d) => [new Date(d.date).toISOString().split('T')[0], d.total]),
);

for (let d = new Date(start); d <= yesterday; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split('T')[0];

filled.push({
date: dateStr,
total: dateMap.get(dateStr) ?? 0,
});
}

return filled;
}

async predictNext(
daysAhead: number = 7,
dailySales: DailySaleDTO[],
): Promise<PredictedSaleDTO[]> {
if (dailySales.length < 2) return [];

const inputs = dailySales.map((_, i) => i);
const labels = dailySales.map((s) => s.total);

const xs = tf.tensor2d(inputs, [inputs.length, 1]);
const ys = tf.tensor2d(labels, [labels.length, 1]);

const model = tf.sequential();
model.add(tf.layers.dense({ inputShape: [1], units: 1 }));
model.compile({ optimizer: 'sgd', loss: 'meanSquaredError' });

await model.fit(xs, ys, { epochs: 3 });

const futureDays = Array.from(
{ length: daysAhead },
(_, i) => inputs.length + i,
);

const predictions = model.predict(
tf.tensor2d(futureDays, [daysAhead, 1]),
) as tf.Tensor;

const predictedValues = await predictions.array();

return futureDays.map((_, index) => {
const date = new Date();
date.setHours(0, 0, 0, 0);
date.setDate(date.getDate() + index);

const formatted = date.toISOString().split('T')[0];

return {
date: formatted,
predictedTotal: parseInt(
(predictedValues as number[][])[index][0].toFixed(2),
),
};
});
}
}
Loading