SurePhone ERD — Domain 3: Shop (ร้านค้าพาร์ทเนอร์) 🚧 WIP

กำลังไล่เก็บ field จากฟอร์มสมัครจริง (7 ขั้น) ทีละฟอร์ม — ยังไม่ fix ตาราง รอครบก่อนประกอบ [?] = รอ confirm


Registration Flow (สมัครเป็นพาร์ทเนอร์)

  1. หน้า Login ร้าน (node 268:33167) → กดปุ่ม “สมัครเลย”
  2. ยอมรับข้อตกลงล่าสุด (181:21565, 181:21556) — ดึง terms version ล่าสุดให้รับก่อนเสมอ
  3. กรอกเบอร์โทร + OTP (181:21632)
    • เช็คเบอร์ซ้ำ: ห้ามซ้ำข้ามร้านสาขา (เบอร์ unique ระดับร้านสาขา)
    • OTP จัดการด้วย Redis (ephemeral — ไม่เก็บใน DB)
  4. ฟอร์มสมัคร 7 ขั้น (เริ่ม 532:88707) — ดู field inventory ด้านล่าง

หมายเหตุ: terms acceptance ของร้าน คล้าย customer.consents → ผูก sys.terms_and_conditions (Domain 8)


Field Inventory — ฟอร์มสมัคร 7 ขั้น

Form 1/7 — ข้อมูลผู้สมัคร (เจ้าของ/ตัวแทนร้าน) · node 532:88707

  • ภาพบัตรประชาชน* → file_id (private) · 2–5 mb
  • คำนำหน้าชื่อ* → enum: นาย / นาง / นางสาว
  • หมายเลขบัตรประชาชน* → varchar(13), check ^[0-9]{13}$
  • ชื่อ-นามสกุล* → owner_full_name (ฟิลด์เดียว — ชื่อตามบัตร)
  • วันเดือนปีเกิด* → date
  • ที่อยู่ตามบัตรประชาชน:
    • เลขที่อยู่* → address_line (บ้านเลขที่ ซอย หมู่บ้าน)
    • จังหวัด / เขต-อำเภอ / แขวง-ตำบล / รหัสไปรษณีย์* → เก็บ sub_district_id (FK → sys.sub_districts) แล้ว derive จังหวัด/อำเภอ/ไปรษณีย์ ผ่าน join (แบบ phonerefun)
  • รู้จักเราจากช่องทางไหน* → enum (ดูด้านล่าง)

Form 2/7 — ข้อมูลร้านค้าพาร์ทเนอร์ · node 181:22772

  • ชื่อร้าน / บริษัท* → shop_name
  • ชื่อสาขา* → branch_name ⭐ ไม่แยกตารางสาขา — 2 คอลัมน์ในตารางร้านเลย (แต่ละ row = 1 สาขา)
  • อีเมล* → email
  • แนบลิงก์ช่องทางการขาย* (Facebook/IG/TikTok) → sales_channel_link

Form 3/7 — ที่อยู่ร้าน + เอกสาร · (อธิบาย ยังไม่มี node)

  • รูปหน้าร้าน* → file_id (private)
  • ใบทะเบียนพาณิชย์ (ถ้ามี) → file_id (optional)
  • ภ.พ.20 (ถ้ามี) → file_id (optional)
  • ที่อยู่ตามใบทะเบียนไหม → bool toggle
  • รายละเอียดที่อยู่ร้าน → address_line + sub_district_id (FK sys.sub_districts)

Form 4/7 — เลขที่บัญชีธนาคาร · node 181:24165

  • ชื่อธนาคาร* (dropdown) → bank_code (FK → sys.banks, Domain 8)
  • เลขที่บัญชี* → bank_account_number
  • ชื่อบัญชี* → bank_account_name

1 ร้านสาขา = 1 บัญชี → เก็บ inline ในตารางร้าน

