Private Docs

JuristicBindingStep — คำนำหน้าองค์กร (Implementation Spec)

ขั้นตอนลงมือทำรายไฟล์ ต่อ repo ของงานเก็บคำนำหน้าองค์กร — branch/worktree, contract, migration, cherry-pick, IaC/APIM, ด่านตรวจบนเครื่อง และ definition of done

อัปเดต: 2026-09-10

0. กติการวมทุก repo

  • ฐาน branch = ตัด feature จาก uat ของแต่ละ repo (ยกเว้น lib Frontend_SuperAppLibraryUi ที่มีแต่ development และ IaC ที่ PR เข้า development) · ชื่อ branch ที่ใช้อยู่: feature/onboarding-crm-profile-with-address
  • ห้าม merge development กลับเข้า feature — UserService ต่าง 146 commit · HostApp 47 · IaC 1,632 จะติดขึ้น UAT
  • ชื่อ field ตามตารางในหัวข้อ 8 ของ Overview: custTitleNameCode / custTitleNameTh / custTitleNameEn / custTitleSource (API + FE + stepData) · CustTitleNameCode / CustTitleNameTh / CustTitleNameEn (คอลัมน์) · nameTitleTH / nameTitleEN (report)
  • code ที่ map ไม่เจอ → คืน code ดิบ ชื่อ null ห้าม throw
  • ทุก null field ไม่ต้องทำอะไรพิเศษ — เขียน null ลง stepData ตามปกติ (handler วันนี้เขียน companyId: null อยู่แล้ว ผู้อ่านทุกตัวใช้ TryGetProperty) ไม่ตั้ง JsonIgnoreCondition.WhenWritingNull เพราะเป็นการเปลี่ยน behavior ของ key เดิมโดยไม่จำเป็น
  • worktree ที่มีอยู่แล้ว (ตรวจ 10/09 ทุกตัว = HEAD ของ base ไม่มี diff ยกเว้น IaC):
repoworktreebaseหมายเหตุ
Backend_ThirdPartyService_worktrees/TPS-crmuat @ 065b55c
Backend_UserServiceBackend_UserService/.worktrees/crm-profile-with-addressuat @ f45664e
Frontend_SuperAppLibraryUi_worktrees/LibUi-crmdevelopment @ d872846repo นี้ไม่มี uat
Frontend_HostAppSuperAppFrontend_HostAppSuperApp/.worktrees/crm-profile-with-addressuat @ e27a7c8
Frontend_AdminSuperAppยังไม่มีตัดจาก origin/uat
Backend_ReportServiceยังไม่มีตัดจาก origin/development (ดูข้อ 6)
Backend_IacBackend_Iac/.worktrees/crm-profile-with-address🔴 ตัดจาก uat ผิดฐาน — ลบแล้ว re-cut จาก origin/developmentตรวจแล้ว merge-base --is-ancestor origin/uat HEAD = 0, origin/development = 1

1. Backend_ThirdPartyService

1.1 Config — src/ThirdParty01.API/appsettings.json (base เท่านั้น)

เพิ่ม key ใน section CommonApi (บรรทัด ~140) และ section ใหม่ CustomerTitle · ไม่แตะ appsettings.{Development,Sit,Uat}.json และ ไม่แตะ IaC — .NET merge base กับ env ต่อ key, overlay ทับเฉพาะไฟล์ env, parity script ไม่อ่าน base

"CommonApi": {
  "GetCustomerProfileWithAddress": "/api/v1/Customer/CustomerProfileWithAddress/{taxId}"
},
"CustomerTitle": {
  "Names": [
    { "Code": "01", "NameTh": "บจก.", "NameEn": null },
    { "Code": "06", "NameTh": "องค์กร", "NameEn": "ORGANIZATION" }
  ]
}

ใส่ให้ครบ 24 code โดยคัดลอกทั้ง array จากไฟล์ Atlas/docs/.dev-team/runs/onboarding-crm-profile-with-address/logs/customer-title-names-appsettings.json (10 code ไม่มี NameEn · ชื่อยาวสุด 39 ตัวอักษร “กิจการร่วมค้า”)

