Commit Graph

71 Commits

Author SHA1 Message Date
Bastian Wagner
451b5c4e42 feat: add notification domain events, listener, and module 2026-08-04 18:53:48 +02:00
Bastian Wagner
05f4c2ddf0 chore: add and register @nestjs/event-emitter 2026-08-04 18:46:14 +02:00
Bastian Wagner
273c25eccb test: assert recipient-filter query clauses in NotificationsService.create 2026-08-04 18:42:03 +02:00
Bastian Wagner
97b0a5c19a feat: add NotificationsService 2026-08-04 18:34:42 +02:00
Bastian Wagner
7410672630 feat: add notification data model and migration 2026-08-04 18:26:50 +02:00
Bastian Wagner
df634e7601 fix: update stale assertion for unconditional start/finish logging
runDueRecurringTransactions() always logs a start/finish marker for
observability, even when nothing is due, but the "does nothing" test
still asserted logger.info was never called. Pre-existing baseline
failure, unrelated to the notification-center work about to start.
2026-08-04 18:10:40 +02:00
Bastian Wagner
020b390953 address code review: fix inclusive to-date filter, add retention error handling
- LoggingService.findLogs(): the `to` date filter compared a date-only
  string (e.g. from a date picker) against a timestamp column, which
  parses to midnight and silently excludes the entire last day. Widen
  it to end-of-day so the range is genuinely inclusive.
- LogRetentionScheduler.cleanupOldLogs(): wrap the delete in try/catch
  and log failures via logger.error, matching the existing convention
  in CashboxExportScheduler/RecurringTransactionsScheduler. Without
  this, a failed nightly cleanup would fail silently - exactly what
  this feature exists to prevent.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 16:34:34 +02:00
Bastian Wagner
f1b4f7e5b4 feat: add admin log viewer, log retention cleanup, and manual job triggers
Global admins couldn't see the app's event log (no read endpoint or UI
existed for it) and had no way to clean up old entries or re-run a
scheduled job without touching the database or server directly.

Backend:
- LoggingService.findLogs() + admin-only LogsController (GET
  admin/logs) with level/event/date-range/search filtering and
  pagination, mirroring AdminUsersService.findPlayers().
- LogRetentionScheduler deletes log entries older than
  LOG_RETENTION_DAYS (default 365, via app.config.ts), following the
  existing @Cron scheduler pattern.
- Admin-only POST admin/run endpoints on CashboxExportController and
  RecurringTransactionsController that invoke the existing schedulers'
  public run methods on demand - both are safe to re-run since their
  "due" queries advance nextRunDate only after a successful run.

Frontend:
- New /logs page (global-admin gated, same pattern as /users): AG-Grid
  infinite-scroll table with level/event/date-range/search filters,
  plus buttons to trigger the two jobs now and see the result land in
  the grid immediately.
- LogsApi, and triggerRunNow() added to the existing CashboxExportApi
  and RecurringTransactionApi.
- Discoverability link from /users to /logs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 16:21:39 +02:00
Bastian Wagner
24c509c0d5 address code review: assert page count in multi-page test, document fixes
- Lock in the exact expected page count (4) for the 60+60-row pagination
  test, which previously only checked the buffer was non-trivial. This is
  the scenario most likely to expose a footer/pagination regression.
- Add short comments explaining the footerY height-bound fix and the
  pdfPageCount() regex's coupling to pdfkit's serialization format.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:33:48 +02:00
Bastian Wagner
4185efb83a fix: negate expense amounts and stop blank trailing pages in cashbox PDF export
Expenses were stored as positive amounts (DB convention) and buildRows()
never negated them, so they were added to the running budget total
instead of subtracted. Negate expense amounts for team-wallet
transactions, mirroring the existing signedFlowAmount() convention in
teams.service.ts.

