JuristicBindingStep — คำนำหน้าองค์กร (Architecture Overview)
ภาพรวมการเก็บคำนำหน้าองค์กร (custTitleNameCode) ตั้งแต่ CommonApi → ThirdPartyService → stepData → ตาราง Companies → หน้าจอ → ใบสมัคร PDF พร้อมมติ Owner และลำดับปล่อยของช่วง UAT freeze
อัปเดต: 2026-09-10
1. สรุปใน 1 นาที
เป้าหมาย — เก็บ คำนำหน้าองค์กร (บจก. / บมจ. / หจก. …) ของบริษัทที่สมัครใช้ SuperApp โดยไม่ทำให้ UAT ที่ freeze อยู่แตก
ค่าเดินทางอย่างไร
- ThirdPartyService (TPS) ยิง CommonApi
CustomerProfileWithAddress/{taxId}เพิ่มเป็น call ที่ 5 ในเส้นfx-registration-profileแล้ว mapcustTitleNameCode→code + nameTh + nameEn - FE (
@exim/onboarding) รับมาแสดง แล้วส่งลง stepData ของJuristicBindingStep - UserService (US) เก็บลง 3 คอลัมน์ใหม่ในตาราง
Companiesตอน approve - ปุ่ม sync company ดึงค่าใหม่มาทับได้ · หน้า corporate-profile และใบสมัคร PDF แสดงค่านี้
มติ Owner ที่ล็อกแล้ว
| เรื่อง | มติ |
|---|---|
| แหล่งข้อมูล | ใช้ call ที่ 5 (CustomerProfileWithAddress) เป็นหลัก · fallback ไป call ที่ 1 (CustomerProfile) ซึ่งมีค่าเดียวกันอยู่แล้ว |
| เส้นเดิม 4 เส้น | คงไว้ครบ ไม่แตะ |
| ตาราง code→ชื่อ | เก็บใน appsettings.json base ของ TPS (24 code · 10 code ไม่มีชื่ออังกฤษ) |
| cache call ที่ 5 | ไม่ทำ |
| ใบสมัคร PDF | ย้าย engine ไป Backend_ReportService (NestJS) ทั้ง dev และ uat (วันนี้ uat ยังวาดที่ FE) |
Frontend_AdminSuperApp | bump lib ในรอบนี้ด้วย |
| ลำดับ env | UAT ก่อน แล้ว DEV ตาม — ด่านก่อนขึ้น = รันบนเครื่อง |
Repo ที่แตะรอบ UAT = 6 + IaC: ThirdPartyService · UserService · SuperAppLibraryUi · HostAppSuperApp · AdminSuperApp · ReportService · Backend_Iac (เฉพาะ manifest ของ report-service)
2. ทำไมถึงใช้ call ที่ 5 ทั้งที่ call ที่ 1 ก็มีค่านี้
ข้อเท็จจริงจาก probe 10/09/2026: CustomerProfile/{taxId} (call ที่ 1) คืน custTitleNameCode: "01" ค่าเดียวกัน record เดียวกันกับ CustomerProfileWithAddress · ทั้งสองอยู่บน host commonuat เหมือนกัน ไม่มีตัวไหนยิง CRM โดยตรง (crmwebapi-uat = call ที่ 3 เท่านั้น) · CommonCustomerProfileDto.cs:31 มี property รับไว้แล้ว แค่ CustomerProfileAdapter.cs:16-23 ไม่ได้ส่งต่อ
Owner ตัดสินให้ใช้ call ที่ 5 เพราะถือว่า CustomerProfileWithAddress เป็น SOT ของข้อมูลชุดนี้ในระยะยาว และเป็นเส้นเดียวกับที่จะย้าย call ที่ 3 มาใช้ในอนาคต · ราคาที่จ่าย = DTO ใหม่ · config key ใหม่ · fallback · field custTitleSource — ยอมรับได้
3. Flow ที่ 1 — ค้นหาบริษัท (fx-registration-profile)
วันนี้
Client เรียกเส้นเดียว ข้างใน TPS ยิงออกนอก 4 เส้น (GetFxCustomerRegistrationProfileHandler.cs)
- root CommonApi
CustomerProfile/{taxId}— ว่าง = 404 · พัง = ตายทั้งเส้น (:44-52) - CommonApi
Limit/MasterLimit?custCode=(:61) - CRM
POST GetCustomerByCustcode— ที่อยู่ +leadCode(:65) - CommonApi
CommonData/MOC/{businessCodeMOC}(:69)
เส้น 2-4 ห่อ SendSafe (:147-157) พังแล้วคืน null + ใส่ชื่อลง failedSections · เส้น 2-3 ถูกกั้นด้วย hasCustCode (:60-66)
เป้าหมาย
sequenceDiagram
autonumber
participant FE as "@exim/onboarding"
participant TPS as ThirdPartyService
participant CM as CommonApi
participant CRM as CRM
participant US as UserService
participant DB as Companies
FE->>TPS: GET /v1/Customer/fx-registration-profile/{juristicId}
TPS->>CM: 1 CustomerProfile/{taxId} (คงเดิม)
par ยิงขนาน
TPS->>CM: 2 Limit/MasterLimit (คงเดิม)
TPS->>CRM: 3 POST GetCustomerByCustcode (คงเดิม)
TPS->>CM: 4 CommonData/MOC (คงเดิม)
TPS->>CM: 5 CustomerProfileWithAddress/{taxId} ← ใหม่
end
TPS->>TPS: map code→ชื่อ · 5 ล่ม/ว่าง → ใช้ค่าจาก 1
TPS-->>FE: response เดิม + custTitleNameCode/Th/En + custTitleSource
FE->>US: POST steps/JuristicBindingStep/actions/{action} + stepData
US->>US: JuristicBindingStepHandler whitelist key ใหม่
US->>DB: approve → เขียน 3 คอลัมน์
กติกาที่ทำให้ปลอดภัย
- คีย์ด้วย
taxIdตัวเดียวกับที่ client ส่งมา ไม่ใช่custCode— เส้นใหม่รับได้ทั้งสองคีย์ (probe: custCode0000131และเลขนิติบุคคล0105533146651ได้แถวเดียวกันเป๊ะ · คีย์ที่ไม่มีจริงได้ HTTP 200data: []ไม่ใช่ 404) ⇒ ไม่ติดทางแยกhasCustCodeและไม่ต้องพึ่งข่าวว่า “จะไม่มีเคส Lead” (ยังไม่ verify ในโค้ด) - DTO ใหม่ประกาศ field เดียว (
custTitleNameCode) — payload เส้นใหม่มีcustBirthDate: "19901221"(ไม่ใช่ ISO) และtemp: 1(ตัวเลข) ลงCommonCustomerProfileDtoเดิมจะโยนJsonException(:111,:316) และ repo ไม่มีJsonConverterเลย ·System.Text.Jsonข้าม property ที่ไม่ประกาศ ⇒ ไม่ต้องเขียนตัวแปลง - envelope แบบทน —
messageของ CommonApi บางเคสมาเป็น array (CommonApiClient.cs:170,:260) ใช้ patternMocEnvelope/MasterLimitEnvelopeเดิม - fallback ไป call ที่ 1 เมื่อ call ที่ 5 ล่ม หรือ คืน
data: []· call ที่ 1 เป็น root ที่ต้องสำเร็จอยู่แล้ว ⇒ คำนำหน้าว่างได้กรณีเดียวคือทั้งสองแหล่งไม่มี code · ติดcustTitleSource: "CustomerProfileWithAddress" | "CustomerProfile"ไปกับ response และ stepData (ไม่เป็นคอลัมน์ DB) - ไม่เพิ่มค่าใน
failedSections— ค่าไม่ได้หาย แค่เปลี่ยนแหล่ง ·Frontend_LINE_Connectivityกินเส้นนี้อยู่ (juristic.service.ts:22,juristic.model.ts:14) แต่ไม่มีJuristicBindingStep⇒ ไม่กระทบ - lookup + fallback + map = query กลางตัวเดียว ใช้ทั้ง flow ค้นหาและ flow sync — แยกเขียน 2 ที่แล้วพฤติกรรมจะต่างกันทันที และวันลบ call ที่ 5 ต้องไล่แก้สองจุด
4. Flow ที่ 2 — stepData → ตาราง Companies
🔴 จุดที่พลาดง่ายที่สุด: JuristicBindingStepHandler.cs:60-73 ไม่ pass-through ของที่ FE ส่งมา — มัน serialize object ใหม่จาก key list ตายตัว (juristicId, nameTH, …, custCode) ⇒ FE ส่ง key ใหม่มาแล้ว handler ทิ้งเงียบถ้าไม่เพิ่มชื่อ key ตรงนั้น และ approve จะไม่มีวันเห็นค่า
ผลพลอยได้: handler อ่านด้วย StepInputReader.GetStringStrict (StepInputReader.cs:66-67) ซึ่ง guard ValueKind แล้ว — FE เผลอส่ง 01 เป็นตัวเลขจะกลายเป็น null ไม่ throw · แต่ ParseJuristicBinding (CreateCompanyApplicationHandler.cs:424) ที่อ่าน DataJson ตอน approve เรียก v.GetString() โดยไม่เช็คชนิด ต้องแก้ให้เช็ค ValueKind ด้วย เพราะเป็นด่านที่ทำให้ approve พังทั้งใบ
กติกาฝั่ง DB
- 3 คอลัมน์ nullable ทั้งหมด กันแถวเก่าระเบิด
- code ที่ map ไม่เจอ → คืน code ดิบ ชื่อเป็น null ห้าม throw และ ห้าม CHECK constraint ล็อก 24 code (วันที่ต้นทางเพิ่ม code ที่ 25 approve จะพังด้วย
23514) - ชื่อในตาราง = snapshot ณ วันเขียน ต้นทางแก้ชื่อภายหลังแถวเก่าไม่เปลี่ยนจนกด sync ใหม่
- admin search ค้นด้วย
ILIKEบนDataJsonทั้งก้อน (FlowInstanceRepository.cs:190,206) ⇒ ส่งลง stepData แค่ 4 key ห้ามแนบ payload ดิบ
5. Flow ที่ 3 — ปุ่ม sync company
POST /company/sync (CompanyController.cs:122-135, cooldown 24 ชม.) ไม่ยิงออกนอกเอง แต่เรียก TPS
sequenceDiagram
autonumber
participant U as ผู้ใช้กด sync
participant US as UserService
participant TPS as ThirdPartyService
participant DBD as DBD (EGov)
participant CM as CommonApi
participant DB as Companies
U->>US: POST /company/sync { juristicId }
US->>TPS: GET JuristicShareholder/organization-juristic-person-full-access/{juristicId}
TPS->>DBD: api.egov.go.th/ws/dbd/juristic/v7/general/profile (cache 24 ชม.)
TPS->>CM: CustomerProfileWithAddress/{juristicId} ← ใหม่ · นอกบล็อก cache · SendSafe
TPS-->>US: ข้อมูล DBD เดิม + 3 field
US->>DB: Company.Update ทับค่าเดิม (preserve-if-blank)
- เติมลงเส้น
organization-juristic-person-full-accessที่มีอยู่ ไม่สร้าง route ใหม่ — consumer เดียวคือ sync (OrganizationJuristicPersonClient.cs:13) - DBD ไม่มี
custTitleNameCode⇒ ค่ามาจาก CommonApi เท่านั้น - 🔴 degrade ที่ TPS —
SyncCompanyHandler.cs:134-140เป็น hard-fail (EXT001→ sync ตายทั้งปุ่ม) ⇒ TPS ต้องห่อการยิง title แบบSendSafeแล้วคืน 3 field เป็น null - 🔴 ยิง title นอกบล็อกที่ cache —
GetOrganizationJuristicPersonFullAccessHandler.cs:109,174cache ทั้งก้อน 24 ชม. เท่า cooldown พอดี ถ้า CommonApi สะดุดครั้งเดียว entry ไม่มีคำนำหน้าจะค้างจนกดใหม่ก็ไม่หาย - ห้ามล้างค่าเดิม —
Company.Update(Company.cs:159-174) ไม่มี field คำนำหน้า ⇒ เพิ่ม optional parameter defaultnull+ guard preserve-if-blank แบบCustCode(Company.cs:189-191) SyncCompanyResponse(SyncCompanyDtos.cs:50-59) ต้องคืน 3 field ด้วย ไม่งั้นจอไม่ refresh · หน้า corporate-profile อ่านจากCompaniesผ่านauthService.user()?.companies(corporate-profile.state.ts:14-33) ⇒ ค่าที่ sync เขียนมีผลกับจอจริง
6. Flow ที่ 4 — ใบสมัคร PDF
วันนี้ UAT กับ DEV วาด PDF คนละที่
- UAT วาดที่ FE — HostApp
origin/uatมีdocument.ts:12importbuildFxApplicationDocDefinitionจากapps/shell/src/app/reports/fx-application/fx-application.template.ts(1,181 บรรทัด) วาดในเบราว์เซอร์ด้วยข้อมูลจาก USapplication-report·environments.uat.tsบน uat ไม่มี keyreport - DEV วาดที่ ReportService — ย้ายแล้วที่ commit
d60f202+e20257b(01/09/2026) บนdevelopment
Owner ตัดสินให้ทั้งสอง env ใช้ ReportService ⇒ รอบนี้พ่วงการย้าย engine เข้ามาด้วย
sequenceDiagram
autonumber
participant FE as HostApp
participant RS as ReportService (NestJS)
participant US as UserService
FE->>RS: GET report-api/api/report-service/v1/reports/fx-application/{id}/pdf
RS->>US: GET /v1/onboarding/instances/{id}/application-report (forward cookie)
US->>US: ApplicationReportMapper อ่าน stepData JuristicBindingStep
US-->>RS: company { nameTH, nameEN, nameTitleTH, nameTitleEN, … }
RS-->>FE: PDF
สิ่งที่ UAT ยังไม่มี (ทั้งหมดไม่เกี่ยวกับ feature คำนำหน้า)
| ของ | สถานะบน UAT | ต้องทำ |
|---|---|---|
โค้ด Backend_ReportService | origin/uat = ed4094b "Added README.md" ตามหลัง development 14 commit ไม่มี template เลย | promote 14 commit + เพิ่มแถวคำนำหน้า |
| Deployment | src/yamls/uat/superapp/ มีแต่ fx-report-* (คนละ service) | เพิ่ม manifest 5 ไฟล์ใน IaC — PR เข้า development (ArgoCD uat อ่านจาก branch นั้น) |
| APIM | มีแค่ uat-fxreport-api (fx-report) · dev มี report-service-api path report-api ให้ลอก | เพิ่ม API uat-report-api ด้วยมือ |
| HostApp | ยังวาดที่ FE | cherry-pick d60f202 + e20257b (ไม่ clean — 3 จุดต้อง resolve) |
PDF ฝั่ง admin เป็นคนละเส้น — Frontend_AdminSuperApp โหลดจาก US application-pdf (application-tracking.service.ts:63,67 → OnboardingController.cs:108 → StubApplicationPdfGenerator, DependencyInjection.cs:472 “SA-1725 mock”) ไม่ผ่าน ReportService ⇒ รอบนี้ไม่ทำให้ PDF ฝั่ง admin มีคำนำหน้า ยกให้ Owner ตัดเป็นงานแยก
7. หน้าจอ
@exim/onboarding(repoFrontend_SuperAppLibraryUi, branchdevelopmentเท่านั้น) —CompanyInfo+ labels + template fx/admin + mapper +toStepData+ resume path (juristic-binding-fx.state.ts:175-183ต่อยอด pattern “ไม่มี key → lookup ใหม่” ของcustCode)- ฐาน lib ปลอดภัย — HostApp uat pin
^1.11.14= commit001dab2· lib HEADd872846ห่าง 36 commit ทั้ง repo แต่แตะlibs/onboardingแค่77567df(SonarQube fix) · PR 3055 auth-sdk ไม่แตะ onboarding Frontend_HostAppSuperApp— bump lib + cherry-pick ย้าย PDF (ข้อ 6)Frontend_AdminSuperApp— หน้าservice-request-management-detail.tsrenderOnboardingComponentจาก lib ตรง ๆ ⇒ แถวคำนำหน้ามาเองจาก lib งานจริง = bump lib · ความเสี่ยงคือระยะห่าง^1.9.27→ ใหม่ (03515d5→001dab2= 16 commit · 26 ไฟล์ · +490 −44 บรรทัด ในlibs/onboarding) ต้องอ่าน log + build/test + กดจอจริงก่อน · เจอ breaking change ที่ไม่เกี่ยว = กลับมาคุย อย่าฝืน
8. ชื่อ field — ตั้งครั้งเดียวใช้ทุก repo
| ที่ | ชื่อ |
|---|---|
| TPS response · FE · stepData | custTitleNameCode · custTitleNameTh · custTitleNameEn · custTitleSource |
คอลัมน์ Companies | CustTitleNameCode · CustTitleNameTh · CustTitleNameEn (ไม่มี source) |
| report DTO (US) · ReportService | nameTitleTH · nameTitleEN — ตามที่ RS ใช้กับคำนำหน้าบุคคลอยู่แล้ว (fx-application.model.ts:18-20) ไม่มี code ไม่มี source |
ตาราง mapping ใน appsettings.json base ของ TPS — array ของคู่ ไม่ใช้ object ที่เอา code เป็น key
"CustomerTitle": {
"Names": [
{ "Code": "01", "NameTh": "บจก.", "NameEn": null },
{ "Code": "06", "NameTh": "องค์กร", "NameEn": "ORGANIZATION" },
{ "Code": "31", "NameTh": "นาย", "NameEn": "MR." }
]
}
ทำไมส่วน TPS ไม่ต้องแตะ IaC — overlay ทับเฉพาะ appsettings.{ENV}.json (overlay-cloud-config.yml:30,39) · CommonApiClient อ่าน configuration["CommonApi:..."] ตรง ๆ ซึ่ง .NET merge base กับ env ให้แล้ว · check-config-parity.py รับแค่ 2 ไฟล์นั้น ไฟล์ base ไม่เข้าสมการ ⇒ ทั้ง URL ของ call ที่ 5 และตาราง Names วางที่ base ที่เดียวพอ
9. ลำดับปล่อยของ — UAT ก่อน DEV ตาม
ci-orchestrate.yml:20-28 เลือก env จากชื่อ branch (development / sit / uat) เท่านั้น ⇒ รัน pipeline บน feature branch ไม่ได้ ⇒ cluster แรกที่โค้ดนี้รันจริงคือ UAT ที่ freeze อยู่ ด่านก่อนขึ้นจึงเป็นการรันบนเครื่อง (รายละเอียดใน Implementation Spec)
ลำดับขึ้น UAT
- รัน DDL บน
UAT_AuthDb(3 คอลัมน์ nullable) - deploy TPS — เพิ่ม field ล้วน FE เก่าไม่สนใจ
- deploy UserService
- IaC manifest + APIM + deploy ReportService
- publish lib → deploy HostApp (bump + cherry-pick PDF) และ AdminSuperApp (bump)
invariant ที่ห้ามสลับจริง ๆ มี 2 ข้อ
- 🔴 1 ก่อน 3 — pod UserService ใหม่ที่ขึ้นก่อน DDL จะยิง
42703ทันที - 🔴 4 ก่อน 5 — HostApp ลบตัววาดฝั่ง FE แล้ว ถ้า report-service ยังไม่ขึ้นหรือ APIM ยังไม่มี route ปุ่มโหลดใบสมัครพังทันที ไม่ใช่แค่ไม่มีคำนำหน้า
ที่เหลือสลับได้ ผลแย่สุดคือค่ายังไม่โผล่ ไม่ใช่ error
DEV ตามทีหลัง — migration id ไม่ซ้ำ + __EFMigrationsHistory เป็นเซ็ต ⇒ script idempotent apply เฉพาะ id ที่ยังไม่มี · เงื่อนไข: ไฟล์ migration เดิมเดินทางด้วย merge/cherry-pick เท่านั้น ห้าม generate ใหม่บน development · conflict ที่รู้ล่วงหน้า = AuthDbContextModelSnapshot.cs (development มี AMLO table ที่ uat ไม่มี) · ห้าม merge development กลับเข้า feature (UserService ต่าง 146 commit · HostApp 47 · IaC 1,632)
10. เรื่องที่ยังไม่จบ
- ยังไม่ verify ว่าไม่มีเคส Lead จริง — ทางแยก
hasCustCodeยังอยู่ แต่แผนคีย์ด้วยtaxIdจึงไม่พึ่งข้อนี้ - ยังไม่ verify ค่า
leadCodeจริงจาก CRM — credential อยู่ใน Key Vault · ไม่กระทบเพราะ call ที่ 3 คงเดิม - ด่านก่อนขึ้น UAT = รันบนเครื่อง ไม่ผ่าน DEV cluster — Owner รับทราบ
- ยังไม่ verify ว่า PDF ที่ ReportService วาดหน้าตาตรงกับที่ FE วาดวันนี้แค่ไหน — ต้องเทียบใบต่อใบ
- ยังไม่ verify policy ที่ APIM uat ต้องตั้งให้
uat-report-api(auth / rewrite) — ลอกจากreport-service-apiบน dev ได้แต่ต้องอ่านก่อน - PDF ฝั่ง admin (stub) — รอ Owner ตัด