Form 5/7 — สร้าง Account เจ้าของร้าน · (อธิบาย ยังไม่มี node)

  • username + password (login ร้าน)
  • ⭐ แต่ละร้านสาขา: เจ้าของ 1 + พนักงานได้อีก ไม่เกิน 5 คน → ตาราง shop.users (role OWNER/STAFF, max 6/สาขา)

Form 6/7 — ลายเซ็นร้านค้า · node 485:86408

  • ลายเซ็น (วาด) → signature_file_id (private)

Form 7/7 — รีวิวสัญญา + ส่ง · (ปุ่ม “ดูพรีวิวสัญญา”)

  • ส่งใบสมัคร → สถานะ PENDING รอแอดมิน (AD3) อนุมัติ

DBML

//// audit_c, audit_cu (partials) + sys.r2_file นิยามใน 00-shared.dbml
//// sys.banks, sys.sub_districts (geo) นิยามใน sys (Domain 8)
 
//// ───────── shop ─────────
Enum shop.title_prefix {
  "MR"   [note: 'นาย']
  "MRS"  [note: 'นาง']
  "MISS" [note: 'นางสาว']
}
 
Enum shop.partner_status {
  "PENDING"  [note: 'รออนุมัติ (สมัครใหม่ หรือ แก้ไขส่งใหม่)']
  "RETURNED" [note: 'ตีกลับให้แก้ไข — ถ้าไม่แก้ใน 14 วัน → CANCELED (auto)']
  "REJECTED" [note: 'ไม่อนุมัติ → ประวัติ · ลบ row ได้เพื่อปล่อยเบอร์ให้สมัครใหม่']
  "CANCELED" [note: 'ตีกลับแล้วไม่แก้ใน 14 วัน (auto) → ประวัติ · ลบ row ได้']
  "ACTIVE"   [note: 'เป็นพาร์ทเนอร์ ใช้งานได้ (เดิม APPROVED)']
  "INACTIVE" [note: 'ยกเลิกพาร์ทเนอร์จาก ACTIVE — ปรับกลับ ACTIVE ได้']
}
 
Enum shop.grade {
  "A" [note: 'หนี้เสีย ≤ 3% ของลูกค้าทำสัญญาทั้งหมดของร้าน']
  "B" [note: '> 3% และ ≤ 8%']
  "C" [note: '> 8% และ ≤ 12%']
  "E" [note: '> 12% (ข้าม D)']
}
 
Enum shop.user_role {
  "OWNER" [note: 'เจ้าของร้าน — 1 ต่อสาขา']
  "STAFF" [note: 'พนักงานร้าน — ไม่เกิน 5 ต่อสาขา']
}
 
Table shop.referral_channels {
  id smallserial [pk]
  name varchar(100) [not null, unique, note: 'ช่องทางที่ลูกค้ารู้จัก']
  is_active bool [not null, default: true]
  ~audit_c
 
  Note: 'Lookup ช่องทางรู้จัก (form 1) — seed: Facebook, Line OA, TikTok, เพื่อน/คนรู้จักบอกต่อ, พนักงานบริษัทแนะนำ, อื่นๆ · แอดมินจัดการเพิ่ม/ปิดได้'
}
 