Separately, addFooters() placed footer text inside the reserved bottom
margin without an explicit height option, which made pdfkit's
LineWrapper treat every footer draw as overflowing the page and call
continueOnNewPage() twice per page - inflating page counts 3x with
blank trailing pages. Bounding the footer text to its own small height
box prevents pdfkit's automatic pagination from firing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 13:26:45 +02:00
Bastian Wagner
cd9d7b165f pdf export 2026-08-04 13:11:14 +02:00
Bastian Wagner
6fceee5a07 feat: wire receivables into manual and recurring cashbox PDF export
Both the manual download endpoint and the recurring email subscription
now pass buildReceivableRows() output into buildPdf, so every PDF
report includes the Forderungen section regardless of how it was
generated.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 12:26:23 +02:00
Bastian Wagner
02a4d2e59d feat: redesign cashbox PDF report with styled tables and receivables section
The PDF export was an unformatted list of doc.text() lines and only
showed real cash movements (payment type). Rebuilds it as a proper
two-section report: a branded header band, a bordered/zebra-striped
table with colored amounts and bold running balance for cash
movements, and a second "Forderungen" section listing fine/levy/fee
entries created in the period with their own total. Tables paginate
across pages and every page gets a footer with page numbers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 12:20:33 +02:00
Bastian Wagner
7b499b361f feat: add buildReceivableRows for fine/levy/fee entries
Cashbox export previously only saw payment transactions. This adds
the query for fine/levy/fee entries (Forderungen) that the redesigned
PDF report will show in a separate section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 10:22:59 +02:00
Bastian Wagner
0bad154971 fix: remove invalid default on CashboxExportSubscription.recipients
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>
2026-08-04 09:56:39 +02:00
Bastian Wagner
da5998487a fix: address cashbox-export whole-branch review findings
- 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.
2026-08-04 09:20:44 +02:00
Bastian Wagner
d628d5e4d7 fix: add LoggingModule import to CashboxExportModule 2026-08-04 08:10:04 +02:00
Bastian Wagner
57869d5fc1 feat: register CashboxExportModule 2026-08-04 08:03:29 +02:00
Bastian Wagner
eed8266da2 fix: add error handling for CashboxExportScheduler subscription processing
- 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>
2026-08-04 07:56:39 +02:00
Bastian Wagner
5779483b21 feat: add CashboxExportScheduler for recurring PDF mailing
- Implement CashboxExportScheduler with @Cron(EVERY_DAY_AT_4AM)
- Query due subscriptions (active=true, nextRunDate <= today)
- For each subscription: fetch team, build PDF, send email, advance nextRunDate
- Support monthly/quarterly/yearly intervals via INTERVAL_MONTHS map
- Add cashbox_export_subscription_run to LOGEVENT type for logging
- All 6 tests passing: empty state, monthly/quarterly/yearly periods, multiple subscriptions

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 07:49:58 +02:00
Bastian Wagner
196898b993 feat: add MailService.cashboxExport and email template 2026-08-04 07:44:12 +02:00
Bastian Wagner
42e2bbc4ff feat: add cashbox export subscription endpoints
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-04 07:39:20 +02:00
Bastian Wagner
a11eb73ce9 feat: add CashboxExportSubscription entity and service 2026-08-04 07:28:01 +02:00
Bastian Wagner
009aae1f2b feat: add cashbox export download endpoint 2026-08-03 21:37:14 +02:00
Bastian Wagner
18df224386 test: enforce permission check ordering in CashboxExportService 2026-08-03 21:33:36 +02:00
Bastian Wagner
396dc29cf5 feat: add CashboxExportService.exportForUser 2026-08-03 21:30:22 +02:00
Bastian Wagner
f8857f71e1 feat: add buildPdf for cashbox export
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 21:20:57 +02:00
Bastian Wagner
0b408f7d73 fix: use symmetric rounding for negative amounts in formatGermanAmount 2026-08-03 21:16:05 +02:00
Bastian Wagner
c6df6a6d38 fix: handle negative-zero and half-cent rounding in formatGermanAmount 2026-08-03 21:12:27 +02:00
Bastian Wagner
cab04c5869 feat: add buildCsv for cashbox export 2026-08-03 21:08:03 +02:00
Bastian Wagner
cf7c3efb0f fix: add null type guards to buildRows to prevent crashes on missing types 2026-08-03 21:02:52 +02:00
Bastian Wagner
a9df62a249 feat: add buildRows for cashbox export row filtering 2026-08-03 20:57:36 +02:00
Bastian Wagner
d8883d4687 chore: add pdfkit for cashbox PDF export 2026-08-03 20:51:48 +02:00
Bastian Wagner
9e55c0549b feat: add recurring transactions (Wiederkehrende Buchungen)
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>
2026-08-03 20:20:57 +02:00
Bastian Wagner
6531f2553f fix: wrap team creation in a transaction, add more-menu entry point, fix lint
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 17:27:23 +02:00
Bastian Wagner
c1478f07f1 feat: make team creator a captain of the new team 2026-08-03 15:44:12 +02:00
Bastian Wagner
92eacbc5bb feat: allow any logged-in user to create a team 2026-08-03 15:38:43 +02:00
Bastian Wagner
84bab04523 fix export type 2026-08-03 15:01:49 +02:00
Bastian Wagner
b21641f37a ag grid 2026-08-03 14:49:35 +02:00
Bastian Wagner
3cb8cd9a4a fix: strengthen deactivation-exclusion test and align sign rule with canonical logic
- 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>
2026-08-03 11:24:34 +02:00
Bastian Wagner
d8f13d674d refactor: extract shared backward-reconstruction logic
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>
2026-08-03 10:58:48 +02:00
Bastian Wagner
1b6ce57fbf feat: add theoretical balance history to team overview stats
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>
2026-08-03 10:44:22 +02:00
Bastian Wagner
27ab505aba berechtigungen 2026-08-03 10:16:42 +02:00
Bastian Wagner
26391b5b2c Merge branch 'feature/manager-guide' 2026-08-02 09:25:09 +02:00
Bastian Wagner
0e8ac74116 feat: add manager guide and contextual help 2026-08-02 09:22:16 +02:00
Bastian Wagner
a94caed85f Merge branch 'worktree-kasse-kpi-charts' 2026-08-01 21:49:07 +02:00
Bastian Wagner
e2f271fd35 fix(teams): gate empty-state and add membership check to overview stats
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>
2026-08-01 21:41:54 +02:00
Bastian Wagner
1058d641e7 fix(teams): anchor balanceHistory to team.balance instead of forward-summing
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>
2026-08-01 21:02:32 +02:00
Bastian Wagner
ecb6fd4394 feat(teams): add overview stats endpoint for KPI charts
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>
2026-08-01 19:44:49 +02:00
Bastian Wagner
6eab6251d5 fehlerhafte tests entfernt 2026-08-01 19:44:07 +02:00