Skip to content

feat(seasons): real Season entity replaces legacy year-list (PR #612)#612

Merged
barach6662001-bit merged 3 commits intomainfrom
pr612-season-model
Apr 24, 2026
Merged

feat(seasons): real Season entity replaces legacy year-list (PR #612)#612
barach6662001-bit merged 3 commits intomainfrom
pr612-season-model

Conversation

@barach6662001-bit
Copy link
Copy Markdown
Owner

Summary

Replaces the legacy /api/seasons endpoint that returned a sorted int[] of years derived from transaction timestamps with a real tenant-scoped Season entity. This fixes the silent dashboard bug where users navigated through synthesised Jan 1 – Dec 31 calendar windows that never matched the actual crop cycle (Aug 1 → Jul 31 in UA).

Backend

  • Domain: Season : AuditableEntity { Code(16), Name(100), StartDate: DateOnly, EndDate: DateOnly, IsCurrent: bool }.
  • EF configuration: unique (TenantId, Code) filtered on IsDeleted=false; partial unique IX_Seasons_TenantId_IsCurrent_Unique filtered on IsCurrent=true AND IsDeleted=false (at most one current season per tenant); CHECK CK_Seasons_EndAfterStart; standard soft-delete query filter.
  • Migration AddSeasons: creates the table + indexes + check constraint, plus an idempotent raw-SQL seed that gives every active tenant three default seasons (2023/2024, 2024/2025, 2025/2026-current). Guarded by NOT EXISTS so replays are safe.
  • DataSeeder.SeedDefaultSeasonsAsync: same idempotent backfill path for environments using EnsureCreated() (tests, first-run dev).
  • /api/seasons/* rewritten — tenant-scoped CRUD. Reads: any authenticated user of the tenant. Mutations: CompanyAdmin or platform super-admin. Delete blocks on linked CostRecord/Sale/AgroOperation rows falling inside the season's range; super-admin can override with ?force=true. set-current performs a two-step flip (clear existing → set target) to avoid transient violation of the partial unique index.
  • /api/admin/tenants/{tenantId}/seasons/* — same CRUD under [SuperAdminRequired], bypasses tenant filter via IgnoreQueryFilters(), emits one SuperAdminAuditLog per mutation with before/after payloads.

Frontend

  • Breaking change: getSeasons() now returns SeasonDto[] (was number[]).
  • Dashboard.tsx: navigation in period === 'season' mode now steps through real Season records by start date (using each season's actual startDate/endDate as the window), not through synthesised year boundaries. The active season label uses season.name (e.g. “Сезон 2025/2026”) instead of a bare year number.

Tests

Six integration tests in tests/AgroPlatform.IntegrationTests/Seasons/SeasonsTests.cs:

  1. List_ReturnsOnlySeasonsOfCurrentTenant — tenant isolation.
  2. SetCurrent_FlipsExactlyOneCurrentSeason — transactional flip invariant.
  3. Delete_WithLinkedCostRecord_Returns409ForTenantAdmin — safety rail.
  4. Delete_ForceByPlatformSuperAdmin_DeletesDespiteLinkedCostRecord — escape hatch.
  5. Create_AsNonAdmin_Returns403 — authorization.
  6. SuperAdmin_Create_WritesExactlyOneAuditLogEntry — audit wiring.

Unit tests: 309 passed, 0 failed.

Verification

dotnet build AgroPlatform.slnx   # 0 errors
dotnet test tests/AgroPlatform.UnitTests   # 309 passed
cd frontend && npx tsc --noEmit   # clean

Notes / follow-ups

- Domain: Season (Code, Name, StartDate, EndDate, IsCurrent) : AuditableEntity
- EF: unique (TenantId, Code), partial unique (TenantId, IsCurrent=true),
  CHECK (EndDate > StartDate), tenant soft-delete query filter
- Migration AddSeasons + idempotent data seed (3 default seasons per tenant,
  Aug 1 -> Jul 31). DataSeeder backfills existing/test tenants at startup.
- /api/seasons rewritten: List/GetCurrent/Create/Update/SetCurrent/Delete,
  CompanyAdmin or platform super-admin required for mutations. Delete blocks
  on linked CostRecord/Sale/AgroOperation within the season range; super-admin
  can force=true.
- /api/admin/tenants/{id}/seasons/* for platform super-admin, audited via
  ISuperAdminAuditService.
- Breaking frontend API change: getSeasons() returns SeasonDto[] (was number[]).
  Dashboard now navigates through real season records using StartDate/EndDate
  instead of synthesising Jan 1 - Dec 31 year windows from transaction
  timestamps.
- 6 integration tests covering tenant isolation, SetCurrent flip semantics,
  delete safety rails, non-admin 403, and super-admin audit emission.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

}, [period, hasExplicitRange, fromDate, resolvedWindow, t.dashboard.allTime]);

P2 Badge Add sortedSeasons to resolvedRangeLabel memo deps

resolvedRangeLabel reads sortedSeasons, but the dependency list omits it, so on the common path where seasons load asynchronously after the initial render (with unchanged period/fromDate) the memo stays on the fallback label and does not update to the active season name until some unrelated state change occurs. This leaves the dashboard showing an incorrect range label in season mode.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +102 to +103
if (await _db.Seasons.AnyAsync(s => s.TenantId == tenantId && s.Code == req.Code, ct))
return Conflict(new { error = "Season with this code already exists." });
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize season code before uniqueness checks

The duplicate check uses raw req.Code, but the entity is persisted with req.Code.Trim(), so inputs that differ only by surrounding whitespace (for example "2025/2026 ") bypass the AnyAsync conflict path and then hit the unique (TenantId, Code) index during SaveChanges, returning a server error instead of a 409. The same normalization mismatch appears in update logic too.

Useful? React with 👍 / 👎.

- ROADMAP.md: move PR #612 to Completed, promote PR #613 (Currency) to In progress
- TZ.md: mark point 2 (Dashboard period switcher) fully CLOSED with PR #612 shipment notes
@barach6662001-bit barach6662001-bit merged commit 014bbb1 into main Apr 24, 2026
3 checks passed
@barach6662001-bit barach6662001-bit deleted the pr612-season-model branch April 24, 2026 08:58
barach6662001-bit added a commit that referenced this pull request Apr 24, 2026
#617)

Captures completed work across PR #602-611, remaining scope across PR #612-617,
locked architectural decisions, technical debt, and agent protocol.
barach6662001-bit added a commit that referenced this pull request Apr 24, 2026
…ason-audit

docs: record PR #612 + #616 completion, tighten theme lock, add tech …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant