SurePhone ERD — Domain 1: Org & Access (กิจการ/บ้าน & การเข้าถึง)

บ้าน (org.businesses) = ระดับบนสุด สร้างโดย Super Admin เท่านั้น field ของ businesses ยึดจากฟอร์ม Figma “เพิ่มกิจการใหม่” (node 2957:413689) convention: schema prefix, audit_trail partial, enum, note, ref delete rule · [?] = รอ confirm


DBML

//// audit_c, audit_cu (partials) + sys.r2_file นิยามใน 00-shared.dbml (concat ก่อน compile)
 
//// ───────── org (บ้าน/กิจการ) ─────────
Table org.businesses {
  id uuid [pk, default: `gen_random_uuid()`]
  name varchar(255) [not null, note: 'ชื่อกิจการ/บ้าน — ห้ามซ้ำ (unique)']
  logo_file_id uuid [not null, note: 'FK → sys.r2_file (โลโก้, is_public=true)']
 
  // สำหรับร้านค้า (ช่องทางสื่อสารฝั่งร้าน)
  facebook_link varchar(255) [not null]
  facebook_qr_file_id uuid [not null, note: 'FK → sys.r2_file (QR Facebook, is_public=true)']
  shop_line_messaging_token varchar(512) [not null, note: 'LINE Messaging Channel Access Token (ร้านค้า) [TODO] encrypt']
 
  // สำหรับลูกค้า (LINE OA + LINE Login ฝั่งลูกค้า)
  line_oa_link varchar(255) [not null]
  line_oa_qr_file_id uuid [not null, note: 'FK → sys.r2_file (QR Line OA, is_public=true)']
  line_login_channel_id varchar(255) [not null]
  line_login_channel_secret varchar(512) [not null, note: '[TODO] encrypt']
  customer_line_messaging_token varchar(512) [not null, note: 'LINE Messaging Channel Access Token (ลูกค้า) [TODO] encrypt']
  phone_number varchar(10) [not null]
  collect_vat bool [not null, default: true, note: 'เก็บ VAT 7% หรือไม่']
 
  // ที่อยู่สำหรับคืนเครื่อง
  return_name varchar(255) [not null, note: 'ชื่อกิจการสำหรับคืนเครื่อง']
  return_address text [not null, note: 'ที่อยู่กิจการ (free text)']
  return_phone varchar(10) [not null]
 
  ~audit_cu
 
  indexes {
    name [name: 'idx_business_name', unique, note: 'ชื่อบ้านห้ามซ้ำ']
  }
  checks {
    `phone_number ~ '^[0-9]{10}$'`
    `return_phone ~ '^[0-9]{10}$'`
  }
  Note: 'บ้าน/กิจการ — ระดับบนสุด สร้างโดย Super Admin เท่านั้น · ไม่มีลบ/ปิดใช้งาน (ไม่มี is_active/is_deleted) · ชื่อห้ามซ้ำ · ไม่มีสาขา (สาขาอยู่ที่ร้านพาร์ทเนอร์ — Domain 3) · รูปหน้าปก login ย้ายไป sys (ระดับระบบ)'
}
Ref fk_business_logo_file: org.businesses.logo_file_id > sys.r2_file.id [delete: restrict, update: no action]
Ref fk_business_fb_qr_file: org.businesses.facebook_qr_file_id > sys.r2_file.id [delete: restrict, update: no action]
Ref fk_business_lineoa_qr_file: org.businesses.line_oa_qr_file_id > sys.r2_file.id [delete: restrict, update: no action]
 
Table org.investors {
  id uuid [pk, default: `gen_random_uuid()`]
  business_id uuid [not null, note: 'นายทุน 1 คน ผูก 1 บ้าน']
  display_name varchar(255) [not null, note: 'ชื่อ-นามสกุล / Display name (ฟิลด์เดียว)']
  email varchar(255) [not null]
  phone_number varchar(10) [not null, note: 'ใช้ login + OTP']
  investment_amount bigint [not null, default: 0, note: 'เงินลงทุน (สตางค์)']
  password varchar(255) [not null, note: 'Hash — รหัสผ่าน 8+ ตัว มีพิมพ์เล็ก/พิมพ์ใหญ่/ตัวเลข [TODO]']
  is_phone_verified bool [not null, default: false, note: 'false = login ครั้งแรกต้องยืนยัน OTP; true แล้ว login ด้วยเบอร์+รหัส (เปลี่ยนเบอร์ → OTP ใหม่)']
  ~audit_cu
 
  indexes {
    phone_number [name: 'idx_investor_phone', unique]
  }
  checks {
    `phone_number ~ '^[0-9]{10}$'`
  }
 
  Note: 'นายทุน/ผู้ลงทุนของบ้าน — login เอง (เบอร์+รหัสผ่าน+OTP) ดู dashboard (AF) · ลบถาวรได้ ไม่มีปิดใช้งาน (ไม่มี is_active/is_deleted) · field ตามฟอร์มสร้างนักลงทุน (node 1858:309897)'
}
Ref fk_investor_business: org.investors.business_id > org.businesses.id [delete: restrict, update: no action]
 
//// ───────── org: ข้อตกลง/เงื่อนไข (ระดับกิจการ) ─────────
Enum org.terms_type {
  "TERMS_OF_SERVICE" [note: 'เงื่อนไขการให้บริการ — ใช้ทั้งร้านและลูกค้า (versioned, AD11.3) · เพิ่ม type อื่นได้ภายหลัง']
}
 
Table org.terms_and_conditions {
  id uuid [pk, default: `gen_random_uuid()`]
  business_id uuid [not null, note: 'ข้อตกลงระดับกิจการ — admin แต่ละบ้านแก้เอง (AD11.3)']
  type org.terms_type [not null]
  version integer [not null, note: 'เวอร์ชันของ (business, type)']
  content text [not null]
  is_latest bool [not null, default: false, note: 'เวอร์ชันล่าสุดที่บังคับใช้ต่อ (business, type)']
  ~audit_cu
 
  indexes {
    (business_id, type, version) [name: 'idx_terms_version', unique]
    (business_id, type, is_latest) [name: 'idx_terms_latest']
  }
  Note: 'ข้อตกลง/เงื่อนไข ระดับกิจการ แยกตามประเภท + เวอร์ชัน · ร้าน/ลูกค้ารับเวอร์ชันล่าสุดก่อนใช้งาน · เก็บทุกเวอร์ชันเพื่ออ้างอิงการยอมรับ'
}
Ref fk_terms_business: org.terms_and_conditions.business_id > org.businesses.id [delete: cascade, update: no action]
 
//// ───────── staff (back-office: super admin / admin / employee) ─────────
Enum staff.role {
  "SUPER_ADMIN" [note: 'สร้างบ้าน/สร้างพนักงานได้ เข้าทุกบ้าน จัดการระบบ']
  "ADMIN"       [note: 'ผู้ดูแล — ต้องถูกกำหนดบ้าน + สิทธิ์ ก่อนใช้งาน']
}
 
Table staff.users {
  id uuid [pk, default: `gen_random_uuid()`]
  profile_file_id uuid [note: 'FK → sys.r2_file (รูปโปรไฟล์, optional)']
  phone_number varchar(10) [not null, note: 'login ด้วยเบอร์ + รหัสผ่าน']
  display_name varchar(255) [not null, note: 'ชื่อ-นามสกุล / Display name (ฟิลด์เดียว)']
  password varchar(255) [not null, note: 'Hash — รหัสผ่าน 8+ ตัว มีพิมพ์เล็ก/พิมพ์ใหญ่/ตัวเลข [TODO]']
  must_change_password bool [not null, default: true, note: 'true = บังคับตั้งรหัสใหม่ตอน login ครั้งแรก → false หลังเปลี่ยน']
  role staff.role [not null, default: 'ADMIN', note: '[?] ฟอร์มสร้างไม่เลือก role → default ADMIN; SUPER_ADMIN seed/พิเศษ']
  is_active bool [not null, default: true, note: 'ปิดการใช้งานได้']
  is_deleted bool [not null, default: false, note: 'Soft delete — ลบแบบเก็บข้อมูล']
  ~audit_cu
 
  indexes {
    phone_number [name: 'idx_staff_phone', unique, note: 'where is_deleted = false']
  }
  checks {
    `phone_number ~ '^[0-9]{10}$'`
  }
 
  Note: 'ผู้ใช้หลังบ้าน (SUPER_ADMIN/ADMIN) — login เบอร์+รหัสผ่าน · สร้างเสร็จยังไม่ผูกบ้าน/ไม่มีสิทธิ์ ต้องกำหนดผ่าน user_businesses + user_permissions แยก · ปิดใช้งาน/ลบ(เก็บข้อมูล)ได้ · field ตามฟอร์ม node 1855:237840'
}
Ref fk_staff_profile_file: staff.users.profile_file_id > sys.r2_file.id [delete: set null, update: no action]
 