1.2 Domain — src/ThirdParty04.Domain/

  • Common/DTOs/Common/CommonCustomerProfileWithAddressDto.cs (ใหม่) — ประกาศ property เดียว public string? CustTitleNameCode { get; set; } · ห้าม reuse CommonCustomerProfileDto (payload ใหม่มี custBirthDate: "19901221" และ temp: 1 จะโยน JsonException ที่ :111, :316)
  • Ports/Gateway/ICommonApiClient.cs — เพิ่ม Task<CommonApiResponse<List<CommonCustomerProfileWithAddressDto>>> GetCustomerProfileWithAddressAsync(string taxId, CancellationToken ct) ต่อจาก 3 method เดิม (:22,32,42)
  • Common/Options/CustomerTitleOptions.cs (ใหม่) — public sealed class CustomerTitleOptions { public List<CustomerTitleName> Names { get; set; } = []; } + record CustomerTitleName(string Code, string NameTh, string? NameEn)

1.3 Infrastructure — src/ThirdParty03.Infrastructure/Gateway/CommonApiClient.cs

  • implement GetCustomerProfileWithAddressAsync แบบเดียวกับ GetMasterLimitAsync (:126-172): อ่าน configuration["CommonApi:GetCustomerProfileWithAddress"] · แทน {taxId} · deserialize ด้วย envelope แบบทน ที่ ignore message (ทำ CustomerProfileWithAddressEnvelope คู่กับ MocEnvelope :324 / MasterLimitEnvelope :340)
  • คีย์ที่ไม่มีจริง CommonApi ตอบ HTTP 200 {"isSuccess":true,"data":[]} — ต้องคืน list ว่าง ไม่ throw
  • ใส่ comment นี้เหนือ method (คัดลอกตรงตัว):
// ชั่วคราว: เส้นนี้ยิงเพื่อเอา custTitleNameCode อย่างเดียว
// วันที่ย้าย GetCustomerByCustcode (CRM) มาใช้ CustomerProfileWithAddress แล้ว
// ให้อ่านคำนำหน้าจาก call นั้นแทน แล้วลบ method นี้ + GetCustomerTitleByTaxIdQuery ทิ้ง
  • DI: services.Configure<CustomerTitleOptions>(configuration.GetSection("CustomerTitle")) ในที่เดียวกับ options อื่นของ Infrastructure

1.4 Application — query กลางตัวเดียว

src/ThirdParty02.Application/Features/CustomerTitle/Queries/GetCustomerTitleByTaxId/

  • GetCustomerTitleByTaxIdQuery(string TaxId) : IQuery<Result<CustomerTitleResponse>>
  • CustomerTitleResponse(string? CustTitleNameCode, string? CustTitleNameTh, string? CustTitleNameEn, string? CustTitleSource)
  • GetCustomerTitleByTaxIdHandler ลำดับ:
    1. GetCustomerProfileWithAddressAsync(taxId) → ถ้าสำเร็จและ data มีแถว → code = data[0].CustTitleNameCode, source = "CustomerProfileWithAddress"
    2. ถ้า throw / ไม่สำเร็จ / data ว่าง → GetCustomerProfileByTaxIdAsync(taxId)code = data[0].CustTitleNameCode, source = "CustomerProfile" (log Warning ว่า fallback)
    3. ทั้งสองไม่มี code → คืน 4 field เป็น null (ไม่ใช่ Failure)
    4. map ชื่อจาก IOptions<CustomerTitleOptions> — ไม่เจอ code → คืน code ดิบ ชื่อ null
  • ⚠️ ผลข้างเคียงที่ยอมรับ: ใน flow ค้นหา ถ้าเข้า fallback จะยิง CustomerProfile ซ้ำเป็นครั้งที่ 2 (call ที่ 1 ยิงไปแล้วเป็น root) — ยอมเพราะ flow sync ไม่มี call ที่ 1 ให้ยืม และ query ต้องเป็นตัวเดียวกันทั้งสอง flow
  • comment เดียวกับข้อ 1.3 ใส่หัว handler

