feat(customers): the customer file's estado de cuenta follows the same year rule
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m48s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m5s

`/clientes/:id` carries its own "Estado de cuenta" card, fed by
CustomersService.detail rather than by BillingService.statement, so the
previous commit left it reading the old way: the last 100 movements of all
time, newest first. Two pages, one label, two orders.

It now covers the current calendar year oldest-first, like the statement and
like the sheet the office prints. The `take: 100` is gone with it — capping a
descending list hid the oldest rows, but capping an ascending one would hide
the newest, and a single year is small (365 rows for the heaviest customer in
the book).

The per-domain totals above the list are untouched and still historical: they
are an unfloored groupBy, so they double-count every customer's opening
balance. That is a separate bug from this one; the card now says out loud that
those figures are lifetime, not this year's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 20:21:51 -07:00
co-authored by Claude Opus 5
parent 8b8de0fdca
commit 9929a9a3ac
3 changed files with 22 additions and 9 deletions
+11 -2
View File
@@ -96,6 +96,13 @@ export class CustomersService {
/** Full unified customer view: identity + both business lines + ledger. */
async detail(id: string) {
// The movement list on the customer file is the same statement the office
// prints, so it follows the same rule as BillingService.statement: this
// calendar year, oldest-first. No `take` any more — the cap used to hide
// the end of a busy customer's year once the order flipped, and a single
// year is small (365 rows for the heaviest customer in the book).
const yearStart = new Date(Date.UTC(new Date().getUTCFullYear(), 0, 1));
const customer = await this.prisma.customer.findUnique({
where: { id },
include: {
@@ -117,8 +124,8 @@ export class CustomersService {
},
},
transactions: {
orderBy: { transactionDate: "desc" },
take: 100,
where: { transactionDate: { gte: yearStart } },
orderBy: [{ transactionDate: "asc" }, { id: "asc" }],
include: { type: true },
},
},
@@ -140,6 +147,8 @@ export class CustomersService {
return {
...customer,
/** Calendar year the movement list covers. */
transactionYear: yearStart.getUTCFullYear(),
transactionSummary: summary.map((s) => ({
domain: s.domain,
currency: s.currency,
+9 -7
View File
@@ -128,6 +128,7 @@ function Detail({ id }: { id: string }) {
customerId={data.id}
summary={data.transactionSummary}
transactions={data.transactions}
year={data.transactionYear}
/>
<DocumentosSection data={data} />
</div>
@@ -746,16 +747,18 @@ function EstadoCuentaSection({
customerId,
summary,
transactions,
year,
}: {
customerId: string;
summary: TransactionSummaryRow[];
transactions: Transaction[];
year: number;
}) {
return (
<section className="section">
<SectionHead
rule="cuenta"
title="Estado de cuenta"
title={`Estado de cuenta ${year}`}
count={transactions.length}
countSuffix="movimientos"
/>
@@ -782,7 +785,7 @@ function EstadoCuentaSection({
<div className="card">
{transactions.length === 0 ? (
<div className="empty-inline">Sin movimientos registrados.</div>
<div className="empty-inline">Sin movimientos en {year}.</div>
) : (
<div className="tx-scroll">
<table className="tx-table">
@@ -804,11 +807,10 @@ function EstadoCuentaSection({
</table>
</div>
)}
{transactions.length >= 100 && (
<div className="section-note" style={{ padding: "0 16px 14px" }}>
Mostrando los 100 movimientos más recientes.
</div>
)}
<div className="section-note" style={{ padding: "0 16px 14px" }}>
Movimientos de {year}, del más antiguo al más reciente. Los saldos de
arriba son el acumulado histórico, no el del año.
</div>
</div>
{transactions.length > 0 && (
<p className="section-note">
+2
View File
@@ -1129,6 +1129,8 @@ export interface CustomerDetail {
properties: Property[];
policies: Policy[];
transactions: Transaction[];
/** Calendar year `transactions` covers. */
transactionYear: number;
transactionSummary: TransactionSummaryRow[];
}