Appearance
Exam Print Template And Export Runtime Proof
Scope
This proof covers service-owned Word print-template configuration, native DOCX rendering, and durable artifact persistence through document-service. It does not claim template-to-exam binding, a public gateway route, authenticated browser artifact reads, or browser UI parity.
Local Compose Evidence
On 2026-07-12, the local Compose migration runner rebuilt from the current worktree and applied:
text
exam-service / 000006_exam_print_templates.sql
exam-service / 000007_exam_print_exports.sqlThe real hoctapaz_exam_db then contained exam_print_templates with the expected service-owned columns:
text
id, organization_id, created_by_id, title, description, config_json,
config_version, created_at, updated_atexam-service and document-service were rebuilt and became healthy at http://127.0.0.1:8090 and http://127.0.0.1:8088. Both use the same Compose INTERNAL_SERVICE_TOKEN; exam-service uses the internal DOCUMENT_SERVICE_URL=http://document-service:8080 boundary.
HTTP Workflow
An authenticated teacher in org_runtime_template completed this internal workflow against the running service:
POST /v1/exam-print-templatesreturned201with a generatedept_*id andconfigVersion=1.PATCH /v1/exam-print-templates/{id}replaced the configuration and returnedconfigVersion=2.GET /v1/exam-print-templatesreturned the caller's template with the A5 configuration.- A different teacher in the same organization received
404when reading that id, proving exact owner scope rather than metadata disclosure. DELETE /v1/exam-print-templates/{id}returned a delete marker; a later read returned404, leaving the local test tenant clean.
The running request logs recorded 201, 200, 200, 404, 200, and 404 for create, update, list, cross-owner read, delete, and post-delete read.
Native DOCX Render Evidence
After rebuilding only exam-service, a second real Compose smoke created a draft native exam, stored one native question snapshot, created a print template, and called:
text
GET /v1/exams/{examId}/print-docx?templateId={templateId}&includeAnswers=trueThe response was a 3,154-byte application/vnd.openxmlformats-officedocument.wordprocessingml.document attachment with X-Exam-Print-Template-Version: 1. unzip -t accepted the file, and its OpenXML parts included word/document.xml, word/header1.xml, word/footer1.xml, styles, relationships, and content types.
The rendered XML contained the normalized question text, Câu 1: B, and the stored explanation. Header and footer XML contained the template-specific school title and footer text. The request used includeAnswers=true; the default remains no answer appendix. The smoke deleted both draft exam and template before exit.
Current Owner Recheck (2026-07-12)
The current Compose exam-service was rechecked with a new scoped draft exam, one SINGLE_CHOICE snapshot, and one caller-owned template. The internal print-docx request returned a 3,197-byte DOCX with content type application/vnd.openxmlformats-officedocument.wordprocessingml.document, X-Exam-Print-Template-Id, and X-Exam-Print-Template-Version: 1. unzip -t accepted the archive; word/document.xml contained the question, Câu 1: B, and the reviewed explanation, while header/footer XML retained Runtime Header and Runtime Footer. The scoped template and draft exam were deleted through their owner APIs after verification.
Generic Answer Sheet Recheck (2026-07-12)
The current Compose exam-service was rebuilt from this worktree, then a disposable teacher in a new org_runtime_answer_sheet_* tenant created an offline draft, three immutable snapshots, and a caller-owned template with:
json
{
"includeAnswerSheet": true,
"answerSheetMode": "separate-page"
}GET /v1/exams/{id}/print-docx?templateId={templateId} returned a valid DOCX ZIP with X-Exam-Print-Template-Version: 1. Its word/document.xml proves the normal question body precedes a DOCX page break and PHIẾU TRẢ LỜI; the three rows are Câu 1: [ ] A [ ] B for single choice, true/false boxes for each of two sub-items, and a write-in line for short numeric input. The isolated exam_* and ept_* records were deleted through Exam Service owner APIs after extraction. This is renderer/runtime proof only, not a default Gateway route, browser download, or legacy optical answer-sheet variant.
Durable Artifact Evidence
A later real Compose smoke used the isolated tenant org_runtime_print_export to create a draft exam, one native snapshot, and one caller-owned template, then called:
text
POST /v1/exams/{examId}/print-exportsThe response was 201 with a COMPLETED export, templateConfigVersion=1, a document asset id, and a document-service object key under:
text
org_runtime_print_export/generated-artifacts/exam-service/{exportId}/...The real exam_print_exports row had status=COMPLETED, the exact pinned template version, matching document asset id/object key, and size_bytes=3203. The matching hoctapaz_document_db.media_assets row had source_service=exam-service, source_kind=generated-artifact, the same key, and the DOCX content type/size. Streaming the artifact back from GET /v1/media-assets/{assetId}/content produced a valid 3,203-byte DOCX; unzip -t accepted all OpenXML parts. The content contained the runtime question text and Câu 1: A; word/footer1.xml contained the pinned footer.
GET /v1/exams/{examId}/print-exports returned the owner record. A different teacher in the same organization received 404 EXAM_NOT_FOUND, proving the exam ownership check happens before export listing. The persistent QA tenant is kept locally as evidence because no artifact-delete lifecycle is part of this bounded service contract.
BFF Adapter Evidence
bff-service was started independently at http://127.0.0.1:8102 with the existing Compose exam-service and document-service upstreams. Using the same isolated tenant and teacher actor, a real request to:
text
POST /v1/bff/teacher/exams/{examId}/print-exportsreturned another COMPLETED export and document asset. The BFF list endpoint returned that exact record, and its content endpoint streamed a 3,133-byte DOCX that passed unzip -t for all OpenXML parts. The BFF route accepts an exportId, not a document asset id, and checks the actor-owned exam export through exam-service before reading document-service bytes.
The same path under /api/teacher/... returned 404 from BFF. No default gateway table was changed; authenticated browser download and rollback proof remain mandatory before public promotion.
Non-Default Gateway Rehearsal Evidence
After rebuilding BFF from the current worktree, a separate BFF-only smoke used org_runtime_bff_template and teacher_runtime_bff_template to create, list, and update ept_4e6e033f746001a19181206ca5533b14. The owner-scoped template version changed from 1 to 2 and the updated footer was returned by the BFF adapter, proving the BFF-to-exam-service template path at runtime.
On 2026-07-12, a temporary local api-gateway process listened on :18090 with deploy/gateway/routes.teacher-print-export-bff-native-localhost-example.json. Using a locally signed teacher bearer token for org_runtime_gateway_template, it created, listed, and updated ept_d2143164fe2b8879c520e5f050ba0c5b through /api/teacher/exam-print-templates. Gateway returned X-Hoctapaz-Gateway-Route: teacher-print-template-create and X-Hoctapaz-Gateway-Route-State: native_write; the version changed from 1 to 2. A STUDENT bearer token for the same organization received 403 AUTH_FORBIDDEN before BFF was called.
This is a non-default route rehearsal only. deploy/gateway/routes.json was not changed, and this proof does not claim an authenticated browser download, gateway export-content runtime, rollback drill, or default public cutover.
The frontend selects this owner workflow only with a stored Gateway session and ?teacherPrintGateway=native; the default Word-template route stays local.
Regression Commands
bash
GOTOOLCHAIN=go1.25.11 go test ./services/exam-service/... -count=1
GOTOOLCHAIN=go1.25.11 go test ./services/exam-service/internal/usecase -run TestRenderExamPrintDOCX -count=1
GOTOOLCHAIN=go1.25.11 go test ./services/document-service/... -count=1
GOTOOLCHAIN=go1.25.11 go test ./services/bff-service/... -count=1
make test-service-task-packs
pnpm docs:build
docker compose -f deploy/docker-compose.yml up -d --build --force-recreate \
exam-service-migrations document-service-migrations document-service exam-serviceRemaining Work
- Build the teacher/admin experience through BFF or gateway only, including an authenticated document artifact-read contract, then prove browser and rollback behavior before any default public route promotion.
- Add template-to-exam binding semantics only after the authoring UX and revision rules are specified.
- Add full source-DOCX equation-layout parity, unsupported-media conversion, legacy answer-sheet variants, and multi-paper shuffle as separate renderer parity slices.
Rich Content Contract Regression (2026-07-13)
The current source tree has focused unit and HTTP coverage for both DOCX output paths. A direct GET /v1/exams/{id}/print-docx route test passes a scoped resolver and confirms the package contains word/media/image1.png plus its rIdMedia1 relationship. The durable POST /v1/exams/{id}/print-exports use case likewise verifies that the uploaded artifact contains the same embedded image. Renderer tests additionally assert PNG content types, original image bytes, <w:drawing>, and OMML fraction output from formulaRefs[].latex.
Document-service tests reject missing internal tokens and mismatched organizations before object bytes are opened.
An independently named Compose project then ran the real Physics 28-question fixture through Import -> Question Bank -> BFF matrix -> Exam -> Word output. The artifact is output/qa/docx-physics-rich-print-runtime-20260713-213402. It produced a valid 933,058-byte DOCX with seven unique word/media image parts and seven <w:drawing> references, exactly matching the seven immutable snapshot media references. Of the 84 snapshot formula references, 62 emitted OMML and 22 emitted visible review fallbacks; the smoke requires those two counts to sum to the snapshot total, so no formula reference is silently lost. unzip -t passed before the project and its fresh volumes were removed.
The same runtime smoke now derives each non-empty solution_html by source question ID and requires semantic-text equality after canonical persistence and in both ONLINE and OFFLINE snapshots. It also requires every retained solution to occur in word/document.xml, rather than merely checking that the answer-and-solution appendix heading exists. The fresh artifact output/qa/docx-physics-solution-parity-runtime-20260713 passed with five source solutions at every boundary.
This is isolated owner/BFF runtime evidence only. It does not establish Microsoft Word visual parity for the 22 fallback formulas, an authenticated browser download, default Gateway promotion, or legacy-route replacement.
Isolated Owner Recheck (2026-07-13)
A published 28-question Physics ONLINE exam from the current Import -> Question Bank -> Exam workflow was rendered with a caller-owned template at config version 2. The flat renderer configuration used includeAnswerSheet=true and answerSheetMode=separate-page, matching the Teacher UI's Gateway/BFF request contract.
The owner API returned a valid 10,226-byte DOCX. unzip -t passed and word/document.xml contained the imported question, PHIẾU TRẢ LỜI, a page break before the answer sheet, and ĐÁP ÁN VÀ LỜI GIẢI. This is isolated service-owner proof; it does not promote a default Gateway route or establish visual parity in Microsoft Word.