eCourt-Doc-Edit/docs/plans/fr16-draft-generation-plan.md
Suphonchai Phoonsawat b3c1c667f7 Phase 12: draft generation (FR-16)
ไฟล์ร่าง (draft) — generate docx จาก template แบบ stateless ไม่สร้าง
Document/version; stamp เฉพาะ custom property TemplateId เพื่อให้ add-in
เห็นเป็น untracked; Start Versioning ส่ง form field templateId ให้ backend
ผูก template ตอน auto-create — เอกสารเริ่มที่ v1 (ไม่มี v0) + field sync เต็มระบบ

- Backend: POST /api/templates/{id}/generate-draft (200 คืนไฟล์) +
  DocxGenerationService.GenerateDraft + แยก TemplateFieldValidation ใช้ร่วมกับ
  generate + UploadVersion รับ templateId (non-GUID/unknown → 400,
  deactivated → ผูกได้, document เดิม → เฉย) + CORS expose Content-Disposition
- Add-in: readTrackedProperties อ่าน TemplateId + ส่ง templateId เฉพาะ
  startVersioning + hint ไฟล์ร่างใน task pane
- Web: ปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม + downloadFile รับ init (POST → blob)
- Docs: spec v8 (FR-16, AC-19) + Phase 12 + CONTEXT.md + ADR 0002 +
  acceptance-review AC-19
- Tests: backend 82 / add-in 66 / web 59 + build ครบ; QA reviewer PASS
  (2 rounds); ค้าง manual บน Word จริง

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-10 10:51:13 +07:00

15 KiB
Raw Permalink Blame History

แผนงาน: ไฟล์ร่าง (draft) — generate docx จาก template โดยยังไม่สร้าง version (FR-16 / spec v8 / Phase 12)

สถานะ: implement แล้ว (2026-09-10) — ติดตามผลที่ officejs-implementation-plan.md Phase 12 และ acceptance-review.md AC-19 (ค้าง manual บน Word จริง) แผนนี้เก็บไว้เป็นหลักฐานการตัดสินใจของ grill session — รายละเอียด decision เพิ่มที่ docs/adr/0002-draft-stateless.md

Context

ปัจจุบันการสร้างเอกสารจาก template ผ่านเว็บ (POST /api/documents/generate, FR-10) สร้าง Document + field values + v0 ฉบับตั้งต้น ทันที แม้ผู้ใช้จะยังไม่เคยเปิดไฟล์ใน Word

Flow ใหม่: ฟอร์มเว็บกรอกค่า → กด "สร้างไฟล์ร่าง" → API คืน docx → เปิดใน Word → add-in เห็น untracked → กด Start Versioning → เกิด Document (ผูก template) + v1 + field sync

การทบทวน version numbering (ไม่ต้องแก้โค้ด): UploadVersion คำนวณ (max ?? 0) + 1 ที่ backend/WordVersioning.Api/Controllers/DocumentsController.cs:140-144 — Document ใหม่ไม่มี version → max = null → v1 อัตโนมัติ มี test เดิมยืนยันแล้ว · เพิ่ม test ยืนยันผ่าน TemplateLinkingTests (ด้านล่าง)

Decisions (จาก grill session — final)

