All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 1m49s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 43s
Deploy to Staging / Verify Staging (pull_request) Successful in 4s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 3s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
node-postgres returns numeric/decimal columns as JavaScript strings, but the TypeScript interfaces for MaintenanceRecord and OwnershipCost declare numeric fields as number. The mappers were passing values through raw, breaking type-safe arithmetic and display (e.g. the amount column on the vehicle summary screen was empty until the recent frontend workaround in PR #240, and OwnershipCostsList silently no-ops toLocaleString on the string). Backend - mapMaintenanceRecord: coerce cost via Number() when non-null. - ownership-costs mapRow: coerce amount via Number(). Frontend (remove now-redundant workarounds) - MaintenanceRecordsList: drop Number() coercion on cost and odometerReading; use the number values directly. - VehicleDetailPage / VehicleDetailMobile: revert the PR #240 cost coercion to the simple typeof number guard now that the backend honors the type. Scope notes - Other repositories with the same pattern (stations, community-stations, fuel-logs enhanced methods) are tracked separately because they have unclear downstream consumers and warrant their own investigation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
4.9 KiB
TypeScript
162 lines
4.9 KiB
TypeScript
import { Pool } from 'pg';
|
|
import pool from '../../../core/config/database';
|
|
import type { OwnershipCost, OwnershipCostType } from '../domain/ownership-costs.types';
|
|
|
|
export class OwnershipCostsRepository {
|
|
constructor(private readonly db: Pool = pool) {}
|
|
|
|
// ========================
|
|
// Row Mappers
|
|
// ========================
|
|
|
|
private mapRow(row: any): OwnershipCost {
|
|
return {
|
|
id: row.id,
|
|
userId: row.user_id,
|
|
vehicleId: row.vehicle_id,
|
|
documentId: row.document_id,
|
|
costType: row.cost_type,
|
|
// node-postgres returns numeric/decimal columns as strings; coerce to honor the number type.
|
|
amount: Number(row.amount),
|
|
description: row.description,
|
|
periodStart: row.period_start,
|
|
periodEnd: row.period_end,
|
|
notes: row.notes,
|
|
createdAt: row.created_at,
|
|
updatedAt: row.updated_at
|
|
};
|
|
}
|
|
|
|
// ========================
|
|
// CRUD Operations
|
|
// ========================
|
|
|
|
async insert(cost: {
|
|
id: string;
|
|
userId: string;
|
|
vehicleId: string;
|
|
documentId?: string | null;
|
|
costType: OwnershipCostType;
|
|
amount: number;
|
|
description?: string | null;
|
|
periodStart?: string | null;
|
|
periodEnd?: string | null;
|
|
notes?: string | null;
|
|
}): Promise<OwnershipCost> {
|
|
const res = await this.db.query(
|
|
`INSERT INTO ownership_costs (
|
|
id, user_id, vehicle_id, document_id, cost_type, amount, description, period_start, period_end, notes
|
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
RETURNING *`,
|
|
[
|
|
cost.id,
|
|
cost.userId,
|
|
cost.vehicleId,
|
|
cost.documentId ?? null,
|
|
cost.costType,
|
|
cost.amount,
|
|
cost.description ?? null,
|
|
cost.periodStart ?? null,
|
|
cost.periodEnd ?? null,
|
|
cost.notes ?? null,
|
|
]
|
|
);
|
|
return this.mapRow(res.rows[0]);
|
|
}
|
|
|
|
async findById(id: string, userId: string): Promise<OwnershipCost | null> {
|
|
const res = await this.db.query(
|
|
`SELECT * FROM ownership_costs WHERE id = $1 AND user_id = $2`,
|
|
[id, userId]
|
|
);
|
|
return res.rows[0] ? this.mapRow(res.rows[0]) : null;
|
|
}
|
|
|
|
async findByUserId(
|
|
userId: string,
|
|
filters?: { vehicleId?: string; costType?: OwnershipCostType; documentId?: string }
|
|
): Promise<OwnershipCost[]> {
|
|
const conds: string[] = ['user_id = $1'];
|
|
const params: any[] = [userId];
|
|
let i = 2;
|
|
|
|
if (filters?.vehicleId) {
|
|
conds.push(`vehicle_id = $${i++}`);
|
|
params.push(filters.vehicleId);
|
|
}
|
|
if (filters?.costType) {
|
|
conds.push(`cost_type = $${i++}`);
|
|
params.push(filters.costType);
|
|
}
|
|
if (filters?.documentId) {
|
|
conds.push(`document_id = $${i++}`);
|
|
params.push(filters.documentId);
|
|
}
|
|
|
|
const sql = `SELECT * FROM ownership_costs WHERE ${conds.join(' AND ')} ORDER BY period_start DESC, created_at DESC`;
|
|
const res = await this.db.query(sql, params);
|
|
return res.rows.map(row => this.mapRow(row));
|
|
}
|
|
|
|
async findByVehicleId(vehicleId: string, userId: string): Promise<OwnershipCost[]> {
|
|
const res = await this.db.query(
|
|
`SELECT * FROM ownership_costs WHERE vehicle_id = $1 AND user_id = $2 ORDER BY period_start DESC, created_at DESC`,
|
|
[vehicleId, userId]
|
|
);
|
|
return res.rows.map(row => this.mapRow(row));
|
|
}
|
|
|
|
async update(
|
|
id: string,
|
|
userId: string,
|
|
patch: Partial<Pick<OwnershipCost, 'documentId' | 'costType' | 'amount' | 'description' | 'periodStart' | 'periodEnd' | 'notes'>>
|
|
): Promise<OwnershipCost | null> {
|
|
const fields: string[] = [];
|
|
const params: any[] = [];
|
|
let i = 1;
|
|
|
|
if (patch.documentId !== undefined) {
|
|
fields.push(`document_id = $${i++}`);
|
|
params.push(patch.documentId);
|
|
}
|
|
if (patch.costType !== undefined) {
|
|
fields.push(`cost_type = $${i++}`);
|
|
params.push(patch.costType);
|
|
}
|
|
if (patch.amount !== undefined) {
|
|
fields.push(`amount = $${i++}`);
|
|
params.push(patch.amount);
|
|
}
|
|
if (patch.description !== undefined) {
|
|
fields.push(`description = $${i++}`);
|
|
params.push(patch.description);
|
|
}
|
|
if (patch.periodStart !== undefined) {
|
|
fields.push(`period_start = $${i++}`);
|
|
params.push(patch.periodStart);
|
|
}
|
|
if (patch.periodEnd !== undefined) {
|
|
fields.push(`period_end = $${i++}`);
|
|
params.push(patch.periodEnd);
|
|
}
|
|
if (patch.notes !== undefined) {
|
|
fields.push(`notes = $${i++}`);
|
|
params.push(patch.notes);
|
|
}
|
|
|
|
if (!fields.length) return this.findById(id, userId);
|
|
|
|
params.push(id, userId);
|
|
const sql = `UPDATE ownership_costs SET ${fields.join(', ')} WHERE id = $${i++} AND user_id = $${i++} RETURNING *`;
|
|
const res = await this.db.query(sql, params);
|
|
return res.rows[0] ? this.mapRow(res.rows[0]) : null;
|
|
}
|
|
|
|
async delete(id: string, userId: string): Promise<void> {
|
|
await this.db.query(
|
|
`DELETE FROM ownership_costs WHERE id = $1 AND user_id = $2`,
|
|
[id, userId]
|
|
);
|
|
}
|
|
}
|