JuristicBindingStep — คำนำหน้าองค์กร (Implementation Spec)
ขั้นตอนลงมือทำรายไฟล์ ต่อ repo ของงานเก็บคำนำหน้าองค์กร — branch/worktree, contract, migration, cherry-pick, IaC/APIM, ด่านตรวจบนเครื่อง และ definition of done
อัปเดต: 2026-09-10
0. กติการวมทุก repo
- ฐาน branch = ตัด feature จาก
uatของแต่ละ repo (ยกเว้น libFrontend_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):
| repo | worktree | base | หมายเหตุ |
|---|---|---|---|
| Backend_ThirdPartyService | _worktrees/TPS-crm | uat @ 065b55c | |
| Backend_UserService | Backend_UserService/.worktrees/crm-profile-with-address | uat @ f45664e | |
| Frontend_SuperAppLibraryUi | _worktrees/LibUi-crm | development @ d872846 | repo นี้ไม่มี uat |
| Frontend_HostAppSuperApp | Frontend_HostAppSuperApp/.worktrees/crm-profile-with-address | uat @ e27a7c8 | |
| Frontend_AdminSuperApp | ยังไม่มี | ตัดจาก origin/uat | |
| Backend_ReportService | ยังไม่มี | ตัดจาก origin/development (ดูข้อ 6) | |
| Backend_Iac | Backend_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; }· ห้าม reuseCommonCustomerProfileDto(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; } = []; }+ recordCustomerTitleName(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 แบบทน ที่ ignoremessage(ทำ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ลำดับ:GetCustomerProfileWithAddressAsync(taxId)→ ถ้าสำเร็จและdataมีแถว →code = data[0].CustTitleNameCode,source = "CustomerProfileWithAddress"- ถ้า throw / ไม่สำเร็จ /
dataว่าง →GetCustomerProfileByTaxIdAsync(taxId)→code = data[0].CustTitleNameCode,source = "CustomerProfile"(log Warning ว่า fallback) - ทั้งสองไม่มี code → คืน 4 field เป็น null (ไม่ใช่ Failure)
- 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เดิม · ถ้าtitleTaskFailure → 4 field null และ ไม่เพิ่มค่าในfailedSectionsDTOs/FxCustomerRegistrationProfileDtos.cs:19—FxCustomerRegistrationProfileResponseเพิ่ม 4 property ต่อท้าย:string? CustTitleNameCode, string? CustTitleNameTh, string? CustTitleNameEn, string? CustTitleSource
1.6 Flow sync — Features/OrganizationJuristicPerson/Queries/GetOrganizationJuristicPersonFullAccess/
GetOrganizationJuristicPersonFullAccessHandler.cs— handler นี้ยังไม่ injectIMediator(: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 ตัว defaultnullต่อจากcustCodeUpdate(...)(:159-174) — เพิ่ม optional parameter 3 ตัว defaultnullต่อจากcustCodeและ guard แบบเดียวกับCustCode(:189-191):if (!string.IsNullOrWhiteSpace(custTitleNameCode)) { CustTitleNameCode = ...; CustTitleNameTh = custTitleNameTh?.Trim(); CustTitleNameEn = custTitleNameEn?.Trim(); }— ทับเป็นชุดเมื่อมี code ใหม่ · ไม่มี code = ไม่แตะทั้ง 3 (กัน sync ที่ title ล่มมาล้างของเดิม)
- property ใหม่ 3 ตัว
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")ฯลฯ (:46pattern เดียวกับcustCode) แล้ว เพิ่มทั้ง 4 ลง anonymous object ที่:60-73— ไม่เพิ่ม = ค่าหายเงียบOnboarding/Engine/Handlers/PostApproval/CreateCompanyApplicationHandler.csParseJuristicBinding(:424) — เปลี่ยนGet()เป็นroot.TryGetProperty(p, out var v) && v.ValueKind == JsonValueKind.String ? v.GetString() ?? "" : ""JuristicBindingDatarecord — เพิ่ม 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 อยู่ใน entityCompanies/Commands/SyncCompany/SyncCompanyDtos.cs:50-59—SyncCompanyResponseเพิ่ม 3 fieldOnboarding/Dtos/ApplicationReportDto.cs—ApplicationReportCompanyDtoเพิ่มstring? NameTitleTH, string? NameTitleEN(serialize เป็นnameTitleTH/nameTitleENตามที่ RS ใช้)Onboarding/ApplicationReportMapper.cs:60-76—MapCompanyเติมStr(j, "custTitleNameTh")→NameTitleTH,Str(j, "custTitleNameEn")→NameTitleEN- ทุกที่ที่คืน company ให้ FE (
/users/me→companies[],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:13—CompanyInfoเพิ่มcustTitleNameCode: string; custTitleNameTh: string; custTitleNameEn: string; custTitleSource: string;(string ว่างเมื่อไม่มี ให้เหมือนcustCode)engine/engine.models.ts:129—JuristicLookupResultเพิ่ม 4 field รับจาก TPSsteps/juristic-binding/juristic-binding.labels.ts—JuristicBindingLabels.rowsเพิ่ม label “คำนำหน้า” (แสดงcustTitleNameThถ้ามี ไม่มีแสดง code ดิบ)steps/juristic-binding/juristic-binding.mapper.ts— ทั้งทางrawDataและทาง flat keysteps/juristic-binding/modes/fx/juristic-binding-fx.state.tstoStepData(:70) — เพิ่ม 4 key · ค่าเป็น string เสมอ ("01"ไม่ใช่01)- resume path (
:175-183) — ต่อยอด patterncustCode: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.jsonversion →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 (ระยะกระโดด 03515d5→001dab2 = 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-57—FxApplicationCompanyเพิ่ม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 เข้า development — ci-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.yaml | image: …/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.yaml | namespace |
ingresses/report-service-ingress.yaml | host: api.sua-az-uat.internal · path /api/report-service(/|$)(.*) · konghq.com/strip-path: "false" ตาม fx-report-service-ingress.yaml |
secret-providers/report-secret-provider.yaml | objectName: uat--OpenTelemetry--OtlpEndpoint (มีใน KV แล้ว — provider อื่นของ uat ใช้อยู่) · secretName: report-service-secrets |
services/report-service.yaml | namespace |
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-apipathreport-api→ ใช้เป็นต้นแบบ - uat มี id
uat-report-service-apiแต่ pathuat-fxreport-api(fx-report คนละ service) ⇒ id ใหม่ห้ามซ้ำ ใช้uat-report-apipathuat-report-api - path ต้อง =
servicePathPrefix + 'report-api'ที่ HostApp uat ใช้หลัง cherry-pick (environments.uat.ts:1prefix'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
| # | ทำอะไร | ต้องรอ |
|---|---|---|
| 1 | DDL ข้อ 9 | — |
| 2 | merge PR TPS → uat | — |
| 3 | merge PR UserService → uat | 1 เสร็จแล้วเท่านั้น (42703 ถ้าสลับ) |
| 4 | merge PR IaC → development · เพิ่ม APIM ข้อ 8 · merge PR ReportService → uat · รอ pod READY | 3 |
| 5 | pnpm publish:onboarding 1.11.15 → merge PR HostApp → uat → merge PR AdminSuperApp → uat | 4 เสร็จแล้วเท่านั้น (ปุ่มโหลดใบสมัครพังถ้าสลับ) |
หลัง UAT ผ่าน: เปิด PR feature → development ของทุก repo (ยกเว้น IaC ที่เข้า development ไปแล้ว และ ReportService ที่ตัดจาก development อยู่แล้ว) · resolve AuthDbContextModelSnapshot.cs ตามข้อ 2.4 · ไฟล์ report ของ HostApp ไม่ conflict แล้วเพราะทั้งสองฝั่งลบเหมือนกัน
11. Definition of done
| repo | หลักฐานที่ต้องมีก่อน claim |
|---|---|
| TPS | build 0 error · test Passed · curl ข้อ 1.7 ได้ 4 field + fallback ได้ custTitleSource: "CustomerProfile" |
| UserService | build 0 error · dotnet test Passed รวม Postgres tests · has-pending-model-changes = none · Up() มี AddColumn 3 ตัว |
| Lib | nx test onboarding Passed · publish 1.11.15 ขึ้น feed สำเร็จ (npm view @exim/onboarding version = 1.11.15) |
| HostApp | build uat 0 error · จำนวน suite ที่รันไม่ใช่ 0 · กดจอจริง 4 เคสในข้อ 4.2 ผ่าน (screenshot) |
| AdminSuperApp | build uat 0 error · กดจอ service-request-management-detail เห็นแถวคำนำหน้า (screenshot) |
| ReportService | test Passed · PDF บนเครื่องมีแถวคำนำหน้า · เทียบใบเก่า/ใหม่แล้ว |
| IaC + APIM | pod READY 1/1 · curl ผ่าน APIM = 200 PDF |
| UAT รวม | สมัครบริษัทใหม่ผ่านจอจริง → approve → SELECT "CustTitleNameCode","CustTitleNameTh" FROM "Companies" WHERE "JuristicId"='…' มีค่า → PDF มีแถว → sync แล้วค่ายังอยู่ |