DATGRAL.NOMBRE is blank on 266 legacy rows (140 utilities, 126 insurance), which surfaced in the UI as 257 customers literally named "(SIN NOMBRE)". The blank is real — those cells are empty in the Access files, not lost in extraction — but the rows mostly are not junk: 176 of the 257 carry a property, a policy, or transactions. The old PHP importer handled this by skipping blank-name rows outright (jorgecuadros-intra-webapp/src/tools/customerAdapter.php:47,81). That was worse than it looks: every other adapter resolved its customer FK through the customer_mapping table those skipped rows never entered, so their properties and policies were silently dropped (customerServiceAdapter.php:45) and their transactions were written against customer_id 0 (customerBalanceAdapter.php:52). So: recover the name instead of skipping. Names come from the secondary tables that still carry them, most trustworthy first — UTILSEG (the office's own hand-maintained name <-> id cross-reference spanning both lines), then the billing runs (IVA 2015, COBRO3) and the policy rows' NOMBRE ASEG (MULT, M EMPR, INCENDIO). A linked customer can also borrow the name its insurance record resolved to. Result: 213 of 257 recovered, 44 still genuinely nameless anywhere in the source. customers.nameSource records which table each recovered name came from, so a reconstructed name is never mistaken for one that was really on the record — the list tags it "nombre recuperado", the detail header names the source, and a still-unnamed customer renders muted italic instead of as a normal name. Also fixes run_all.py: transform_properties and transform_policies truncate service_documents/policy_documents, but blob_extract.py was not in the step list, so a full re-run left the uploaded MinIO objects with no rows pointing at them. Hit exactly that while reloading for this change. Verified end-to-end: full pipeline re-run against dev reproduces every prior count (1682 customers, 1519 properties, 2378 policies, 45861 transactions, 22354 bank rows, 70 documents) with zero orphans, and both apps build clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
4.5 KiB
TypeScript
151 lines
4.5 KiB
TypeScript
import { Injectable, NotFoundException } from "@nestjs/common";
|
|
import { Prisma } from "@jorgecuadros/database";
|
|
import { PrismaService } from "../prisma/prisma.service";
|
|
|
|
export interface ListParams {
|
|
query?: string;
|
|
page: number;
|
|
pageSize: number;
|
|
line?: "utility" | "insurance" | "both";
|
|
}
|
|
|
|
@Injectable()
|
|
export class CustomersService {
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
/** Unified customer list with search + business-line filter, paginated. */
|
|
async list({ query, page, pageSize, line }: ListParams) {
|
|
const where: Prisma.CustomerWhereInput = {};
|
|
|
|
if (query && query.trim()) {
|
|
const q = query.trim();
|
|
where.OR = [
|
|
{ name: { contains: q } },
|
|
{ email: { contains: q } },
|
|
{ phone: { contains: q } },
|
|
{ mobile: { contains: q } },
|
|
{ city: { contains: q } },
|
|
{ legacyRefs: { some: { legacyId: { contains: q } } } },
|
|
];
|
|
}
|
|
if (line === "utility") where.properties = { some: {} };
|
|
if (line === "insurance") where.policies = { some: {} };
|
|
if (line === "both") {
|
|
where.properties = { some: {} };
|
|
where.policies = { some: {} };
|
|
}
|
|
|
|
const [total, rows] = await this.prisma.$transaction([
|
|
this.prisma.customer.count({ where }),
|
|
this.prisma.customer.findMany({
|
|
where,
|
|
skip: (page - 1) * pageSize,
|
|
take: pageSize,
|
|
orderBy: { name: "asc" },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
nameSource: true,
|
|
city: true,
|
|
state: true,
|
|
email: true,
|
|
phone: true,
|
|
mobile: true,
|
|
status: true,
|
|
_count: { select: { properties: true, policies: true, transactions: true } },
|
|
},
|
|
}),
|
|
]);
|
|
|
|
const items = rows.map((r) => ({
|
|
id: r.id,
|
|
name: r.name,
|
|
nameSource: r.nameSource,
|
|
city: r.city,
|
|
state: r.state,
|
|
email: r.email,
|
|
phone: r.phone,
|
|
mobile: r.mobile,
|
|
status: r.status,
|
|
propertyCount: r._count.properties,
|
|
policyCount: r._count.policies,
|
|
transactionCount: r._count.transactions,
|
|
hasUtilities: r._count.properties > 0,
|
|
hasInsurance: r._count.policies > 0,
|
|
}));
|
|
|
|
return { items, total, page, pageSize, pageCount: Math.ceil(total / pageSize) };
|
|
}
|
|
|
|
/** Full unified customer view: identity + both business lines + ledger. */
|
|
async detail(id: string) {
|
|
const customer = await this.prisma.customer.findUnique({
|
|
where: { id },
|
|
include: {
|
|
legacyRefs: true,
|
|
properties: {
|
|
include: { services: true, trustAccount: true, documents: true },
|
|
},
|
|
policies: {
|
|
orderBy: { policyFrom: "desc" },
|
|
include: {
|
|
policyType: true,
|
|
insuranceProvider: true,
|
|
installments: { orderBy: { sequence: "asc" } },
|
|
vehicles: true,
|
|
insuredDrivers: true,
|
|
beneficiaries: true,
|
|
claims: true,
|
|
documents: true,
|
|
},
|
|
},
|
|
transactions: {
|
|
orderBy: { transactionDate: "desc" },
|
|
take: 100,
|
|
include: { type: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!customer) {
|
|
throw new NotFoundException(`Customer ${id} not found`);
|
|
}
|
|
|
|
// Ledger totals per domain + currency (the "one statement across both
|
|
// business lines" payoff), computed in the DB rather than in JS.
|
|
const summary = await this.prisma.transaction.groupBy({
|
|
by: ["domain", "currency"],
|
|
where: { customerId: id },
|
|
_sum: { amount: true },
|
|
_count: { _all: true },
|
|
});
|
|
|
|
return {
|
|
...customer,
|
|
transactionSummary: summary.map((s) => ({
|
|
domain: s.domain,
|
|
currency: s.currency,
|
|
total: s._sum.amount,
|
|
count: s._count._all,
|
|
})),
|
|
};
|
|
}
|
|
|
|
/** Top-line counts for a dashboard header. */
|
|
async stats() {
|
|
const [customers, withUtilities, withInsurance, policies, properties, transactions] =
|
|
await this.prisma.$transaction([
|
|
this.prisma.customer.count(),
|
|
this.prisma.customer.count({ where: { properties: { some: {} } } }),
|
|
this.prisma.customer.count({ where: { policies: { some: {} } } }),
|
|
this.prisma.policy.count(),
|
|
this.prisma.property.count(),
|
|
this.prisma.transaction.count(),
|
|
]);
|
|
const bothLines = await this.prisma.customer.count({
|
|
where: { properties: { some: {} }, policies: { some: {} } },
|
|
});
|
|
return { customers, withUtilities, withInsurance, bothLines, policies, properties, transactions };
|
|
}
|
|
}
|