Project

General

Profile

CR #201 » SDD-001_領域模型與ER-Diagram.md

內容更新:內部稱謂去識別化(2026-10-18) - Redmine Admin, 10/18/2026 05:32 AM

 

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.12.13、AUT-1.11.3、ACQ-4.14.6、RWD-5.15.3、SYS-2.1、COM-6.1~6.2

§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_records DDL 相牴觸
(該二表為 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 實作
——後者為平台層之邏輯模型,前者為 party schema 之實體表。
⚠️ 不含 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.15 IDEMPOTENCY_RECORD 於 card schema 之實體實作。
⚠️ 無 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|SHADOW
  • status ∈ 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.sql auth.authorizations 新增 hold_id
V10__authorization_entry_emv.sql auth.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_seq
V5__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(欄) 兌換為扣點之財務操作,重送不得重複扣點 UNIQUE
rw.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 NULL
V1 基線已凍結,不得修改(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_NO UNIQUE;ORGS(逗號分隔)
CLEARING_RECONCILIATIONS(新表) 對帳結果——依「執行 × 組織」記載差異;契約 GET /clearing/reconciliations/{runId} 所繫 difference_type IN ('TIMING','FX_RATE','SUBSTANTIVE');FK(RUN_ID) REFERENCES CLEARING_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 核定後修訂本節):

  1. 新增 tx_date DATE NOT NULL 業務日期欄位,以之為分區鍵(維持月分區能力;需應用層於寫入時帶入)
  2. 放棄分區,改以 (card_id, tx_time DESC) 等索引支撐查詢
  3. 將 tx_time 改存為不含時區之 UTC TIMESTAMP(與 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_RECONCILIATIONS 2,
+CR-017 裁定 D 之 REPORT_DEFINITIONS/REPORT_JOBS/rpt.daily_auth_summary 3,
+裁定 E 之 MIGRATION_WAVES/MIGRATION_JOBS/LEGACY_ACCOUNT_MAPPINGS 3,
+裁定 F 之 NOTIFICATION_TEMPLATES/NOTIFICATIONS 2 = 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;
platform schema 於 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 仍凍結
(32-32/67)