1.5 Flow ค้นหา — Features/FxCustomerRegistrationProfile/

  • Queries/GetFxCustomerRegistrationProfile/GetFxCustomerRegistrationProfileHandler.cs — เพิ่ม var titleTask = SendSafe(new GetCustomerTitleByTaxIdQuery(query.TaxId), ct); ต่อจาก businessTypeTask (:68-70) ไม่กั้นด้วย hasCustCode · รวมเข้า Task.WhenAll เดิม · ถ้า titleTask Failure → 4 field null และ ไม่เพิ่มค่าใน failedSections
  • DTOs/FxCustomerRegistrationProfileDtos.cs:19FxCustomerRegistrationProfileResponse เพิ่ม 4 property ต่อท้าย: string? CustTitleNameCode, string? CustTitleNameTh, string? CustTitleNameEn, string? CustTitleSource

1.6 Flow sync — Features/OrganizationJuristicPerson/Queries/GetOrganizationJuristicPersonFullAccess/

  • GetOrganizationJuristicPersonFullAccessHandler.cs — handler นี้ยังไม่ inject IMediator (:16-20 มีแค่ repository / client / ICacheService / IUnitOfWork / logger) ⇒ เพิ่ม IMediator เข้า constructor แล้วเรียก _mediator.Send(new GetCustomerTitleByTaxIdQuery(juristicId), ct) ใน try/catch ที่ log Warning และคืน null (SendSafe ของ fx handler เป็น private ใช้ข้ามไฟล์ไม่ได้ — คัดลอก pattern มา หรือดึงออกเป็น helper กลางก็ได้) · ยิง หลัง ส่วนที่อ่าน/เขียน cache (:109, :174) ทุกครั้ง แม้ cache hit — ผล title ห้ามเข้าไปอยู่ใน object ที่ถูก _cacheService.SetAsync · Failure → 3 field null (US จะไม่รู้ว่าล่ม ตามมติ)
  • GetOrganizationJuristicPersonFullAccessResponse (ไฟล์ GetOrganizationJuristicPersonFullAccessQuery.cs) — เพิ่ม CustTitleNameCode / CustTitleNameTh / CustTitleNameEn (ไม่ต้องมี source)

1.7 ตรวจ

dotnet build src/ThirdParty01.API -c Release 2>&1 | grep -E "error|Warn.*CS|Build succeeded"
dotnet test tests -c Release 2>&1 | grep -E "Passed!|Failed!|error"
# รัน local ชี้ commonuat แล้ว
curl -s localhost:5000/api/v1/Customer/fx-registration-profile/0105533146651 | jq '{custTitleNameCode,custTitleNameTh,custTitleNameEn,custTitleSource}'
# คาด: {"custTitleNameCode":"01","custTitleNameTh":"บจก.","custTitleNameEn":null,"custTitleSource":"CustomerProfileWithAddress"}
# ตัด fallback: ตั้ง CommonApi:GetCustomerProfileWithAddress เป็น path มั่ว แล้วยิงซ้ำ → custTitleSource = "CustomerProfile"

unit test ที่ต้องมี: query กลาง 4 เคส (5 สำเร็จ / 5 ว่าง→1 / 5 throw→1 / ทั้งคู่ไม่มี code) + map ไม่เจอ code


2. Backend_UserService

2.1 Domain — src/UserService04.Domain/

  • Entities/Companies/Company.cs
    • property ใหม่ 3 ตัว string? CustTitleNameCode, string? CustTitleNameTh, string? CustTitleNameEn
    • Create(...) (:82-98) — เพิ่ม optional parameter 3 ตัว default null ต่อจาก custCode
    • Update(...) (:159-174) — เพิ่ม optional parameter 3 ตัว default null ต่อจาก custCode และ guard แบบเดียวกับ CustCode (:189-191): if (!string.IsNullOrWhiteSpace(custTitleNameCode)) { CustTitleNameCode = ...; CustTitleNameTh = custTitleNameTh?.Trim(); CustTitleNameEn = custTitleNameEn?.Trim(); } — ทับเป็นชุดเมื่อมี code ใหม่ · ไม่มี code = ไม่แตะทั้ง 3 (กัน sync ที่ title ล่มมาล้างของเดิม)
  • Ports/Gateway/ViewModel/OrganizationJuristicPersonViewModel.cs — เพิ่ม 3 property รับจาก TPS

