458 lines
17 KiB
TypeScript
458 lines
17 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import {
|
|
getFinancialSummary,
|
|
getMonthlyFinancialData,
|
|
getIncomeByMaintenanceType,
|
|
getExpenseBreakdown,
|
|
getTopCustomersByRevenue,
|
|
getFinancialTrends
|
|
} from '../financial-reporting.server';
|
|
import { prisma } from '../db.server';
|
|
|
|
describe('Financial Reporting', () => {
|
|
beforeEach(async () => {
|
|
// Clean up test data
|
|
await prisma.income.deleteMany();
|
|
await prisma.expense.deleteMany();
|
|
await prisma.maintenanceVisit.deleteMany();
|
|
await prisma.vehicle.deleteMany();
|
|
await prisma.customer.deleteMany();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up test data
|
|
await prisma.income.deleteMany();
|
|
await prisma.expense.deleteMany();
|
|
await prisma.maintenanceVisit.deleteMany();
|
|
await prisma.vehicle.deleteMany();
|
|
await prisma.customer.deleteMany();
|
|
});
|
|
|
|
describe('getFinancialSummary', () => {
|
|
beforeEach(async () => {
|
|
// Create test customer
|
|
const customer = await prisma.customer.create({
|
|
data: {
|
|
name: 'عميل تجريبي',
|
|
phone: '0501234567',
|
|
},
|
|
});
|
|
|
|
// Create test vehicle
|
|
const vehicle = await prisma.vehicle.create({
|
|
data: {
|
|
plateNumber: 'ABC-123',
|
|
bodyType: 'سيدان',
|
|
manufacturer: 'تويوتا',
|
|
model: 'كامري',
|
|
year: 2020,
|
|
transmission: 'Automatic',
|
|
fuel: 'Gasoline',
|
|
useType: 'personal',
|
|
ownerId: customer.id,
|
|
},
|
|
});
|
|
|
|
// Create test maintenance visit
|
|
const visit = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle.id,
|
|
customerId: customer.id,
|
|
maintenanceType: 'تغيير زيت',
|
|
description: 'تغيير زيت المحرك',
|
|
cost: 200.00,
|
|
kilometers: 50000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
// Create test income
|
|
await prisma.income.create({
|
|
data: {
|
|
maintenanceVisitId: visit.id,
|
|
amount: 200.00,
|
|
incomeDate: new Date('2024-01-15'),
|
|
},
|
|
});
|
|
|
|
// Create test expenses
|
|
await prisma.expense.createMany({
|
|
data: [
|
|
{
|
|
description: 'قطع غيار',
|
|
category: 'قطع غيار',
|
|
amount: 50.00,
|
|
expenseDate: new Date('2024-01-10'),
|
|
},
|
|
{
|
|
description: 'أدوات',
|
|
category: 'أدوات',
|
|
amount: 30.00,
|
|
expenseDate: new Date('2024-01-12'),
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should calculate financial summary correctly', async () => {
|
|
const summary = await getFinancialSummary();
|
|
|
|
expect(summary.totalIncome).toBe(200.00);
|
|
expect(summary.totalExpenses).toBe(80.00);
|
|
expect(summary.netProfit).toBe(120.00);
|
|
expect(summary.incomeCount).toBe(1);
|
|
expect(summary.expenseCount).toBe(2);
|
|
expect(summary.profitMargin).toBe(60.0); // (120/200) * 100
|
|
});
|
|
|
|
it('should filter by date range', async () => {
|
|
const dateFrom = new Date('2024-01-11');
|
|
const dateTo = new Date('2024-01-20');
|
|
|
|
const summary = await getFinancialSummary(dateFrom, dateTo);
|
|
|
|
expect(summary.totalIncome).toBe(200.00);
|
|
expect(summary.totalExpenses).toBe(30.00); // Only one expense in range
|
|
expect(summary.netProfit).toBe(170.00);
|
|
});
|
|
});
|
|
|
|
describe('getIncomeByMaintenanceType', () => {
|
|
beforeEach(async () => {
|
|
// Create test data
|
|
const customer = await prisma.customer.create({
|
|
data: { name: 'عميل تجريبي', phone: '0501234567' },
|
|
});
|
|
|
|
const vehicle = await prisma.vehicle.create({
|
|
data: {
|
|
plateNumber: 'ABC-123',
|
|
bodyType: 'سيدان',
|
|
manufacturer: 'تويوتا',
|
|
model: 'كامري',
|
|
year: 2020,
|
|
transmission: 'Automatic',
|
|
fuel: 'Gasoline',
|
|
useType: 'personal',
|
|
ownerId: customer.id,
|
|
},
|
|
});
|
|
|
|
// Create maintenance visits with different types
|
|
const visit1 = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle.id,
|
|
customerId: customer.id,
|
|
maintenanceType: 'تغيير زيت',
|
|
description: 'تغيير زيت المحرك',
|
|
cost: 200.00,
|
|
kilometers: 50000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
const visit2 = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle.id,
|
|
customerId: customer.id,
|
|
maintenanceType: 'فحص دوري',
|
|
description: 'فحص دوري شامل',
|
|
cost: 150.00,
|
|
kilometers: 51000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
const visit3 = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle.id,
|
|
customerId: customer.id,
|
|
maintenanceType: 'تغيير زيت',
|
|
description: 'تغيير زيت مرة أخرى',
|
|
cost: 180.00,
|
|
kilometers: 52000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
// Create corresponding income records
|
|
await prisma.income.createMany({
|
|
data: [
|
|
{ maintenanceVisitId: visit1.id, amount: 200.00 },
|
|
{ maintenanceVisitId: visit2.id, amount: 150.00 },
|
|
{ maintenanceVisitId: visit3.id, amount: 180.00 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should group income by maintenance type', async () => {
|
|
const result = await getIncomeByMaintenanceType();
|
|
|
|
expect(result).toHaveLength(2);
|
|
|
|
const oilChange = result.find(r => r.category === 'تغيير زيت');
|
|
expect(oilChange?.amount).toBe(380.00);
|
|
expect(oilChange?.count).toBe(2);
|
|
expect(oilChange?.percentage).toBeCloseTo(71.7, 1); // 380/530 * 100
|
|
|
|
const inspection = result.find(r => r.category === 'فحص دوري');
|
|
expect(inspection?.amount).toBe(150.00);
|
|
expect(inspection?.count).toBe(1);
|
|
expect(inspection?.percentage).toBeCloseTo(28.3, 1); // 150/530 * 100
|
|
});
|
|
});
|
|
|
|
describe('getExpenseBreakdown', () => {
|
|
beforeEach(async () => {
|
|
await prisma.expense.createMany({
|
|
data: [
|
|
{ description: 'قطع غيار 1', category: 'قطع غيار', amount: 100 },
|
|
{ description: 'قطع غيار 2', category: 'قطع غيار', amount: 150 },
|
|
{ description: 'أدوات 1', category: 'أدوات', amount: 200 },
|
|
{ description: 'إيجار', category: 'إيجار', amount: 3000 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should group expenses by category with percentages', async () => {
|
|
const result = await getExpenseBreakdown();
|
|
|
|
expect(result).toHaveLength(3);
|
|
|
|
const rent = result.find(r => r.category === 'إيجار');
|
|
expect(rent?.amount).toBe(3000);
|
|
expect(rent?.count).toBe(1);
|
|
expect(rent?.percentage).toBeCloseTo(86.96, 1); // 3000/3450 * 100
|
|
|
|
const spareParts = result.find(r => r.category === 'قطع غيار');
|
|
expect(spareParts?.amount).toBe(250);
|
|
expect(spareParts?.count).toBe(2);
|
|
expect(spareParts?.percentage).toBeCloseTo(7.25, 1); // 250/3450 * 100
|
|
|
|
const tools = result.find(r => r.category === 'أدوات');
|
|
expect(tools?.amount).toBe(200);
|
|
expect(tools?.count).toBe(1);
|
|
expect(tools?.percentage).toBeCloseTo(5.80, 1); // 200/3450 * 100
|
|
});
|
|
});
|
|
|
|
describe('getTopCustomersByRevenue', () => {
|
|
beforeEach(async () => {
|
|
// Create test customers
|
|
const customer1 = await prisma.customer.create({
|
|
data: { name: 'عميل أول', phone: '0501111111' },
|
|
});
|
|
|
|
const customer2 = await prisma.customer.create({
|
|
data: { name: 'عميل ثاني', phone: '0502222222' },
|
|
});
|
|
|
|
// Create vehicles
|
|
const vehicle1 = await prisma.vehicle.create({
|
|
data: {
|
|
plateNumber: 'ABC-111',
|
|
bodyType: 'سيدان',
|
|
manufacturer: 'تويوتا',
|
|
model: 'كامري',
|
|
year: 2020,
|
|
transmission: 'Automatic',
|
|
fuel: 'Gasoline',
|
|
useType: 'personal',
|
|
ownerId: customer1.id,
|
|
},
|
|
});
|
|
|
|
const vehicle2 = await prisma.vehicle.create({
|
|
data: {
|
|
plateNumber: 'ABC-222',
|
|
bodyType: 'SUV',
|
|
manufacturer: 'هيونداي',
|
|
model: 'توسان',
|
|
year: 2021,
|
|
transmission: 'Automatic',
|
|
fuel: 'Gasoline',
|
|
useType: 'personal',
|
|
ownerId: customer2.id,
|
|
},
|
|
});
|
|
|
|
// Create maintenance visits
|
|
const visit1 = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle1.id,
|
|
customerId: customer1.id,
|
|
maintenanceType: 'تغيير زيت',
|
|
description: 'تغيير زيت',
|
|
cost: 300.00,
|
|
kilometers: 50000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
const visit2 = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle1.id,
|
|
customerId: customer1.id,
|
|
maintenanceType: 'فحص دوري',
|
|
description: 'فحص دوري',
|
|
cost: 200.00,
|
|
kilometers: 51000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
const visit3 = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle2.id,
|
|
customerId: customer2.id,
|
|
maintenanceType: 'تغيير زيت',
|
|
description: 'تغيير زيت',
|
|
cost: 150.00,
|
|
kilometers: 30000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
// Create income records
|
|
await prisma.income.createMany({
|
|
data: [
|
|
{ maintenanceVisitId: visit1.id, amount: 300.00 },
|
|
{ maintenanceVisitId: visit2.id, amount: 200.00 },
|
|
{ maintenanceVisitId: visit3.id, amount: 150.00 },
|
|
],
|
|
});
|
|
});
|
|
|
|
it('should return top customers by revenue', async () => {
|
|
const result = await getTopCustomersByRevenue(10);
|
|
|
|
expect(result).toHaveLength(2);
|
|
|
|
// Should be sorted by revenue descending
|
|
expect(result[0].customerName).toBe('عميل أول');
|
|
expect(result[0].totalRevenue).toBe(500.00);
|
|
expect(result[0].visitCount).toBe(2);
|
|
|
|
expect(result[1].customerName).toBe('عميل ثاني');
|
|
expect(result[1].totalRevenue).toBe(150.00);
|
|
expect(result[1].visitCount).toBe(1);
|
|
});
|
|
|
|
it('should limit results correctly', async () => {
|
|
const result = await getTopCustomersByRevenue(1);
|
|
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].customerName).toBe('عميل أول');
|
|
});
|
|
});
|
|
|
|
describe('getFinancialTrends', () => {
|
|
beforeEach(async () => {
|
|
// Create test customer and vehicle
|
|
const customer = await prisma.customer.create({
|
|
data: { name: 'عميل تجريبي', phone: '0501234567' },
|
|
});
|
|
|
|
const vehicle = await prisma.vehicle.create({
|
|
data: {
|
|
plateNumber: 'ABC-123',
|
|
bodyType: 'سيدان',
|
|
manufacturer: 'تويوتا',
|
|
model: 'كامري',
|
|
year: 2020,
|
|
transmission: 'Automatic',
|
|
fuel: 'Gasoline',
|
|
useType: 'personal',
|
|
ownerId: customer.id,
|
|
},
|
|
});
|
|
|
|
// Create maintenance visits for different periods
|
|
const currentVisit = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle.id,
|
|
customerId: customer.id,
|
|
maintenanceType: 'تغيير زيت',
|
|
description: 'تغيير زيت حالي',
|
|
cost: 300.00,
|
|
kilometers: 50000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
const previousVisit = await prisma.maintenanceVisit.create({
|
|
data: {
|
|
vehicleId: vehicle.id,
|
|
customerId: customer.id,
|
|
maintenanceType: 'تغيير زيت',
|
|
description: 'تغيير زيت سابق',
|
|
cost: 200.00,
|
|
kilometers: 45000,
|
|
nextVisitDelay: 3,
|
|
},
|
|
});
|
|
|
|
// Create income for current period (Jan 2024)
|
|
await prisma.income.create({
|
|
data: {
|
|
maintenanceVisitId: currentVisit.id,
|
|
amount: 300.00,
|
|
incomeDate: new Date('2024-01-15'),
|
|
},
|
|
});
|
|
|
|
// Create income for previous period (Dec 2023)
|
|
await prisma.income.create({
|
|
data: {
|
|
maintenanceVisitId: previousVisit.id,
|
|
amount: 200.00,
|
|
incomeDate: new Date('2023-12-15'),
|
|
},
|
|
});
|
|
|
|
// Create expenses for current period
|
|
await prisma.expense.create({
|
|
data: {
|
|
description: 'مصروف حالي',
|
|
category: 'قطع غيار',
|
|
amount: 100.00,
|
|
expenseDate: new Date('2024-01-10'),
|
|
},
|
|
});
|
|
|
|
// Create expenses for previous period
|
|
await prisma.expense.create({
|
|
data: {
|
|
description: 'مصروف سابق',
|
|
category: 'قطع غيار',
|
|
amount: 80.00,
|
|
expenseDate: new Date('2023-12-10'),
|
|
},
|
|
});
|
|
});
|
|
|
|
it('should calculate financial trends correctly', async () => {
|
|
const dateFrom = new Date('2024-01-01');
|
|
const dateTo = new Date('2024-01-31');
|
|
|
|
const result = await getFinancialTrends(dateFrom, dateTo);
|
|
|
|
expect(result.currentPeriod.totalIncome).toBe(300.00);
|
|
expect(result.currentPeriod.totalExpenses).toBe(100.00);
|
|
expect(result.currentPeriod.netProfit).toBe(200.00);
|
|
|
|
expect(result.previousPeriod.totalIncome).toBe(200.00);
|
|
expect(result.previousPeriod.totalExpenses).toBe(80.00);
|
|
expect(result.previousPeriod.netProfit).toBe(120.00);
|
|
|
|
// Income growth: (300-200)/200 * 100 = 50%
|
|
expect(result.trends.incomeGrowth).toBeCloseTo(50.0, 1);
|
|
|
|
// Expense growth: (100-80)/80 * 100 = 25%
|
|
expect(result.trends.expenseGrowth).toBeCloseTo(25.0, 1);
|
|
|
|
// Profit growth: (200-120)/120 * 100 = 66.67%
|
|
expect(result.trends.profitGrowth).toBeCloseTo(66.67, 1);
|
|
});
|
|
});
|
|
}); |