MySQL/MariaDB reject a DEFAULT value on TEXT-backed columns (TypeORM's
simple-array maps to TEXT), so table creation failed with
"BLOB, TEXT, GEOMETRY or JSON column 'recipients' can't have a default
value". The service always assigns recipients before saving, so no
DB-level default was ever needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Reject dates that are not strict YYYY-MM-DD (was accepting full ISO
datetimes, which silently produced empty exports instead of a 400)
and reject from > to with a 400 before touching the team/DB.
- Emit the cashbox_export_download and cashbox_export_subscription_update
audit log events that were declared but never fired, matching the
audit trail every sibling feature already has.
- Restore full type checking on the pdfkit import via `import = require()`
instead of an untyped require() with an eslint-disable.
- Tighten a cashbox.spec.ts assertion to check the exact dialog class
instead of expect.anything(), so it can't pass with the wrong dialog
wired to the Export button.
- Style and announce the export dialogs' error messages using this
codebase's established error-message/role=alert pattern.
- Wrap runOne(subscription) in try/catch to ensure one subscription failure doesn't block remaining subscriptions
- Log failed subscriptions with new 'cashbox_export_subscription_run_fail' event
- Add new LOGEVENT type for subscription run failures
- Add test to verify second subscription processes even when first fails (continues processing independently)
- All 7 tests passing: 6 original + 1 new failure handling test
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets treasurers/captains/coaches define recurring fee/levy dues that
are automatically booked for all active players on a monthly,
quarterly, or yearly schedule via a daily cron job, instead of having
to book them manually every cycle.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- teams.service.spec.ts: pick a checkpoint older than the adjustment's own
month so the exclusion test actually fails without the exclusion filter
- teams.service.ts: only negate fine/levy/fee amounts when positive,
matching TeamMembersService.recomputeBalance and Transaction.setBalance()
exactly, instead of negating unconditionally
- teams.service.ts: outstanding-history helper now returns a positive
value when players owe money, matching the house convention already
established by getOverview()'s team.outstanding
- overview.spec.ts: assert the balance chart's legend becomes visible
- teams.service.spec.ts: add coverage for inactive-player exclusion and
a positive-balance (prepaid credit) case
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extract the month-by-month backward-walk algorithm (used for both
team-level cash balance and player-level debt history) into a single
shared private helper `reconstructBackward()`. This eliminates code
duplication while preserving behavior:
- Team-level balanceHistory: maps movements through signedFlowAmount(),
rounds each point, calls the shared helper
- Player-level balance history: maps transactions using type.id rule,
skips rounding (only rounds at final merge to avoid compounding
errors), calls the shared helper
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reconstructs each active player's balance per month (same backward
technique as the existing cash-balance history) so the overview stats
endpoint can report what the team balance would be if all currently
open dues had already been paid.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Whole-branch review findings:
1. balanceHistory/monthlyFlow always returned 12 entries, even for a
brand-new team with zero transactions, so the frontend's empty-state
(gated on .length === 0) could never fire for a real "no movements yet"
team. Now returns empty arrays when there are no relevant movements at
all (not just none in the last 12 months, so a team with older-but-real
history still gets a flat chart). Also added the same defensive
`?? []` guard on players/transactions that getOverview already has, so a
team with no players/relations loaded doesn't throw.
2. GET :id/overview/stats had no team-membership check -- any logged-in
user (RoleEnum.user is the default role) could read any other team's
financial stats by iterating ids. Injected TeamAccessService into
TeamsService (already a sibling provider in TeamsModule, no module
wiring needed) and call assertMember(actorUserId, teamId) as the first
line of getOverviewStats, threaded from the controller via @Req(). Read
access only (assertMember, not assertManager), matching who can already
view the overview page. Sibling routes with the same pre-existing gap
were left untouched, per review scope.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Manual verification against real data (Task 3) found balanceHistory drifted
from team.balance for real teams, since team.balance carries historical
adjustments (e.g. from the removed legacy backend) that don't trace back to
the current payment/credit/expense rows. Forward-summing those rows from
zero could never be trusted to tie out.
Rewrite balanceHistory to anchor on team.balance (the authoritative current
value) and walk the movements backward, newest to oldest, undoing each one
to reconstruct earlier month-end balances. This guarantees the most recent
point equals team.balance by construction, and is mathematically identical
to the old forward sum for teams whose movements fully explain their
balance. monthlyFlow/topOutstanding are unaffected and left as-is.
Updated teams.service.spec.ts to use a fixture where team.balance
intentionally does not equal the sum of its own movements, so the tests
actually exercise the drift-handling behavior instead of a case where
forward-sum and backward-anchor happen to coincide.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds GET :id/overview/stats + TeamsService#getOverviewStats, aggregating
team-wallet and player payment transactions into a 12-month balanceHistory
(cumulative, carry-forward), monthlyFlow (income/expense), and topOutstanding
(top 10 active debtors) for the upcoming overview KPI charts. fine/levy/fee
and player-level credit are excluded, matching the "Ist-Kasse" cash-flow rule.
Replaces the unmodified NestJS-boilerplate placeholder specs for
TeamsService/TeamsController (which already failed at baseline) with real
tests using the team-access.service.spec.ts direct-construction convention.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Team managers (captain and above) can now deactivate/reactivate a player and
change their team-role from the player detail page. Deactivation zeroes the
open balance via an auditable adjustment transaction instead of overwriting
the balance field, and both actions are blocked if they would leave a team
without an active treasurer. Also hardens the existing PUT teams/:id/players
endpoint down to profile-only fields, fixing a typo bug and closing a gap
where any authenticated user could mutate a player's active/role/balance in
any team.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Docker runtime image only ships dist/, but mail-config.service.ts
pointed at src/mail/mail-templates and nest-cli.json never copied the
.hbs files into dist/ either. This was silently masked before because
the return-before-sendMail bug meant the path was never touched; fixing
that bug now surfaces it as a hard crash on boot (readFileSync throwing
synchronously inside the MailerModule factory). Resolve the templates
dir from __dirname instead, which is correct in both dev (src/mail) and
the compiled image (dist/mail), and add the mail-templates .hbs files
to nest-cli.json's asset copy list so they actually land in dist/.
The HandlebarsAdapter reads partials config from a top-level sibling of
`template` (mailerOptions.options.partials), not from
template.options.partials where it was nested. Because the mail templates
use partial blocks ({{#> layout}}...{{/layout}}), the unregistered partial
rendered silently as an unstyled fragment instead of throwing, so this went
unnoticed. A config-only fix also breaks on Windows because the adapter's
glob-based directory loader mishandles backslash path separators.
Fix registers the shared `layout` partial directly on the handlebars module
singleton in MailConfigService, bypassing the broken glob loader entirely.
Also:
- add mail-config.service.spec.ts, an integration test that drives the real
MailerOptions + HandlebarsAdapter wiring (would have caught this bug,
unlike the existing template-only spec which registers the partial itself)
- remove stale nestjs-i18n references from .env.example, env-example, and
the backend README (i18n was already removed from the code)
- add missing trailing newlines to activation.hbs and reset-password.hbs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>