Appearance
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 -> ACTIVEorDRAFT -> ARCHIVED; - transition
ACTIVE -> ARCHIVEDonly; - leave
ARCHIVEDimmutable; - allow one
ACTIVErow 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-yearsasnative_read;POST /api/organizations/{organizationId}/academic-yearsasnative_write;PATCH /api/organizations/{organizationId}/academic-years/{academicYearId}asnative_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; TEACHERcreate returns403 AUTH_FORBIDDEN;ADMINcreates aDRAFT, activates it, and lists oneACTIVErow throughnative_write,native_write, andnative_readrespectively;- direct Gateway create latency was
0.98 mslocally; - restarting that same temporary Gateway with
deploy/gateway/routes.jsonselects broadorganizations/legacy_proxyand returns the expected unreachable-legacy502; - 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-2026asDRAFTthroughorganization-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.yamlRollback 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.