refactor: Link ownership-costs to documents feature (#29) #30
@@ -1,15 +1,18 @@
|
||||
import { randomUUID } from 'crypto';
|
||||
import type { CreateDocumentBody, DocumentRecord, DocumentType, UpdateDocumentBody } from './documents.types';
|
||||
import { DocumentsRepository } from '../data/documents.repository';
|
||||
import { OwnershipCostsService } from '../../ownership-costs/domain/ownership-costs.service';
|
||||
import type { OwnershipCostType } from '../../ownership-costs/domain/ownership-costs.types';
|
||||
import pool from '../../../core/config/database';
|
||||
|
||||
export class DocumentsService {
|
||||
private readonly repo = new DocumentsRepository(pool);
|
||||
private readonly ownershipCostsService = new OwnershipCostsService(pool);
|
||||
|
||||
async createDocument(userId: string, body: CreateDocumentBody): Promise<DocumentRecord> {
|
||||
await this.assertVehicleOwnership(userId, body.vehicleId);
|
||||
const id = randomUUID();
|
||||
return this.repo.insert({
|
||||
const doc = await this.repo.insert({
|
||||
id,
|
||||
userId,
|
||||
vehicleId: body.vehicleId,
|
||||
@@ -22,6 +25,70 @@ export class DocumentsService {
|
||||
emailNotifications: body.emailNotifications ?? false,
|
||||
scanForMaintenance: body.scanForMaintenance ?? false,
|
||||
});
|
||||
|
||||
// Auto-create ownership_cost when insurance/registration has cost data
|
||||
await this.autoCreateOwnershipCost(userId, doc, body);
|
||||
|
||||
return doc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-creates an ownership_cost record when an insurance or registration
|
||||
* document is created with cost data (premium or cost field in details).
|
||||
*/
|
||||
private async autoCreateOwnershipCost(
|
||||
userId: string,
|
||||
doc: DocumentRecord,
|
||||
body: CreateDocumentBody
|
||||
): Promise<void> {
|
||||
const costType = this.mapDocumentTypeToCostType(body.documentType);
|
||||
if (!costType) return; // Not a cost-linkable document type
|
||||
|
||||
const costAmount = this.extractCostAmount(body);
|
||||
if (!costAmount || costAmount <= 0) return; // No valid cost data
|
||||
|
||||
try {
|
||||
await this.ownershipCostsService.createCost(userId, {
|
||||
vehicleId: body.vehicleId,
|
||||
documentId: doc.id,
|
||||
costType,
|
||||
amount: costAmount,
|
||||
description: doc.title,
|
||||
periodStart: body.issuedDate,
|
||||
periodEnd: body.expirationDate,
|
||||
});
|
||||
} catch (err) {
|
||||
// Log but don't fail document creation if cost creation fails
|
||||
console.error('Failed to auto-create ownership cost for document:', doc.id, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps document types to ownership cost types.
|
||||
* Returns null for document types that don't auto-create costs.
|
||||
*/
|
||||
private mapDocumentTypeToCostType(documentType: string): OwnershipCostType | null {
|
||||
const typeMap: Record<string, OwnershipCostType> = {
|
||||
'insurance': 'insurance',
|
||||
'registration': 'registration',
|
||||
};
|
||||
return typeMap[documentType] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts cost amount from document details.
|
||||
* Insurance uses 'premium', registration uses 'cost'.
|
||||
*/
|
||||
private extractCostAmount(body: CreateDocumentBody): number | null {
|
||||
if (!body.details) return null;
|
||||
|
||||
const premium = body.details.premium;
|
||||
const cost = body.details.cost;
|
||||
|
||||
if (typeof premium === 'number' && premium > 0) return premium;
|
||||
if (typeof cost === 'number' && cost > 0) return cost;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDocument(userId: string, id: string): Promise<DocumentRecord | null> {
|
||||
@@ -36,12 +103,78 @@ export class DocumentsService {
|
||||
const existing = await this.repo.findById(id, userId);
|
||||
if (!existing) return null;
|
||||
if (patch && typeof patch === 'object') {
|
||||
return this.repo.updateMetadata(id, userId, patch as any);
|
||||
const updated = await this.repo.updateMetadata(id, userId, patch as any);
|
||||
|
||||
// Sync cost changes to linked ownership_cost if applicable
|
||||
if (updated && patch.details) {
|
||||
await this.syncOwnershipCost(userId, updated, patch);
|
||||
}
|
||||
|
||||
return updated;
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs cost data changes to linked ownership_cost record.
|
||||
* If document has linked cost and details.premium/cost changed, update it.
|
||||
*/
|
||||
private async syncOwnershipCost(
|
||||
userId: string,
|
||||
doc: DocumentRecord,
|
||||
patch: UpdateDocumentBody
|
||||
): Promise<void> {
|
||||
const costType = this.mapDocumentTypeToCostType(doc.documentType);
|
||||
if (!costType) return;
|
||||
|
||||
const newCostAmount = this.extractCostAmountFromDetails(patch.details);
|
||||
if (newCostAmount === null) return; // No cost in update
|
||||
|
||||
try {
|
||||
// Find existing linked cost
|
||||
const linkedCosts = await this.ownershipCostsService.getCosts(userId, { documentId: doc.id });
|
||||
|
||||
if (linkedCosts.length > 0 && newCostAmount > 0) {
|
||||
// Update existing linked cost
|
||||
await this.ownershipCostsService.updateCost(userId, linkedCosts[0].id, {
|
||||
amount: newCostAmount,
|
||||
periodStart: patch.issuedDate ?? undefined,
|
||||
periodEnd: patch.expirationDate ?? undefined,
|
||||
});
|
||||
} else if (linkedCosts.length === 0 && newCostAmount > 0) {
|
||||
// Create new cost if none exists
|
||||
await this.ownershipCostsService.createCost(userId, {
|
||||
vehicleId: doc.vehicleId,
|
||||
documentId: doc.id,
|
||||
costType,
|
||||
amount: newCostAmount,
|
||||
description: doc.title,
|
||||
periodStart: patch.issuedDate ?? doc.issuedDate ?? undefined,
|
||||
periodEnd: patch.expirationDate ?? doc.expirationDate ?? undefined,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to sync ownership cost for document:', doc.id, err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts cost amount from details object (for updates).
|
||||
*/
|
||||
private extractCostAmountFromDetails(details?: Record<string, any> | null): number | null {
|
||||
if (!details) return null;
|
||||
|
||||
const premium = details.premium;
|
||||
const cost = details.cost;
|
||||
|
||||
if (typeof premium === 'number') return premium;
|
||||
if (typeof cost === 'number') return cost;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async deleteDocument(userId: string, id: string): Promise<void> {
|
||||
// Note: Linked ownership_cost records are CASCADE deleted via FK
|
||||
await this.repo.softDelete(id, userId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user