# หัวข้อ การตัดสินใจ
1 การเก็บ draft Stateless — POST คืน docx stream ตรงๆ ไม่สร้าง Document/version ไม่แตะ MinIO · เว็บ browser-download · ไม่มีปุ่ม "เปิดใน Word" (ms-word:ofe ต้องการ URL ซึ่ง draft ไม่มี)
2 Stamping Stamp custom property TemplateId เท่านั้น — ไม่มี SavedByAddIn/DocumentTrackingId (กันสถานะ "tracking + history 404" ซึ่ง add-in จัดการไม่ได้ในปัจจุบัน)
3 กลไกผูก template ตอน Start Versioning ขยาย POST /api/documents/{documentId}/versions เพิ่ม form field ทางเลือก templateId — ใช้เฉพาะตอน auto-create Document; document มีอยู่แล้ว → เฉย (validate ทุกครั้งที่ส่งมา)
4 Validation upload: ไม่ใช่ GUID → 400 · ไม่พบ template → 400 · deactivated → ผูกได้ · draft endpoint เอง: unknown → 404, deactivated → 400 ตามนโยบาย FR-10 (deactivated สร้างไฟล์ใหม่ไม่ได้ แต่ draft เก่าที่สร้างตอน active ผูกต่อได้)
5 Web UI คง flow v0 + เพิ่มปุ่ม "สร้างไฟล์ร่าง" บนฟอร์มเดิม · re-generate (FR-12) ใช้ v0 flow เดิม
6 ศัพท์ "ไฟล์ร่าง (draft)" ทั้งระบบ (UI/spec/CONTEXT.md)

Endpoint design

POST /api/templates/{templateId:guid}/generate-draft (ใน TemplatesController — draft เป็น stateless function ของ template; controller นี้มี DI ครบและมี precedent sub-resource {id}/documents แล้ว)

  • Request: GenerateDraftRequest { [Required] JsonElement Values } (ใน Models/TemplateDtos.cs) — ไม่มี message (ไม่มี version จึงไม่มี commit message)
  • Response: 200 FileContentResult — File(bytes, docxContentType, BuildGeneratedFileName(template.Name, now)) (ไม่ใช่ 201 — ไม่มีอะไรถูกสร้าง)
  • Status: unknown template → 404 · !IsActive → 400 · values ผิด format/required/type → 400 (ข้อความเดิมของ Generate) · template file หายใน MinIO/corrupt → 502

Implementation

1. Backend

  1. Services/AddInMetadata.cs — เพิ่ม TemplateIdPropertyName = "TemplateId"
  2. Services/DocxGenerationService.cs — แยก SDT-replace loop ของ Generate เป็น private static ReplaceSdtValues(...) ใช้ร่วม · เพิ่ม byte[] GenerateDraft(Stream templateStream, IReadOnlyDictionary<string,string> values, Guid templateId) (replace เหมือนเดิม + stamp เฉพาะ TemplateId) · ย้าย BuildGeneratedFileName จาก DocumentsController มาเป็น public static ที่นี่
  3. Services/TemplateFieldValidation.cs (ไฟล์ใหม่) — ยก required/type validation block จาก Generate (~lines 260-285) มาเป็น static string? Validate(IEnumerable<TemplateField>, IReadOnlyDictionary<string,string?>) (ข้อความ error ไทยต้องเหมือนเดิมเป๊ะ — GenerateTests assert ไว้)
  4. Controllers/DocumentsController.cs:
    • Generate เปลี่ยนไปใช้ validator + filename ที่แยกออกมาแล้ว
    • UploadVersion: early validation request.TemplateId (ไม่ใช่ GUID → 400) → หลัง doc lookup ถ้ามี templateId ให้ db.Templates.AnyAsync (ไม่พบ → 400; deactivated ผ่าน) → ใส่ TemplateId = templateIdToLink ใน auto-create initializer (~lines 105-111) ก่อน branch doc.TemplateId is not null (~line 120) ไม่งั้น field sync จะหลุดเป็น raw-map path
  5. Models/VersionDtos.cs — UploadVersionRequest เพิ่ม string? TemplateId
  6. Controllers/TemplatesController.cs — action ใหม่ GenerateDraft(Guid id, [FromBody] GenerateDraftRequest, CancellationToken): load template (404/400) → load field defs → FieldMapJson.TryNormalize → TemplateFieldValidation.Validate → load template stream (502) → GenerateDraft(...) → log → File(...)
  7. Program.cs — CORS เพิ่ม .WithExposedHeaders("Content-Disposition") (จำเป็น — draft download อ่านชื่อไฟล์ไทยจาก header นี้ผ่าน fetch; ปัจจุบันไม่ได้ expose ชื่อจะหายเงียบๆ — integration tests จับไม่ได้เพราะ same-origin)

