Refactor liability schemas and enhance asset/invoice repositories

- Updated LiabilityController and LiabilityService to consolidate liability properties into a single 'balance' field for improved clarity.
- Added new methods in AssetRepository to retrieve assets grouped by type.
- Introduced a method in InvoiceRepository to calculate user-specific invoice statistics, including total, paid, outstanding, and overdue invoices.
- Adjusted DashboardService to reflect changes in asset value calculations.
This commit is contained in:
2025-12-11 02:22:48 -05:00
parent df2cf418ea
commit 51074d02a9
5 changed files with 56 additions and 48 deletions

View File

@@ -6,29 +6,13 @@ import {z} from 'zod';
const createLiabilitySchema = z.object({ const createLiabilitySchema = z.object({
name: z.string().min(1).max(255), name: z.string().min(1).max(255),
type: z.string().min(1), type: z.string().min(1),
currentBalance: z.number().min(0), balance: z.number().min(0)
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
creditor: z.string().max(255).optional(),
notes: z.string().optional()
}); });
const updateLiabilitySchema = z.object({ const updateLiabilitySchema = z.object({
name: z.string().min(1).max(255).optional(), name: z.string().min(1).max(255).optional(),
type: z.string().min(1).optional(), type: z.string().min(1).optional(),
currentBalance: z.number().min(0).optional(), balance: z.number().min(0).optional()
interestRate: z.number().min(0).max(100).optional(),
minimumPayment: z.number().min(0).optional(),
dueDate: z
.string()
.transform(str => new Date(str))
.optional(),
creditor: z.string().max(255).optional(),
notes: z.string().optional()
}); });
/** /**

View File

@@ -46,4 +46,24 @@ export class AssetRepository {
return result._sum.value || 0; return result._sum.value || 0;
} }
/**
* Get assets grouped by type
*/
async getByType(userId: string): Promise<Record<string, Asset[]>> {
const assets = await this.findAllByUser(userId);
return assets.reduce(
(acc, asset) => {
const type = asset.type;
if (!acc[type]) {
acc[type] = [];
}
acc[type].push(asset);
return acc;
},
{} as Record<string, Asset[]>
);
}
} }

View File

@@ -76,4 +76,31 @@ export class InvoiceRepository implements IUserScopedRepository<Invoice> {
return `INV-${year}-${String(count + 1).padStart(3, '0')}`; return `INV-${year}-${String(count + 1).padStart(3, '0')}`;
} }
/**
* Get invoice statistics for a user
*/
async getStats(userId: string): Promise<{
totalInvoices: number;
paidInvoices: number;
outstandingAmount: number;
overdueInvoices: number;
}> {
const invoices = await prisma.invoice.findMany({
where: {userId},
select: {status: true, total: true, dueDate: true}
});
const now = new Date();
const totalInvoices = invoices.length;
const paidInvoices = invoices.filter(inv => inv.status === 'paid').length;
const outstandingAmount = invoices
.filter(inv => inv.status !== 'paid' && inv.status !== 'cancelled')
.reduce((sum, inv) => sum + inv.total, 0);
const overdueInvoices = invoices.filter(
inv => inv.status !== 'paid' && inv.status !== 'cancelled' && inv.dueDate < now
).length;
return {totalInvoices, paidInvoices, outstandingAmount, overdueInvoices};
}
} }

View File

@@ -55,7 +55,7 @@ export class DashboardService {
const assetAllocation = Object.entries(assetsByType).map(([type, assets]) => ({ const assetAllocation = Object.entries(assetsByType).map(([type, assets]) => ({
type, type,
count: assets.length, count: assets.length,
totalValue: assets.reduce((sum, asset) => sum + asset.currentValue, 0) totalValue: assets.reduce((sum, asset) => sum + asset.value, 0)
})); }));
return { return {

View File

@@ -5,23 +5,13 @@ import {NotFoundError, ValidationError, ForbiddenError} from '../utils/errors';
export interface CreateLiabilityDTO { export interface CreateLiabilityDTO {
name: string; name: string;
type: string; type: string;
currentBalance: number; balance: number;
interestRate?: number;
minimumPayment?: number;
dueDate?: Date;
creditor?: string;
notes?: string;
} }
export interface UpdateLiabilityDTO { export interface UpdateLiabilityDTO {
name?: string; name?: string;
type?: string; type?: string;
currentBalance?: number; balance?: number;
interestRate?: number;
minimumPayment?: number;
dueDate?: Date;
creditor?: string;
notes?: string;
} }
/** /**
@@ -41,12 +31,7 @@ export class LiabilityService {
return this.liabilityRepository.create({ return this.liabilityRepository.create({
name: data.name, name: data.name,
type: data.type, type: data.type,
currentBalance: data.currentBalance, balance: data.balance,
interestRate: data.interestRate,
minimumPayment: data.minimumPayment,
dueDate: data.dueDate,
creditor: data.creditor,
notes: data.notes,
user: { user: {
connect: {id: userId} connect: {id: userId}
} }
@@ -85,7 +70,7 @@ export class LiabilityService {
// Verify ownership // Verify ownership
await this.getById(id, userId); await this.getById(id, userId);
if (data.currentBalance !== undefined || data.interestRate !== undefined || data.minimumPayment !== undefined) { if (data.balance !== undefined) {
this.validateLiabilityData(data as CreateLiabilityDTO); this.validateLiabilityData(data as CreateLiabilityDTO);
} }
@@ -120,16 +105,8 @@ export class LiabilityService {
* Validate liability data * Validate liability data
*/ */
private validateLiabilityData(data: CreateLiabilityDTO | UpdateLiabilityDTO): void { private validateLiabilityData(data: CreateLiabilityDTO | UpdateLiabilityDTO): void {
if (data.currentBalance !== undefined && data.currentBalance < 0) { if (data.balance !== undefined && data.balance < 0) {
throw new ValidationError('Current balance cannot be negative'); throw new ValidationError('Balance cannot be negative');
}
if (data.interestRate !== undefined && (data.interestRate < 0 || data.interestRate > 100)) {
throw new ValidationError('Interest rate must be between 0 and 100');
}
if (data.minimumPayment !== undefined && data.minimumPayment < 0) {
throw new ValidationError('Minimum payment cannot be negative');
} }
} }
} }