Table staff.user_businesses {
  user_id uuid
  business_id uuid
 
  indexes {
    (user_id, business_id) [pk]
  }
  Note: 'บ้านที่ staff เข้าถึงได้ (หน้าเลือกกิจการ) — staff ฝั่งระบบเข้าได้หลายบ้าน; SUPER_ADMIN เข้าทุกบ้าน เช็คที่ app layer'
}
Ref fk_userbiz_user: staff.user_businesses.user_id > staff.users.id [delete: cascade, update: no action]
Ref fk_userbiz_business: staff.user_businesses.business_id > org.businesses.id [delete: cascade, update: no action]
 
Table staff.permissions {
  id smallserial [pk]
  code varchar(30) [not null, unique, note: 'รหัสสั้นของ feature']
  description varchar(255)
  ~audit_c
 
  Note: 'แคตตาล็อก feature/สิทธิ์ทั้งหมดในระบบ — ผูกกับพนักงานผ่าน user_permissions (SA4)'
}
 
Table staff.user_permissions {
  user_id uuid
  permission_id smallint
 
  indexes {
    (user_id, permission_id) [pk]
  }
  Note: 'SA4 จัดการสิทธิ์ราย feature ต่อพนักงาน — per-user ทั้งระบบ (ไม่แยกตามบ้าน)'
}
Ref fk_userperm_user: staff.user_permissions.user_id > staff.users.id [delete: cascade, update: no action]
Ref fk_userperm_permission: staff.user_permissions.permission_id > staff.permissions.id [delete: cascade, update: no action]

Mermaid ER