Table shop.shops {
  id uuid [pk, default: `gen_random_uuid()`]
  business_id uuid [not null, note: 'บ้านที่ร้านสมัครเข้าเป็นพาร์ทเนอร์']
 
  // ───── ข้อมูลร้าน (form 2) — 1 row = 1 สาขา ─────
  shop_name varchar(255) [not null, note: 'ชื่อร้าน/บริษัท']
  branch_name varchar(255) [not null, note: 'ชื่อสาขา (ไม่แยกตารางสาขา)']
  email varchar(255) [not null]
  sales_channel_link varchar(255) [not null, note: 'ลิงก์ช่องทางการขาย (FB/IG/TikTok)']
  phone_number varchar(10) [not null, note: 'เบอร์ร้าน — unique ต่อบ้าน · ยืนยัน OTP (Redis) · = เบอร์ owner (shop.users role=OWNER) sync กัน · แก้ไขได้']
 
  // ───── เจ้าของ/ตัวแทน (form 1) ─────
  owner_title_prefix shop.title_prefix [not null]
  owner_national_id varchar(13) [not null]
  owner_full_name varchar(255) [not null]
  owner_date_of_birth date [not null]
  owner_id_card_file_id uuid [not null, note: 'FK → sys.r2_file (รูปบัตร, private)']
  owner_address_line varchar(255) [not null, note: 'ที่อยู่ตามบัตร']
  owner_sub_district_id integer [not null, note: 'FK → sys.sub_districts']
  referral_channel_id smallint [not null, note: 'FK → shop.referral_channels']
 
  // ───── ที่อยู่ร้าน + เอกสาร (form 3) ─────
  address_line varchar(255) [not null]
  sub_district_id integer [not null, note: 'FK → sys.sub_districts']
  address_same_as_registration bool [not null, default: false, note: 'ที่อยู่ร้านตามใบทะเบียนหรือไม่']
  storefront_file_id uuid [not null, note: 'FK → sys.r2_file (รูปหน้าร้าน, private)']
  commercial_reg_file_id uuid [note: 'FK → sys.r2_file (ใบทะเบียนพาณิชย์ — optional)']
  por_por_20_file_id uuid [note: 'FK → sys.r2_file (ภ.พ.20 — optional)']
 
  // ───── บัญชีธนาคาร (form 4) ─────
  bank_code varchar(10) [not null, note: 'FK → sys.banks · แก้ไขได้']
  bank_account_number varchar(20) [not null, note: 'แก้ไขได้']
  bank_account_name varchar(255) [not null, note: 'ชื่อบัญชี — ห้ามเปลี่ยน']
 
  // ───── ลายเซ็น (form 6) ─────
  signature_file_id uuid [not null, note: 'FK → sys.r2_file (ลายเซ็น, private)']
 
  // ───── เอกสารสัญญา + เชื่อม LINE group (AD3 ตอนพิจารณา) ─────
  contract_file_id uuid [note: 'FK → sys.r2_file — เอกสารสัญญาที่ระบบ gen ตอนยื่น (เปลี่ยนตามข้อมูลที่กรอก, private)']
  line_group_id varchar(255) [unique, note: 'groupId ของไลน์กลุ่ม — webhook จับเมื่อเจ้าของส่งรหัส (OTP/Redis) ในกลุ่ม · null จนผูกสำเร็จ']
  line_group_name varchar(255) [note: 'ชื่อไลน์กลุ่ม (พนักงานกรอกตอนพิจารณา)']
  line_connected_at timestamptz [note: 'เวลาที่ผูกกลุ่มไลน์สำเร็จ']
 
  // ───── สถานะพาร์ทเนอร์ ─────
  status shop.partner_status [not null, default: 'PENDING']
  grade shop.grade [not null, default: 'A', note: 'เกรดร้าน (materialized) — ร้านใหม่ = A · คำนวณใหม่จากสัดส่วนลูกค้าหนี้เสีย/ลูกค้าทำสัญญาทั้งหมด']
  grade_calculated_at timestamptz [note: 'เวลาคำนวณเกรดจริงล่าสุด — null = ยังไม่เคยคำนวณ (ใช้ default A)']
 
  ~audit_cu
 
  indexes {
    (business_id, phone_number) [name: 'idx_shop_business_phone', unique, note: 'เบอร์ร้านห้ามซ้ำต่อบ้าน']
    business_id [name: 'idx_shop_business']
    owner_national_id [name: 'idx_shop_owner_nid']
  }
  checks {
    `phone_number ~ '^[0-9]{10}$'`
    `owner_national_id ~ '^[0-9]{13}$'`
  }
  Note: 'ร้านค้าพาร์ทเนอร์ — 1 row = 1 สาขา (denormalized: เจ้าของ+ที่อยู่+บัญชี+ลายเซ็น+สถานะ รวมในตารางเดียว) · สมัคร 7 ขั้น · เจ้าของ identity อยู่ owner_* (พนักงานไม่มี) · REJECTED/CANCELED hard-delete row ได้เพื่อปล่อยเบอร์สมัครใหม่ · ACTIVE↔INACTIVE ยกเลิก/คืนสถานะพาร์ทเนอร์'
}
Ref fk_shop_business: shop.shops.business_id > org.businesses.id [delete: restrict, update: no action]
Ref fk_shop_referral: shop.shops.referral_channel_id > shop.referral_channels.id [delete: restrict, update: no action]
Ref fk_shop_owner_subdistrict: shop.shops.owner_sub_district_id > sys.sub_districts.id [delete: restrict, update: no action]
Ref fk_shop_subdistrict: shop.shops.sub_district_id > sys.sub_districts.id [delete: restrict, update: no action]
Ref fk_shop_bank: shop.shops.bank_code > sys.banks.code [delete: restrict, update: no action]
Ref fk_shop_idcard_file: shop.shops.owner_id_card_file_id > sys.r2_file.id [delete: restrict, update: no action]
Ref fk_shop_storefront_file: shop.shops.storefront_file_id > sys.r2_file.id [delete: restrict, update: no action]
Ref fk_shop_commreg_file: shop.shops.commercial_reg_file_id > sys.r2_file.id [delete: set null, update: no action]
Ref fk_shop_pp20_file: shop.shops.por_por_20_file_id > sys.r2_file.id [delete: set null, update: no action]
Ref fk_shop_signature_file: shop.shops.signature_file_id > sys.r2_file.id [delete: restrict, update: no action]
Ref fk_shop_contract_file: shop.shops.contract_file_id > sys.r2_file.id [delete: set null, update: no action]
 
