Private Docs

JuristicBindingStep — คำนำหน้าองค์กร (Architecture Overview)

ภาพรวมการเก็บคำนำหน้าองค์กร (custTitleNameCode) ตั้งแต่ CommonApi → ThirdPartyService → stepData → ตาราง Companies → หน้าจอ → ใบสมัคร PDF พร้อมมติ Owner และลำดับปล่อยของช่วง UAT freeze

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

1. สรุปใน 1 นาที

เป้าหมาย — เก็บ คำนำหน้าองค์กร (บจก. / บมจ. / หจก. …) ของบริษัทที่สมัครใช้ SuperApp โดยไม่ทำให้ UAT ที่ freeze อยู่แตก

ค่าเดินทางอย่างไร

  1. ThirdPartyService (TPS) ยิง CommonApi CustomerProfileWithAddress/{taxId} เพิ่มเป็น call ที่ 5 ในเส้น fx-registration-profile แล้ว map custTitleNameCodecode + nameTh + nameEn
  2. FE (@exim/onboarding) รับมาแสดง แล้วส่งลง stepData ของ JuristicBindingStep
  3. UserService (US) เก็บลง 3 คอลัมน์ใหม่ในตาราง Companies ตอน approve
  4. ปุ่ม 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_AdminSuperAppbump lib ในรอบนี้ด้วย
ลำดับ envUAT ก่อน แล้ว 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)

  1. root CommonApi CustomerProfile/{taxId} — ว่าง = 404 · พัง = ตายทั้งเส้น (:44-52)
  2. CommonApi Limit/MasterLimit?custCode= (:61)
  3. CRM POST GetCustomerByCustcode — ที่อยู่ + leadCode (:65)
  4. 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: custCode 0000131 และเลขนิติบุคคล 0105533146651 ได้แถวเดียวกันเป๊ะ · คีย์ที่ไม่มีจริงได้ HTTP 200 data: [] ไม่ใช่ 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) ใช้ pattern MocEnvelope / 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 ที่ TPSSyncCompanyHandler.cs:134-140 เป็น hard-fail (EXT001 → sync ตายทั้งปุ่ม) ⇒ TPS ต้องห่อการยิง title แบบ SendSafe แล้วคืน 3 field เป็น null
  • 🔴 ยิง title นอกบล็อกที่ cacheGetOrganizationJuristicPersonFullAccessHandler.cs:109,174 cache ทั้งก้อน 24 ชม. เท่า cooldown พอดี ถ้า CommonApi สะดุดครั้งเดียว entry ไม่มีคำนำหน้าจะค้างจนกดใหม่ก็ไม่หาย
  • ห้ามล้างค่าเดิมCompany.Update (Company.cs:159-174) ไม่มี field คำนำหน้า ⇒ เพิ่ม optional parameter default null + 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:12 import buildFxApplicationDocDefinition จาก apps/shell/src/app/reports/fx-application/fx-application.template.ts (1,181 บรรทัด) วาดในเบราว์เซอร์ด้วยข้อมูลจาก US application-report · environments.uat.ts บน uat ไม่มี key report
  • 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_ReportServiceorigin/uat = ed4094b "Added README.md" ตามหลัง development 14 commit ไม่มี template เลยpromote 14 commit + เพิ่มแถวคำนำหน้า
Deploymentsrc/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ยังวาดที่ FEcherry-pick d60f202 + e20257b (ไม่ clean — 3 จุดต้อง resolve)

PDF ฝั่ง admin เป็นคนละเส้นFrontend_AdminSuperApp โหลดจาก US application-pdf (application-tracking.service.ts:63,67OnboardingController.cs:108StubApplicationPdfGenerator, DependencyInjection.cs:472 “SA-1725 mock”) ไม่ผ่าน ReportService ⇒ รอบนี้ไม่ทำให้ PDF ฝั่ง admin มีคำนำหน้า ยกให้ Owner ตัดเป็นงานแยก


7. หน้าจอ

  • @exim/onboarding (repo Frontend_SuperAppLibraryUi, branch development เท่านั้น) — 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 = commit 001dab2 · lib HEAD d872846 ห่าง 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.ts render OnboardingComponent จาก lib ตรง ๆ ⇒ แถวคำนำหน้ามาเองจาก lib งานจริง = bump lib · ความเสี่ยงคือระยะห่าง ^1.9.27 → ใหม่ (03515d5001dab2 = 16 commit · 26 ไฟล์ · +490 −44 บรรทัด ใน libs/onboarding) ต้องอ่าน log + build/test + กดจอจริงก่อน · เจอ breaking change ที่ไม่เกี่ยว = กลับมาคุย อย่าฝืน

8. ชื่อ field — ตั้งครั้งเดียวใช้ทุก repo

ที่ชื่อ
TPS response · FE · stepDatacustTitleNameCode · custTitleNameTh · custTitleNameEn · custTitleSource
คอลัมน์ CompaniesCustTitleNameCode · CustTitleNameTh · CustTitleNameEn (ไม่มี source)
report DTO (US) · ReportServicenameTitleTH · 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

  1. รัน DDL บน UAT_AuthDb (3 คอลัมน์ nullable)
  2. deploy TPS — เพิ่ม field ล้วน FE เก่าไม่สนใจ
  3. deploy UserService
  4. IaC manifest + APIM + deploy ReportService
  5. 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. เรื่องที่ยังไม่จบ

  1. ยังไม่ verify ว่าไม่มีเคส Lead จริง — ทางแยก hasCustCode ยังอยู่ แต่แผนคีย์ด้วย taxId จึงไม่พึ่งข้อนี้
  2. ยังไม่ verify ค่า leadCode จริงจาก CRM — credential อยู่ใน Key Vault · ไม่กระทบเพราะ call ที่ 3 คงเดิม
  3. ด่านก่อนขึ้น UAT = รันบนเครื่อง ไม่ผ่าน DEV cluster — Owner รับทราบ
  4. ยังไม่ verify ว่า PDF ที่ ReportService วาดหน้าตาตรงกับที่ FE วาดวันนี้แค่ไหน — ต้องเทียบใบต่อใบ
  5. ยังไม่ verify policy ที่ APIM uat ต้องตั้งให้ uat-report-api (auth / rewrite) — ลอกจาก report-service-api บน dev ได้แต่ต้องอ่านก่อน
  6. PDF ฝั่ง admin (stub) — รอ Owner ตัด