feat(statistics): implement top alert stocks, top last sales, top supplier debts, and top selling products endpoints with SQL queries

This commit is contained in:
2026-01-04 13:45:26 +03:30
parent eb6e0e7a0d
commit a2db2daa70
72 changed files with 11934 additions and 2559 deletions
@@ -92,13 +92,13 @@ export class InventoriesService {
console.log(movements)
const mapped = movements.map(movement => {
const { id, quantity, fee, totalCost, avgCost, product } = movement
const { id, quantity, unitPrice, totalCost, avgCost, product } = movement
if (info === null) {
info = {
date: movement.createdAt,
type: movement.type,
quantity: Number(movement.quantity),
fee: Number(movement.fee),
unitPrice: Number(movement.unitPrice),
totalCost: Number(movement.totalCost),
referenceType: movement.referenceType,
referenceId: movement.referenceId,
@@ -107,13 +107,13 @@ export class InventoriesService {
}
} else {
info.quantity += Number(movement.quantity)
info.fee += Number(movement.fee)
info.unitPrice += Number(movement.unitPrice)
info.totalCost += Number(movement.totalCost)
}
return {
id,
quantity: Number(quantity),
fee: Number(fee),
unitPrice: Number(unitPrice),
totalCost: Number(totalCost),
avgCost: Number(avgCost),
product,
+1 -1
View File
@@ -33,5 +33,5 @@ export class CreateOrderItemDto {
@ApiProperty()
@IsNumber()
@Type(() => Number)
fee: number
unitPrice: number
}
+6 -3
View File
@@ -170,7 +170,10 @@ export class PosService {
const newCode = String(Number(lastCode) + 1)
const totalAmount = data.items.reduce((acc, item) => acc + item.count * item.fee, 0)
const totalAmount = data.items.reduce(
(acc, item) => acc + item.count * item.unitPrice,
0,
)
const preparedOrder: SalesInvoiceCreateInput = {
...res,
@@ -182,8 +185,8 @@ export class PosService {
create: items.map(item => ({
product: { connect: { id: Number(item.productId) } },
count: item.count,
fee: item.fee,
total: item.count * item.fee,
unitPrice: item.unitPrice,
total: item.count * item.unitPrice,
})),
},
}
@@ -0,0 +1,17 @@
SELECT p.id
FROM Products p
WHERE (
SELECT COALESCE(SUM(sb.quantity), 0)
FROM Stock_Balance sb
WHERE
sb.productId = p.id
) < p.minimumStockAlertLevel
ORDER BY (
p.minimumStockAlertLevel - (
SELECT COALESCE(SUM(sb.quantity), 0)
FROM Stock_Balance sb
WHERE
sb.productId = p.id
)
) DESC
LIMIT 10
@@ -0,0 +1,27 @@
import { Controller, Get } from '@nestjs/common'
import { StatisticsService } from './statistics.service'
@Controller('statistics')
export class StatisticsController {
constructor(private readonly statisticsService: StatisticsService) {}
@Get('top-last-sales')
getTopLastSales() {
return this.statisticsService.getTopLastSales()
}
@Get('top-alert-stocks')
getTopAlertStocks() {
return this.statisticsService.getTopAlertStocks()
}
@Get('top-supplier-debts')
getTopSupplierDebts() {
return this.statisticsService.getTopSupplierDebts()
}
@Get('top-sales')
getTopSellProducts() {
return this.statisticsService.getTopSellProducts()
}
}
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common'
import { PrismaService } from '../../prisma/prisma.service'
import { StatisticsController } from './statistics.controller'
import { StatisticsService } from './statistics.service'
@Module({
controllers: [StatisticsController],
providers: [StatisticsService, PrismaService],
})
export class StatisticsModule {}
@@ -0,0 +1,184 @@
import { Injectable } from '@nestjs/common'
import * as fs from 'fs'
import * as path from 'path'
import { ResponseMapper } from '../../common/response/response-mapper'
import { PrismaService } from '../../prisma/prisma.service'
@Injectable()
export class StatisticsService {
constructor(private prisma: PrismaService) {}
private readSqlFile(filename: string): string {
const filePath = path.join(__dirname, 'queries', filename)
return fs.readFileSync(filePath, 'utf8')
}
async getTopLastSales() {
const sales = await this.prisma.salesInvoice.findMany({
take: 10,
orderBy: { createdAt: 'desc' },
include: {
customer: { select: { id: true, firstName: true, lastName: true } },
posAccount: { select: { id: true, name: true } },
items: {
include: {
product: { select: { id: true, name: true } },
},
},
},
})
const mapped = sales.map(sale => ({
...sale,
totalAmount: Number(sale.totalAmount),
itemsCount: sale.items.length,
}))
return ResponseMapper.list(mapped)
}
async getTopAlertStocks() {
const productIds = await this.prisma.$queryRaw<{ id: number }[]>`
SELECT p.id
FROM Products p
WHERE (SELECT COALESCE(SUM(sb.quantity), 0) FROM Stock_Balance sb WHERE sb.productId = p.id) < p.minimumStockAlertLevel
-- ORDER BY (p.minimumStockAlertLevel - (SELECT COALESCE(SUM(sb.quantity), 0) FROM stock_balance sb WHERE sb.productId = p.id)) DESC
LIMIT 10
`
const ids = productIds.map(p => p.id)
if (ids.length === 0) {
return ResponseMapper.list([])
}
const products = await this.prisma.product.findMany({
where: { id: { in: ids } },
select: {
id: true,
name: true,
sku: true,
minimumStockAlertLevel: true,
stockBalances: {
select: {
quantity: true,
inventory: { select: { id: true, name: true } },
},
},
category: { select: { id: true, name: true } },
brand: { select: { id: true, name: true } },
},
})
const mappedData = products.map(product => ({
...product,
stock: product.stockBalances.reduce((acc, sb) => acc + Number(sb.quantity), 0),
deficit:
Number(product.minimumStockAlertLevel) -
product.stockBalances.reduce((acc, sb) => acc + Number(sb.quantity), 0),
}))
return ResponseMapper.list(mappedData)
}
async getTopSupplierDebts() {
// Get unpaid receipts
const receipts = await this.prisma.purchaseReceipt.findMany({
where: {
status: { not: 'PAID' },
},
select: {
id: true,
totalAmount: true,
paidAmount: true,
supplierId: true,
},
})
const debtMap = new Map<
number,
{ supplierId: number; totalDebt: number; unpaidReceipts: number }
>()
for (const receipt of receipts) {
const debt = Number(receipt.totalAmount) - Number(receipt.paidAmount)
if (debt > 0) {
const existing = debtMap.get(receipt.supplierId)
if (existing) {
existing.totalDebt += debt
existing.unpaidReceipts += 1
} else {
debtMap.set(receipt.supplierId, {
supplierId: receipt.supplierId,
totalDebt: debt,
unpaidReceipts: 1,
})
}
}
}
const supplierIds = Array.from(debtMap.keys())
const suppliers = await this.prisma.supplier.findMany({
where: { id: { in: supplierIds } },
select: { id: true, firstName: true, lastName: true },
})
const supplierMap = new Map(
suppliers.map(s => [s.id, `${s.firstName} ${s.lastName}`.trim()]),
)
const mapped = Array.from(debtMap.values())
.map(item => ({
id: item.supplierId,
name: supplierMap.get(item.supplierId) || 'Unknown',
totalDebt: item.totalDebt,
unpaidReceipts: item.unpaidReceipts,
}))
.sort((a, b) => b.totalDebt - a.totalDebt)
.slice(0, 10)
return ResponseMapper.list(mapped)
}
async getTopSellProducts() {
const productData = await this.prisma.$queryRaw<any[]>`
SELECT p.id, p.name, p.sku, COALESCE(SUM(sii.count), 0) as totalSold
FROM Products p
LEFT JOIN Sales_Invoice_Items sii ON p.id = sii.productId
GROUP BY p.id
HAVING totalSold > 0
ORDER BY totalSold DESC
LIMIT 10
`
const ids = productData.map(p => p.id)
if (ids.length === 0) {
return ResponseMapper.list([])
}
const products = await this.prisma.product.findMany({
where: { id: { in: ids } },
select: {
id: true,
category: { select: { id: true, name: true } },
brand: { select: { id: true, name: true } },
},
})
const productMap = new Map(
products.map(p => [p.id, { category: p.category, brand: p.brand }]),
)
const mapped = productData.map(product => ({
id: product.id,
name: product.name,
sku: product.sku,
totalSold: Number(product.totalSold),
category: productMap.get(product.id)?.category,
brand: productMap.get(product.id)?.brand,
}))
return ResponseMapper.list(mapped)
}
}
@@ -23,8 +23,8 @@ export class SupplierInvoicesService {
select: {
id: true,
count: true,
fee: true,
total: true,
unitPrice: true,
totalAmount: true,
product: {
select: {
id: true,
@@ -149,6 +149,8 @@ export class SupplierInvoicesService {
async createPayment(invoiceId: number, data: CreateReceiptPaymentDto) {
const { bankAccountId, ...rest } = data
console.log(data)
const payment = await this.prisma.purchaseReceiptPayments.create({
data: {
...rest,