Table shop.users {
  id uuid [pk, default: `gen_random_uuid()`]
  shop_id uuid [not null]
  business_id uuid [not null, note: 'denormalize จาก shops.business_id — ใช้บังคับ username unique ต่อกิจการ']
  username varchar(32) [not null, note: 'login · a-z A-Z 0-9 . _ - · ขึ้นต้นตัวอักษร · 3-32 ตัว · unique ต่อกิจการ']
  display_name varchar(255) [not null, note: 'ชื่อ-นามสกุล แสดงในร้าน (ซ้ำได้)']
  phone_number varchar(10) [not null, note: 'OWNER: เบอร์เดียวกับ shops.phone_number (sync)']
  password varchar(255) [not null, note: 'Hash — 8+ ตัว พิมพ์เล็ก/พิมพ์ใหญ่/ตัวเลข [TODO]']
  role shop.user_role [not null]
  is_deleted bool [not null, default: false, note: 'Soft delete — ไม่มีปิดใช้งาน (is_active)']
  ~audit_cu
 
  indexes {
    (business_id, username) [name: 'idx_shop_user_username', unique, note: 'where is_deleted=false · username ห้ามซ้ำต่อกิจการ']
    shop_id [name: 'idx_shop_user_shop']
  }
  checks {
    `username ~ '^[a-zA-Z][a-zA-Z0-9._-]{2,31}$'`
    `phone_number ~ '^[0-9]{10}$'`
  }
  Note: 'บัญชี login ร้าน (เจ้าของ + พนักงาน · OWNER 1 + STAFF ≤5/สาขา) · username unique ต่อกิจการ · display_name ซ้ำได้ · เปลี่ยน username/password/display_name/เบอร์ ได้เสมอ · soft delete (ไม่มีปิดใช้งาน)'
}
Ref fk_shopuser_shop: shop.users.shop_id > shop.shops.id [delete: cascade, update: no action]
Ref fk_shopuser_business: shop.users.business_id > org.businesses.id [delete: cascade, update: no action]
 