2. Add-in

  1. addin/src/services/metadataService.ts — TEMPLATE_ID_PROPERTY + templateId?: string ใน TrackedDocumentProperties + อ่านเป็น property ที่ 5 ใน readTrackedProperties() (Word.run เดิม)
  2. addin/src/services/versioningService.ts — uploadVersion(..., pdf?, templateId?) (trailing param, caller เดิมไม่กระทบ) append form field เมื่อมีค่า
  3. addin/src/services/versionFlow.ts — startVersioning(message?, templateId?, onProgress?) (แทรกก่อน callback — TS จะจับ call site เก่าทั้งหมดให้แก้) · saveAndVersion ไม่ส่ง templateId
  4. addin/src/taskpane/components/App.tsx — state documentTemplateId · restructure detect() เป็น readTrackedProperties() ครั้งเดียวแล้ว derive tracked เอง (ถ้าคงเรียก isTrackedDocument() ก่อน ไฟล์ร่าง untracked จะไม่ได้อ่าน TemplateId เลย) · เคลียร์ state ตอน sign-out/detect ใหม่ · untracked phase แสดง hint เมื่อเป็นไฟล์ร่าง: "ไฟล์นี้เป็นไฟล์ร่างจาก template — กดเริ่มติดตามแล้วระบบจะผูกกับ template ต้นทางและ sync ค่า fields ให้อัตโนมัติ" · handleStartVersioning ส่ง documentTemplateId ?? undefined

3. Web

  1. web/app/composables/useApi.ts — downloadFile(path, fallbackName, init: RequestInit = {}) ส่ง init ต่อให้ request (รองรับ POST+JSON→blob; ปัจจุบัน GET-only และไม่มี caller — non-breaking)
  2. web/app/pages/documents/create/[templateId].vue — state draftSubmitting/draftNotice · submitDraft(): validate ด้วย validateFieldValues เดิม → downloadFile('/api/templates/{id}/generate-draft', fallback, { method:'POST', body: JSON.stringify({ values }) }) → notice แนะนำเปิดใน Word แล้วกด Start Versioning · ฟอร์มอยู่ต่อ (ไม่มี result card, ไม่มีปุ่มเปิดใน Word) · ปุ่มรอง "สร้างไฟล์ร่าง" (type="button", disabled gating เดียวกับปุ่มหลักรวม !detail.isActive) · helper text ใต้ช่องคำอธิบาย "(ใช้กับการสร้างเอกสาร version 0 เท่านั้น — ไฟล์ร่างไม่มี version)" · resetForm() เคลียร์ draftNotice

4. Docs (repo rule: spec ก่อน แล้ว sync plan)

  1. specs/spec-officejs.md — v8 header note · ### FR-16: ไฟล์ร่าง (draft) — สร้าง .docx จาก template โดยไม่สร้าง Document (เพิ่มเติม v8) · section 8 metadata table เพิ่มแถว TemplateId · AC-19 · section เพิ่มเติม v8
  2. docs/plans/officejs-implementation-plan.md — append ## Phase 12: ไฟล์ร่าง (draft) — FR-16 (เพิ่มทีหลัง, v8) ท้ายไฟล์ (รูปแบบเดียวกับ Phase 11: ตาราง mapping + checklist + DoD)
  3. CONTEXT.md — glossary เพิ่ม: - **ไฟล์ร่าง (draft)**: ไฟล์ .docx ที่สร้างจาก template แบบ stateless — ยังไม่มี Document/version ในระบบ; ระบุ template ต้นทางผ่าน custom property TemplateId และเริ่ม track จริงตอนกด Start Versioning ใน Word (version 1)
  4. docs/adr/0002-draft-stateless.md (ไฟล์ใหม่ — format ตาม ADR 0001): Decision (stateless endpoint, stamp TemplateId ตัวเดียว, ผูก template ทีหลังผ่าน form field) + Alternatives rejected (สร้าง Document สถานะร่าง + lifecycle/cleanup / stamp tracked props เต็มชุด = สถานะ "tracking + 404" พัง / ปุ่ม "เปิดใน Word" สำหรับ draft) + Consequences (draft หาย = ไม่มีของค้างในระบบ, v1 ไม่มี v0, field sync เริ่ม v1, ต้อง expose Content-Disposition)
  5. docs/acceptance-review.md — แถว AC-19 · CLAUDE.md — sync สถานะ (FR-1–16, AC 1–19, Phase 0–12) + ย่อหน้าสถาปัตยกรรมสั้นๆ หลัง section FR-15