erDiagram
  businesses ||--o{ investors : "มีนายทุน"
  businesses ||--o{ user_businesses : "เข้าถึงโดย"
  staff_users ||--o{ user_businesses : "เข้าถึงบ้าน"
  staff_users ||--o{ user_permissions : "ได้รับสิทธิ์"
  permissions ||--o{ user_permissions : "เป็นสิทธิ์ใน"
  r2_file ||--o{ businesses : "โลโก้/QR"
  r2_file ||--o{ staff_users : "โปรไฟล์"
  businesses ||--o{ terms_and_conditions : "ข้อตกลง"

  businesses {
    uuid id PK
    varchar name UK
    uuid logo_file_id FK
    varchar facebook_link
    uuid facebook_qr_file_id FK
    varchar shop_line_messaging_token
    varchar line_oa_link
    uuid line_oa_qr_file_id FK
    varchar line_login_channel_id
    varchar line_login_channel_secret
    varchar customer_line_messaging_token
    varchar phone_number
    bool collect_vat
    varchar return_name
    text return_address
    varchar return_phone
    varchar created_by
    varchar updated_by
    timestamptz created_at
    timestamptz updated_at
  }
  investors {
    uuid id PK
    uuid business_id FK
    varchar display_name
    varchar email
    varchar phone_number UK
    bigint investment_amount
    varchar password
    bool is_phone_verified
    varchar created_by
    varchar updated_by
    timestamptz created_at
    timestamptz updated_at
  }
  staff_users {
    uuid id PK
    uuid profile_file_id FK
    varchar phone_number UK
    varchar display_name
    varchar password
    bool must_change_password
    enum role
    bool is_active
    bool is_deleted
    varchar created_by
    varchar updated_by
    timestamptz created_at
    timestamptz updated_at
  }
  user_businesses {
    uuid user_id PK
    uuid business_id PK
  }
  permissions {
    smallserial id PK
    varchar code UK
    varchar description
    varchar created_by
    timestamptz created_at
  }
  user_permissions {
    uuid user_id PK
    smallint permission_id PK
  }
  r2_file {
    uuid id PK
    varchar object_key UK
    bool is_public
    varchar mime_type
  }
  terms_and_conditions {
    uuid id PK
    uuid business_id FK
    enum type
    integer version
    text content
    bool is_latest
    varchar created_by
    varchar updated_by
    timestamptz created_at
    timestamptz updated_at
  }

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

  1. บ้าน (org.businesses) = ระดับบนสุด สร้างโดย Super Admin · field ยึดฟอร์ม Figma จริง (node 2957:413689) ✓
  2. ตัดทิ้ง: business_bank_accounts, branches, business_platforms, enum platform_type — ไม่มีในฟอร์ม/ระบบ ✓
  3. โมเดล staff → ตารางเดียว + role; เข้าหลายบ้านผ่าน user_businesses; สิทธิ์ per-user ทั้งระบบ ✓
  4. นายทุนorg.investors login เอง, ผูก 1 บ้าน ✓
  5. สาขา (branches) → ย้ายไปฝั่งร้านพาร์ทเนอร์ (Domain 3) — บ้านไม่มีสาขา ✓

✅ Resolved — review รอบ 2 (16 มิ.ย. 2026)

  1. businesses ไม่มีลบ/ปิดใช้งาน → ตัด is_active, is_deleted ออก ✓
  2. ชื่อบ้านห้ามซ้ำidx_business_name เป็น unique ✓
  3. รูปหน้าปก login → ระดับระบบ ไม่ใช่ระดับบ้าน → ตัด cover_img_path ออกจาก org ย้ายไป sys (Domain 8) ✓
  4. นายทุน → ฟิลด์ตามฟอร์มจริง (node 1858:309897): display_name ฟิลด์เดียว, email required · ลบถาวรได้ ไม่มีปิดใช้งาน → ตัด is_active, is_deleted · มี is_phone_verified (OTP ครั้งแรก) ✓

✅ Resolved — review รอบ 3 (16 มิ.ย. 2026)

  1. check เบอร์โทร → มีทุกตารางที่มีเบอร์ เหมือนกันทั้งระบบ ~ '^[0-9]{10}$' (เพิ่มให้ businesses.phone_number + return_phone แล้ว) ✓
  2. นายทุน อายุ/สถานะ → ไม่มีการแสดง (ยืนยัน node 2961:417051) → ไม่เก็บ ✓
  3. พนักงานมี 2 role เท่านั้น → ตัด EMPLOYEE; เหลือ SUPER_ADMIN, ADMIN ✓
  4. ฟอร์มสร้างพนักงาน (node 1855:237840) → display_name ฟิลด์เดียว, ไม่มี email, profile_file_id optional · ปิดใช้งาน + soft delete ได้ · must_change_password ตั้งรหัสครั้งแรก ✓
  5. flow กำหนดสิทธิ์ → สร้างพนักงานเสร็จ = ยังไม่ผูกบ้าน/ไม่มีสิทธิ์ → กำหนดผ่าน user_businesses (เลือกกิจการ node 2581:364525) + user_permissions (กำหนดสิทธิ์ node 592:115505) แยกขั้นตอน ✓

✅ Resolved — review รอบ 4 (R2 file storage, 16 มิ.ย. 2026)

  1. ไฟล์ R2 = Normalized FK (แบบ A) → ทุก xxx_img_path เปลี่ยนเป็น xxx_file_id uuid FK → sys.r2_file (นิยามใน 00-shared) ✓
  2. ไม่เก็บ public URL ถาวร → r2_file เก็บแค่ object_key + flag is_public (โลโก้/QR = public; บัตร/เซลฟี่/รูปเครื่อง/สลิป/วิดีโอ = private เข้าผ่าน signed URL) · bucket เดียว = app config ✓
  3. Domain 1 แปลงแล้ว: logo_file_id, facebook_qr_file_id, line_oa_qr_file_id (businesses, public), profile_file_id (staff, optional) ✓

📝 หมายเหตุข้ามโดเมน / ค้าง

  • customer.users.registered_shop_id (Domain 2) → ชี้ shop.shops (Domain 3) ✓ แก้แล้ว
  • รูปหน้าปก login: ของกิจการorg.business_settings.login_cover_file_id (01b) · ของ Portal หลักsys.portal_settings
  • LINE tokens/secrets ควร encrypt at rest (มาร์ค [TODO])
  • convention ใหม่: ทุก field เบอร์โทร มี check ~ '^[0-9]{10}$' เสมอ
  • [?] role assignment: ฟอร์มสร้างพนักงานไม่เลือก role → default ADMIN; SUPER_ADMIN มาจากไหน (seed/พิเศษ) รอเคลียร์