Table shop.partner_status_logs {
  id uuid [pk, default: `gen_random_uuid()`]
  shop_id uuid [not null]
  from_status shop.partner_status [note: 'null = ตอนสมัครครั้งแรก']
  to_status shop.partner_status [not null]
  note text [note: 'เหตุผล (ไม่อนุมัติ/ขอแก้ไข/ยกเลิก)']
  ~audit_c
 
  indexes {
    shop_id [name: 'idx_shop_status_log']
  }
  Note: 'ประวัติเปลี่ยนสถานะพาร์ทเนอร์ (AD3 อนุมัติ/ปฏิเสธ/ขอแก้ไข/ยกเลิก)'
}
Ref fk_shop_statuslog_shop: shop.partner_status_logs.shop_id > shop.shops.id [delete: cascade, update: no action]
 
Enum shop.reason_applicable {
  "RETURNED" [note: 'หัวข้อตอนตีกลับแก้ไข (= form section)']
  "REJECTED" [note: 'เหตุผลตอนไม่อนุมัติ']
  "INACTIVE" [note: 'เหตุผลตอนยกเลิกพาร์ทเนอร์ (ACTIVE→INACTIVE)']
}
 
Table shop.partner_reasons {
  id smallserial [pk]
  description varchar(255) [not null, note: 'หัวข้อแก้ไข / เหตุผลไม่อนุมัติ ที่ให้พนักงานเลือก']
  applicable_to shop.reason_applicable [not null]
  is_active bool [not null, default: true]
  ~audit_cu
 
  Note: 'Lookup หัวข้อตีกลับ (RETURNED = form section → เด้งไปฟอร์มนั้น) + เหตุผลไม่อนุมัติ (REJECTED) + เหตุผลยกเลิกพาร์ทเนอร์ (INACTIVE)'
}
 
Table shop.partner_log_reasons {
  log_id uuid [not null]
  reason_id smallint [not null]
 
  indexes {
    (log_id, reason_id) [pk, name: 'idx_partner_log_reason']
  }
  Note: 'M:N เชื่อม status log กับหัวข้อ/เหตุผลที่เลือก (เลือกได้หลายข้อ · หมายเหตุอยู่ที่ partner_status_logs.note)'
}
Ref fk_partner_logreason_log: shop.partner_log_reasons.log_id > shop.partner_status_logs.id [delete: cascade, update: no action]
Ref fk_partner_logreason_reason: shop.partner_log_reasons.reason_id > shop.partner_reasons.id [delete: restrict, update: no action]
 
Table shop.consents {
  id uuid [pk, default: `gen_random_uuid()`]
  shop_user_id uuid [not null, note: 'ผู้ที่กดยอมรับ (ปกติ = OWNER) → shop.users · ร้านได้จาก join shop.users.shop_id']
  terms_id uuid [not null, note: 'FK → org.terms_and_conditions (type=TERMS_OF_SERVICE)']
  accepted_at timestamptz [not null, default: `CURRENT_TIMESTAMP`]
 
  indexes {
    (shop_user_id, terms_id) [name: 'idx_shop_consent', unique]
  }
  Note: 'การยอมรับเงื่อนไขบริการ ระดับบุคคล (ใครกด + เวอร์ชันไหน + เมื่อไร) — owner ยอมรับตอนสมัคร · symmetric กับ customer.consents'
}
Ref fk_shop_consent_user: shop.consents.shop_user_id > shop.users.id [delete: cascade, update: no action]
Ref fk_shop_consent_terms: shop.consents.terms_id > org.terms_and_conditions.id [delete: restrict, update: no action]
 