Tests

Backend (integration — Testcontainers, [Collection(IntegrationTestCollection.Name)]):

  • DraftTests.cs (ใหม่): happy path 200 + ค่า SDT ครบ (ใช้ TestDocx.ReadSdtText) + ReadCustomProperty("TemplateId") ตรง + SavedByAddIn/DocumentTrackingId เป็น null + DB ไม่เกิด Document/version ใดๆ · missing-required 400 · wrong-type 400 · unknown template 404 · deactivated 400 · extra keys ignored
  • TemplateLinkingTests.cs (ใหม่): upload ใหม่ + templateId → 201 VersionNumber=1, doc.TemplateId ชัด, AddInSignature="", DocumentFieldValue ครบ (SourceVersionId/UpdatedByUserId ถูก), snapshot non-null · upload ครั้งสอง (ไม่ส่ง) → v2 · ไม่ใช่ GUID → 400 · unknown GUID → 400 · deactivated → ผูกได้ · document เดิม + templateId → เฉย (TemplateId คงเดิม) · document เดิม + templateId เสีย → 400
  • TestDtos.cs — VersionForm เพิ่ม optional templateId ต่อท้าย (caller เดิมไม่กระทบ) · AuthTests.cs — เพิ่ม [InlineData("POST", "api/templates/{0}/generate-draft")]

Add-in (jest): metadataService อ่าน templateId ได้ (ปรับ expectation toEqual ของ readTrackedProperties เดิม) · versionFlow ส่ง templateId เฉพาะ startVersioning ไม่ใช่ saveAndVersion · versioningService append form field

Web (vitest): web/test/useApi.spec.ts (ใหม่ — pattern ตาม useVersionOpen.spec.ts): POST init ส่งถึง fetch + Bearer · 401 retry ครั้งเดียว · ชื่อไฟล์ filename* UTF-8 ไทย ไปอยู่ที่ anchor download · !ok → ApiError พร้อมข้อความจาก backend

Verification (DoD)

cd backend && dotnet test                    # เขียวทั้งชุด (รวม suite เดิมที่ไม่ถูกแตะ)
cd addin && npm test
cd web && npm test && npm run build

Manual บน Word for Mac: สร้างไฟล์ร่างจากเว็บ → ได้ .docx ชื่อไทยถูกต้อง → เปิดใน Word → task pane แสดง untracked + hint ไฟล์ร่าง → กด Start Versioning → ได้ v1 + หน้า document detail บนเว็บแสดง template และค่า fields · Save & Version ถัดไป → v2

Risks

  • Content-Disposition ไม่ถูก CORS-expose — จับไม่ได้ด้วย integration tests (same-origin) ครอบด้วย useApi.spec + manual check
  • ลำดับแทรกใน UploadVersion สำคัญ (TemplateId ต้อง set ก่อน field-sync branch) — TemplateLinkingTests คุมไว้
  • startVersioning เปลี่ยน positional signature — TS จับ call site ทั้งหมด แก้ให้ครบใน commit เดียวกัน
  • ไม่มี DB migration ใดๆ (ไม่แตะ schema — UploadVersionRequest.TemplateId เป็น form field, คอลัมน์ Document.TemplateId มีอยู่แล้ว)