Skip to content

Organization Academic-Year Lifecycle Proof

Status: local owner-storage and non-default Gateway candidate evidence only. It does not promote a public/default organization route or establish legacy parity.

Ownership And Policy

organization-service, implemented by school-service, owns tenant-scoped academic-year metadata. The owner database records name, startsOn, endsOn, and lifecycle status:

  • create as DRAFT;
  • edit dates/name only while DRAFT;
  • transition DRAFT -> ACTIVE or DRAFT -> ARCHIVED;
  • transition ACTIVE -> ARCHIVED only;
  • leave ARCHIVED immutable;
  • allow one ACTIVE row per organization.

Calendar values are ISO dates, and the owner rejects a range where endsOn is not after startsOn. IAM remains the permission-bearing membership owner; academic-year operations do not read IAM, Profile, Classroom, Course, Exam, or legacy databases.

The read-only legacy organization modules and Prisma schema were inspected for this slice. They have organization/unit/member behavior but no academic-year record or route, so this evidence makes no legacy-parity or migration claim.

Owner Storage Proof

ACADEMIC_YEAR_POSTGRES_CONFIRM=isolated-postgres make test-academic-year-postgres creates a fresh initdb cluster, applies every school-service migration Up section, then starts a temporary owner binary. It creates one draft period, activates it, restarts the same temporary binary, and reads back one ACTIVE|2025-09-01|2026-05-31 row. PostgreSQL also reports:

  • academic_years_org_name_unique;
  • academic_years_one_active_per_organization;
  • academic_years_organization_period_idx.

Artifact: output/qa/academic-year-postgres-isolated-20260715-042021/summary.json. The temporary cluster, listener, and service process were removed on exit. No configured local, Docker, or legacy database was used; the artifact contains no database URL or bearer token.

Candidate Gateway Proof

make test-academic-year-routes parses the default and two candidate tables. The default table contains no academic-year native route and retains broad organizations as legacy_proxy. The candidate tables expose only:

  • GET /api/organizations/{organizationId}/academic-years as native_read;
  • POST /api/organizations/{organizationId}/academic-years as native_write;
  • PATCH /api/organizations/{organizationId}/academic-years/{academicYearId} as native_write.

Every candidate route requires authentication, organization context, and the ADMIN role. ACADEMIC_YEAR_ROUTE_RUNTIME_CONFIRM=isolated-runtime make test-academic-year-route-runtime then runs a memory-only school-service and a temporary Gateway on random loopback ports. It proved:

  • anonymous list returns 401 AUTH_UNAUTHORIZED;
  • TEACHER create returns 403 AUTH_FORBIDDEN;
  • ADMIN creates a DRAFT, activates it, and lists one ACTIVE row through native_write, native_write, and native_read respectively;
  • direct Gateway create latency was 0.98 ms locally;
  • restarting that same temporary Gateway with deploy/gateway/routes.json selects broad organizations / legacy_proxy and returns the expected unreachable-legacy 502;
  • all request artifacts were scanned for bearer tokens and none were retained.

Artifact: output/qa/academic-year-route-runtime-20260715-042017/summary.json. The temporary owner, Gateway, and runtime directory were removed on exit.

Admin Browser Candidate Proof

make test-academic-year-browser creates a fresh initdb cluster, seeds only the disposable org-anh-duong owner row required by the Admin fixture, and starts temporary school-service, Gateway, and Admin Vite processes on random loopback ports. With adminAcademicYearGateway=native and a disposable ADMIN JWT, Playwright proves the browser:

  • lists an empty organization scope through organization-academic-years-list / native_read;
  • creates Năm học 2025-2026 as DRAFT through organization-academic-years-create / native_write;
  • activates that same row through organization-academic-years-update / native_write;
  • renders no raw bearer token, while written request artifacts redact it.

The runner reads the resulting ACTIVE record directly from its fresh school-service PostgreSQL database, then restarts only its temporary Gateway with deploy/gateway/routes.json. The same path selects organizations / legacy_proxy and returns the expected unreachable-legacy 502.

Artifact: output/qa/academic-year-browser-local-runtime-20260715-043216/summary.json. No existing local service, default route table, configured database, Docker database, or legacy source/database was changed.

Verification

bash
GOTOOLCHAIN=go1.25.11 go test ./services/school-service/... -count=1
make test-academic-year-routes
make test-academic-year-route-runtime
make test-academic-year-postgres
make test-academic-year-browser
ruby -e 'require "yaml"; ARGV.each { |path| YAML.load_file(path) }' services/school-service/api/openapi.yaml contracts/openapi/services/school-service.yaml
diff -u services/school-service/api/openapi.yaml contracts/openapi/services/school-service.yaml

Rollback And Open Gates

No default table was changed. Disable callers of the candidate route table and run the Gateway with GATEWAY_ROUTE_TABLE=deploy/gateway/routes.json; broad organization requests return to legacy. The migration Down section removes only the service-owned academic_years table when a disposable environment is reset.

Open gates are any required legacy data mapping or parity review, deployed target evidence, default-route promotion review, and rollback evidence after a real candidate deployment. This proof does not make those claims.

Go-platform documentation is generated from repository Markdown.