Table shop.grade_config {
  id smallint [pk, default: 1, note: 'singleton — บังคับ 1 row (id=1)']
  grade_a_max_pct numeric(5,2) [not null, default: 3, note: 'หนี้เสีย ≤ ค่านี้ = A (%)']
  grade_b_max_pct numeric(5,2) [not null, default: 8, note: '> A และ ≤ ค่านี้ = B']
  grade_c_max_pct numeric(5,2) [not null, default: 12, note: '> B และ ≤ ค่านี้ = C · เกินกว่านี้ = E']
  ~audit_cu
 
  checks {
    `id = 1`
    `grade_a_max_pct < grade_b_max_pct`
    `grade_b_max_pct < grade_c_max_pct`
  }
  Note: 'เกณฑ์เกรดร้าน (cutoff %) ระดับระบบ — singleton 1 row · เกรด = f( ลูกค้าหนี้เสียของร้าน / ลูกค้าทำสัญญาทั้งหมดของร้าน ×100 ) · A≤a_max, B≤b_max, C≤c_max, E>c_max'
}

Mermaid ER

erDiagram
  businesses ||--o{ shops : "มีร้านพาร์ทเนอร์"
  referral_channels ||--o{ shops : "ช่องทางรู้จัก"
  shops ||--o{ shop_users : "เจ้าของ+พนักงาน"
  businesses ||--o{ shop_users : "username unique/กิจการ"
  shop_users ||--o{ shop_consents : "รับข้อตกลง"
  terms_and_conditions ||--o{ shop_consents : "เวอร์ชัน"
  shops ||--o{ partner_status_logs : "ประวัติสถานะ"
  partner_status_logs ||--o{ partner_log_reasons : "เหตุผล"
  partner_reasons ||--o{ partner_log_reasons : "ถูกเลือก"
  r2_file ||--o{ shops : "บัตร/หน้าร้าน/เอกสาร/ลายเซ็น/สัญญา"
  sub_districts ||--o{ shops : "ที่อยู่"
  banks ||--o{ shops : "บัญชีธนาคาร"

  shops {
    uuid id PK
    uuid business_id FK
    varchar shop_name
    varchar branch_name
    varchar email
    varchar sales_channel_link
    varchar phone_number
    enum owner_title_prefix
    varchar owner_national_id
    varchar owner_full_name
    date owner_date_of_birth
    uuid owner_id_card_file_id FK
    varchar owner_address_line
    int owner_sub_district_id FK
    smallint referral_channel_id FK
    varchar address_line
    int sub_district_id FK
    bool address_same_as_registration
    uuid storefront_file_id FK
    uuid commercial_reg_file_id FK
    uuid por_por_20_file_id FK
    varchar bank_code FK
    varchar bank_account_number
    varchar bank_account_name
    uuid signature_file_id FK
    uuid contract_file_id FK
    varchar line_group_id UK
    varchar line_group_name
    timestamptz line_connected_at
    enum status
    enum grade
    timestamptz grade_calculated_at
    varchar created_by
    varchar updated_by
    timestamptz created_at
    timestamptz updated_at
  }
  referral_channels {
    smallserial id PK
    varchar name UK
    bool is_active
  }
  shop_users {
    uuid id PK
    uuid shop_id FK
    uuid business_id FK
    varchar username UK
    varchar display_name
    varchar phone_number
    varchar password
    enum role
    bool is_deleted
  }
  partner_status_logs {
    uuid id PK
    uuid shop_id FK
    enum from_status
    enum to_status
    text note
  }
  partner_reasons {
    smallserial id PK
    varchar description
    enum applicable_to
    bool is_active
  }
  partner_log_reasons {
    uuid log_id PK
    smallint reason_id PK
  }
  shop_consents {
    uuid id PK
    uuid shop_user_id FK
    uuid terms_id FK
    timestamptz accepted_at
  }
  grade_config {
    smallint id PK
    numeric grade_a_max_pct
    numeric grade_b_max_pct
    numeric grade_c_max_pct
  }

✅ Resolved (Domain 3 — registration) — 16 มิ.ย. 2026

  1. ไม่แยกตารางสาขาshop_name + branch_name 2 คอลัมน์ใน shop.shops (1 row = 1 สาขา) ✓
  2. เจ้าของ identity → owner_* บน shop.shops (พนักงานไม่มี identity) ✓
  3. referral → lookup table shop.referral_channels
  4. shop ผูกบ้านbusiness_id ✓ · เบอร์ unique ต่อบ้าน → idx (business_id, phone_number) ✓
  5. OTP → Redis (ephemeral) ไม่มีตารางใน DB ✓
  6. usersshop.users (OWNER 1 + STAFF ≤5, รวม ≤6 บังคับ app layer) ✓
  7. ทุกไฟล์ (บัตร/หน้าร้าน/ใบทะเบียน/ภ.พ.20/ลายเซ็น) → file_id FK → sys.r2_file ✓

✅ Resolved (Domain 3 — AD3 พิจารณาคำขอ) — 16 มิ.ย. 2026

  1. 3 action (อนุมัติ/ตีกลับ/ไม่อนุมัติ) → partner_status_logs.to_status (ACTIVE/RETURNED/REJECTED) ✓ 8b. lifecycle → enum: PENDING·RETURNED·REJECTED·CANCELED(timeout 14 วัน auto)·ACTIVE·INACTIVE · REJECTED/CANCELED ลบ row ปล่อยเบอร์ได้ · ACTIVE↔INACTIVE คืนสถานะได้ ✓

  2. เหตุผลshop.partner_reasons (applicable_to RETURNED=หัวข้อแก้ไข / REJECTED=เหตุผล) + M:N partner_log_reasons + หมายเหตุที่ log.note ✓

  3. เชื่อม LINE groupshops.line_group_id (webhook จับ groupId, unique) + line_group_name (พนักงานกรอก) + line_connected_at · รหัสเชื่อม = OTP/Redis (ephemeral) ✓

  4. เอกสารสัญญา gen ตอนยื่น → shops.contract_file_id (regen ตามข้อมูล) ✓

  5. badge ใหม่/แก้ไข ในรายการ → derive จาก partner_status_logs (เคยมี RETURNED = “แก้ไข”) ไม่ต้องมีคอลัมน์ ✓

  6. shop.users (ฟอร์มสร้างพนักงานร้าน node 1119:256259) → username (unique ต่อกิจการ, pattern a-z A-Z 0-9 . _ - ขึ้นต้นตัวอักษร 3-32) + display_name (ซ้ำได้) + phone + password · ตัด is_active (ไม่มีปิดใช้งาน) เหลือ soft delete · เพิ่ม business_id (denormalize) ✓

  7. shop.grade → enum A/B/C/E (ข้าม D) · materialized: เก็บ grade + grade_calculated_at คำนวณ batch/event (ไม่คำนวณสด) · เกณฑ์ใน shop.grade_config (A≤3%, B≤8%, C≤12%, E>12% ของลูกค้าหนี้เสีย/ลูกค้าทำสัญญาทั้งหมด) ปรับได้ · customer.grade ใช้แนวเดียวกัน (+grade_calculated_at) ✓

❓ ยังค้าง

  • เกณฑ์/สูตร customer.grade (ยังไม่ระบุ — shop.grade เสร็จแล้ว)
  • dependency: sys.banks, sys.sub_districts, org.terms_and_conditions (✅ นิยามแล้ว)

Cross-domain dependencies (รอ Domain 8 / shared)

  • sys.provinces, sys.districts, sys.sub_districts — geo seed data (ที่อยู่)
  • sys.terms_and_conditions — เวอร์ชันข้อตกลง (ร้านรับก่อนสมัคร)
  • sys.r2_file — ภาพบัตร/เอกสาร (private)

Open modeling questions (ประกอบตารางหลังเก็บครบ)

  • ข้อมูลเจ้าของ (form 1) อยู่ตาราง shop.shops (owner_* fields) หรือแยก shop.owners?
  • เบอร์ unique “ระดับร้านสาขา” → โครงสร้าง shop ↔ branch ↔ phone เป็นยังไง (รอ form ถัดไป)