一句話:現行 M1 —— 新增「同業」客戶分類實體(
agencies表,含負責業務owner_user_id),同業通路訂單經orders.agency_id直連歸屬;會員層只有客戶分類member_profiles.customer_type。→ M2(未落地):會員 / 同業各自掛「預設負責業務」並接成訂單派發的來源之一(不改派發規則本身)、同業 B2B 登入下單入口(見 §9)。
assigned_to_user_id),沒有客戶層的長期歸屬。assigned_to_user_id 是單一訂單的業績歸屬,可重派、可離職換手 —— 它回答「這張單算誰的業績」。owner_user_id 回答「這個客戶長期由誰維護」—— 它是 CRM 語意,不是業績語意。decideAssignment 派發規則(三模式:manual_review / auto_round_robin / auto_customer_choice)。探查
packages/core/src/schema/business.ts、packages/core/src/members/admin-repo.ts、packages/core/src/orders/*後的結論。沒有獨立的members業務表;「會員 / 旅客 / customer」散落三個地方,必須先釐清才能決定 owner 掛點。
| 概念 | 實際落地 | DB | 說明 |
|---|---|---|---|
| 登入帳號 / 使用者 | user | 各 tenant <tenant>_db | better-auth 管理(auth 表與商業表同庫,無共享 auth_db);id 由 better-auth 產生。員工 + 旅客會員都在這。角色記在 user.role(admin plugin;無 organization/member 表)。 |
| 後台「旅客會員」頁的 source of truth | user(role='customer') | 各 tenant <tenant>_db | members/admin-repo.ts 的 listMembers 直接讀同庫 user(role 篩客戶)。orders.user_id 原生 FK → user.id。 |
| 會員偏好擴充 | member_profiles | 各 tenant <tenant>_db | 1:1 掛 user.id(user_id unique FK,onDelete cascade)。存 phone / address / birthDate / preferredDestinations。後台會員頁 LEFT JOIN 取這些欄位。 |
| 旅客名單(per-order PII) | travelers | 各 tenant <tenant>_db | 掛 orders.id,存每位同行者姓名 / 身分證 / 護照 / 緊急聯絡人 / 房間。不是客戶帳號——同一個人在不同訂單會有多筆 traveler row。is_primary 標訂購人。 |
結論:
user(role='customer')+ member_profiles。「會員」概念實際對應到 同庫 user(帳號層)+ member_profiles(偏好層)。沒有也不要新建 members 表。travelers 是訂單明細層的 PII,不是 owner 掛點(它沒有帳號語意、會重複)。member_profiles(不是 user 表)。理由:
user 是 better-auth 管理的帳號表,欄位應由 better-auth 主導;塞業務欄位會破壞「帳號表 vs 租戶自有資料」的職責分離。member_profiles 已經是「租戶自有的會員擴充表」,是放 CRM 欄位的正確位置;它本來就是 admin 會員頁 join 的對象。member_profiles_user_unique 保證。member_profiles row 的會員(純註冊未填偏好)就沒有 owner。對策見 §4.1(owner 寫入時 upsert profile row)。agencies 表(§4.2)。同業仍以某個 user(auth 帳號)作為「下單帳號」(現階段由內部代下單),agencies 表透過 primary_user_id 連回 user。member_profiles.owner_user_id(FK → user.id,nullable)。後台會員頁可指派 / 變更負責業務 + 依負責業務篩選。agencies 表(同業公司資料 + owner_user_id + primary_user_id)。後台新增獨立 nav「同業管理」(/admin/agencies)。member_profiles.customer_type('direct' 直客 / 'agency_contact' 同業聯絡人),接同業價(同業價的「該不該套用」判斷依此 + agency 關聯)。agency.read / agency.manage / member.assign_owner。agencies 表只留 payment_terms free-text 欄位佔位)。assigned_to_user_id 計佣,owner 不直接影響佣金。對齊 CLAUDE.md「同庫 user FK」慣例:所有指向使用者的欄位都原生 FK 到本租戶 DB 的
user(auth 與商業表同庫)。Code Is Truth:穩定業務欄位一律直接進packages/core/src/schema/business.ts(不再走 setup-extras ALTER)。下方 §4.1–§4.3 描述現行 M1 落地(以 code 為準),§4.4 列 → M2(未落地) 的客戶層 owner 擴充。
member_profiles(現行 M1:只有客戶分類)現行 code 的 member_profiles 客戶層只落地 customer_type('direct' / 'agency_contact'):沒有 owner_user_id、沒有 agency_id(兩者屬 M2,見 §4.4)。customer_type 是「客戶是誰」的客戶層分類(CRM 單一同業辨識來源);成交時套用的價格通路快照寫在 product line(tour line 的 price_channel_snapshot,'direct'|'agency'),與本欄解耦、不放訂單外殼。
現行 drizzle(packages/core/src/schema/business.ts,節錄實況):
export const memberProfiles = pgTable(
'member_profiles',
{
id: idColumn('id', 'memberProfile').primaryKey(),
userId: idColumn('user_id', 'user')
.notNull()
.references(() => user.id, { onDelete: 'cascade' }),
phone: text('phone'),
address: text('address'),
birthDate: date('birth_date'),
preferredDestinations: text('preferred_destinations').array(),
/** CRM 客戶分類:'direct'(直客,預設)| 'agency_contact'(同業聯絡人)。接同業價判斷。 */
customerType: text('customer_type').notNull().default('direct'),
/** PDPA 匿名化軟刪標記(見 member-pdpa-self-service)。 */
anonymizedAt: timestamp('anonymized_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('member_profiles_user_unique').on(t.userId),
check(
'member_profiles_customer_type_valid',
sql`${t.customerType} IN ('direct','agency_contact')`,
),
],
);
agencies(現行 M1:同業公司對照表)agencies 是全新獨立實體(跨梯次、非 per-departure),已進 drizzle schema。現行 code(packages/core/src/schema/business.ts,實況):
/**
* 同業管理(agencies)— B2B 旅行社/團主對照表。跨梯次獨立實體(非 per-departure)。
* 負責業務 = staff user(owner_user_id → user.id)。同業通路訂單經 orders.agency_id
* 反向關聯(累計訂單 = COUNT orders WHERE agency_id)。tax_id 可空(個人團主無統編)。
*/
export const agencies = pgTable(
'agencies',
{
id: idColumn('id', 'agency').primaryKey(), // 'agc_<ulid>'
name: text('name').notNull(),
/** 統一編號(個人團主可留空)。 */
taxId: text('tax_id'),
contactName: text('contact_name'),
phone: text('phone'),
email: text('email'),
address: text('address'),
/** 帳期/結算方式(自由文字,例:'月結 30 天')。 */
paymentTerm: text('payment_term'),
/** 負責業務(staff user)。NULL = 未指派。原生 FK → 同庫 `user`。 */
ownerUserId: idColumn('owner_user_id', 'user').references(() => user.id, { onDelete: 'set null' }),
status: text('status').notNull().default('active'), // 'active' | 'archived'
note: text('note'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('agencies_status_idx').on(t.status),
index('agencies_owner_idx').on(t.ownerUserId),
check('agencies_status_valid', sql`${t.status} IN ('active','archived')`),
],
);
現行 M1 與早期設計草稿的差異(以 code 為準):
owner_user_id = 負責這家同業的業務(CRM owner,M1 已落地,onDelete: 'set null')。primary_user_id(同業主要下單帳號)—— 屬 M2 B2B portal(§4.4 / §9)。status 值域是 'active' | 'archived'(軟下架),非早期草稿的 'inactive';由 agencies_status_valid CHECK 收斂。phone / email(非草稿的 contact_phone / contact_email);帳期欄為 payment_term(單數)、備註欄為 note。tax_id 現行無 DB unique 約束;個人團主可留空,格式 / 唯一性驗證待產品畫押(見 apps/web/src/app/(admin)/admin/agencies/schemas.ts:taxId 只做 trim + 長度上限)。orders.agency_id 直連)M1 的同業訂單歸屬走 orders.agency_id 直連 FK,不是早期設計的「member_profiles.agency_id 推導 + agencies.primary_user_id」路徑:
// packages/core/src/schema/business.ts(orders 表節錄,實況)
/** 同業通路訂單歸屬的同業(agencies)。NULL = 直客 / 未掛同業。 */
agencyId: idColumn('agency_id', 'agency').references(() => agencies.id, { onDelete: 'set null' }),
// index: orders_agency_idx on (agency_id, created_at)
累計同業訂單 = COUNT(orders) WHERE agency_id = ?(反向關聯)。
關聯總覽(現行 M1):
user (同庫 better-auth 帳號表)
▲
└── agencies.owner_user_id (同業的負責業務 / staff)
agencies ◀── orders.agency_id (同業通路訂單 → 所屬同業,直連 FK)
member_profiles.customer_type (客戶層分類,M1 無 FK 到 agencies)
現行 M1 欄位清單(schema 回報,以 code 為準):
| 表 | 欄位 | 型別 | Nullable | FK / 約束 |
|---|---|---|---|---|
member_profiles | customer_type | text NOT NULL DEFAULT 'direct' | no | CHECK IN ('direct','agency_contact') |
orders | agency_id | text | yes | → agencies.id(set null) |
agencies(新表) | id | text PK | no | 'agc_<ulid>' |
agencies | name | text NOT NULL | no | — |
agencies | tax_id | text | yes | 無 DB unique(app 層驗證) |
agencies | contact_name / phone / email / address | text | yes | — |
agencies | payment_term / note | text | yes | — |
agencies | status | text NOT NULL DEFAULT 'active' | no | CHECK IN ('active','archived') |
agencies | owner_user_id | text | yes | → user.id(set null) |
| index | orders_agency_idx / agencies_status_idx / agencies_owner_idx | — | — | — |
下列尚未進 code,屬 member-ownership 的 M2 階段(未上線、無真資料,屆時改回客戶層模型成本低):
member_profiles.owner_user_id(會員層 CRM owner)+ 對應 index / upsert 指派(無 profile row 的會員以 INSERT ... ON CONFLICT (user_id) DO UPDATE 掛 owner)。member_profiles.agency_id(同業聯絡人 → 所屬公司),取代 M1 的 orders.agency_id 直連推導。agencies.primary_user_id(同業主要下單帳號,B2B portal 上線後對應的同庫 user)。resolveCustomerDefaultOwner 派發來源接點(§5)。owner 掛點(M2):owner 掛
member_profiles.owner_user_id(無獨立members表,理由見 §2 結論 3)。
⚠ 整個 §5 屬 M2:
resolveCustomerDefaultOwner與「客戶預設業務 → 派發來源」接點依賴 §4.4 的member_profiles.owner_user_id/agency_id,M1 尚未建。以下為 M2 設計意圖(保留供實作參考),現行 code 無此路徑。
原則:
decideAssignment(packages/core/src/orders/assignment.ts)保持原樣。本 spec 只在「呼叫 dispatcher 之前」加一個來源解析步驟,把客戶預設業務轉成既有的customerChoiceUserId輸入(或在manual_review模式下作為 pre-fill),不新增模式、不改三模式行為。
createOrderWithReservation(packages/core/src/orders/repo.ts)收 assignment? 參數。decideAssignment({ mode, defaultAssigneeUserId, customerChoiceUserId?, db }) → 得 { assignedToUserId, assignmentStatus, assignedAt } → 傳進 repo。createManualOrderAction)直接 assignment = { assignedToUserId: actorUserId, ... 'assigned' },覆寫 tenant 模式(K 單一律歸 K 的業務)—— 本 spec 不動此路徑。resolveCustomerDefaultOwner)在公開下單 action(呼叫 decideAssignment 之前)插入一支查詢函式:
// 新檔:packages/core/src/members/owner-source.ts(或併入 orders action 層)
import 'server-only';
import { sql } from 'drizzle-orm';
import type { ScopedDb } from '@toko-cloud/core/db/business';
/**
* 解析下單客戶的「預設負責業務」作為派發來源之一。
*
* 來源優先序:
* 1. 客戶若為同業聯絡人(member_profiles.agency_id 有值)→ 取 agency.owner_user_id
* 2. 否則取 member_profiles.owner_user_id(會員直接負責業務)
* 3. 都沒有 → null(交還既有派發模式決定)
*
* 回傳的 userId 仍會被 validateAssignee 驗證(必須是同庫 `user` 表內
* assignable role:sales / admin),所以這裡不重複驗角色 —— 失效時自動 fall back。
*/
export async function resolveCustomerDefaultOwner(
db: ScopedDb,
customerUserId: string,
): Promise<string | null> {
const rows = await db.execute<{ owner_user_id: string | null }>(sql`
SELECT COALESCE(a.owner_user_id, mp.owner_user_id) AS owner_user_id
FROM member_profiles mp
LEFT JOIN agencies a ON a.id = mp.agency_id
WHERE mp.user_id = ${customerUserId}
LIMIT 1
`);
return [...rows][0]?.owner_user_id ?? null;
}
在公開下單 action 內:
const defaultOwner = await resolveCustomerDefaultOwner(db, customerUserId);
const decision = await decideAssignment({
mode: tenantSettings.assignmentMode,
defaultAssigneeUserId: tenantSettings.defaultAssigneeUserId,
// 把客戶預設業務當成「客戶選擇」來源餵進去。
// - auto_customer_choice:客戶自己在 checkout 有選 → 用客戶選的;沒選才用 defaultOwner。
// - 其餘模式:見下表。
customerChoiceUserId: explicitCustomerChoice ?? defaultOwner ?? null,
db,
});
各模式下「客戶預設業務」的作用(不改派發程式邏輯,只透過餵 customerChoiceUserId 達成):
| tenant 模式 | 客戶有預設業務時的行為 | 是否改派發程式 |
|---|---|---|
auto_customer_choice | 客戶 checkout 沒手動選 → 落到預設業務(經 validateAssignee 驗證後 assigned)。手動選優先。 | 否(既有 fall-through 即可) |
manual_review | 訂單仍進 pending_review(不自動派,尊重主管審核);但在 /admin/orders/review 的派發 dropdown 預選 (pre-fill) 客戶預設業務,主管一鍵確認即可。pre-fill 是 UI 層,不改 dispatcher。 | 否 |
auto_round_robin | 預設業務優先於輪派:若 resolveCustomerDefaultOwner 有值且通過驗證 → 直接 assigned 給該業務,跳過 round-robin(語意:老客戶回到原業務手上比公平輪派重要)。實作:action 層在 mode==='auto_round_robin' 且 defaultOwner 有效時,不呼叫 dispatcher、直接組 decision;否則照常呼叫。 | 否(包在 action 層的分支,dispatcher 本體不動) |
設計取捨:
auto_round_robin的「預設業務優先」是 action 層的一個 if 分支,不是改decideAssignment。若 user 認為 round-robin 應該無視客戶歸屬(純公平),把這個 if 拿掉即可 —— 列入 §12 未決。
audit_log:action='member.assign_owner' / 'agency.assign_owner',metadata 記 before/after owner。defaultOwner 而 assigned,沿用既有的 order.assign audit(assigned_by = null 表系統 / 客戶歸屬來源),metadata 加 source: 'customer_default_owner' 以利追溯。| action | 檔案(建議) | 權限 | 行為 |
|---|---|---|---|
assignMemberOwnerAction({ userId, ownerUserId | null }) | src/app/(admin)/admin/members/[id]/actions.ts | member.assign_owner | upsert member_profiles(保證 row 存在)→ set owner → audit member.assign_owner。ownerUserId=null 為取消歸屬。 |
setMemberCustomerTypeAction({ userId, type, agencyId? }) | 同上 | member.assign_owner | 設 customer_type;agency_contact 時必填 agencyId(驗證 agency 存在)。 |
createAgencyAction(input) | src/app/(admin)/admin/agencies/actions.ts | agency.manage | 建 agencies row(驗證統編格式 + 唯一)→ audit agency.create。 |
updateAgencyAction(id, patch) | 同上 | agency.manage | 更新欄位(含 owner / status)→ audit agency.update / agency.assign_owner。 |
archiveAgencyAction(id) | 同上 | agency.manage | status='inactive'(軟下架,不刪 —— 保留歷史訂單關聯)→ audit。 |
listAgenciesForOwnerPicker() / listAssignableStaffForOwner() | repo helper | agency.read / member.read | 下拉選單資料:負責業務候選沿用既有 listAssignableStaff(讀同庫 "user" 表 role IN ('sales','admin'))。 |
owner 候選池:直接重用 listAssignableStaff(packages/core/src/orders/assignment-repo.ts)—— 同一組「可受派業務」(ASSIGNABLE_ROLES = ['sales','admin']),避免兩套定義。
repo 層:新 packages/core/src/agencies/admin-repo.ts(listAgencies / getAgencyDetail / createAgency / updateAgency),第一個參數一律 db: ScopedDb(對齊 branded type 慣例)。會員 owner 相關擴充進 packages/core/src/members/admin-repo.ts(既有檔)。
RBAC 走 better-auth admin plugin 的 createAccessControl statement catalog(packages/core/src/permissions-ac.ts),不是 flat string + ROLE_PERMS 物件;權限閘經 auth.api.userHasPermission 強制(見 租戶架構 §4)。角色集只有 admin / sales / op / accountant / customer,無 owner 角色。
本 spec 在 statement catalog 新增一個 agency resource,並把 owner 指派接到既有 member resource 的一個新 action:
// packages/core/src/permissions-ac.ts,擴充 statement catalog(節錄)
export const statement = {
...defaultStatements,
// ...既有 order / trip / departure / payment_request 等...
member: ['read', 'update', 'assign_owner'], // 既有 read/update + 新增 assign_owner
agency: ['read', 'manage'], // 新 resource
} as const;
對應的 dotted perm 字串(caller 傳給 requirePermission / userHasPermission):agency.read、agency.manage、member.assign_owner。
各角色(ac.newRole)授予:
| dotted perm | 動作 | 授予角色(newRole 加入該 (resource, action)) |
|---|---|---|
agency.read | 看同業列表 / 詳情 | admin(catalog 全包)、sales、op、accountant |
agency.manage | 建 / 改 / 下架同業 | admin、sales |
member.assign_owner(→ M2) | 指派會員 / 同業負責業務 | admin、op(member 層 owner 屬 M2,尚未進 code) |
admin role 自動涵蓋整個 catalog(permissions-ac.ts 把 statement 攤平進 admin newRole),故新增的 agency.* / member.assign_owner 自動歸 admin。sales 可讀可管同業(現行 code salesGrant.agency: ['read','manage']):業務維護自己的同業夥伴 —— 報同業價、聯絡、建檔 / 改資料 / 改歸屬都在業務日常職責內。op(對帳需讀帳期)、accountant(對款需讀同業)為 read-only(agency: ['read'])。agency_price)只後台內部看:前台行程頁不顯示;後台是否顯示同業價依 agency.read gate(任何後台角色可見內部報價)。/admin/agencies(新頁,獨立 nav)依 CLAUDE.md「線控 UX 原則」:同業是跨梯次的客戶資料(非 per-departure 批次處理),所以開獨立 nav(與 members / guides 同類)。畫面:見 apps/wireframes 線稿。
listAssignableStaff 下拉。/admin/members(既有頁)/admin/orders/review(既有頁):派發 dropdown 在 manual_review 模式下 pre-fill 客戶預設業務(§5.3)—— 屬派發頁的微調,本 spec 只註記。本章節只描述佔位,不畫 wireframe、不進實作計畫。
agencies.primary_user_id → 未來 B2B 登入後對應的同庫 user。member_profiles.customer_type='agency_contact' + agency_id → 區分同業帳號 vs 直客。agencies.payment_terms → 未來帳期 / 月結。agency_b2b)+ 受限前台(只看同業價、只看自己訂單)。resolveCustomerDefaultOwner:
agency.owner_user_id(優先於 member owner)。assignMemberOwnerAction:無 profile row 的會員指派 owner → upsert 成功、可查回。createAgencyAction:重複統編 → 拒絕;無統編團主 → 允許(多筆 NULL 共存)。agency_contact 必須帶 agencyId,否則拒絕。auto_customer_choice + 客戶有預設業務 + checkout 未手動選 → 訂單 assigned 給預設業務。manual_review + 客戶有預設業務 → 訂單仍 pending_review(驗證不自動派);review 頁 pre-fill 正確(前端測 / e2e)。auto_round_robin + 客戶有有效預設業務 → 跳過輪派、直接給預設業務(驗 action 層分支)。validateAssignee fall back(不會派給無效對象)。auto_customer_choice 模式)。member.assign_owner / agency.create(用 TRUNCATE 清資料,trigger 不 fire)。| 類型 | 數量 | 細節 |
|---|---|---|
| Schema drizzle(business.ts) | 3 欄 + 2 index | member_profiles:owner_user_id / customer_type / agency_id + owner/agency index |
| Schema 新表(drizzle) | 1 | agencies(11 欄 + 3 index) |
| 新 page | 1 | /admin/agencies(列表 + 詳情 + 新增 / 編輯) |
| 修改 page | 1 | /admin/members(負責業務欄 + 篩選 + 詳情可編輯);/admin/orders/review 微調 pre-fill |
| Server action | 5 | member owner / customer_type;agency create / update / archive |
| Repo | 2 | 新 agencies/admin-repo.ts;擴 members/admin-repo.ts |
| Service 函式 | 1 | resolveCustomerDefaultOwner(派發來源接點) |
| Permission | 3 | agency.read / agency.manage / member.assign_owner |
| 線稿 | — | 同業管理頁(新)/ 會員頁(負責業務欄 + 篩選):見 apps/wireframes |
| 派發衝擊 | 0 改寫 | decideAssignment 不動;新增來源在 action 層 |
| 項目 | 狀態 |
|---|---|
auto_round_robin 是否該被「客戶預設業務」覆寫? | 預設「覆寫」(老客戶回原業務);若要純公平輪派則拿掉 action 層 if(§5.3) |
owner 掛 member_profiles 而非 user 表 | 已定(§2 結論 3);扶正進 drizzle 的時機跟派發 / created_by_user_id 一起決定 |
| 同業 owner 是否影響佣金 | 否 —— 佣金仍依訂單層 assigned_to_user_id。若未來要 owner 抽成另議 |
| 同業聯絡人下單時自動套同業價? | 現階段同業價只後台看;自動套價屬 B2B future(§9) |
| 一個會員是否可同時隸屬多家同業? | 現行模型:單一 agency_id(1:1)。多對多 → 未來加 join 表 |
| 統編格式驗證強度 | 現行模型:8 碼數字 + 唯一即可,不做台灣統編 checksum(後期可加) |
member.assign_owner 是否該讓 sales 自己「認領」客戶 | 現行模型否(與不准搶單一致);視客戶需求再開 |
packages/core/src/orders/assignment.ts(decideAssignment — 不改)packages/core/src/orders/assignment-repo.ts(listAssignableStaff — 重用為 owner 候選池)packages/core/src/members/admin-repo.ts(會員列表 source = 同庫 user + member_profiles)scripts/setup-business-extras.ts(idempotent ALTER 落地處)| 版本 | 日期 | 變更 | Commit |
|---|---|---|---|
| 1 | 2026-05-29 | 初版 | — |
| 2 | 2026-05-30 | 整理:對齊現行 code + 路徑搬移 | — |
| 3 | 2026-06-20 | §4.3 / §11 落地方式對齊現行慣例:member_profiles 三新欄(含 owner/agency index)由「ALTER(setup-extras)」改為「drizzle schema(business.ts)」 | — |
| 4 | 2026-06-23 | M1 first-pass 落地,建模簡化(user 拍板):本輪先做「同業建檔目錄」最淺層 —— agencies 表(migration 0010,含 owner_user_id/status)+ /admin/agencies 管理頁 + 訂單關聯改用 orders.agency_id 直連 FK(非本 spec 的 member_profiles.agency_id 推導 + agencies.primary_user_id)。客戶層 CRM 全套(owner 篩選、客戶換手不動歷史業績、同業價、B2B portal、member_profiles.agency_id/primary_user_id)仍守 M2。決策脈絡見 docs/reviews/2026-06-23-new-epics-decisions.md「最重要:與既有 spec 的分歧」。未上線無真資料,M2 要改回客戶層模型成本低。 | — |
| 5 | 2026-07-01 | 三方稽核收斂(Code Is Truth):§7 agency.manage 授予角色對齊現行 code —— 由 admin、op 改為 admin、sales(salesGrant.agency: ['read','manage'],業務維護自己的同業夥伴;op/accountant read-only),並改寫「sales 不能改同業(防搶客戶)」段落。§4/§5 主敘述 realign 現行 M1 模型(member_profiles 只有 customer_type、agencies.owner_user_id、orders.agency_id 直連、agencies.status 值域 active/archived、無 primary_user_id/無 tax_id unique),把完整 owner 歸屬 / member_profiles.owner_user_id/agency_id / agencies.primary_user_id / resolveCustomerDefaultOwner 派發接點 / B2B portal 整套移入 §4.4 + §5 標為「→ M2」future。SSOT:docs/reviews/2026-07-01-three-way-realign-audit.md。 | — |