2.2 Infrastructure — EF (ทำบน branch MIGRATION เท่านั้น ดู 2.6)

Persistence/Configurations/Companies/CompanyConfiguration.cs — 3 คอลัมน์ nullable · CustTitleNameCode HasMaxLength(10) · CustTitleNameTh / CustTitleNameEn HasMaxLength(100) (ชื่อยาวสุดใน mapping = 39) · ห้าม CHECK constraint · ห้าม index

2.3 Application — src/UserService03.Application/Features/

  • Onboarding/Engine/Handlers/Requestor/JuristicBindingStepHandler.cs — อ่าน 4 key ด้วย StepInputReader.GetStringStrict(root, "custTitleNameCode") ฯลฯ (:46 pattern เดียวกับ custCode) แล้ว เพิ่มทั้ง 4 ลง anonymous object ที่ :60-73 — ไม่เพิ่ม = ค่าหายเงียบ
  • Onboarding/Engine/Handlers/PostApproval/CreateCompanyApplicationHandler.cs
    • ParseJuristicBinding (:424) — เปลี่ยน Get() เป็น root.TryGetProperty(p, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : ""
    • JuristicBindingData record — เพิ่ม 3 field · อ่าน custTitleNameCode/Th/En จาก stepData
    • ส่งต่อทั้งใน Company.Create (:54) และ company.Update (:60)
  • Companies/Commands/SyncCompany/SyncCompanyHandler.cs — ส่ง 3 ค่าจาก data.CustTitleName* เข้า Company.Create (branch :159) และ existing.Update (:169-183) · ไม่ต้องเช็ค null เอง guard อยู่ใน entity
  • Companies/Commands/SyncCompany/SyncCompanyDtos.cs:50-59SyncCompanyResponse เพิ่ม 3 field
  • Onboarding/Dtos/ApplicationReportDto.csApplicationReportCompanyDto เพิ่ม string? NameTitleTH, string? NameTitleEN (serialize เป็น nameTitleTH / nameTitleEN ตามที่ RS ใช้)
  • Onboarding/ApplicationReportMapper.cs:60-76MapCompany เติม Str(j, "custTitleNameTh")NameTitleTH, Str(j, "custTitleNameEn")NameTitleEN
  • ทุกที่ที่คืน company ให้ FE (/users/mecompanies[], GET /company/...) — เพิ่ม 3 field ใน DTO ที่ corporate-profile.state.ts:14-33 อ่าน (authService.user()?.companies) · หา DTO ด้วย grep -rn "CustCode" src/UserService03.Application --include=*Dto*.cs

2.4 Migration — runbook branch MIGRATION (hook guard-migration.js:65,86 ดูแค่ชื่อ branch)

git branch -m MIGRATION MIGRATION-dev            # เก็บตัวเดิม (สาย development) ไว้
git checkout -b MIGRATION feature/onboarding-crm-profile-with-address
# port เฉพาะ Company.cs + CompanyConfiguration.cs (ห้ามพา handler/DTO)
export VSS_NUGET_EXTERNAL_FEED_ENDPOINTS='{"endpointCredentials":[{"endpoint":"<feed>","password":"<PAT>"}]}'
export ConnectionStrings__Default="Host=localhost;Database=x;Username=x;Password=x"   # host ปลอมให้ design-time factory ผ่าน
dotnet ef migrations add AddCompanyCustTitle -p src/UserService02.Infrastructure -s src/UserService01.API
dotnet ef migrations has-pending-model-changes -p src/UserService02.Infrastructure -s src/UserService01.API   # คาด: No changes
git checkout feature/onboarding-crm-profile-with-address && git merge MIGRATION

ตรวจ Up() มีแค่ AddColumn 3 ตัว · ห้าม push MIGRATION ขึ้น origin (มี origin/MIGRATION ของสาย development อยู่แล้ว) · ตอน merge feature เข้า development ทีหลัง resolve AuthDbContextModelSnapshot.cs ให้เป็นผลรวมสองฝั่ง บน branch MIGRATION สาย development เกณฑ์ = has-pending-model-changes = none

2.5 ตรวจ

dotnet build src/UserService01.API -c Release 2>&1 | grep -E " error |Build succeeded"
# test ที่แตะ JuristicBinding / SyncCompany / CreateCompanyApplication / ApplicationReport (6 ไฟล์) + Postgres tests
export USERSERVICE_COVERAGE_POSTGRES_CONNECTION="Host=localhost;Port=5432;Database=us_test;Username=<superuser ไม่ใช่ postgres>;Password=..."
dotnet test tests/UserService05.Tests -c Release 2>&1 | grep -E "Passed!|Failed!"

unit test ที่ต้องมี: JuristicBindingStepHandler เขียน 4 key ลง output · ส่ง custTitleNameCode เป็นตัวเลข → null ไม่ throw · Company.Update code ว่างไม่ล้างของเดิม / code ใหม่ทับทั้ง 3 · ParseJuristicBinding token ไม่ใช่ string → ไม่ throw


3. Frontend_SuperAppLibraryUi (@exim/onboarding)

ฐาน = development @ d872846 (HostApp uat pin ^1.11.14 = 001dab2 · ระหว่างนั้นแตะ libs/onboarding แค่ 77567df SonarQube fix) · version ใหม่ = 1.11.15

ไฟล์ใต้ libs/onboarding/src/lib/

  • models/onboarding.types.ts:13CompanyInfo เพิ่ม custTitleNameCode: string; custTitleNameTh: string; custTitleNameEn: string; custTitleSource: string; (string ว่างเมื่อไม่มี ให้เหมือน custCode)
  • engine/engine.models.ts:129JuristicLookupResult เพิ่ม 4 field รับจาก TPS
  • steps/juristic-binding/juristic-binding.labels.tsJuristicBindingLabels.rows เพิ่ม label “คำนำหน้า” (แสดง custTitleNameTh ถ้ามี ไม่มีแสดง code ดิบ)
  • steps/juristic-binding/juristic-binding.mapper.ts — ทั้งทาง rawData และทาง flat key
  • steps/juristic-binding/modes/fx/juristic-binding-fx.state.ts
    • toStepData (:70) — เพิ่ม 4 key · ค่าเป็น string เสมอ ("01" ไม่ใช่ 01)
    • resume path (:175-183) — ต่อยอด pattern custCode: if (id && !('custTitleNameCode' in parsed))lookupJuristic(id) แล้ว set 4 field
  • steps/juristic-binding/modes/fx/juristic-binding-fx.component.html + modes/admin/juristic-binding-admin.component.html / .state.ts — แถวคำนำหน้าใต้ชื่อบริษัท
  • package.json version → 1.11.15

ตรวจ

pnpm nx test onboarding 2>&1 | grep -E "Tests:|failed"
pnpm nx build onboarding 2>&1 | grep -E "error|Successfully"
pnpm publish:onboarding          # nx build + nx release publish --projects=onboarding

4. Frontend_HostAppSuperApp

ฐาน uat @ e27a7c8 · งาน 2 ส่วน

4.1 bump lib

package.json:52 "@exim/onboarding": "^1.11.15"pnpm install → commit pnpm-lock.yaml

4.2 ย้าย PDF ไปเรียก ReportService — cherry-pick 2 commit (ไม่ clean: ชน 1 + 2 ไฟล์)

git cherry-pick -n d60f202
# ชน 1 ไฟล์: modify/delete fx-application.template.ts → รับ deletion
git rm apps/shell/src/app/reports/fx-application/fx-application.template.ts
git status --short | grep -v "^[MADR] " ; git commit -m "refactor(shell): move FX application PDF to report-service (cherry-pick d60f202)"

git cherry-pick -n e20257b
# ชน 2 ไฟล์: package.json + pnpm-lock.yaml (hunk = bump @exim/ui-kit ^1.8.26→^2.0.0 แต่ uat อยู่ ^2.0.2 แล้ว) → ยึด uat
git checkout --ours package.json pnpm-lock.yaml && git add package.json pnpm-lock.yaml
# ตัดของที่ไม่เกี่ยว PDF ออก
git checkout HEAD -- apps/shell/src/app/pages/corporate-team/corporate-team.html
git commit -m "fix(shell): point report API at report-api path (cherry-pick e20257b, env + service only)"

หลังทำ: apps/shell/src/environments/environments.uat.ts ต้องมี key report = ${servicePathPrefix}report-api/api/report-service (prefix บน uat = 'uat-' ⇒ path จริง uat-report-api/api/report-service) · application-tracking.service.ts ใช้ reportBaseUrl เรียก reports/fx-application/{id}/pdf · document.ts ไม่ import buildFxApplicationDocDefinition แล้ว · pdfmake / @exim/pdf-sdk ยังค้างใน package.json ได้ ไม่ต้องถอนรอบนี้

ตรวจ

pnpm nx build shell --configuration=uat 2>&1 | grep -E "error|Application bundle generation complete"
pnpm nx test shell --testPathPatterns="document|application-tracking|juristic" 2>&1 | grep -E "Tests:|Suites:"   # ตรวจจำนวน suite ไม่ใช่แค่ exit code

รันบนเครื่องผ่าน proxy ชี้ dev/uat แล้วกดจริง: ค้นหาบริษัท → เห็นแถวคำนำหน้า · resume draft เก่า → แถวโผล่หลัง lookup · corporate-profile → กด sync → แถวเปลี่ยนตามค่าใหม่ · ปุ่มโหลดใบสมัคร → ได้ PDF จาก report-service


5. Frontend_AdminSuperApp

ฐาน origin/uat · งาน = bump อย่างเดียว: package.json:36 "@exim/onboarding": "^1.11.15"pnpm install → commit lock

ก่อนเปิด PR (ระยะกระโดด 03515d5001dab2 = 16 commit · 26 ไฟล์ · +490 −44 ใน libs/onboarding)

git -C ../Frontend_SuperAppLibraryUi log --oneline 03515d5..HEAD -- libs/onboarding   # อ่านทุกบรรทัด หาอะไรที่แตะ admin mode / registry
pnpm nx build admin --configuration=uat 2>&1 | grep -E "error|complete"
pnpm nx test admin 2>&1 | grep -E "Tests:|Suites:"

กดจอจริง: service-request-management-detail (render OnboardingComponent) + flow ที่ใช้ OnboardingReview · เจอ breaking change ที่ไม่เกี่ยวงานนี้ → หยุด กลับมาคุย


6. Backend_ReportService

origin/uat = ed4094b "Added README.md" (= origin/sit) ตามหลัง development 14 commit — template ไม่มีบน uat เลย

6.1 promote + เพิ่มแถว

git checkout -b feature/onboarding-crm-profile-with-address origin/development   # ฐาน development เพราะ uat ว่าง
  • src/domain/models/fx-application.model.ts:43-57FxApplicationCompany เพิ่ม nameTitleTH: string | null; nameTitleEN: string | null;
  • src/infrastructure/pdf/templates/fx-application/fx-application.template.ts:206-211 — ก่อนแถว companyNameTh / companyNameEn เพิ่มแถวคำนำหน้า · ซ่อนแถวเมื่อ null ไม่พิมพ์ช่องว่าง
  • fx-application.message.ts — label ใหม่
  • fx-application.template.spec.ts — snapshot/assert แถวใหม่ทั้งกรณีมีค่าและ null
  • RS ไม่มี mapper แยก — JSON จาก US application-report ไหลเข้า model ตรง ⇒ ชื่อ nameTitleTH / nameTitleEN ต้องตรงกับ US ข้อ 2.3

PR เข้า uat (pipeline pipelines/ci-uat.yml trigger uat แล้ว patch src/yamls/uat/superapp/deployments/report-deployment.yaml ใน IaC — ci-uat.yml:147-148 ⇒ manifest ข้อ 7 ต้องชื่อนี้เป๊ะ)

6.2 ตรวจ

pnpm test 2>&1 | grep -E "Tests:|Suites:"
pnpm build 2>&1 | grep -E "error|webpack compiled|Nest"
# รัน local ชี้ USER_SERVICE_BASE_URL ไป US บนเครื่อง แล้ว
curl -s -o out.pdf -b "$COOKIE" localhost:3000/api/report-service/v1/reports/fx-application/<instanceId>/pdf && pdftotext out.pdf - | grep -n "บจก\|คำนำหน้า"

🔴 เทียบ PDF ที่ RS วาดกับที่ FE วาดวันนี้ ใบต่อใบ (ฟอนต์ ระยะ การขึ้นหน้า) ก่อนเปิด PR — ยังไม่ verify ว่าตรงกันแค่ไหน


7. Backend_Iac — manifest report-service สำหรับ uat

ฐาน = origin/development (ลบ worktree เดิมที่ตัดจาก uat แล้ว re-cut) · PR เข้า developmentci-orchestrate.yml:39 pin ref: refs/heads/development และ ArgoCD src/argocd/apps/superapp-uat-application.yaml:14-15 sync src/yamls/uat/superapp จาก development

ลอก 5 ไฟล์จาก src/yamls/dev/superapp/ ไป src/yamls/uat/superapp/ ชื่อเดิม:

ไฟล์ค่าที่ต้องเปลี่ยน (อ้าง uat/superapp/deployments/fx-report-deployment.yaml ที่มีอยู่)
deployments/report-deployment.yamlimage: …/report-service:uat-<sha> (pipeline patch เอง) · APP_ENV=uat · NODE_ENV=production · USER_SERVICE_BASE_URL=http://user-service.superappuat.svc.cluster.local · AUTH_JWKS_ISSUERS เท่า dev · AUTH_JWT_AUDIENCES = ค่าเดียวกับ uat/…/fx-report-deployment.yaml:62-63 (cf5521c5-…) — ใช้กฎ “ลอกจาก fx-report” ได้เพราะบน dev ทั้ง report-deployment.yaml และ fx-report-deployment.yaml ใช้ audience ชุดเดียวกัน (dba195d4-…) ตรวจแล้ว 10/09 · namespace superappuat
hpas/report-hpa.yamlnamespace
ingresses/report-service-ingress.yamlhost: api.sua-az-uat.internal · path /api/report-service(/|$)(.*) · konghq.com/strip-path: "false" ตาม fx-report-service-ingress.yaml
secret-providers/report-secret-provider.yamlobjectName: uat--OpenTelemetry--OtlpEndpoint (มีใน KV แล้ว — provider อื่นของ uat ใช้อยู่) · secretName: report-service-secrets
services/report-service.yamlnamespace

report-service-secrets บน uat ยังไม่มี ⇒ CSI สร้างใหม่ได้ key ครบ ไม่ติดปัญหา “CSI ไม่เติม key ให้ Secret ที่มีอยู่แล้ว”

ตรวจ: kubectl -n superappuat get deploy report-service READY 1/1 · kubectl -n superappuat logs deploy/report-service | grep -i "listening\|error" · curl -s http://api.sua-az-uat.internal/api/report-service/health (จาก pod ใน cluster)


8. APIM — uat-report-api (ทำด้วยมือ ไม่มีใน IaC)

APIM exim-az-hub-apim (rg exim-azure-nonprd-hub-connectivity, subscription EximNonPrdHubSua) · ตรวจ 10/09 ด้วย az rest:

  • dev มี id report-service-api path report-api → ใช้เป็นต้นแบบ
  • uat มี id uat-report-service-api แต่ path uat-fxreport-api (fx-report คนละ service) ⇒ id ใหม่ห้ามซ้ำ ใช้ uat-report-api path uat-report-api
  • path ต้อง = servicePathPrefix + 'report-api' ที่ HostApp uat ใช้หลัง cherry-pick (environments.uat.ts:1 prefix 'uat-')
  • backend = ingress uat http://api.sua-az-uat.internal/api/report-service · policy ลอกจาก report-service-api (dev) แล้วเปลี่ยน env — ยังไม่ verify ว่าต้องมี sentinel facade (cookie→JWT) แบบ sentinel-facade-inbound-v6.xml หรือไม่ ต้องอ่าน policy ของ dev ก่อน
  • 🔴 PUT ราย operation ห้าม import ทับ (บทเรียน AMLO ขึ้น UAT 31/08)

ตรวจ: curl -s -b "$UAT_COOKIE" https://<apim-host>/uat-report-api/api/report-service/v1/reports/fx-application/<id>/pdf -o out.pdf -w "%{http_code}" = 200 และไฟล์เป็น PDF


9. DDL บน UAT_AuthDb (คนยิงเอง — pipeline ไม่มี step apply migration)

ห้าม dotnet ef database update (design-time factory resolve connection เอง) · ท่าที่พิสูจน์แล้ว 31/08/2026:

dotnet ef migrations script --idempotent --no-transactions -p src/UserService02.Infrastructure -s src/UserService01.API -o /tmp/AddCompanyCustTitle.sql

# guard + script ต่อกันเป็น stream เดียวแล้ว pipe เข้า psql ใน container
# (ห้ามใช้ \i — path บนเครื่องเรามองไม่เห็นจากใน container)
GUARD="DO \$\$ BEGIN IF current_database() <> 'UAT_AuthDb' THEN RAISE EXCEPTION 'wrong db: %', current_database(); END IF; END \$\$;"
{ printf '%s\n' "$GUARD"; cat /tmp/AddCompanyCustTitle.sql; } \
  | docker run --rm -i postgres:16 psql "$UAT_AUTHDB_CONN" -v ON_ERROR_STOP=1

docker run --rm -i postgres:16 psql "$UAT_AUTHDB_CONN" -c "SELECT column_name FROM information_schema.columns WHERE table_name='Companies' AND column_name LIKE 'CustTitle%';"
# คาด 3 แถว

script จาก --idempotent ห่อทุก migration ด้วย IF NOT EXISTS (SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '…') อยู่แล้ว ⇒ รันซ้ำได้ปลอดภัย และตอน promote dev→uat รอบถัดไป (ที่มี 20260904111245_AddAmloCompanyUnblock) จะ apply เฉพาะตัวที่ยังไม่มี ตราบใดที่ไฟล์ migration เดิมเดินทางด้วย merge/cherry-pick ไม่ generate ใหม่


10. ลำดับปล่อยขึ้น UAT

#ทำอะไรต้องรอ
1DDL ข้อ 9
2merge PR TPS → uat
3merge PR UserService → uat1 เสร็จแล้วเท่านั้น (42703 ถ้าสลับ)
4merge PR IaC → development · เพิ่ม APIM ข้อ 8 · merge PR ReportService → uat · รอ pod READY3
5pnpm publish:onboarding 1.11.15 → merge PR HostApp → uat → merge PR AdminSuperApp → uat4 เสร็จแล้วเท่านั้น (ปุ่มโหลดใบสมัครพังถ้าสลับ)

หลัง UAT ผ่าน: เปิด PR feature → development ของทุก repo (ยกเว้น IaC ที่เข้า development ไปแล้ว และ ReportService ที่ตัดจาก development อยู่แล้ว) · resolve AuthDbContextModelSnapshot.cs ตามข้อ 2.4 · ไฟล์ report ของ HostApp ไม่ conflict แล้วเพราะทั้งสองฝั่งลบเหมือนกัน


11. Definition of done

repoหลักฐานที่ต้องมีก่อน claim
TPSbuild 0 error · test Passed · curl ข้อ 1.7 ได้ 4 field + fallback ได้ custTitleSource: "CustomerProfile"
UserServicebuild 0 error · dotnet test Passed รวม Postgres tests · has-pending-model-changes = none · Up() มี AddColumn 3 ตัว
Libnx test onboarding Passed · publish 1.11.15 ขึ้น feed สำเร็จ (npm view @exim/onboarding version = 1.11.15)
HostAppbuild uat 0 error · จำนวน suite ที่รันไม่ใช่ 0 · กดจอจริง 4 เคสในข้อ 4.2 ผ่าน (screenshot)
AdminSuperAppbuild uat 0 error · กดจอ service-request-management-detail เห็นแถวคำนำหน้า (screenshot)
ReportServicetest Passed · PDF บนเครื่องมีแถวคำนำหน้า · เทียบใบเก่า/ใหม่แล้ว
IaC + APIMpod READY 1/1 · curl ผ่าน APIM = 200 PDF
UAT รวมสมัครบริษัทใหม่ผ่านจอจริง → approve → SELECT "CustTitleNameCode","CustTitleNameTh" FROM "Companies" WHERE "JuristicId"='…' มีค่า → PDF มีแถว → sync แล้วค่ายังอยู่