Fix dashboard data findings from browser verification
All checks were successful
Deploy to VPS / test (push) Successful in 1m32s
Deploy to VPS / deploy (push) Successful in 1m2s

- Próximas cuotas: headline is the upcoming billing batch (sum of every
  active plan's next cuota, using the rounding-absorbing last cuota
  when it's the pending one) — the in-cycle formula was ~always zero
  since cuotas bill on the 19th, day one of the next cycle.
- useAgent must name the 'wealthysmart' agent (default crashed the page).
- /transactions/recent excludes future-dated Tasa Cero cuotas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-07-04 11:02:26 -06:00
parent b1c387c415
commit 10d9bd209f
3 changed files with 32 additions and 19 deletions

View File

@@ -253,9 +253,15 @@ def recent_transactions(
session: Session = Depends(get_session), session: Session = Depends(get_session),
_user: str = Depends(get_current_user), _user: str = Depends(get_current_user),
): ):
from app.timeutil import utcnow
query = ( query = (
select(Transaction) select(Transaction)
.where(Transaction.source == TransactionSource.CREDIT_CARD) .where(
Transaction.source == TransactionSource.CREDIT_CARD,
# Tasa Cero generates future-dated cuotas; "recent" means billed.
Transaction.date <= utcnow(),
)
.order_by(col(Transaction.date).desc(), col(Transaction.id).desc()) .order_by(col(Transaction.date).desc(), col(Transaction.id).desc())
.limit(limit) .limit(limit)
) )

View File

@@ -27,7 +27,7 @@ export default function Asistente() {
// append it as a user message and run the agent once. // append it as a user message and run the agent once.
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();
const { agent } = useAgent(); const { agent } = useAgent({ agentId: "wealthysmart" });
const { copilotkit } = useCopilotKit(); const { copilotkit } = useCopilotKit();
const sentRef = useRef(false); const sentRef = useRef(false);
useEffect(() => { useEffect(() => {

View File

@@ -132,18 +132,21 @@ function ProximasCuotasCard() {
const active = (q.data?.plans ?? []).filter( const active = (q.data?.plans ?? []).filter(
(p) => !p.is_completed && p.next_cuota_date, (p) => !p.is_completed && p.next_cuota_date,
); );
const inCycle = active.filter((p) => { // One cuota per plan per cycle, so the sum of every active plan's next
const d = new Date(p.next_cuota_date!); // cuota is the load of the upcoming billing batch (next 19th). The last
return d >= CYCLE.start && d < CYCLE.end; // cuota absorbs BAC's rounding, so use it when it's the one pending.
}); const nextCuotaOf = (p: (typeof active)[number]) =>
const cycleTotal = inCycle.reduce((s, p) => s + p.installment_amount, 0); p.cuotas_billed === p.num_installments - 1
const upcoming = [...active] ? p.last_installment_amount
.sort( : p.installment_amount;
const nextBatchTotal = active.reduce((s, p) => s + nextCuotaOf(p), 0);
const upcoming = [...active].sort(
(a, b) => (a, b) =>
new Date(a.next_cuota_date!).getTime() - new Date(a.next_cuota_date!).getTime() -
new Date(b.next_cuota_date!).getTime(), new Date(b.next_cuota_date!).getTime(),
) );
.slice(0, 3); const nextDate = upcoming[0]?.next_cuota_date;
const topThree = upcoming.slice(0, 3);
return ( return (
<StatCardShell to="/financiamientos" ariaLabel="Ver financiamientos"> <StatCardShell to="/financiamientos" ariaLabel="Ver financiamientos">
@@ -151,21 +154,25 @@ function ProximasCuotasCard() {
<CardTitle className="text-sm font-medium text-muted-foreground"> <CardTitle className="text-sm font-medium text-muted-foreground">
Próximas cuotas Próximas cuotas
</CardTitle> </CardTitle>
<CardDescription>Tasa Cero en este ciclo</CardDescription> <CardDescription>
{nextDate
? `Tasa Cero · próximo cobro ${formatShortDate(nextDate)}`
: "Tasa Cero"}
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-2"> <CardContent className="space-y-2">
<StatValue <StatValue
loading={q.isPending} loading={q.isPending}
value={q.isError ? null : formatAmount(cycleTotal, "CRC")} value={q.isError ? null : formatAmount(nextBatchTotal, "CRC")}
/> />
{upcoming.length > 0 && ( {topThree.length > 0 && (
<ul className="space-y-0.5 text-xs text-muted-foreground"> <ul className="space-y-0.5 text-xs text-muted-foreground">
{upcoming.map((p) => ( {topThree.map((p) => (
<li key={p.id} className="flex justify-between gap-2"> <li key={p.id} className="flex justify-between gap-2">
<span className="truncate">{p.merchant}</span> <span className="truncate">{p.merchant}</span>
<span data-sensitive className="font-mono shrink-0"> <span data-sensitive className="font-mono shrink-0">
{formatShortDate(p.next_cuota_date!)} ·{" "} {formatShortDate(p.next_cuota_date!)} ·{" "}
{formatAmount(p.installment_amount, p.currency)} {formatAmount(nextCuotaOf(p), p.currency)}
</span> </span>
</li> </li>
))} ))}