uup
This commit is contained in:
458
app/lib/__tests__/financial-reporting.test.ts
Normal file
458
app/lib/__tests__/financial-reporting.test.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user