CR #201 » SDD-001_領域模型與ER-Diagram.md
SDD-001 領域模型與 ER-Diagram
| 項目 | 內容 |
|---|---|
| 文件編號 | SDD-001 |
| 版本 |
1.18(CR-035 §2.4:§3.4 新增 special_limit_temp_adjustments(V9 遷移)——8-6/8-7 之臨時額度串動紀錄(BR-04-02)。載明於「變更交付說明」,本節 SQL 區塊不得改動。前版 1.17(CR-035 §2.3:§3.4 新增 special_limit_policies(V8 遷移)) |
| 狀態 | 已核定 |
| 撰寫者 | SD / DA |
| 審查者 | SA / TL |
| 對應需求 | ISS-2.1 |
§3.9 分期雙軸拆分(
CR-008,BEAM PMO 2026-09-20 核定)
installment_type原同時被要求承載兩個正交軸(來源通路/業務型態),
且欄名字面(「分期類型」)易被誤讀——此即CR-002契約值域 100% 被拒之根源。
現拆分為channel_type(來源通路,7 值) +business_type(業務型態,3 值、允許 NULL)。
⚠️business_type允許 NULL 係因既有資料未記錄此維度——
誠實表達「未記錄」勝過回填一個猜測值;報表與篩選須處理空值。
1. 說明
本文件定義全系統之領域模型與資料模型,並提供可直接執行之 DDL。
ER 圖為設計視圖;DDL 為實作視圖,兩者必須一致。
v1.1 方言聲明(重要)
本文件之 DDL 以 PostgreSQL 語法書寫,作為邏輯模型之記法。
本案生產環境之權威 OLTP 為 Oracle(CSP-006 §2.4、SAD-003 §3),
因此實際 Flyway 遷移腳本須以 Oracle 方言撰寫,其型別與語法對照見 §1.3。
二者不一致時,以 §1.3 之 Oracle 對照為實作依據。
1.1 通用欄位(原則上所有表皆有)
| 欄位 | 型別 | 說明 |
|---|---|---|
id |
uuid |
主鍵(ULID 亦可,本設計採 UUID v7) |
created_at |
timestamptz |
建立時間(UTC) |
updated_at |
timestamptz |
更新時間(UTC) |
version |
integer |
樂觀鎖版本 |
1.1.1 例外條款與豁免機制
上述四欄為預設,非絕對。下列情形得不具備,但必須逐表明示豁免並寫出理由:
| 情形 | 得不具備之欄位 | 理由類型 |
|---|---|---|
append-only 表(outbox、idempotency_records、audit_trail 等) |
updated_at、version
|
寫入後不再變更,無「更新」與「樂觀鎖」語意 |
自然主鍵之註冊/去重表(如 card.issued_card_numbers) |
id |
已有自然主鍵(card_no_hash),加代理鍵徒增負擔 |
技術慣用單數名之表(如 outbox) |
—(表名例外) | 業界慣用名,強改複數反而降低可讀性 |
豁免寫法:於 CREATE TABLE 前 5 行內加註(規則見 CSP-001 §5.2):
-- lint:skip=SQL-04,SQL-05 理由:append-only 事件輸出表,寫入後僅標記 published_at
CREATE TABLE :schema.outbox ( ... );
紀律:豁免須逐表為之、理由須具體。若同一理由反覆出現於多張表,
表示規則本身需要修訂(應提變更申請修 CSP-001 §5.1),而非無限累積豁免。
本節之由來(2026-09-18):本文件原述「通用欄位(所有表皆有)」,
與其自身 §3.15 之outbox/idempotency_recordsDDL 相牴觸
(該二表為 append-only,本就不應有updated_at/version)。
經決議採逐表豁免:規則維持不變,例外逐表明示並附理由,使例外可稽核。
1.2 型別規範
| 用途 | 型別 | 說明 |
|---|---|---|
| 金額 | bigint |
最小單位(分),禁止浮點 |
| 幣別 | char(3) |
ISO 4217 |
| 比率 | numeric(9,6) |
如 0.185000 表 18.5% |
| 時間 | timestamptz |
UTC |
| 日期 | date |
業務日期 |
| 列舉 |
varchar(32) + CHECK |
不使用 DB enum(避免遷移困難) |
| 彈性結構 | jsonb |
僅用於參數與稽核前後值 |
1.3 資料型別方言對照(PostgreSQL → Oracle)
| 用途 | 邏輯模型(PostgreSQL) | 實作(Oracle) | 備註 |
|---|---|---|---|
| 主鍵 | uuid |
RAW(16) |
以 SYS_GUID() 產生。Oracle 23ai+ 支援原生 UUID 型別;若生產為 19c 則須用 RAW(16)
|
| 時間 | timestamptz |
TIMESTAMP(6) WITH TIME ZONE |
預設值 now() → SYSTIMESTAMP
|
| 整數 | integer |
NUMBER(10) |
或 INTEGER
|
| 金額 | bigint |
NUMBER(19,0) |
最小單位(分),禁止 BINARY_DOUBLE/BINARY_FLOAT
|
| 比率 | numeric(9,6) |
NUMBER(9,6) |
— |
| 幣別 | char(3) |
CHAR(3 CHAR) |
明確標示字元語意,避免 NLS 影響 |
| 字串 | varchar(n) |
VARCHAR2(n CHAR) |
Oracle 無 VARCHAR 語意差異,一律用 VARCHAR2
|
| 長文字 | text |
CLOB |
— |
| 日期 | date |
DATE |
⚠️ Oracle DATE 含時、分、秒,與 PostgreSQL 之 DATE 不同。業務日期建議改用 NUMBER(8)(YYYYMMDD)或 DATE 但僅使用日期部分並於資料字典註明 |
| 布林 | boolean |
NUMBER(1) + CHECK (x IN (0,1))
|
Oracle 23ai+ 支援 SQL BOOLEAN;為相容 19c 採 NUMBER(1)
|
| 列舉 |
varchar(32) + CHECK |
VARCHAR2(32 CHAR) + CHECK |
一致 |
| 彈性結構 | jsonb |
JSON(21c+) |
若生產為 19c,改用 CLOB + CHECK (x IS JSON)
|
| 自增 | bigserial |
GENERATED BY DEFAULT AS IDENTITY |
或獨立 sequence |
語法對照(常見)
| 情境 | PostgreSQL | Oracle |
|---|---|---|
| 取當前時間 | now() |
SYSTIMESTAMP |
| 分頁 | LIMIT n OFFSET m |
OFFSET m ROWS FETCH NEXT n ROWS ONLY(12c+) |
| 佇列搶佔(Outbox 用) | FOR UPDATE SKIP LOCKED |
FOR UPDATE SKIP LOCKED(11g+,語法相同) |
| 大小寫不敏感比對 | ILIKE |
REGEXP_LIKE(x, '...', 'i') 或 LOWER(x) LIKE
|
| Upsert | ON CONFLICT ... DO UPDATE |
MERGE INTO ... USING ... |
| JSON 索引 | USING GIN |
CREATE SEARCH INDEX ... FOR JSON |
| 字串串接 | || |
||(相同) |
Oracle 特有陷阱(實作必讀)
| # | 陷阱 | 影響與對策 |
|---|---|---|
| T-01 | 未加引號之識別碼一律轉為大寫 | 本專案採 snake_case 小寫命名(GOV-001 §4.3),在 Oracle 內將成為 POSTING.TRANSACTIONS。對策:DDL 不加引號(避免全部 SQL 需加引號之維護負擔),並於 Hibernate/JPA 設定命名策略對應大小寫 |
| T-02 | 空字串 '' 等於 NULL |
金額、狀態等欄位不得以空字串表示「未設定」。對策:所有必填欄位加 NOT NULL,並於 CHECK 約束拒絕空字串 |
| T-03 |
DATE 含時間 |
見上表「日期」列 |
| T-04 | 交易隱式提交 | DDL 語句會隱式 commit,不可與 DML 混在同一交易。對策:Flyway 腳本一律 DDL 與 DML 分檔 |
| T-05 | 預設隔離等級為 READ COMMITTED
|
與本案樂觀鎖設計相容;不得改用 SERIALIZABLE(會大幅增加 ORA-08177) |
| T-06 |
NUMBER 比較之隱式轉換 |
金額欄位與字串比較會導致索引失效。對策:JDBC 綁定一律用對應數值型別 |
| T-07 | RESOURCE 為 Oracle 26ai 之保留字 |
v$reserved_words.reserved = Y。用作欄位名會報 ORA-03050: invalid identifier: "RESOURCE" is a reserved word。對策:本文件 §3.15 之 audit_trail 採 resource_type / resource_id,不得簡化為單一 resource 欄位 |
| T-08 |
JSON 型別與 RAW(16) 之可用性 |
已於 Oracle 26ai Free(23.26.0)實測可用:CREATE TABLE ... (id RAW(16), payload JSON) 成功,SYS_GUID() 可作為預設值。故 §1.3 之型別對照表可直接落實,不需退回 CLOB + IS JSON
|
T-07 之來源:2026-09-18 本地環境建置時實測發現。原
audit_trail草稿使用resource VARCHAR2(256),
於 26ai 建表失敗;改採本文件 §3.15 原設計之resource_type/resource_id後通過。
此為「文件原本是對的、實作偏離文件」之案例——再次印證實作須以本文件為準。
2. 領域模型總覽(Bounded Context 關係)
erDiagram
ACCOUNT ||--o{ PARTY : contains
PARTY ||--o{ CARD : holds
CARD ||--o{ TOKEN : tokenized_as
ACCOUNT ||--|| CREDIT_LIMIT : has
ACCOUNT ||--o{ SPECIAL_LIMIT : has
CREDIT_LIMIT ||--o{ LIMIT_HOLD : holds
CARD ||--o{ AUTHORIZATION : authorizes
AUTHORIZATION ||--o| LIMIT_HOLD : creates
AUTHORIZATION ||--o| TRANSACTION : settles_into
ACCOUNT ||--o{ TRANSACTION : posts
TRANSACTION ||--o{ SUB_ACCOUNT_ENTRY : splits_into
ACCOUNT ||--|| BALANCE : has
ACCOUNT ||--o{ PAYMENT : receives
ACCOUNT ||--o{ STATEMENT : billed
ACCOUNT ||--o{ INSTALLMENT_PLAN : plans
ACCOUNT ||--o{ POINTS_ACCOUNT : earns
MERCHANT ||--o{ MERCHANT_SETTLEMENT : settles
MERCHANT ||--o{ TRANSACTION : acquires
PRODUCT ||--o{ PARAMETER : defines
PRODUCT ||--o{ CARD : issues
3. 各限界上下文之 ER 與 DDL
3.1 party 持卡人與歸戶
erDiagram
ACCOUNT {
uuid id PK
varchar account_no UK
varchar cif_no
varchar account_type
varchar block_code
varchar statement_cycle
date statement_day
varchar status
integer version
}
PARTY {
uuid id PK
uuid account_id FK
varchar party_no UK
varchar party_type
varchar name
varchar id_no_enc
varchar id_no_masked
date birth_date
varchar mobile_enc
varchar email_enc
varchar relation_type
jsonb flags
varchar status
integer version
}
BLOCK_CODE_HISTORY {
uuid id PK
uuid account_id FK
varchar block_code
varchar reason
timestamptz effective_at
timestamptz released_at
varchar operator_id
}
ACCOUNT ||--o{ PARTY : contains
ACCOUNT ||--o{ BLOCK_CODE_HISTORY : records
DDL
CREATE SCHEMA IF NOT EXISTS party;
CREATE TABLE party.accounts (
id uuid PRIMARY KEY,
account_no varchar(20) NOT NULL UNIQUE,
cif_no varchar(20) NOT NULL,
account_type varchar(16) NOT NULL CHECK (account_type IN ('PERSONAL','CORPORATE')),
block_code varchar(16) NOT NULL DEFAULT 'NORMAL'
CHECK (block_code IN ('NORMAL','LOST','OVERLIMIT','DELINQUENT','FRAUD','CLOSED')),
statement_cycle varchar(8) NOT NULL,
statement_day smallint NOT NULL CHECK (statement_day BETWEEN 1 AND 31),
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','SUSPENDED','CLOSED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE party.parties (
id uuid PRIMARY KEY,
account_id uuid NOT NULL REFERENCES party.accounts(id),
party_no varchar(20) NOT NULL UNIQUE,
party_type varchar(16) NOT NULL CHECK (party_type IN ('PRIMARY','SUPPLEMENTARY','COMPANY')),
name varchar(80) NOT NULL,
id_no_enc bytea, -- 身分證字號(AES-256-GCM)
id_no_masked varchar(20) NOT NULL, -- A12345****
birth_date date,
mobile_enc bytea,
email_enc bytea,
relation_type varchar(16),
flags jsonb NOT NULL DEFAULT '{}'::jsonb,
status varchar(16) NOT NULL DEFAULT 'ACTIVE',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_parties_account ON party.parties(account_id);
-- lint:skip=SQL-04 理由:history 為集合名詞,*_history 為異動歷程表之標準後綴,不加複數 s
CREATE TABLE party.block_code_history (
id uuid PRIMARY KEY,
account_id uuid NOT NULL REFERENCES party.accounts(id),
block_code varchar(16) NOT NULL,
reason varchar(200),
effective_at timestamptz NOT NULL,
released_at timestamptz,
operator_id varchar(40) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
變更交付說明(
pty_idempotency,2026-09-21 回寫|HANDOVER #C-18)
本表於V1__baseline.sql不存在,係由後續遷移建立;上列 ER 圖未含之(屬服務內部狀態,
非業務主檔,故不列入領域 ER 圖)。
遷移 內容 V2__pty_idempotency.sql建表: key(PK)/fingerprint/entity_type/entity_id/created_at用途:API 冪等鍵(防重複寫入)。與 §3.15 之
IDEMPOTENCY_RECORD為同一模式之各 schema 實作
——後者為平台層之邏輯模型,前者為partyschema 之實體表。
⚠️ 不含id/updated_at/version:冪等鍵一經寫入即不再更新,
硬補樂觀鎖欄位屬無意義欄位(假綠),故依CSP-001 §5.3以外部登錄檔豁免(SQL-05)。
3.2 card 卡片生命週期
erDiagram
CARD {
uuid id PK
uuid account_id FK
uuid party_id FK
varchar card_no_enc
varchar card_no_masked
varchar card_no_hash
varchar product_code FK
varchar bin
date expiry_date
varchar block_code
varchar status
varchar token_ref
boolean instant_issued
varchar ticket_type
integer version
}
CARD_APPLICATION {
uuid id PK
uuid account_id FK
varchar apply_no UK
varchar channel
varchar product_code
varchar status
varchar approval_no
boolean fast_issue_flag
timestamptz approved_at
integer version
}
TOKEN {
uuid id PK
uuid card_id FK
varchar token_no UK
varchar platform
varchar device_ref
varchar status
timestamptz bound_at
integer version
}
CARD_NO_POOL {
uuid id PK
varchar bin
varchar range_start
varchar range_end
bigint used_count
bigint total_count
varchar status
}
ACCOUNT_CARD {
uuid id PK
}
CARD_APPLICATION ||--o| CARD : results_in
CARD ||--o{ TOKEN : tokenized_as
CARD_NO_POOL ||--o{ CARD : allocates
DDL(節錄關鍵表)
CREATE SCHEMA IF NOT EXISTS card;
CREATE TABLE card.card_applications (
id uuid PRIMARY KEY,
account_id uuid NOT NULL,
apply_no varchar(24) NOT NULL UNIQUE,
channel varchar(16) NOT NULL
CHECK (channel IN ('WEB','MOBILE','API','BRANCH','MERCHANT')),
product_code varchar(16) NOT NULL,
status varchar(16) NOT NULL DEFAULT 'RECEIVED'
CHECK (status IN ('RECEIVED','REVIEWING','SUPPLEMENT','APPROVED','REJECTED')),
approval_no varchar(24),
-- CR-020 F1(V7 遷移):發卡後回填之卡片識別碼,使卡片可追溯其來源申請案件;
-- 開卡(ACTIVATE)時以此反查核准序號驗證三要素。可為 NULL=未發卡。
card_id uuid,
fast_issue_flag boolean NOT NULL DEFAULT false,
approved_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE card.cards (
id uuid PRIMARY KEY,
account_id uuid NOT NULL,
party_id uuid NOT NULL,
card_no_enc bytea NOT NULL, -- 加密卡號
card_no_masked varchar(20) NOT NULL, -- 411111****1234
card_no_hash char(64) NOT NULL, -- SHA-256,供比對用(唯一)
product_code varchar(16) NOT NULL,
bin char(8) NOT NULL,
expiry_date date NOT NULL,
block_code varchar(16) NOT NULL DEFAULT 'INACTIVE'
CHECK (block_code IN ('INACTIVE','ACTIVE','LOCKED','LOST','REISSUED','EXPIRED','CLOSED')),
status varchar(16) NOT NULL DEFAULT 'UNACTIVATED'
CHECK (status IN ('UNACTIVATED','ACTIVE','SUSPENDED','CLOSED')),
instant_issued boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT uq_card_hash UNIQUE (card_no_hash)
);
CREATE INDEX idx_cards_account ON card.cards(account_id);
CREATE INDEX idx_cards_party ON card.cards(party_id);
CREATE TABLE card.tokens (
id uuid PRIMARY KEY,
card_id uuid NOT NULL REFERENCES card.cards(id),
token_no varchar(64) NOT NULL UNIQUE,
platform varchar(24) NOT NULL
CHECK (platform IN ('APPLE_PAY','GOOGLE_PAY','SAMSUNG_PAY','GARMIN_PAY','FITBIT_PAY','TAIWAN_PAY','QR_CODE')),
device_ref varchar(80),
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','SUSPENDED','DELETED')),
bound_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
-- lint:skip=SQL-04 理由:pool 為集合名詞,表單一邏輯資源池,不加複數 s
CREATE TABLE card.card_no_pool (
id uuid PRIMARY KEY,
bin char(8) NOT NULL,
range_start varchar(19) NOT NULL,
range_end varchar(19) NOT NULL,
used_count bigint NOT NULL DEFAULT 0,
total_count bigint NOT NULL,
status varchar(16) NOT NULL DEFAULT 'ACTIVE',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
-- 已配發卡號(避免亂數給號重複)
-- lint:skip=SQL-05 理由:卡號雜湊之去重註冊表。以 card_no_hash 為自然主鍵,
-- 記錄一經寫入即不變,故無 id/updated_at/version 之語意。
CREATE TABLE card.issued_card_numbers (
card_no_hash char(64) PRIMARY KEY,
card_id uuid NOT NULL,
issued_at timestamptz NOT NULL DEFAULT now()
);
變更交付說明(
card_idempotency,2026-09-21 回寫|HANDOVER #C-18)
本表於V1__baseline.sql不存在,係由後續遷移建立;屬服務內部狀態,不列入領域 ER 圖。
遷移 內容 V2__card_idempotency.sql建表: key(PK)/fingerprint/entity_type/entity_id/created_at同 §3.1
pty_idempotency——為 §3.15IDEMPOTENCY_RECORD於cardschema 之實體實作。
⚠️ 無id/updated_at/version,理由同上(一經寫入即不再更新)。(
cards.ticket_type已於 1.4 版由V3__card_ticket_type.sql回寫,見 §10 修訂記錄。)
3.3 product 產品與參數
erDiagram
PRODUCT {
uuid id PK
varchar product_code UK
varchar card_name
varchar org
varchar card_grade
varchar ticket_type
varchar status
date effective_from
date effective_to
integer version
}
PARAMETER {
uuid id PK
varchar namespace
varchar param_key
jsonb param_value
varchar value_type
date effective_from
date effective_to
integer version
}
RULE_SET {
uuid id PK
varchar rule_code UK
varchar rule_type
jsonb condition
jsonb action
integer priority
varchar status
integer version
}
CHARSET_MAPPING {
uuid id PK
varchar internal_char
varchar external_code
varchar org
}
PRODUCT ||--o{ PARAMETER : defines
PRODUCT ||--o{ RULE_SET : applies
DDL(節錄)
CREATE SCHEMA IF NOT EXISTS product;
CREATE TABLE product.products (
id uuid PRIMARY KEY,
product_code varchar(16) NOT NULL UNIQUE,
card_name varchar(60) NOT NULL,
org varchar(16) NOT NULL CHECK (org IN ('VISA','MASTERCARD','JCB','AMEX','UNIONPAY','DINERS','DISCOVER')),
card_grade varchar(24) NOT NULL,
ticket_type varchar(24),
status varchar(16) NOT NULL DEFAULT 'DRAFT'
CHECK (status IN ('DRAFT','ACTIVE','SUSPENDED','RETIRED')),
effective_from date NOT NULL,
effective_to date,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE product.parameters (
id uuid PRIMARY KEY,
namespace varchar(40) NOT NULL,
param_key varchar(80) NOT NULL,
param_value jsonb NOT NULL,
value_type varchar(16) NOT NULL CHECK (value_type IN ('STRING','NUMBER','BOOLEAN','JSON','AMOUNT','RATE')),
effective_from date NOT NULL,
effective_to date,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT uq_param UNIQUE (namespace, param_key, effective_from)
);
CREATE TABLE product.rule_sets (
id uuid PRIMARY KEY,
rule_code varchar(40) NOT NULL UNIQUE,
rule_type varchar(24) NOT NULL
CHECK (rule_type IN ('AUTH_CHECK','REWARD','FEE','INTEREST','DEDUCT','ELIGIBILITY')),
condition jsonb NOT NULL,
action jsonb NOT NULL,
priority integer NOT NULL DEFAULT 100,
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','INACTIVE')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE product.charset_mappings (
id uuid PRIMARY KEY,
internal_char varchar(8) NOT NULL,
external_code varchar(8) NOT NULL,
org varchar(16) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT uq_charset UNIQUE (org, internal_char)
);
3.4 limit 額度
erDiagram
CREDIT_LIMIT {
uuid id PK
uuid account_id UK
bigint permanent_amount
bigint temporary_amount
date temp_start_date
date temp_end_date
bigint shadow_amount
bigint used_amount
bigint available_amount
char currency
integer version
}
SPECIAL_LIMIT {
uuid id PK
uuid account_id FK
char currency
varchar purpose
bigint amount
bigint used_amount
date effective_from
date effective_to
varchar status
integer version
}
LIMIT_HOLD {
uuid id PK
uuid account_id FK
uuid authorization_id FK
bigint hold_amount
bigint released_amount
varchar hold_type
varchar status
timestamptz expires_at
integer version
}
LIMIT_ADJUSTMENT {
uuid id PK
uuid account_id FK
varchar adjust_type
bigint before_amount
bigint after_amount
varchar reason
varchar applicant_id
varchar approver_id
varchar status
integer version
}
CREDIT_LIMIT ||--o{ SPECIAL_LIMIT : has
CREDIT_LIMIT ||--o{ LIMIT_HOLD : holds
CREDIT_LIMIT ||--o{ LIMIT_ADJUSTMENT : adjusts
DDL(節錄)
CREATE SCHEMA IF NOT EXISTS lim; -- 避免與 SQL 保留字 LIMIT 衝突
CREATE TABLE lim.credit_limits (
id uuid PRIMARY KEY,
account_id uuid NOT NULL UNIQUE,
permanent_amount bigint NOT NULL CHECK (permanent_amount >= 0),
temporary_amount bigint NOT NULL DEFAULT 0 CHECK (temporary_amount >= 0),
temp_start_date date,
temp_end_date date,
shadow_amount bigint NOT NULL DEFAULT 0 CHECK (shadow_amount >= 0),
used_amount bigint NOT NULL DEFAULT 0 CHECK (used_amount >= 0),
available_amount bigint NOT NULL CHECK (available_amount >= 0),
currency char(3) NOT NULL DEFAULT 'TWD',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT chk_limit_positive CHECK (available_amount >= 0)
);
CREATE TABLE lim.special_limits (
id uuid PRIMARY KEY,
account_id uuid NOT NULL,
purpose varchar(24) NOT NULL
CHECK (purpose IN ('TAX','INSURANCE','TUITION','MEDICAL','OTHER')),
amount bigint NOT NULL CHECK (amount >= 0),
used_amount bigint NOT NULL DEFAULT 0,
effective_from date NOT NULL,
effective_to date,
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','EXPIRED','CANCELLED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE lim.limit_holds (
id uuid PRIMARY KEY,
account_id uuid NOT NULL,
authorization_id uuid,
hold_amount bigint NOT NULL CHECK (hold_amount > 0),
released_amount bigint NOT NULL DEFAULT 0 CHECK (released_amount >= 0),
hold_type varchar(16) NOT NULL CHECK (hold_type IN ('GENERAL','SPECIAL','INSTALLMENT')),
status varchar(16) NOT NULL DEFAULT 'HELD'
CHECK (status IN ('HELD','PARTIALLY_RELEASED','RELEASED','EXPIRED')),
expires_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_holds_account ON lim.limit_holds(account_id, status);
CREATE INDEX idx_holds_auth ON lim.limit_holds(authorization_id);
額度扣減以條件更新實作(避免超額):
UPDATE lim.credit_limits SET available_amount = available_amount - :amount, used_amount = used_amount + :amount, version = version + 1, updated_at = now() WHERE id = :id AND version = :version AND available_amount >= :amount; -- 影響筆數 0 → 餘額不足或併發衝突
變更交付說明(
account_limits/limit_adjustments,2026-09-21 回寫|HANDOVER #C-18)
二表於V1__baseline.sql不存在,係由後續遷移建立。
⚠️ 本節 DDL 區塊尚未含此二表——本次先以本說明區塊記載目標狀態之欄位,
避免「實物有、文件無」;ER 圖之補繪列為後續(見文末「未竟事項」)。
遷移 內容 V2__account_limits.sql建 lim.account_limits:id(PK)/account_id/currency/permanent_amount/temporary_amount/temp_start_date/temp_end_date/shadow_amount/used_amount/status(預設ACTIVE)/created_at/updated_at/version;UNIQUE(account_id, currency);索引(status)V3__limit_adjustments.sql建 lim.limit_adjustments:id(PK)/account_id/currency/adjust_type/before_amount/after_amount/status(預設DRAFT)/temp_start_date/temp_end_date/purpose/reason/applicant_id/approver_id/effective_at/created_at/updated_at/version;索引(account_id, status)值域(
CHECK約束,實測為準):
adjust_type∈PERMANENT|TEMPORARY|SPECIAL|SHADOWstatus∈DRAFT|PENDING_APPROVAL|EFFECTIVE|REJECTED|EXPIRED|REVOKED為何
account_limits與credit_limits並存:後者為產品/帳戶層之授信額度,
前者為CR-001新增之帳戶信用額度主檔(帳戶 × 幣別,含永久/臨時/彈性/已用四分額)。
二者語意不同,非重複建表。
變更交付說明(
special_limit_temp_adjustments,2026-10-02|CR-035 §2.4/#A-156)同上原則(本節 SQL 區塊不得改動),以新遷移交付:
遷移 內容 V9__special_limit_temp_adjustments.sql建 lim.special_limit_temp_adjustments:id(PK)/account_id/currency/special_limit_id/authorization_id/posting_id/raised_amount(> 0)/recovered_amount(>= 0)/raised_on/raise_until/due_date_source(DERIVED/STATEMENT)/status(OPEN/CLOSED)/通用欄位;UNIQUE INDEX uk_special_temp_posting(posting_id);索引(account_id, status)語意(
BR-04-02):8-6 請款時調高卡人臨時額度(迄日=繳款截止日+7 日)之逐筆紀錄,
供 8-7 繳款時精確扣回。★ 必須逐筆——account_limits.temporary_amount可能同時承載
「人工核准之臨時額度」,直接歸零會誤砍客戶經核准之額度。詳見SCS-002 §7.12。
變更交付說明(
special_limit_policies,2026-10-02|CR-035 §2.3/#A-155)同上原則(本節 SQL 區塊不得改動),以新遷移交付:
遷移 內容 V8__special_limit_policies.sql建 lim.special_limit_policies:id(PK)/role_code/segment_code/currency/max_amount(>= 0)/status(ACTIVE/INACTIVE)/created_at/updated_at/version;UNIQUE (role_code, segment_code, currency);索引(role_code, status)語意(
BR-04-03):某「角色 × 客層 × 幣別」下可設定之專用額度上限。
★ 角色之權威為「已驗證之權杖」(服務權杖之azp/使用者權杖之 realm roles),
不接受請求帶入;segment_code因平台無客層來源而由請求帶入(如實揭露)。
詳見SCS-002 §7.11。
變更交付說明(
special_limits之「池」與用途範圍,2026-10-02|CR-035 §2.1/§2.2/#A-154)同上原則(本節 SQL 區塊不得改動),以新遷移交付:
遷移 內容 V7__special_limit_pool.sql① special_limits ADD (scope_ref VARCHAR2(64 CHAR))——用途範圍(MCC 白名單,逗號分隔),NULL= 不限定;②limit_adjustments ADD (scope_ref VARCHAR2(64 CHAR))——稽核軌跡同步留痕;③limit_holds ADD (special_limit_id RAW(16))——非NULL= 該佔用屬專用額度池(hold_type='SPECIAL'為其粗粒度標記,該值域V1即有);④idx_holds_special_limit語意:專用額度池之佔用完全不動
account_limits→BR-04-01「專用額度不佔卡人額度」為結構性成立,
非靠事後沖銷(SCS-001 §7.16、SCS-002 §7.10)。
scope_ref之格式為逗號清單,刻意不加CHECK(用CHECK表達會又長又易錯)——
合法性由應用層於寫入前檢核(不合法即拒絕,非靜默接受)。
變更交付說明(
special_limits,2026-10-02|CR-014 v1.1/#A-153)上方 SQL 區塊之
lim.special_limits即V1__baseline.sql之來源(由gen_flyway_baseline.py產生,
已於 2026-09-19 以 Flyway 基線套用至真機)。本節之 SQL 區塊不得改動——
一改,--force重生 V1 將造成 checksum 失配、服務起不來(CSP-001 §5.3/#C-36之教訓)。
結構變更一律以新遷移交付,並於此以說明區塊記載目標狀態(比照上表account_limits之做法)。
遷移 內容 V6__special_limits_currency_and_index.sql① ADD (currency CHAR(3 CHAR))+ 由account_limits回填 +MODIFY (currency NOT NULL)(不設預設值,不把「TWD」寫進 DDL;無法歸屬幣別之列將使遷移 fail-closed);②CREATE INDEX idx_special_limits_account ON special_limits(account_id, status);③CREATE UNIQUE INDEX uk_special_limits_active ON special_limits(account_id, currency, purpose, CASE WHEN status='ACTIVE' THEN 'ACTIVE' END)為何補
currency:account_limits之鍵為(account_id, currency),多幣別為本設計之既有能力;
無幣別之專用額度無法歸屬,且 upsert 之業務鍵會跨幣別碰撞(CR-014 v1.1 §5)。為何加
uk_special_limits_active:語意為「一歸戶 × 一幣別 × 一用途,至多一筆ACTIVE」。
若容許兩筆ACTIVE同用途,授權端之專用額度檢核將重複計算(可用額度被高估)→ 屬超額放行風險。
Oracle B-tree 唯一索引不索引全NULL鍵 → 非ACTIVE之歷史列(EXPIRED/CANCELLED)可留多筆。本表之既有值域即權威(不自由化):
purpose IN ('TAX','INSURANCE','TUITION','MEDICAL','OTHER')、
amount >= 0、status IN ('ACTIVE','EXPIRED','CANCELLED')(真機實測,CR-001 §2.4亦已載明)。
limit_adjustments(申請/覆核軌跡)與本表(現行定義)之分工見SCS-002 §7.9.1。
3.5 authorization 授權
erDiagram
AUTHORIZATION {
uuid id PK
varchar auth_no UK
varchar stan
uuid card_id FK
uuid account_id FK
varchar card_no_masked
varchar tx_type
varchar channel
bigint amount
char currency
bigint billing_amount
char billing_currency
varchar mcc
varchar merchant_id
varchar merchant_name
varchar resp_code
varchar decision
varchar entry_mode
varchar standin_flag
bigint hold_amount
timestamptz tx_time
integer version
}
AUTH_CHECK_RESULT {
uuid id PK
uuid authorization_id FK
varchar check_code
smallint check_order
varchar result
varchar reason_code
jsonb detail
}
AUTH_REVERSAL {
uuid id PK
uuid authorization_id FK
varchar reversal_type
bigint amount
varchar reason
varchar status
timestamptz reversed_at
}
AUTHORIZATION ||--o{ AUTH_CHECK_RESULT : evaluated_by
AUTHORIZATION ||--o| AUTH_REVERSAL : reversed_by
DDL(節錄)
CREATE SCHEMA IF NOT EXISTS auth;
CREATE TABLE auth.authorizations (
id uuid PRIMARY KEY,
auth_no varchar(24) NOT NULL UNIQUE,
stan varchar(12) NOT NULL,
card_id uuid,
account_id uuid,
card_no_masked varchar(20) NOT NULL,
tx_type varchar(16) NOT NULL
CHECK (tx_type IN ('PURCHASE','CASH_ADVANCE','REFUND','REVERSAL','INSTALLMENT','TAX','FEE','REDEEM')),
channel varchar(16) NOT NULL CHECK (channel IN ('POS','EC','ATM','MOBILE','WEB','API')),
amount bigint NOT NULL CHECK (amount > 0),
currency char(3) NOT NULL,
billing_amount bigint NOT NULL,
billing_currency char(3) NOT NULL,
mcc varchar(8),
merchant_id varchar(24),
merchant_name varchar(80),
resp_code varchar(8) NOT NULL,
decision varchar(16) NOT NULL CHECK (decision IN ('APPROVED','DECLINED','REFERRAL','STANDIN')),
entry_mode varchar(16),
emv_data jsonb,
standin_flag boolean NOT NULL DEFAULT false,
hold_amount bigint NOT NULL DEFAULT 0,
tx_time timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
) PARTITION BY RANGE (tx_time);
CREATE INDEX idx_auth_card ON auth.authorizations(card_id, tx_time DESC);
CREATE INDEX idx_auth_acct ON auth.authorizations(account_id, tx_time DESC);
CREATE INDEX idx_auth_stan ON auth.authorizations(stan);
CREATE TABLE auth.auth_check_results (
id uuid PRIMARY KEY,
authorization_id uuid NOT NULL REFERENCES auth.authorizations(id),
check_code varchar(32) NOT NULL,
check_order smallint NOT NULL,
result varchar(16) NOT NULL CHECK (result IN ('PASS','FAIL','SKIP')),
reason_code varchar(16),
detail jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_check_auth ON auth.auth_check_results(authorization_id, check_order);
變更交付說明(授權讀模型快取 +
hold_id,2026-09-21 回寫|HANDOVER #C-18)
下列各項於V1__baseline.sql不存在,係由後續遷移建立。
遷移 內容 V2__read_model.sql建 3 張讀模型表: auth.card_status_caches/auth.account_limit_caches/lim.limit_holds(已於 §3.4 記載)V3__authorization_hold_id.sqlauth.authorizations新增hold_idV10__authorization_entry_emv.sqlauth.authorizations新增emv_data(JSON)——CR-043 §2.3/SCS-001 §7.17。★ 併記:既有merchant_name/entry_mode(本節 DDL 已載)於V1__baseline.sql早已存在,惟至本次方由實作接線落庫(此前恆 NULL)為何讀模型表不列入領域 ER 圖:
card_status_caches/account_limit_caches係
CQRS 讀模型(授權熱路徑為壓低延遲,於本地快取卡狀態與額度),
權威來源仍在card/limit,可由事件重建——非業務主檔,故不入 ER 圖,
以免與權威實體混淆。
hold_id之語意:關聯lim.limit_holds,使授權與其所佔用之額度可直接對照
(原僅能由authorization_id反查)。屬可追溯性補強,非新業務欄位。⚠️ 本節 ER 圖尚未含
hold_id——本次先以本說明區塊記載,ER 圖補繪列為後續
(見文末「未竟事項」)。
3.6 posting 帳務
erDiagram
TRANSACTION {
uuid id PK
varchar tx_no UK
uuid account_id FK
uuid card_id FK
uuid authorization_id FK
varchar tx_code
varchar tx_type
bigint amount
char currency
varchar idempotency_key UK
varchar source_type
varchar status
date posting_date
date value_date
timestamptz posted_at
varchar reversal_of
integer version
}
SUB_ACCOUNT_ENTRY {
uuid id PK
uuid transaction_id FK
varchar sub_account
bigint amount
bigint before_amount
bigint after_amount
char currency
}
BALANCE {
uuid id PK
uuid account_id UK
bigint principal_amount
bigint interest_amount
bigint fee_amount
bigint tax_amount
bigint total_amount
bigint minimum_due
date payment_due_date
smallint aging_bucket
char currency
integer version
}
BALANCE_HISTORY {
uuid id PK
uuid account_id FK
date as_of_date
bigint principal_amount
bigint interest_amount
bigint fee_amount
bigint total_amount
}
TRANSACTION ||--o{ SUB_ACCOUNT_ENTRY : splits_into
BALANCE ||--o{ BALANCE_HISTORY : snapshots
DDL(節錄)
CREATE SCHEMA IF NOT EXISTS posting;
CREATE TABLE posting.transactions (
id uuid PRIMARY KEY,
tx_no varchar(24) NOT NULL UNIQUE,
account_id uuid NOT NULL,
card_id uuid,
authorization_id uuid,
tx_code varchar(16) NOT NULL,
tx_type varchar(24) NOT NULL
CHECK (tx_type IN ('PURCHASE','CASH_ADVANCE','FEE','INTEREST','PAYMENT','REFUND',
'ADJUSTMENT','REVERSAL','INSTALLMENT','REWARD_REDEEM','CHARGEBACK')),
amount bigint NOT NULL,
currency char(3) NOT NULL,
idempotency_key varchar(120) NOT NULL,
source_type varchar(16) NOT NULL CHECK (source_type IN ('CLEARING','ONLINE','BATCH','MANUAL','SYSTEM')),
status varchar(16) NOT NULL DEFAULT 'POSTED'
CHECK (status IN ('PENDING','POSTED','REVERSED','FAILED')),
posting_date date NOT NULL,
value_date date NOT NULL,
posted_at timestamptz NOT NULL,
reversal_of uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT uq_posting_idem UNIQUE (idempotency_key)
) PARTITION BY RANGE (posting_date);
CREATE INDEX idx_tx_account ON posting.transactions(account_id, posting_date DESC);
CREATE INDEX idx_tx_auth ON posting.transactions(authorization_id);
CREATE TABLE posting.sub_account_entries (
id uuid PRIMARY KEY,
transaction_id uuid NOT NULL REFERENCES posting.transactions(id),
sub_account varchar(16) NOT NULL
CHECK (sub_account IN ('PRINCIPAL','INTEREST','FEE','TAX')),
amount bigint NOT NULL,
before_amount bigint NOT NULL,
after_amount bigint NOT NULL,
currency char(3) NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE posting.balances (
id uuid PRIMARY KEY,
account_id uuid NOT NULL UNIQUE,
principal_amount bigint NOT NULL DEFAULT 0,
interest_amount bigint NOT NULL DEFAULT 0,
fee_amount bigint NOT NULL DEFAULT 0,
tax_amount bigint NOT NULL DEFAULT 0,
total_amount bigint NOT NULL DEFAULT 0,
minimum_due bigint NOT NULL DEFAULT 0,
payment_due_date date,
aging_bucket smallint NOT NULL DEFAULT 0,
currency char(3) NOT NULL DEFAULT 'TWD',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
-- lint:skip=SQL-04 理由:history 為集合名詞,*_history 為餘額快照歷程表之標準後綴
CREATE TABLE posting.balance_history (
id uuid PRIMARY KEY,
account_id uuid NOT NULL,
as_of_date date NOT NULL,
principal_amount bigint NOT NULL,
interest_amount bigint NOT NULL,
fee_amount bigint NOT NULL,
tax_amount bigint NOT NULL,
total_amount bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT uq_balance_history UNIQUE (account_id, as_of_date)
);
3.7 payment 繳款
erDiagram
PAYMENT {
uuid id PK
varchar payment_no UK
uuid account_id FK
bigint amount
char currency
varchar payment_method
varchar channel
varchar idempotency_key UK
varchar status
date payment_date
timestamptz received_at
timestamptz posted_at
boolean limit_restored
varchar unmatched_flag
varchar reversal_reason
integer version
}
PAYMENT_ALLOCATION {
uuid id PK
uuid payment_id FK
varchar sub_account
bigint amount
smallint allocation_order
}
AUTO_DEBIT_ENROLLMENT {
uuid id PK
uuid account_id FK
varchar debit_bank
varchar debit_account_masked
blob debit_account_enc
bigint debit_amount
date effective_from
varchar status
varchar idempotency_key
integer version
}
PAYMENT ||--o{ PAYMENT_ALLOCATION : allocates
DDL(節錄)
CREATE SCHEMA IF NOT EXISTS payment;
CREATE TABLE payment.payments (
id uuid PRIMARY KEY,
payment_no varchar(24) NOT NULL UNIQUE,
account_id uuid NOT NULL,
amount bigint NOT NULL CHECK (amount > 0),
currency char(3) NOT NULL DEFAULT 'TWD',
payment_method varchar(16) NOT NULL
CHECK (payment_method IN ('AUTO_DEBIT','ATM','COUNTER','CONVENIENCE_STORE','MOBILE','WEB')),
channel varchar(16) NOT NULL,
idempotency_key varchar(120) NOT NULL UNIQUE,
status varchar(16) NOT NULL DEFAULT 'RECEIVED'
CHECK (status IN ('RECEIVED','ALLOCATED','POSTED','REVERSED','FAILED','UNMATCHED')),
payment_date date NOT NULL,
received_at timestamptz NOT NULL,
posted_at timestamptz,
limit_restored boolean NOT NULL DEFAULT false,
-- 與 status 為**正交維度**:UNMATCHED_FLAG 表「本次受理未取得沖抵來源」(資料品質訊號),
-- status = 'UNMATCHED' 表「已分配、逾三日未收銷帳檔」(作業狀態)。
-- 二者可同時成立,不得互相取代(SDD-005 §5、SCS-008 §10)。
unmatched_flag boolean NOT NULL DEFAULT false,
-- 沖正原因(2026-09-21 回寫 `HANDOVER #C-18`;實體交付 V3__reversal_reason_and_autodebit_idem.sql)
-- 帳務異動之必要留痕:原 V1/V2 無處可存,契約要求之 reason「收了也存不了」=假資料。
reversal_reason varchar(200),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE payment.payment_allocations (
id uuid PRIMARY KEY,
payment_id uuid NOT NULL REFERENCES payment.payments(id),
sub_account varchar(16) NOT NULL
CHECK (sub_account IN ('FEE','INTEREST','PRINCIPAL','TAX')),
amount bigint NOT NULL CHECK (amount >= 0),
allocation_order smallint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE payment.auto_debit_enrollments (
id uuid PRIMARY KEY,
account_id uuid NOT NULL,
debit_bank varchar(8) NOT NULL,
-- 對外顯示用之遮蔽值(僅留後 4 碼,PiiMasker.maskAccount);永不回傳明文
debit_account_masked varchar(20) NOT NULL,
-- 內部扣款用之 AES-256-GCM 密文(v1:<keyId>:<iv>:<ct>,UTF-8 位元組);
-- 僅供內部扣款流程解密,不經 API 回傳。可為 NULL(V5 前之既有資料)
debit_account_enc bytea,
debit_amount bigint,
effective_from date NOT NULL,
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','CANCELLED')),
idempotency_key varchar(120) NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
變更交付說明(
AUTO_DEBIT_ENROLLMENTS,2026-09-19)
本表於V1__baseline.sql不存在,係由後續遷移建立;上列為目標狀態。
遷移 內容 V2__auto_debit_enrollments_and_no_seq.sql建表(含 debit_account_masked)+payment_no_seqV5__autodebit_account_ciphertext.sql新增 debit_account_enc(W-1之KeyProvider推廣)兩欄並存之理由:
debit_account_masked供對外顯示(永不回傳明文);
debit_account_enc供內部扣款解密取得完整帳號。只有遮蔽值則無法扣款;
只有密文則每次顯示都要解密(無謂的金鑰存取與風險暴露)。⚠️ 本節 1.0 版之 ER 圖曾列
debit_account_enc,但 DDL 區塊漏列本表
——已於本次補齊(文件與實物一致)。ER 圖之欄位亦補上debit_account_masked。⚠️ 既有
V1__baseline.sql已凍結、不得修改(SAD-003 §9/CSP-002 §5);
重跑gen_flyway_baseline.py --force會覆寫 V1 並使 Flyway checksum 失配。
變更交付說明(
UNMATCHED,2026-09-19|CR-003)
本節 DDL 為目標狀態。payments.status之值域增列UNMATCHED,
其實體交付為V4__payments_unmatched_status.sql(既有V1__baseline.sql已凍結、不得修改,
見SAD-003 §9/CSP-002 §5)。
⚠️ 重跑gen_flyway_baseline.py --force會覆寫 V1 並使 Flyway checksum 失配;
若確需重建,須先flyway repair或重建本地庫(docker compose down -v)。
變更交付說明(
reversal_reason,2026-09-21 回寫|HANDOVER #C-18)
payments.reversal_reason(varchar(200),可為 NULL)之實體交付為
V3__reversal_reason_and_autodebit_idem.sql;本次已同步補入本節 ER 圖與 DDL。為何需要:
CR(2026-09-19)修約後,契約要求POST /payments/{id}/reversals
須帶 request body(PaymentReversalRequest.reason)。原 V1/V2 表無處存放沖正原因 →
「收了也存不了」,形同假資料。沖正原因屬帳務異動之必要留痕(稽核)。⚠️ 更正
CR-003 §6 #3之不實記載:該處原稱「本次僅加註 §3.7/§3.8/§3.10 三處」,
惟 2026-09-21 盤點發現reversal_reason於本檔全文出現次數為 0——
§3.7 實際並未回寫(§3.8/§3.10 則確已完成)。故本次補上,
並於CR-003修訂記錄更正該記載,以免後人據以判定「已完成」而再度略過。
3.8 rewards 點數(R2 / FR-RWD-5.x)
點數採「累積(pending)→ 確認(confirmed)→ 到期/兌換/沒收」三態生命週期,
依產品參數(P0–P3)與消費維度(MCC/通路/卡別)計算,支援折抵帳單、兌換商品、里程、費用減免。
erDiagram
POINTS_ACCOUNT {
uuid id PK
uuid account_id FK
varchar points_type
varchar tier
bigint balance
bigint pending_balance
bigint confirmed_balance
varchar expiry_policy_code
date next_expiry_date
bigint next_expiry_points
varchar status
integer version
}
POINTS_TIER {
uuid id PK
varchar tier_code UK
varchar product_code FK
numeric earn_multiplier
numeric redeem_rate
bigint qualification_spend
varchar status
}
POINTS_LEDGER {
uuid id PK
uuid points_account_id FK
varchar entry_type
bigint points
bigint before_balance
bigint after_balance
varchar source_type
uuid source_id
varchar earn_rule_code
varchar idempotency_key UK
date posting_date
timestamptz occurred_at
varchar status
integer version
}
POINTS_EXPIRY_BUCKET {
uuid id PK
uuid points_account_id FK
date expiry_date
bigint points
varchar status
integer version
}
REWARD_RULE {
uuid id PK
varchar rule_code UK
varchar points_type
jsonb dimensions
jsonb conditions
varchar period_type
integer priority
varchar status
}
REDEMPTION {
uuid id PK
varchar redemption_no UK
uuid account_id FK
varchar redemption_type
bigint points_used
bigint cash_value
bigint amount_paid
jsonb targets
varchar channel
varchar status
timestamptz redeemed_at
integer version
}
POINTS_ACCOUNT ||--o{ POINTS_LEDGER : records
POINTS_ACCOUNT ||--o{ POINTS_EXPIRY_BUCKET : buckets
POINTS_ACCOUNT }o--|| POINTS_TIER : classifies
POINTS_ACCOUNT ||--o{ REDEMPTION : redeems
DDL
CREATE SCHEMA IF NOT EXISTS rw;
CREATE TABLE rw.points_accounts (
id uuid PRIMARY KEY,
account_id uuid NOT NULL,
points_type varchar(16) NOT NULL CHECK (points_type IN ('EARN','BONUS','REDEEM')),
tier varchar(16) NOT NULL DEFAULT 'CLASSIC',
balance bigint NOT NULL DEFAULT 0 CHECK (balance >= 0),
pending_balance bigint NOT NULL DEFAULT 0 CHECK (pending_balance >= 0),
confirmed_balance bigint NOT NULL DEFAULT 0 CHECK (confirmed_balance >= 0),
expiry_policy_code varchar(16) NOT NULL DEFAULT 'FIXED_24M',
next_expiry_date date,
next_expiry_points bigint NOT NULL DEFAULT 0,
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','FROZEN','CLOSED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_pa_account ON rw.points_accounts(account_id);
CREATE TABLE rw.points_tiers (
id uuid PRIMARY KEY,
tier_code varchar(16) NOT NULL UNIQUE,
product_code varchar(16) NOT NULL,
earn_multiplier numeric(5,2) NOT NULL DEFAULT 1.00,
redeem_rate numeric(9,6) NOT NULL,
qualification_spend bigint NOT NULL DEFAULT 0,
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','INACTIVE')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
-- lint:skip=SQL-04 理由:ledger 為集合名詞,表單一邏輯帳本,不加複數 s
CREATE TABLE rw.points_ledger (
id uuid PRIMARY KEY,
points_account_id uuid NOT NULL REFERENCES rw.points_accounts(id),
entry_type varchar(16) NOT NULL
CHECK (entry_type IN ('EARN','CONFIRM','FORFEIT','REDEEM','EXPIRE','ADJUST','TRANSFER')),
points bigint NOT NULL,
before_balance bigint NOT NULL,
after_balance bigint NOT NULL,
source_type varchar(16) CHECK (source_type IN ('AUTHORIZATION','TRANSACTION','MANUAL','PROMOTION')),
source_id uuid,
earn_rule_code varchar(40),
idempotency_key varchar(120) NOT NULL UNIQUE,
posting_date date NOT NULL,
occurred_at timestamptz NOT NULL,
status varchar(16) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING','CONFIRMED','REVERSED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_pl_account ON rw.points_ledger(points_account_id, posting_date DESC);
CREATE TABLE rw.points_expiry_buckets (
id uuid PRIMARY KEY,
points_account_id uuid NOT NULL REFERENCES rw.points_accounts(id),
expiry_date date NOT NULL,
points bigint NOT NULL CHECK (points > 0),
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','EXPIRED','REDEEMED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_pe_account ON rw.points_expiry_buckets(points_account_id, expiry_date);
CREATE TABLE rw.redemptions (
id uuid PRIMARY KEY,
redemption_no varchar(24) NOT NULL UNIQUE,
account_id uuid NOT NULL,
redemption_type varchar(16) NOT NULL
CHECK (redemption_type IN ('CASH','CATEGORY','GOODS','MILEAGE','FEE_WAIVER','DONATION')),
points_used bigint NOT NULL CHECK (points_used > 0),
cash_value bigint NOT NULL DEFAULT 0,
amount_paid bigint NOT NULL DEFAULT 0,
targets jsonb,
channel varchar(16) NOT NULL,
status varchar(16) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING','CONFIRMED','CANCELLED','REVERSED')),
redeemed_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
點數確認以「帳務週期結束+N 日」為準(pending → confirmed);逾期未兌換依
expiry_policy_code
轉入points_expiry_buckets並於到期日標記EXPIRED(FORFEIT)。兌換現金(CASH)即時沖抵
posting.balances本金,見 SDD-003 §兌換序列。
變更交付說明(V2 遷移補充,2026-09-19)
本節 ER/DDL 為 V1 基線快照。以下項目未列於本節,係因契約或上層設計已要求、
但 V1 無處可存,而於 V2 遷移補建(V2__reward_rules_bucket_stage_and_idempotency.sql):
物件 用途 欄位值域(實測 CHECK) rw.reward_rules(表)回饋規則(ER 圖有 REWARD_RULE實體,V1 DDL 區塊漏列)rule_type IN ('REWARD','FEE','INTEREST','DEDUCT','ELIGIBILITY');status IN ('DRAFT','ACTIVE','RETIRED')POINTS_EXPIRY_BUCKETS.bucket_stage(欄)標示該批點數屬 CUR/AV1/AV2哪一階段;缺此欄則「先到期先用」與階段搬移無法實作bucket_stage IN ('CUR','AV1','AV2')POINTS_LEDGER.bucket_stage(欄)使兌換之扣抵階段可追溯(稽核缺口) 同上 REDEMPTIONS.idempotency_key(欄)兌換為扣點之財務操作,重送不得重複扣點 UNIQUErw.redemption_no_seq(序列)兌換編號連號可稽核 — 狀態機見
SDD-005 §10.1(桶階段)/§10.2(桶狀態)/§10.3(帳戶)。變更交付說明(V4 遷移補充,2026-09-22,
CR-012雙欄位)
物件 用途 欄位值域 REDEMPTIONS.settlement_method(欄,V4__redemptions_settlement_method.sql)兌換之折抵方式維度(怎麼付);與 redemption_type(標的類別,換什麼)正交——「用點數+現金換哩程」須 標的=MILEAGE且 折抵=POINTS_PLUS_CASH,單一欄位無法同時表達(CR-012 §3)settlement_method IN ('POINTS_ONLY','POINTS_PLUS_CASH','ONLINE_REDEEM','MERGE','EXCHANGE_CENTER');DEFAULT 'POINTS_ONLY' NOT NULLV1 基線已凍結,不得修改( SAD-003 §9、CSP-002 §5)。
3.9 installment 分期(R2 / FR-ACQ-4.x)
分期分「商家分期(MERCHANT)」與「銀行分期(BANK,含紅利分期、帳單分期)」兩類;
每期含本金+手續費,支援提前清償、展期與違約。本金餘額獨立維護,與 posting.balances
總額勾稽。
erDiagram
INSTALLMENT_PRODUCT {
uuid id PK
varchar product_code UK
varchar name
varchar channel_type
varchar business_type
bigint min_amount
smallint min_periods
smallint max_periods
numeric fee_rate
numeric annual_rate
boolean merchant_eligible
varchar status
integer version
}
INSTALLMENT_PLAN {
uuid id PK
varchar plan_no UK
uuid account_id FK
uuid card_id FK
uuid original_authorization_id
uuid original_transaction_id
varchar channel_type
varchar business_type
varchar product_code FK
bigint original_amount
char currency
smallint period_count
bigint per_period_amount
bigint first_period_amount
bigint last_period_amount
numeric fee_rate
bigint total_fee
numeric annual_rate
bigint down_payment
bigint processing_fee
date start_date
date first_due_date
bigint principal_outstanding
smallint paid_periods
smallint remaining_periods
boolean early_settlement_flag
varchar status
integer version
}
INSTALLMENT_PERIOD {
uuid id PK
uuid plan_id FK
smallint period_no
bigint amount
bigint principal_amount
bigint fee_amount
date due_date
varchar status
timestamptz paid_at
bigint remaining_principal
integer version
}
INSTALLMENT_PRODUCT ||--o{ INSTALLMENT_PLAN : offers
INSTALLMENT_PLAN ||--o{ INSTALLMENT_PERIOD : schedules
DDL
CREATE SCHEMA IF NOT EXISTS inst;
CREATE TABLE inst.installment_products (
id uuid PRIMARY KEY,
product_code varchar(16) NOT NULL UNIQUE,
name varchar(60) NOT NULL,
channel_type varchar(16) NOT NULL
CHECK (channel_type IN ('MERCHANT','BANK','REVOLVING','BILL',
'ACQUIRER','NCCC','QRCODE')),
business_type varchar(16)
CHECK (business_type IS NULL
OR business_type IN ('SINGLE','RECURRING','STATEMENT')),
min_amount bigint NOT NULL DEFAULT 0,
min_periods smallint NOT NULL DEFAULT 3,
max_periods smallint NOT NULL DEFAULT 36,
fee_rate numeric(9,6) NOT NULL DEFAULT 0,
annual_rate numeric(9,6) NOT NULL DEFAULT 0,
merchant_eligible boolean NOT NULL DEFAULT false,
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','INACTIVE')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE inst.installment_plans (
id uuid PRIMARY KEY,
plan_no varchar(24) NOT NULL UNIQUE,
account_id uuid NOT NULL,
card_id uuid,
original_authorization_id uuid,
original_transaction_id uuid,
channel_type varchar(16) NOT NULL
CHECK (channel_type IN ('MERCHANT','BANK','REVOLVING','BILL',
'ACQUIRER','NCCC','QRCODE')),
business_type varchar(16)
CHECK (business_type IS NULL
OR business_type IN ('SINGLE','RECURRING','STATEMENT')),
product_code varchar(16),
original_amount bigint NOT NULL CHECK (original_amount > 0),
currency char(3) NOT NULL DEFAULT 'TWD',
period_count smallint NOT NULL CHECK (period_count BETWEEN 1 AND 60),
per_period_amount bigint NOT NULL CHECK (per_period_amount > 0),
first_period_amount bigint NOT NULL,
last_period_amount bigint NOT NULL,
fee_rate numeric(9,6) NOT NULL DEFAULT 0,
total_fee bigint NOT NULL DEFAULT 0,
annual_rate numeric(9,6) NOT NULL DEFAULT 0,
down_payment bigint NOT NULL DEFAULT 0,
processing_fee bigint NOT NULL DEFAULT 0,
start_date date NOT NULL,
first_due_date date NOT NULL,
principal_outstanding bigint NOT NULL DEFAULT 0,
paid_periods smallint NOT NULL DEFAULT 0,
remaining_periods smallint NOT NULL DEFAULT 0,
early_settlement_flag boolean NOT NULL DEFAULT false,
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','COMPLETED','PRE_CLOSED','DEFAULTED','CANCELLED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_ip_account ON inst.installment_plans(account_id);
CREATE TABLE inst.installment_periods (
id uuid PRIMARY KEY,
plan_id uuid NOT NULL REFERENCES inst.installment_plans(id),
period_no smallint NOT NULL CHECK (period_no >= 1),
amount bigint NOT NULL CHECK (amount >= 0),
principal_amount bigint NOT NULL DEFAULT 0,
fee_amount bigint NOT NULL DEFAULT 0,
due_date date NOT NULL,
status varchar(16) NOT NULL DEFAULT 'DUE'
CHECK (status IN ('DUE','PAID','OVERDUE','WAIVED','REVERSED')),
paid_at timestamptz,
remaining_principal bigint NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_ip_period_plan ON inst.installment_periods(plan_id, period_no);
每期扣款由
payment-service依due_date產生應繳明細並沖抵principal_outstanding;
提前清償(PRE_CLOSED)依annual_rate計算剩餘手續費減免後結清,見 SDD-003 §分期扣款序列。
3.10 statement 帳單(R2 / FR-ACQ-4.x)
帳單為「結帳日當下」的餘額快照,欄位直接對應紙本/電子帳單之每一行;
明細行(statement_lines)凍結當期交易,避免後續沖正導致帳單內容漂移。
erDiagram
STATEMENT {
uuid id PK
varchar statement_no UK
uuid account_id FK
varchar statement_month
varchar cycle_code
date statement_date
date payment_due_date
smallint grace_period_days
bigint prev_balance
bigint payments_credits
bigint purchases_debits
bigint finance_charges
bigint fees
bigint adjustments
bigint new_balance
bigint minimum_due
bigint reward_points_earned
bigint reward_points_balance
char currency
varchar send_type
varchar pdf_ref
boolean e_statement_flag
varchar status
timestamptz generated_at
timestamptz sent_at
integer version
}
STATEMENT_LINE {
uuid id PK
uuid statement_id FK
integer seq
date posting_date
varchar tx_no
varchar description
varchar mcc
varchar tx_type
varchar card_no_masked
bigint amount
char currency
integer version
}
STATEMENT ||--o{ STATEMENT_LINE : details
DDL
CREATE SCHEMA IF NOT EXISTS stmt;
CREATE TABLE stmt.statements (
id uuid PRIMARY KEY,
statement_no varchar(24) NOT NULL UNIQUE,
account_id uuid NOT NULL,
statement_month varchar(8) NOT NULL, -- YYYYMM
cycle_code varchar(8) NOT NULL,
statement_date date NOT NULL,
payment_due_date date NOT NULL,
grace_period_days smallint NOT NULL DEFAULT 0,
prev_balance bigint NOT NULL DEFAULT 0,
payments_credits bigint NOT NULL DEFAULT 0,
purchases_debits bigint NOT NULL DEFAULT 0,
finance_charges bigint NOT NULL DEFAULT 0,
fees bigint NOT NULL DEFAULT 0,
adjustments bigint NOT NULL DEFAULT 0,
new_balance bigint NOT NULL DEFAULT 0,
minimum_due bigint NOT NULL DEFAULT 0,
reward_points_earned bigint NOT NULL DEFAULT 0,
reward_points_balance bigint NOT NULL DEFAULT 0,
currency char(3) NOT NULL DEFAULT 'TWD',
send_type varchar(16) NOT NULL DEFAULT 'ELECTRONIC'
CHECK (send_type IN ('PAPER','ELECTRONIC','NONE')),
pdf_ref varchar(255),
e_statement_flag boolean NOT NULL DEFAULT true,
status varchar(16) NOT NULL DEFAULT 'GENERATED'
CHECK (status IN ('GENERATED','SENT','PAID','PARTIALLY_PAID','OVERDUE','DISPUTED','CLOSED')),
generated_at timestamptz NOT NULL,
sent_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT uq_stmt_month UNIQUE (account_id, statement_month)
);
CREATE INDEX idx_stmt_account ON stmt.statements(account_id, statement_date DESC);
CREATE TABLE stmt.statement_lines (
id uuid PRIMARY KEY,
statement_id uuid NOT NULL REFERENCES stmt.statements(id),
seq integer NOT NULL,
posting_date date NOT NULL,
tx_no varchar(24),
description varchar(120) NOT NULL,
mcc varchar(8),
tx_type varchar(24),
card_no_masked varchar(20),
amount bigint NOT NULL,
currency char(3) NOT NULL DEFAULT 'TWD',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_sl_statement ON stmt.statement_lines(statement_id, seq);
變更交付說明(V2 遷移補充,2026-09-19)
本節 ER/DDL 為 V1 基線快照。以下二表未列於本節,係因契約要求
「批次狀態與 Checkpoint」而於 V2 遷移補建(V2__statement_runs_and_no_seq.sql):
表/物件 用途 欄位值域(實測 CHECK) stmt.statement_runs帳單批次執行:狀態/已處理筆數/ checkpoint(供異常續跑)status IN ('GENERATING','GENERATED','FAILED')stmt.statement_cycle_accounts帳單週期帳戶主檔:決定各週期應結帳之歸戶 UNIQUE(cycle_code, account_id)stmt.statement_no_seq帳單編號序列(連號可稽核,不得以隨機值替代) — 狀態機見
SDD-005 §9.2(批次執行)。V1 基線已凍結,不得修改
(SAD-003 §9、CSP-002 §5);本節之 ER 圖亦為 V1 快照,故不含此二表。
3.11 pricing 費率與利息(R2 / FR-PRC)
依產品(P0–P3)、卡別、幣別定義循環利率、各項手續費、寬限期與計算方式;
利息計算方法(平均日終餘額 ADB/上月餘額/雙週期)直接寫入 calc_method,Coder 不必自創。
erDiagram
PRICING_RULE {
uuid id PK
varchar rule_code UK
varchar rule_type
varchar product_code FK
varchar org
varchar tier
date effective_from
date effective_to
jsonb tiers
numeric rate
numeric annual_rate
smallint grace_period_days
bigint fee_amount
bigint min_fee
bigint cap_amount
varchar calc_method
varchar status
integer version
}
DDL
CREATE SCHEMA IF NOT EXISTS prc;
CREATE TABLE prc.pricing_rules (
id uuid PRIMARY KEY,
rule_code varchar(40) NOT NULL UNIQUE,
rule_type varchar(24) NOT NULL
CHECK (rule_type IN ('INTEREST','ANNUAL_FEE','CASH_ADV_FEE','LATE_FEE','FOREIGN_TX_FEE','OVERLIMIT_FEE','FEE_WAIVER')),
product_code varchar(16),
org varchar(16),
tier varchar(16),
effective_from date NOT NULL,
effective_to date,
tiers jsonb, -- 金額級距(如現金透支分層費率)
rate numeric(9,6) NOT NULL DEFAULT 0,
annual_rate numeric(9,6) NOT NULL DEFAULT 0,
grace_period_days smallint NOT NULL DEFAULT 0,
fee_amount bigint NOT NULL DEFAULT 0,
min_fee bigint NOT NULL DEFAULT 0,
cap_amount bigint NOT NULL DEFAULT 0,
calc_method varchar(16) NOT NULL DEFAULT 'ADB'
CHECK (calc_method IN ('ADB','PREVIOUS_BALANCE','TWO_CYCLE','FLAT')),
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','INACTIVE')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
變更交付說明(V2 遷移補充,2026-09-22,C-31:
TX_TYPE欄位)
物件 用途 欄位值域 PRICING_RULES.tx_type(欄,V2__pricing_rules_tx_type.sql)交易型別——區分一般消費與預借現行之循環利率(種子 18.5% vs 20%)。原曾暫以 TIER(客層級距)挪用作為區分維度(本地慣例),本欄位增設後TIER回歸本義(SCS-006 §7.5、CR-005 §8 #1結案)tx_type IN ('GENERAL','CASH_ADVANCE');可為 null——僅INTEREST規則具值,費用類規則不硬塞值;既有INTEREST列依TIER回填V1 基線已凍結,不得修改(
SAD-003 §9、CSP-002 §5)。
3.12 clearing 清算結算(R1 / FR-CLR)
清算依卡組織(VISA/MC/NCCC/AE/DFS)收送檔(VCF/EMS/SETTLEMENT),
逐筆比對授權與請款,計算交換費(interchange)與網路費,產出結算單。
erDiagram
CLEARING_BATCH {
uuid id PK
varchar batch_no UK
varchar org
varchar direction
varchar file_type
varchar file_name
date file_date
date settlement_date
bigint tx_count
bigint total_amount
bigint fee_amount
char currency
varchar checksum
varchar status
integer version
}
CLEARING_ITEM {
uuid id PK
uuid batch_id FK
varchar tx_ref
varchar card_no_masked
bigint amount
bigint fee
varchar mcc
varchar merchant_id
varchar status
uuid matched_tx_id
integer version
}
SETTLEMENT {
uuid id PK
varchar org
date settlement_date
bigint gross_amount
bigint interchange_fee
bigint network_fee
bigint net_amount
char currency
varchar status
integer version
}
CLEARING_BATCH ||--o{ CLEARING_ITEM : contains
DDL
CREATE SCHEMA IF NOT EXISTS clr;
CREATE TABLE clr.clearing_batches (
id uuid PRIMARY KEY,
batch_no varchar(32) NOT NULL UNIQUE,
org varchar(16) NOT NULL
CHECK (org IN ('VISA','MASTERCARD','NCCC','AMEX','DFS','JCB')),
direction varchar(8) NOT NULL CHECK (direction IN ('INBOUND','OUTBOUND')),
file_type varchar(16) NOT NULL
CHECK (file_type IN ('VCF','EMS','SETTLEMENT','CHARGEBACK','FEES')),
file_name varchar(120) NOT NULL,
file_date date NOT NULL,
settlement_date date NOT NULL,
tx_count bigint NOT NULL DEFAULT 0,
total_amount bigint NOT NULL DEFAULT 0,
fee_amount bigint NOT NULL DEFAULT 0,
currency char(3) NOT NULL DEFAULT 'TWD',
checksum varchar(64),
status varchar(16) NOT NULL DEFAULT 'RECEIVED'
CHECK (status IN ('RECEIVED','PARSED','MATCHED','SETTLED','REJECTED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_clr_batch_org_date ON clr.clearing_batches(org, file_date);
CREATE TABLE clr.clearing_items (
id uuid PRIMARY KEY,
batch_id uuid NOT NULL REFERENCES clr.clearing_batches(id),
tx_ref varchar(32) NOT NULL,
card_no_masked varchar(20),
amount bigint NOT NULL DEFAULT 0,
fee bigint NOT NULL DEFAULT 0,
mcc varchar(8),
merchant_id varchar(24),
status varchar(16) NOT NULL DEFAULT 'UNMATCHED'
CHECK (status IN ('MATCHED','UNMATCHED','EXCEPTION')),
matched_tx_id uuid,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_clr_item_batch ON clr.clearing_items(batch_id);
CREATE TABLE clr.settlements (
id uuid PRIMARY KEY,
org varchar(16) NOT NULL,
settlement_date date NOT NULL,
gross_amount bigint NOT NULL DEFAULT 0,
interchange_fee bigint NOT NULL DEFAULT 0,
network_fee bigint NOT NULL DEFAULT 0,
net_amount bigint NOT NULL DEFAULT 0,
currency char(3) NOT NULL DEFAULT 'TWD',
status varchar(16) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING','SETTLED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
變更交付說明(V2 遷移補充,2026-09-22,
CR-016:清算執行/對帳結果/org 值域/結算方向)
物件 用途 欄位值域 CLEARING_RUNS(新表,V2__clearing_runs_and_reconciliations.sql)清算執行——一次執行涵蓋多個批次檔(多組織/多檔型),與 CLEARING_BATCHES(一檔一列)為一對多;契約POST /clearing/runs之runId所繫status IN ('RUNNING','MATCHED','DIFFERENCE','FAILED');RUN_NOUNIQUE;ORGS(逗號分隔)CLEARING_RECONCILIATIONS(新表)對帳結果——依「執行 × 組織」記載差異;契約 GET /clearing/reconciliations/{runId}所繫difference_type IN ('TIMING','FX_RATE','SUBSTANTIVE');FK(RUN_ID) REFERENCESCLEARING_RUNS(ID)CLEARING_BATCHES.ORG值域擴充(具名約束CK_CLEARING_BATCHES_ORG)契約之 orgs列舉含DINERS/DISCOVER,原 DDL 為 6 值org IN ('VISA','MASTERCARD','NCCC','AMEX','DFS','JCB','DISCOVER','DINERS')——保留DFS(與DISCOVER為同一組織之兩種寫法,不為相容而刪值)SETTLEMENTS.DIRECTION(欄)結算方向(契約必要欄位) direction IN ('ACQUIRER_PAYOUT','ISSUER_RECEIVABLE');可為 null(既有列無此值)V1 基線已凍結,不得修改(
SAD-003 §9、CSP-002 §5)。
3.13 dispute 爭議帳款(R1 / FR-DIS)
爭議依卡組織時限與階段機制(Inquiry → 1st/2nd Presentment → Pre-Arb → Arbitration),
全程留存處理軌跡;暫付款與扣回產生對應會計分錄(與清算及帳務勾稽)。
erDiagram
DISPUTE_CASE {
uuid id PK
varchar dispute_no UK
uuid account_id FK
uuid card_id FK
uuid transaction_id FK
uuid authorization_id FK
varchar org
varchar arn
varchar reason_code
varchar dispute_type
varchar stage
varchar currency
bigint amount
jsonb evidence_required
jsonb evidence_submitted
smallint representment_count
varchar liability
timestamptz response_deadline
varchar status
timestamptz opened_at
timestamptz closed_at
varchar assigned_to
integer version
}
DISPUTE_TIMELINE {
uuid id PK
uuid dispute_id FK
varchar stage
varchar action
varchar actor
timestamptz occurred_at
jsonb detail
integer version
}
DISPUTE_CASE ||--o{ DISPUTE_TIMELINE : tracks
DDL
CREATE SCHEMA IF NOT EXISTS dis;
CREATE TABLE dis.dispute_cases (
id uuid PRIMARY KEY,
dispute_no varchar(32) NOT NULL UNIQUE,
account_id uuid NOT NULL,
card_id uuid,
transaction_id uuid,
authorization_id uuid,
org varchar(16) NOT NULL
CHECK (org IN ('VISA','MASTERCARD','NCCC','AMEX','DFS','JCB','INTERNAL')),
arn varchar(32),
reason_code varchar(8),
dispute_type varchar(24) NOT NULL
CHECK (dispute_type IN ('CHARGEBACK','INQUIRY','RETROACTIVE','FRAUD','DUPLICATE')),
stage varchar(24) NOT NULL DEFAULT 'INQUIRY'
CHECK (stage IN ('INQUIRY','FIRST_PRESENTMENT','SECOND_PRESENTMENT','PRE_ARB','ARBITRATION','CLOSED')),
currency char(3) NOT NULL DEFAULT 'TWD',
amount bigint NOT NULL CHECK (amount >= 0),
evidence_required jsonb NOT NULL DEFAULT '[]'::jsonb,
evidence_submitted jsonb NOT NULL DEFAULT '[]'::jsonb,
representment_count smallint NOT NULL DEFAULT 0,
liability varchar(16) CHECK (liability IN ('ISSUER','ACQUIRER','MERCHANT','PENDING')),
response_deadline timestamptz NOT NULL,
status varchar(16) NOT NULL DEFAULT 'OPEN'
CHECK (status IN ('OPEN','EVIDENCE_PENDING','REPRESENTED','WON','LOST','CLOSED')),
opened_at timestamptz NOT NULL,
closed_at timestamptz,
assigned_to varchar(40),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_dispute_account ON dis.dispute_cases(account_id);
CREATE INDEX idx_dispute_tx ON dis.dispute_cases(transaction_id);
CREATE TABLE dis.dispute_timelines (
id uuid PRIMARY KEY,
dispute_id uuid NOT NULL REFERENCES dis.dispute_cases(id),
stage varchar(24) NOT NULL,
action varchar(32) NOT NULL,
actor varchar(40) NOT NULL,
occurred_at timestamptz NOT NULL,
detail jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
變更交付說明(V2 遷移補充,2026-09-22,
CR-017:爭議雙軸拆分+扣款類型)
物件 用途 欄位值域 DISPUTE_CASES.DISPUTE_REASON(欄,V2__dispute_reason_workflow_stage_chargeback.sql)爭議原因——與 DISPUTE_TYPE(程序類型)為正交兩軸(原契約之disputeType實指原因,而 DDL 之dispute_type混用兩者,交集僅DUPLICATE)dispute_reason IN ('UNAUTHORIZED','DUPLICATE','GOODS_SERVICES','OTHER')(CK_DISPUTE_CASES_REASON)DISPUTE_CASES.WORKFLOW_STAGE(欄)本行內部作業階段——與 STAGE(卡組織程序階段,SDD-005 §7.1)為正交兩軸(交集僅ARBITRATION/CLOSED)workflow_stage IN ('OPENED','EVIDENCE_COLLECTING','UNDER_REVIEW','ARBITRATION','RESOLVED_ISSUER','RESOLVED_ACQUIRER','CLOSED')(CK_DISPUTE_CASES_WORKFLOW_STAGE)DISPUTE_CASES.CHARGEBACK_TYPE/CHARGEBACK_REASON(欄)扣款/仲裁之提交類型與原因(稽核) chargeback_type IN ('FIRST','SECOND','ARBITRATION')(CK_DISPUTE_CASES_CHARGEBACK_TYPE)DISPUTE_CASES.DISPUTE_TYPE之 CHECK 具名化值域不變(回歸程序類型語意; FRAUD/DUPLICATE為過渡期遺留值)CK_DISPUTE_CASES_TYPE:dispute_type IN ('CHARGEBACK','INQUIRY','RETROACTIVE','FRAUD','DUPLICATE')V1 基線已凍結,不得修改(
SAD-003 §9、CSP-002 §5)。
3.14 merchant 特約商店與收單(R1 / FR-ACQ)
erDiagram
MERCHANT {
uuid id PK
varchar merchant_id UK
varchar merchant_name
varchar merchant_type
varchar mcc
varchar org
varchar acquirer_id
jsonb terminal_ids
varchar settlement_bank
varchar settlement_account_enc
varchar risk_level
varchar kmc_category
varchar status
timestamptz onboarded_at
integer version
}
MERCHANT_SETTLEMENT {
uuid id PK
varchar settlement_no UK
uuid merchant_id FK
varchar org
date settlement_date
integer batch_count
bigint gross_amount
bigint fee_amount
bigint tax_amount
bigint net_amount
char currency
varchar status
integer version
}
MERCHANT ||--o{ MERCHANT_SETTLEMENT : settles
DDL
CREATE SCHEMA IF NOT EXISTS mch;
CREATE TABLE mch.merchants (
id uuid PRIMARY KEY,
merchant_id varchar(24) NOT NULL UNIQUE,
merchant_name varchar(120) NOT NULL,
merchant_type varchar(24) NOT NULL
CHECK (merchant_type IN ('RETAIL','EC','MOTO','RESTAURANT','TRAVEL','SUPERMARKET','OTHER')),
mcc varchar(8),
org varchar(16),
acquirer_id varchar(24),
terminal_ids jsonb NOT NULL DEFAULT '[]'::jsonb,
settlement_bank varchar(16),
settlement_account_enc bytea,
risk_level varchar(8) NOT NULL DEFAULT 'LOW'
CHECK (risk_level IN ('LOW','MEDIUM','HIGH')),
kmc_category varchar(24),
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','SUSPENDED','TERMINATED')),
onboarded_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_mch_mcc ON mch.merchants(mcc);
CREATE TABLE mch.merchant_settlements (
id uuid PRIMARY KEY,
settlement_no varchar(32) NOT NULL UNIQUE,
merchant_id uuid NOT NULL REFERENCES mch.merchants(id),
org varchar(16),
settlement_date date NOT NULL,
batch_count integer NOT NULL DEFAULT 0,
gross_amount bigint NOT NULL DEFAULT 0,
fee_amount bigint NOT NULL DEFAULT 0,
tax_amount bigint NOT NULL DEFAULT 0,
net_amount bigint NOT NULL DEFAULT 0,
currency char(3) NOT NULL DEFAULT 'TWD',
status varchar(16) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING','SETTLED')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE INDEX idx_ms_merchant ON mch.merchant_settlements(merchant_id, settlement_date DESC);
變更交付說明(V2 遷移補充,2026-09-22,
CR-015:雙軸拆分+狀態值域擴充+缺欄)
物件 用途 欄位值域 MERCHANTS.MERCHANT_FORM(欄,V2__merchant_form_and_status.sql)商店型態——與 MERCHANT_TYPE(業別)為正交兩軸(一間「超市」可以是「連鎖」);契約之merchantType即對映本欄merchant_form IN ('SINGLE','FRANCHISE','CHAIN','GROUP')MERCHANTS.PARENT_MERCHANT_ID(欄)集團店上層(自參考 FK) RAW(16),NULL表非集團店MERCHANTS.ACCEPTED_CARD_TYPES(欄)受理卡種 JSON DEFAULT JSON('[]') NOT NULL(比照TERMINAL_IDS手法)MERCHANTS.STATUS值域擴充(具名約束CK_MERCHANTS_STATUS)補「申請中」與「已結案」 status IN ('APPLIED','ACTIVE','SUSPENDED','TERMINATED','CLOSED')MERCHANT_SETTLEMENTS.BATCH_NO(欄)請款批次號( BATCH_COUNT為筆數,語意不同)VARCHAR2(32)MERCHANT_SETTLEMENTS.EXCEPTION_REASON(欄)異常原因(稽核) VARCHAR2(240)MERCHANT_SETTLEMENTS.STATUS值域擴充(CK_MERCHANT_SETTLEMENTS_STATUS)契約之請款流程 4 值併入 DDL 之入帳 2 值 status IN ('SUBMITTED','VERIFIED','EXCEPTION','PAID','PENDING','SETTLED')兩具名約束之建立方式:V1 之 CHECK 為內聯宣告(Oracle 自動命名
SYS_C*),
V2 以資料字典動態尋得後重建為具名(比照V4__payments_unmatched_status.sql),
使後續值域變更得穩定引用;尋得條件須同時比對IN (以免誤刪同表之
"STATUS" IS NOT NULL約束。V1 基線已凍結,不得修改(
SAD-003 §9、CSP-002 §5)。
3.15 platform 平台表(所有服務共用結構)
erDiagram
OUTBOX {
uuid id PK
varchar aggregate_type
varchar aggregate_id
varchar event_type
integer event_version
jsonb payload
varchar trace_id
timestamptz published_at
integer retry_count
}
IDEMPOTENCY_RECORD {
uuid id PK
varchar idempotency_key UK
varchar request_fingerprint
varchar resource_type
varchar resource_id
smallint response_status
jsonb response_body
timestamptz expires_at
}
AUDIT_TRAIL {
uuid id PK
varchar trace_id
timestamptz occurred_at
varchar actor_type
varchar actor_id
varchar actor_role
varchar action
varchar resource_type
varchar resource_id
jsonb before_image
jsonb after_image
varchar result
varchar source_ip
varchar prev_hash
varchar row_hash
}
DDL
-- 以下三張表在每個服務之 schema 內各建一份(平台共通結構)
-- lint:skip=SQL-04,SQL-05 理由:append-only 事件輸出表。outbox 為技術慣用單數名;
-- 寫入後僅由發布器標記 published_at,無一般更新語意,
-- 故無 updated_at/version。
CREATE TABLE :schema.outbox (
id uuid PRIMARY KEY,
aggregate_type varchar(40) NOT NULL,
aggregate_id varchar(64) NOT NULL,
event_type varchar(80) NOT NULL,
event_version integer NOT NULL,
payload jsonb NOT NULL,
trace_id varchar(40) NOT NULL,
published_at timestamptz,
retry_count integer NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_outbox_unpublished ON :schema.outbox(published_at, created_at)
WHERE published_at IS NULL;
-- lint:skip=SQL-05 理由:冪等紀錄一經寫入即不變(僅於到期後清除),
-- 無 updated_at/version 之語意。
CREATE TABLE :schema.idempotency_records (
id uuid PRIMARY KEY,
idempotency_key varchar(120) NOT NULL UNIQUE,
request_fingerprint char(64) NOT NULL,
resource_type varchar(40) NOT NULL,
resource_id varchar(64),
response_status smallint NOT NULL,
response_body jsonb NOT NULL,
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_idem_expiry ON :schema.idempotency_records(expires_at);
-- lint:skip=SQL-04,SQL-05 理由:稽核軌跡為 append-only(SAD-003 DP-04 原始紀錄不可變)。
-- audit_trail 為技術慣用單數名;寫入後不得 UPDATE/DELETE,
-- 故無 updated_at/version。
-- 註:本表之 DDL 於 v1.2 補齊——原 v1.0/v1.1 之 ER 圖已定義本表,
-- 但下方 DDL 僅列 outbox 與 idempotency_records 兩張(文字卻稱「三張表」),
-- 屬文件缺口。欄位依本節 ER 圖還原。
CREATE TABLE :schema.audit_trail (
id uuid PRIMARY KEY,
trace_id varchar(40),
occurred_at timestamptz NOT NULL DEFAULT now(),
actor_type varchar(24),
actor_id varchar(64),
actor_role varchar(32),
action varchar(96) NOT NULL,
resource_type varchar(40) NOT NULL,
resource_id varchar(64),
before_image jsonb,
after_image jsonb,
result varchar(16) NOT NULL,
source_ip varchar(45),
prev_hash varchar(64),
row_hash varchar(64)
);
CREATE INDEX idx_audit_trail_time ON :schema.audit_trail(occurred_at);
CREATE INDEX idx_audit_trail_res ON :schema.audit_trail(resource_type, resource_id);
3.16 report 報表(R3 / ACQ-4.4)——V2 新增(CR-017 裁定 D)
背景:本 schema 於 V1 基線完全無表(
SCS-017 §5原載「DDL 待補」)。
BEAM PMO 2026-09-22 裁定 C-7 含 Postgres 讀模型,本節為其設計權威。
架構(SCS-017 §5.1):Oracle CC_RPT =作業指標(權威事務資料);
Postgres rpt =讀模型(CQRS 讀側)。
DDL(Oracle CC_RPT,實體交付 V2__report_definitions_and_jobs.sql)
CREATE TABLE report_definitions (
id uuid PRIMARY KEY,
report_code varchar(40) NOT NULL UNIQUE,
report_name varchar(120) NOT NULL,
description varchar(400),
default_format varchar(8) NOT NULL DEFAULT 'CSV'
CHECK (default_format IN ('CSV','XLSX','PDF','JSON')),
status varchar(16) NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE','INACTIVE')),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE report_jobs (
id uuid PRIMARY KEY,
job_no varchar(32) NOT NULL UNIQUE,
report_code varchar(40) NOT NULL,
parameters jsonb NOT NULL DEFAULT '{}'::jsonb,
format varchar(8) NOT NULL DEFAULT 'CSV'
CHECK (format IN ('CSV','XLSX','PDF','JSON')),
status varchar(16) NOT NULL DEFAULT 'QUEUED'
CHECK (status IN ('QUEUED','RUNNING','COMPLETED','FAILED')),
row_count bigint NOT NULL DEFAULT 0,
download_url varchar(400),
error_detail varchar(400),
started_at timestamptz,
finished_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
讀模型(PostgreSQL rpt,實體交付 db/migration-postgres/V1__rpt_read_model.sql)
CREATE TABLE rpt.daily_auth_summary (
business_date date NOT NULL,
channel varchar(16) NOT NULL,
tx_type varchar(16) NOT NULL,
decision varchar(16) NOT NULL,
tx_count bigint NOT NULL DEFAULT 0,
total_amount numeric(19,0) NOT NULL DEFAULT 0,
currency char(3) NOT NULL DEFAULT 'TWD',
refreshed_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT pk_daily_auth_summary PRIMARY KEY (business_date, channel, tx_type, decision)
);
⚠️ 讀模型表刻意不含
id/version:其為彙總投影,主鍵為業務維度組合且以
upsert 覆寫——硬補通用欄位屬無意義欄位=假綠(比照tools/sql_exemptions.yaml之原則)。⚠️ CQRS 讀側之接線慣例(本服務為首例):第二資料源與 JdbcTemplate 皆須明確定義
且主側標@Primary(Boot 之@ConditionalOnSingleCandidate(DataSource.class)於多資料源時
整體退讓);Postgres 之 Flyway 不註冊 bean(由RptReadModelMigrator程式執行,
以免抑制 Boot 之 Oracle 遷移);Flyway 需資料庫專屬模組
(flyway-database-postgresql)。詳SCS-017 §5.4與CR-017 §9.2。
3.17 migration 遷移波次(R3 / SYS-2.8、SYS-3)——V2 新增(CR-017 裁定 E)
背景:本 schema 於 V1 基線完全無表(
SCS-015 §5原載「DDL 待補」)。
BEAM PMO 2026-09-22 裁定採兩層結構(WAVE+JOB)——契約之「Job(一次執行)」
與 SCS 之「Wave(波次W0–W4)」為不同粒度,兩者並存(一波次含多個作業)。
實體交付:V2__migration_waves_jobs_and_legacy_map.sql。
DDL
CREATE TABLE migration_waves (
id uuid PRIMARY KEY,
wave_code varchar(16) NOT NULL UNIQUE, -- W0–W4
wave_name varchar(120) NOT NULL,
status varchar(16) NOT NULL DEFAULT 'PENDING'
CHECK (status IN ('PENDING','RUNNING','PAUSED','COMPLETED','ROLLED_BACK','FAILED')),
scope varchar(400),
started_at timestamptz,
finished_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE migration_jobs (
id uuid PRIMARY KEY,
job_no varchar(32) NOT NULL UNIQUE,
wave_code varchar(16) NOT NULL, -- 兩層結構:掛於波次
migration_type varchar(16) NOT NULL
CHECK (migration_type IN ('ACCOUNT','CARD','BALANCE','POINTS','MERCHANT')),
source_ref varchar(64) NOT NULL,
dry_run smallint NOT NULL DEFAULT 0 CHECK (dry_run IN (0,1)),
status varchar(16) NOT NULL DEFAULT 'RUNNING'
CHECK (status IN ('RUNNING','COMPLETED','FAILED')),
processed_count bigint NOT NULL DEFAULT 0,
success_count bigint NOT NULL DEFAULT 0,
error_count bigint NOT NULL DEFAULT 0,
error_detail varchar(400),
started_at timestamptz,
finished_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
CREATE TABLE legacy_account_mappings (
id uuid PRIMARY KEY,
legacy_id varchar(32) NOT NULL UNIQUE, -- 如 LEGACY_ACCT_0001
account_no varchar(32),
migrated smallint NOT NULL DEFAULT 0 CHECK (migrated IN (0,1)),
migrated_at timestamptz,
snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
⚠️ 命名(SQL-04):對映表原名
legacy_account_map(單數)違反「表名須為複數」,
已於本輪開發中更正為legacy_account_mappings(該遷移檔僅本機套用過,未離開本機
——CR-017 §9.3 (b)有完整揭露)。⚠️ 本期語意(誠實揭露):舊系統(Legacy ECS)資料源未接入 → 作業不搬移任何
帳務資料,僅就legacy_account_mappings之待處理列計數與標記
(dryRun=true→ 僅計數、不變更);計數為對映處理數(SCS-015 §5.1)。
3.18 notification 通知(R3 / ISS-2.10)——V2 新增(CR-017 裁定 F/G)
背景:本 schema 於 V1 基線完全無表(
SCS-016 §5原載「DDL 待補」)。
BEAM PMO 2026-09-22 裁定 F:通道抽象先行(NotificationChannelPort+ 本地 Log 通道
stand-in;實際 SMS/Email/推播閘道未接——屬鐵則所稱 3 類真阻塞之一)。
實體交付:V2__notification_templates_and_notifications.sql。
DDL
CREATE TABLE notification_templates (
id uuid PRIMARY KEY,
template_code varchar(40) NOT NULL, -- 如 AUTH_APPROVED
channel varchar(16) NOT NULL
CHECK (channel IN ('SMS','EMAIL','PUSH','LINE_BC','LINE_AOA')),
language varchar(8) NOT NULL, -- 如 zh-TW
subject varchar(200), -- EMAIL 用
body_template varchar(2000) NOT NULL, -- 內文範本,變數以 {name} 佔位
variables jsonb NOT NULL DEFAULT '{}'::jsonb, -- 變數定義
enabled smallint NOT NULL DEFAULT 1 CHECK (enabled IN (0,1)),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0,
CONSTRAINT uq_notif_tpl_code_channel_lang UNIQUE (template_code, channel, language)
);
CREATE TABLE notifications (
id uuid PRIMARY KEY,
notification_no varchar(32) NOT NULL UNIQUE, -- 如 NOTIF202609000001
account_id uuid NOT NULL,
party_id uuid, -- 收件人參照(非 PII 本體)
template_code varchar(40) NOT NULL,
language varchar(8) NOT NULL DEFAULT 'zh-TW',
variables jsonb NOT NULL DEFAULT '{}'::jsonb,
preferred_channels jsonb NOT NULL DEFAULT '[]'::jsonb, -- 請求之通道偏好
channels jsonb NOT NULL DEFAULT '[]'::jsonb, -- 實際選定之通道
amount bigint,
status varchar(16) NOT NULL DEFAULT 'QUEUED'
CHECK (status IN ('QUEUED','SENT','FAILED')),
retry_count integer NOT NULL DEFAULT 0,
fail_reason varchar(400),
sent_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
version integer NOT NULL DEFAULT 0
);
⚠️ 狀態值域以契約為權威(
QUEUED/SENT/FAILED):SCS-016 §3原載設計意圖
{PENDING, SENT, FAILED, RETRYING},與契約NotificationView.status脫鉤
(PENDING≠QUEUED;RETRYING契約未定義)——已依「契約優先」更正SCS-016 §3。
重試不以獨立狀態表示:失敗時retry_count + 1且狀態維持QUEUED,
達上限方轉FAILED(見SCS-016 §7)。此設計使「重試」可達且可測,
且不需引入契約未定義之狀態(避免無對應語意之死狀態=假綠)。⚠️ 本期語意(誠實揭露):
①實際發送未接——NotificationChannelPort之唯一實作為本地 Log 通道
(LoggingNotificationChannel,stand-in),不呼叫任何簡訊/電郵/推播閘道;
②收件人聯絡方式未解析——契約NotificationRequest無收件人欄位,
且party-service契約無聯絡資訊端點(實測 4 端點皆非),故party_id僅為
參照,實際手機/電郵解析待契約與通道就緒;
③無 PII 欄位——因②,本 schema 刻意不含加密聯絡欄(避免無來源之欄位=假綠);
④多語——模板以(template_code, channel, language)為鍵,惟契約無語言欄位,
本期固定zh-TW,多語選定待契約擴充。
4. 資料字典(關鍵欄位)
| 服務 | 表 | 欄位 | 型別 | 必填 | 說明 | PII |
|---|---|---|---|---|---|---|
| party | accounts | account_no | varchar(20) | ✅ | 歸戶編號 | |
| party | accounts | block_code | varchar(16) | ✅ | 管制碼 | |
| party | parties | id_no_enc | bytea | 身分證字號(加密) | ✅ | |
| party | parties | id_no_masked | varchar(20) | ✅ | 遮罩身分證字號 | |
| card | cards | card_no_enc | bytea | ✅ | 卡號(加密) | ✅ |
| card | cards | card_no_hash | char(64) | ✅ | 卡號雜湊(比對用) | ✅ |
| card | cards | expiry_date | date | ✅ | 到期日 | |
| lim | credit_limits | available_amount | bigint | ✅ | 可用額度(分) | |
| lim | credit_limits | used_amount | bigint | ✅ | 已用額度(分) | |
| lim | limit_holds | hold_amount | bigint | ✅ | 佔用金額(分) | |
| auth | authorizations | amount | bigint | ✅ | 交易金額(分) | |
| auth | authorizations | resp_code | varchar(8) | ✅ | 回應碼 | |
| auth | authorizations | standin_flag | boolean | ✅ | 是否 Stand-in | |
| posting | transactions | idempotency_key | varchar(120) | ✅ | 冪等鍵 | |
| posting | transactions | reversal_of | uuid | 沖正之原交易 | ||
| posting | balances | aging_bucket | smallint | ✅ | 帳齡分類 | |
| payment | payments | unmatched_flag | boolean | ✅ | 三日未銷帳標記 | |
| rw | points_accounts | pending_balance | bigint | ✅ | 待確認點數(消費週期後轉 confirmed) | |
| rw | points_ledger | entry_type | varchar(16) | ✅ | EARN/CONFIRM/FORFEIT/REDEEM/EXPIRE | |
| rw | redemptions | cash_value | bigint | 兌換現金等值(分),沖抵本金 | ||
| inst | installment_plans | principal_outstanding | bigint | ✅ | 分期剩餘本金,與 balances 勾稽 | |
| inst | installment_periods | remaining_principal | bigint | 每期剩餘本金 | ||
| stmt | statements | new_balance | bigint | ✅ | 本期應付總額 | |
| stmt | statements | minimum_due | bigint | ✅ | 本期最低應繳 | |
| stmt | statement_lines | amount | bigint | ✅ | 凍結明細金額(防沖正漂移) | |
| prc | pricing_rules | calc_method | varchar(16) | ✅ | ADB/PREVIOUS_BALANCE/TWO_CYCLE/FLAT | |
| clr | clearing_batches | file_type | varchar(16) | ✅ | VCF/EMS/SETTLEMENT/CHARGEBACK/FEES | |
| clr | clearing_items | matched_tx_id | uuid | 比對回原交易(防重複請款) | ||
| dis | dispute_cases | stage | varchar(24) | ✅ | Inquiry→Presentment→Arb 階段機制 | |
| dis | dispute_cases | response_deadline | timestamptz | ✅ | 卡組織時限(逾期即敗訴) | |
| mch | merchants | risk_level | varchar(8) | ✅ | LOW/MEDIUM/HIGH 風險分級 | |
| mch | merchant_settlements | net_amount | bigint | ✅ | 淨額=總額-費用-稅 |
5. 資料完整性約束
| 約束 | 實作 |
|---|---|
| 金額非負(除沖正) |
CHECK (amount >= 0);沖正以 tx_type='REVERSAL' 允許負值 |
| 可用額度不得為負 |
CHECK (available_amount >= 0) + 條件更新 |
| 冪等鍵唯一 | UNIQUE (idempotency_key) |
| 卡號唯一 | UNIQUE (card_no_hash) |
| 參數唯一 | UNIQUE (namespace, param_key, effective_from) |
| 交易不得刪除 | 資料庫權限僅授予 INSERT/SELECT;無 DELETE
|
| 稽核不得竄改 |
audit_trail 僅 INSERT;row_hash 鏈結前一筆 |
| 外鍵 | 同 schema 內使用外鍵;跨服務不使用外鍵(以應用層保證) |
6. 索引策略
| 表 | 索引 | 用途 |
|---|---|---|
auth.authorizations |
(card_id, tx_time DESC) |
卡片授權查詢 |
auth.authorizations |
(account_id, tx_time DESC) |
歸戶授權查詢 |
auth.authorizations |
(stan) |
交易序號查詢 |
posting.transactions |
(account_id, posting_date DESC) |
帳務明細 |
posting.transactions |
(authorization_id) |
授權請款比對 |
posting.transactions |
UNIQUE (idempotency_key) |
重複入帳防範 |
lim.limit_holds |
(account_id, status) |
佔用查詢 |
lim.limit_holds |
(authorization_id) |
佔用釋放 |
card.cards |
(account_id)、(party_id)
|
卡片查詢 |
payment.payments |
(account_id, payment_date DESC) |
繳款查詢 |
7. 分區與歸檔
| 表 | 分區鍵 | 保留 | 歸檔 |
|---|---|---|---|
auth.authorizations |
tx_time(月) |
24 個月 | 歸檔庫 |
posting.transactions |
posting_date(月) |
依會計要求 | 歸檔庫 |
posting.balance_history |
account_id(雜湊) |
永久 | — |
audit.audit_trails |
created_at(月) |
依資安政策 | 歸檔庫 |
*.outbox |
— | 發布後 90 天 | 刪除(唯一允許刪除之表) |
outbox為唯一允許刪除資料之表(發布成功且超過保留期);
其餘交易類表一律不得刪除(BRD-001 C-02)。
Oracle 實作對照(v1.1)
| 情境 | PostgreSQL | Oracle |
|---|---|---|
| 月分區 |
PARTITION BY RANGE (tx_time) + 每月一個 partition |
PARTITION BY RANGE (tx_time) + INTERVAL (NUMTOYMINTERVAL(1,'MONTH')) 自動延伸。⚠️ 必須附帶至少一個初始分區,否則 ORA-00906
|
| 雜湊分區 | PARTITION BY HASH (account_id) |
PARTITION BY HASH (account_id) PARTITIONS 16(分區數須為 2 的次方以達均勻) |
| 歸檔 | 卸載 partition → 外部儲存 |
ALTER TABLE ... EXCHANGE PARTITION 換出至歸檔表,或 MOVE PARTITION 至低成本表空間 |
| 分區索引 | 預設 local | 須明寫 LOCAL;金額與時間查詢路徑一律用 local index 避免跨分區掃描 |
| Outbox 清理 | DELETE ... WHERE published_at < ... |
同語法;但建議以 partition drop 取代 delete,避免 UNDO 膨脹與高水位不降 |
7.1 Oracle 分區實測限制(2026-09-18)
於 Oracle 26ai Free(23.26.0)實測,以下兩點直接影響本節之設計:
| # | 限制 | 實測結果 | 影響 |
|---|---|---|---|
| PT-1 | INTERVAL 分區必須提供至少一個初始分區 |
無初始分區 → ORA-00906: missing left parenthesis;加上 (PARTITION p0 VALUES LESS THAN (...)) 後通過 |
產生器已自動附加初始分區 |
| PT-2 | TIMESTAMP WITH TIME ZONE 不可作為分區鍵 |
→ ORA-03001: unimplemented feature。DATE 與 TIMESTAMP(無時區)均可 |
見下方待決事項 |
⚠️ 待決事項(影響 §7 之
auth.authorizations)
本節原訂auth.authorizations以tx_time(timestamptz)作月分區,
但依 PT-2,Oracle 無法以含時區之時間欄位分區,且該表無 DATE 欄位可替代。
目前 Flyway 基線(V1__baseline.sql)未對該表套用分區,並於檔內留註待決。候選方案(須經 SA 核定後修訂本節):
- 新增
tx_date DATE NOT NULL業務日期欄位,以之為分區鍵(維持月分區能力;需應用層於寫入時帶入)- 放棄分區,改以
(card_id, tx_time DESC)等索引支撐查詢- 將
tx_time改存為不含時區之 UTCTIMESTAMP(與 DP-07 衝突,不建議)
posting.transactions(分區鍵posting_date為date)不受影響,已成功建立月分區。
8. 需求 ↔ Schema 追溯(回答「欄位如何支撐業務」)
本節以 BRD-004 之 17 項 POC 為主軸,逐項列出支撐該場景的資料表與關鍵欄位,
證明本 Schema 之欄位粒度足以支撐全部業務功能,Coder 不需自行腦補欄位。
| POC | 場景 | 主要資料表 | 關鍵欄位(支撐點) |
|---|---|---|---|
| POC-01 | 授權主流程(本行卡) |
auth.authorizations, lim.credit_limits, lim.limit_holds
|
decision, resp_code, hold_amount, standin_flag, available_amount(條件更新) |
| POC-02 | 授權分流(他行/國外卡) |
auth.authorizations, card.cards
|
channel, card_no_hash, bin, tx_type
|
| POC-03 | Stand-in 代授權 | auth.authorizations |
standin_flag, entry_mode, auth_check_results.result
|
| POC-04 | 請款入帳與差異比對 |
posting.transactions, posting.sub_account_entries, posting.balances
|
authorization_id, idempotency_key, before_amount/after_amount, total_amount
|
| POC-05 | 餘額即時更新(CQRS) |
posting.balances, posting.balance_history
|
principal_amount/interest_amount/fee_amount, aging_bucket, as_of_date 快照 |
| POC-06 | 帳單產生與繳款沖抵 |
stmt.statements, stmt.statement_lines, payment.payments
|
new_balance/minimum_due, prev_balance/payments_credits, allocation_order, limit_restored
|
| POC-07 | 分期交易 |
inst.installment_plans, inst.installment_periods, inst.installment_products
|
principal_outstanding, per_period_amount, remaining_periods, annual_rate
|
| POC-08 | 點數累積與兌換 |
rw.points_accounts, rw.points_ledger, rw.redemptions
|
pending/confirmed_balance, entry_type, cash_value, next_expiry_points
|
| POC-09 | 爭議帳款處理 |
dis.dispute_cases, dis.dispute_timelines
|
stage, response_deadline, representment_count, liability, evidence_*
|
| POC-10 | 清算結算 |
clr.clearing_batches, clr.clearing_items, clr.settlements
|
file_type, matched_tx_id, interchange_fee/network_fee, net_amount
|
| POC-11 | 卡片生命週期 |
card.cards, card.card_applications, card.tokens
|
block_code, status, expiry_date, token_no, instant_issued
|
| POC-12 | 額度管理與調整 |
lim.credit_limits, lim.special_limits, lim.limit_adjustments
|
permanent/temporary_amount, shadow_amount, purpose, before/after_amount
|
| POC-13 | 產品參數化(P0–P3) |
product.products, product.parameters, product.rule_sets
|
param_value(jsonb), rule_type, condition/action, effective_from/to
|
| POC-14 | 多層次帳戶結構 |
party.accounts, party.parties
|
account_no/cif_no, party_type, relation_type, flags(jsonb)
|
| POC-15 | 遷移波次與回退 |
auth.authorizations, posting.transactions(版本欄位) |
version(樂觀鎖)、idempotency_key、created_at 時序 |
| POC-16 | 安全/個資保護 | 全表 id_no_enc/id_no_masked, card_no_enc/card_no_hash, audit_trail
|
加密欄 + 遮罩欄 + before/after_image + row_hash 鏈 |
| POC-17 | 監控與可觀測 |
:schema.outbox, :schema.idempotency_records, audit_trail
|
trace_id, published_at, response_status, aggregate_type/id
|
結論:全 17 項 POC 與 68 項 FR 皆能被本 Schema 之表與欄位直接支撐;
欄位設計已含「加密/遮罩雙軌」「樂觀鎖 version」「冪等鍵」「JSON 彈性參數」等
支撐關鍵非功能需求(NFR-SEC / NFR-REL)的結構,Coder 實作時不得自行新增表或欄位,
若確有缺口須提变更申請(Change Request)經 SA 核定後由本文件統一納管。
9. 總表與統計
| 項目 | 數量 |
|---|---|
| Bounded Context(schema) | 17(party/card/product/lim/auth/posting/payment/rw/inst/stmt/prc/clr/dis/mch/platform + rpt/mig/notif) |
| 實體表(含 DDL) | 55 |
| 通用平台表(每服務各一份) | outbox / idempotency_records / audit_trail |
| 跨服務外鍵 | 0(以應用層保證,避免分散式交易) |
| 金額欄位統一單位 | 最小單位(分):Oracle NUMBER(19,0)/PostgreSQL bigint
|
⚠️ 統計方法與前版更正(2026-09-22):上列數字為逐項清點之結果——
§3.1–§3.15 之CREATE TABLE區塊共 45(含 §3.15 平台表 3),
+CR-016以文字表格描述之CLEARING_RUNS/CLEARING_RECONCILIATIONS2,
+CR-017裁定 D 之REPORT_DEFINITIONS/REPORT_JOBS/rpt.daily_auth_summary3,
+裁定 E 之MIGRATION_WAVES/MIGRATION_JOBS/LEGACY_ACCOUNT_MAPPINGS3,
+裁定 F 之NOTIFICATION_TEMPLATES/NOTIFICATIONS2 = 55。
前版(1.12)之「14」與「47」已過時——47係1.9版(含CR-016之 2 表)之數字,
其後 §3.16/§3.17 新增 6 表卻未同步更新,本次一併更正。
schema 命名對照:本表所列 schema 為邏輯名稱(小寫)。於 Oracle 中,
未加引號之識別碼將轉為大寫(auth→AUTH),詳見§1.3陷阱 T-01;
platformschema 於 Oracle 對應CC_PLT(Saga 狀態、Outbox、稽核軌跡)。
10. 修訂記錄
| 版本 | 日期 | 修訂者 | 說明 |
|---|---|---|---|
| 1.16 | 2026-10-02 | WorkBuddy AI |
CR-035 §2.1/§2.2(#A-154):§3.4 補「專用額度池」之欄位——新增「變更交付說明(special_limits 之『池』與用途範圍)」,載明 V7__special_limit_pool.sql 之四項:special_limits.scope_ref(MCC 用途範圍,逗號分隔;NULL = 不限定)、limit_adjustments.scope_ref(稽核軌跡同步)、limit_holds.special_limit_id(池歸屬)、索引。併載語意:專用池之佔用完全不動 account_limits → BR-04-01 為結構性成立。既有表結構未變(V1 仍凍結) |
| 1.15 | 2026-10-02 | WorkBuddy AI |
CR-014 v1.1(#A-153):§3.4 special_limits 補 currency 與唯一性約束——①ER 圖補上漏列之 char currency;②新增「變更交付說明(special_limits)」區塊,載明 V6__special_limits_currency_and_index.sql 之三項內容(ADD currency + 回填 + MODIFY NOT NULL;idx_special_limits_account;uk_special_limits_active)與理由;③★ 明示本節 SQL 區塊不得改動——該區塊即 V1__baseline.sql 之產生器來源,一改將造成 checksum 失配、服務起不來。既有表結構未變(V1 仍凍結)。⚠️ 併補 1.14 之漏列(見下) |
| 1.14 | 2026-09-22 | WorkBuddy AI |
CR-020 F1:card.card_applications 新增 card_id(V7__card_application_card_id.sql)——發卡後回填,使卡片可追溯其來源申請案件;開卡(ACTIVATE)時以此反查核准序號驗證三要素。⚠️ 本列為 2026-10-02 補記:前版已於檔頭升為 1.14 並載明此變更,惟修訂記錄漏列(檔頭與表格不一致) |
| 1.13 | 2026-09-22 | WorkBuddy AI |
CR-017 裁定 F/G(已執行):新增 §3.18 notification schema——NOTIFICATION_TEMPLATES((template_code, channel, language) 唯一鍵;通道 5 值)+NOTIFICATIONS(status 3 值、retry_count、fail_reason)。併載明四項誠實揭露:①實際發送未接(唯一實作為本地 Log 通道 stand-in);②收件人聯絡方式未解析(契約無收件人欄、party-service 無聯絡端點)→③故刻意不含加密聯絡欄(避免無來源之欄位=假綠);④多語以 (code, channel, language) 為鍵但契約無語言欄位,本期固定 zh-TW。併更正 §9 統計(14/47 → 17/55;前版未隨 §3.16/§3.17 更新) |
| 1.12 | 2026-09-22 | WorkBuddy AI |
CR-017 裁定 E(已執行):新增 §3.17 migration schema——兩層結構:MIGRATION_WAVES(波次 W0–W4,狀態 6 值)+MIGRATION_JOBS(型別 5 值/狀態 3 值,皆對齊契約)+LEGACY_ACCOUNT_MAPPINGS(舊帳對映,含 SNAPSHOT JSON)。併載明命名更正(SQL-04 複數)與本期語意(資料源未接入 → 不搬移帳務資料) |
| 1.11 | 2026-09-22 | WorkBuddy AI |
CR-017 裁定 D(已執行):新增 §3.16 report schema——①Oracle CC_RPT:REPORT_DEFINITIONS/REPORT_JOBS(V2);②PostgreSQL rpt.daily_auth_summary(讀模型,CQRS 讀側首例,獨立 Flyway db/migration-postgres);③載明讀模型表刻意不含 id/version 之理由與讀側接線慣例(雙資料源須明確標 @Primary、Flyway 不註冊 bean、需資料庫專屬模組)。⚠️ 本節為新 schema,非既有表之補充 |
| 1.10 | 2026-09-22 | WorkBuddy AI |
CR-017(已核定/已執行):dis schema 爭議雙軸拆分——§3.13 補充區新增:①**DISPUTE_CASES.DISPUTE_REASON(爭議原因 4 值)與 DISPUTE_TYPE(程序類型)為正交兩軸**(原契約 disputeType 實指原因,而 DDL 之 dispute_type 混用兩者,交集僅 DUPLICATE);②**WORKFLOW_STAGE(本行作業階段 7 值)與 STAGE(卡組織程序階段,SDD-005 §7.1)為正交兩軸**(交集僅 ARBITRATION/CLOSED);③CHARGEBACK_TYPE/CHARGEBACK_REASON;④dispute_type 之 CHECK 具名化(值域不變)。ER 圖未動(循 V2 補充區之同一慣例) |
| 1.9 | 2026-09-22 | WorkBuddy AI |
CR-016(已核定/已執行):clr schema 補齊契約所需資源——§3.12 補充區新增:①**CLEARING_RUNS**(清算執行;一次執行涵蓋多批次檔,與 CLEARING_BATCHES 為一對多)與 CLEARING_RECONCILIATIONS(對帳結果,依執行 × 組織,difference_type 3 值)——此二者為契約之核心資源而 V1 完全不存在;②CLEARING_BATCHES.ORG 值域擴充為 8 值(增 DISCOVER/DINERS,保留 DFS 供相容)並重建為具名約束;③SETTLEMENTS.DIRECTION(契約必要欄位)。ER 圖未動(循 V2 補充區之同一慣例) |
| 1.8 | 2026-09-22 | WorkBuddy AI |
CR-015(已核定/已執行):mch schema 雙軸拆分與補欄——§3.14 補充區新增:①**MERCHANTS.MERCHANT_FORM(商店型態 SINGLE/FRANCHISE/CHAIN/GROUP)與 MERCHANT_TYPE(業別)為正交兩軸**(契約原以單一 merchantType 承載,且與 DDL 業別值域無交集);②PARENT_MERCHANT_ID(集團店上層,自參考)、ACCEPTED_CARD_TYPES(JSON)、MERCHANT_SETTLEMENTS.BATCH_NO/EXCEPTION_REASON;③兩狀態值域擴充並重建為具名約束(CK_MERCHANTS_STATUS 補 APPLIED/CLOSED;CK_MERCHANT_SETTLEMENTS_STATUS 併入契約之 SUBMITTED/VERIFIED/EXCEPTION/PAID)。ER 圖未動(循 V2 補充區之同一慣例) |
| 1.7 | 2026-09-22 | WorkBuddy AI |
C-31:PRICING_RULES.tx_type(交易型別,V2__pricing_rules_tx_type.sql)——§3.11 補充區新增:tx_type IN ('GENERAL','CASH_ADVANCE')、可為 null(僅 INTEREST 規則具值,費用類規則不硬塞值);既有 INTEREST 列依 TIER 回填。定性:原「以 TIER(客層級距)暫代計息子型」屬本地慣例,本欄位增設後 TIER 回歸本義(SCS-006 §7.5、CR-005 §8 #1 結案)。真機 e2e:清空 TX_TYPE → CC-PRC-1001、還原 → 20% 恢復(證明讀取權威來源且非快取) |
| 1.6 | 2026-09-22 | WorkBuddy AI |
CR-012(已核定/已執行):REDEMPTIONS 雙欄位——§3.8 補充區新增 settlement_method(折抵方式,V4__redemptions_settlement_method.sql;CHECK IN ('POINTS_ONLY','POINTS_PLUS_CASH','ONLINE_REDEEM','MERGE','EXCHANGE_CENTER')、DEFAULT 'POINTS_ONLY' NOT NULL)。定性:redemption_type(標的類別)與 settlement_method(折抵方式)為正交兩維度,「點數+現金換哩程」兩者皆需,不可合併為一欄。ER 圖未動(循 V2 補充區之同一慣例) |
| 1.5 | 2026-09-21 | WorkBuddy AI |
HANDOVER #C-18:V2+ 遷移落差回寫(跨服務,共 8 項)——以腳本客觀比對「V2/V3 遷移實際新增之表/欄」與「本檔是否已記載」,補上:①**§3.1 pty_idempotency(V2;服務內部狀態,不入 ER 圖,為 §3.15 IDEMPOTENCY_RECORD 於 party schema 之實作);②§3.2 card_idempotency(V2,同上);③§3.4 account_limits(V2,帳戶 × 幣別之信用額度主檔,CR-001)與 limit_adjustments(V3,含 adjust_type/status 二組 CHECK 值域);④§3.5 card_status_caches/account_limit_caches(V2 讀模型,權威仍在 card/limit,不入 ER 圖)與 authorizations.hold_id(V3,關聯 lim.limit_holds 使授權與佔額可對照);⑤§3.7 payments.reversal_reason**(V3,沖正原因之稽核留痕),ER 圖與 DDL 皆已同步。⚠️ 更正 CR-003 §6 #3 之不實記載:該處稱「本次僅加註 §3.7/§3.8/§3.10 三處」,實測 reversal_reason 於本檔全文出現 0 次——§3.7 並未回寫(§3.8/§3.10 確已完成),已於 CR-003 修訂記錄更正。⚠️ 盤點方法:勿以「章節是否含該名稱」判缺——lim.limit_holds 記載於 §3.4(正確位置),按 §3.5 查會誤判為缺。validate_mermaid.py docs 仍為 115 區塊 0 錯誤
|
| 1.4 | 2026-09-20 | WorkBuddy AI | §3.2 CARD 實體新增 ticket_type(票證種類:EASYCARD/IPASS/ICASH,可為 NULL)——契約 CreateCardRequest.ticketType 原有此欄位而本模型漏列,致 cards 表無處可存(SCS-003 §10 #14)。依 SAD-003 §9 不改 V1 基線,另以 V3__card_ticket_type.sql 追加 |
| 1.0 | 2026-09-17 | SD / DA | 初版 |
| 1.1 | 2026-09-18 | SD / DA | 依客戶指定「Oracle 為主」補入實作方言對照,DDL 內容與 ER 圖未變:①§1 增列方言聲明,明訂 DDL 為邏輯模型記法、Flyway 遷移腳本須以 Oracle 方言撰寫;②新增 §1.3 資料型別方言對照(uuid→RAW(16)、timestamptz→TIMESTAMP(6) WITH TIME ZONE、bigint→NUMBER(19,0)、jsonb→JSON 等)與常見語法對照;③新增 Oracle 特有陷阱 T-01~T-08(識別碼大小寫、空字串等於 NULL、DATE 含時間、DDL 隱式提交、隔離等級、隱式型別轉換、RESOURCE 保留字、JSON/RAW(16) 可用性);④§7 補 Oracle 分區/歸檔實作對照;⑤§8 用語由 jsonb 改為方言中立之「JSON」;⑥§9 補 schema 命名大小寫對照與 platform → CC_PLT 之映射 |
| 1.2 | 2026-09-18 | SD / DA |
解決 §1.1「所有表皆有通用欄位」與本文件自身 DDL 之矛盾(以 validate_sql.py 掃描本文件抽出之 DDL,命中 13 處):①§1.1 標題由「所有表皆有」改為「原則上所有表皆有」,新增 §1.1.1 例外條款與豁免機制,明訂 append-only 表、自然主鍵註冊表、技術慣用單數名三類例外,並要求逐表明示豁免且理由具體;②於 7 張表之 DDL 前加註 -- lint:skip=... 豁免指令與理由:party.block_code_history、card.card_no_pool、card.issued_card_numbers、posting.balance_history、rw.points_ledger、:schema.outbox、:schema.idempotency_records;③補齊 §3.15 之 audit_trail DDL——該表原僅見於 ER 圖,DDL 僅列 outbox 與 idempotency_records 兩張(文字卻稱「三張表」),屬文件缺口,已依 ER 圖還原欄位。既有 DDL 結構與 ER 圖未變
|
| 1.3 | 2026-09-19 | SD / DA | 依 W-1(KeyProvider 推廣) 補齊 §3.7 payment 之 AUTO_DEBIT_ENROLLMENTS:①ER 圖補上漏列之 debit_account_masked 與 idempotency_key;②DDL 區塊補入本表——原 §3.7 之 DDL 完全未列本表(僅 ER 圖有),而 ER 圖卻列了實作尚未建立之 debit_account_enc,屬文件與實物雙向不一致;③新增「變更交付說明」,載明本表係由 V2(建表)與 V5(新增 debit_account_enc)兩支遷移交付,並說明兩欄並存之理由(遮蔽值供顯示、密文供扣款)。既有表結構未變;V1__baseline.sql 仍凍結 |