第13巻 DBスキーマ定義(2) ― トレーサビリティ・清算決済・継続収納

本巻は docs/specs/31_schema.md のトレーサビリティ・着金フィルタ・HTLC Auth〜継続収納までのZCテーブル群を収める。前巻→第12巻の続き、続きは→第14巻。


ZC テーブル(トレーサビリティ・着金フィルタ・HTLC Auth)

TxEventLog(ZC側 詳細処理イベントログ:INSERT ONLY)

FinalityLogが状態遷移イベントを記録するのに対し、TxEventLogはZC↔Bank間の呼び出し結果・フィルタ評価・処理時間を記録する。

CREATE TABLE TxEventLog (
  log_id        TEXT    PRIMARY KEY,             -- UUID
  txid          TEXT,                            -- 関連取引ID(NULL可)
  correlation_id TEXT,                           -- ZC→Bank 横断追跡ID
  actor         TEXT    NOT NULL,                -- 'ZC'|'BANK_{bankId}'|'CUSTOMER'|'SYSTEM'
  action        TEXT    NOT NULL,                -- アクション名
  status        TEXT    NOT NULL,                -- 'OK'|'NG'|'PENDING'
  reason_code   TEXT,                            -- NGの場合の理由コード
  amount        INTEGER,                         -- 関連金額(円)
  bank_id       TEXT,                            -- 関連銀行ID
  account_id    TEXT,                            -- 関連口座(マスク済み可)
  details_json  TEXT,                            -- 追加コンテキスト JSON
  duration_ms   INTEGER,                         -- 処理時間(ミリ秒)
  occurred_at   TEXT    NOT NULL                 -- RFC3339
);
CREATE INDEX idx_evtlog_txid     ON TxEventLog(txid);
CREATE INDEX idx_evtlog_occurred ON TxEventLog(occurred_at);
CREATE INDEX idx_evtlog_actor    ON TxEventLog(actor, action, occurred_at);
CREATE INDEX idx_evtlog_status   ON TxEventLog(status, occurred_at);

action 定数一覧:

  • ZC側: PAYMENT_INITIATED, PRE_CHECK, H_RESERVE, H_LOCK, H_RELEASE, DECIDE_SETTLE, DECIDE_CANCEL, PAYER_EXEC_CONFIRMED, PAYEE_EXEC_CONFIRMED, SETTLED, SUSPENDED, CANCELLED
  • Bank呼出: RESERVE_FUNDS, EXECUTE_DEBIT, EXECUTE_CREDIT, RELEASE_RESERVE, AUTHORITY_CHECK, NAME_CHECK, LEG_READY_CHECK
  • Filter: FILTER_EVALUATED, FILTER_REJECTED, FILTER_PENDING
  • HTLC Auth: HTLC_AUTH_REQUESTED, HTLC_AUTH_APPROVED, HTLC_AUTH_DECLINED, HTLC_CAPTURE, HTLC_VOID

HtlcAuthWhitelist(HTLC受取側起点ロック ホワイトリスト)

CREATE TABLE HtlcAuthWhitelist (
  whitelist_id          TEXT    PRIMARY KEY,     -- UUID
  payee_bank_id         TEXT    NOT NULL,        -- 加盟店の銀行ID
  payee_account_hash    TEXT    NOT NULL,        -- 加盟店の口座ハッシュ
  allowed_payer_bank_id TEXT,                    -- NULL=全銀行からのオーソリOK
  max_amount            INTEGER,                 -- NULL=金額制限なし(円)
  allowed_purposes      TEXT,                    -- JSON配列 ['MERCHANT'] NULL=全目的OK
  description           TEXT,                    -- 加盟店名・端末説明
  is_active             INTEGER NOT NULL DEFAULT 1,
  registered_at         TEXT    NOT NULL,
  expires_at            TEXT,                    -- NULL=無期限
  -- 対象者該当性アテステーション
  -- 設定時、createAuthRequest() は当該テンプレートに対する署名付き PASS
  -- アテステーション(HtlcAuthRequestInput.eligibility_attestation)を要求する
  eligibility_template_id TEXT                   -- FK → ConditionTemplate. NULL=要件なし
);
CREATE INDEX idx_whitelist_payee ON HtlcAuthWhitelist(payee_bank_id, payee_account_hash, is_active);

HtlcAuthRequests(HTLC受取側起点オーソリリクエスト)

カードのオーソリ(authorize → capture/void)に相当。受取側(加盟店)が起点となり、送金側(顧客)の承認を得てHTLCロックを確立する。

CREATE TABLE HtlcAuthRequests (
  auth_id              TEXT    PRIMARY KEY,      -- UUID
  htlc_id              TEXT,                     -- 承認後に生成されるHTLC ID
  txid                 TEXT,                     -- 承認後に生成されるtxid
  status               TEXT    NOT NULL DEFAULT 'AUTH_REQUESTED',
  -- 'AUTH_REQUESTED' : オーソリリクエスト送信済み、送金側未承認
  -- 'AUTH_APPROVED'  : 送金側承認済み、HTLCロック確立
  -- 'AUTH_DECLINED'  : 送金側拒否
  -- 'CAPTURED'       : 受取側がキャプチャ(決済完了)
  -- 'VOIDED'         : 受取側がボイド(取消)
  -- 'EXPIRED'        : 有効期限切れ
  payee_bank_id        TEXT    NOT NULL,
  payee_account_hash   TEXT    NOT NULL,
  payer_bank_id        TEXT    NOT NULL,
  payer_account_hash   TEXT    NOT NULL,
  amount_value         INTEGER NOT NULL,
  purpose              TEXT,
  description          TEXT,                     -- 商品・サービス説明
  auth_expires_at      TEXT    NOT NULL,         -- 送金側が承認する期限
  capture_expires_at   TEXT    NOT NULL,         -- 受取側がキャプチャする期限
  vault_ref            TEXT,                     -- Vault に保管した preimage への参照
  hashlock             TEXT,                     -- SHA256(preimage)(承認後に設定)
  whitelist_id         TEXT    NOT NULL,         -- FK → HtlcAuthWhitelist
  approved_at          TEXT,
  captured_at          TEXT,
  voided_at            TEXT,
  decline_reason       TEXT,
  idempotency_key      TEXT    NOT NULL UNIQUE,
  version              INTEGER NOT NULL DEFAULT 0,
  created_at           TEXT    NOT NULL,
  updated_at           TEXT    NOT NULL,
  -- whitelist.eligibility_template_id が設定されている場合に
  -- createAuthRequest() が記録する検証済みアテステーションのID
  eligibility_attestation_id TEXT,  -- FK → Attestation. NULL=要件なし/未検証
  FOREIGN KEY (whitelist_id) REFERENCES HtlcAuthWhitelist(whitelist_id)
);
CREATE INDEX idx_authreq_payer  ON HtlcAuthRequests(payer_bank_id, status);
CREATE INDEX idx_authreq_payee  ON HtlcAuthRequests(payee_bank_id, payee_account_hash, status);
CREATE INDEX idx_authreq_htlc   ON HtlcAuthRequests(htlc_id);

ZC テーブル(清算・決済機能)

IgsRequests(IGS連携 — 日銀ネット即時グロス清算)

CREATE TABLE IgsRequests (
  ext_instruction_id  TEXT PRIMARY KEY,
  txid                TEXT NOT NULL,
  payer_bank_id       TEXT NOT NULL,
  payee_bank_id       TEXT NOT NULL,
  amount_value        INTEGER NOT NULL,
  amount_currency     TEXT NOT NULL DEFAULT 'JPY',
  status              TEXT NOT NULL DEFAULT 'REQUESTED', -- REQUESTED|SETTLED|FAILED|HOLD|TIMEOUT
  boj_settle_ref      TEXT,
  requested_at        TEXT NOT NULL,
  settled_at          TEXT,
  failed_reason       TEXT,
  retry_count         INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX idx_igs_txid   ON IgsRequests(txid);
CREATE INDEX idx_igs_status ON IgsRequests(status);

AccountVerifications(事前口座確認)

CREATE TABLE AccountVerifications (
  verification_id     TEXT PRIMARY KEY,
  request_bank_id     TEXT NOT NULL,
  target_bank_id      TEXT NOT NULL,
  target_account_hash TEXT NOT NULL,
  target_account_name TEXT,
  status              TEXT NOT NULL DEFAULT 'PENDING', -- PENDING|MATCHED|UNMATCHED|NOT_FOUND|ERROR|EXPIRED
  name_provided       TEXT,
  match_score         REAL,
  fraud_warning       INTEGER NOT NULL DEFAULT 0,
  cached_until        TEXT,
  idempotency_key     TEXT UNIQUE,
  created_at          TEXT NOT NULL,
  responded_at        TEXT
);
CREATE INDEX idx_av_target ON AccountVerifications(target_bank_id, target_account_hash);
CREATE INDEX idx_av_status ON AccountVerifications(status);

CreditNotifications(入金結果通知)

CREATE TABLE CreditNotifications (
  notification_id     TEXT PRIMARY KEY,
  txid                TEXT NOT NULL,
  payee_bank_id       TEXT NOT NULL,
  payee_account_hash  TEXT NOT NULL,
  amount_value        INTEGER NOT NULL,
  amount_currency     TEXT NOT NULL DEFAULT 'JPY',
  payer_bank_id       TEXT NOT NULL,
  payer_name_masked   TEXT,
  purpose             TEXT,
  edi_summary         TEXT,
  status              TEXT NOT NULL DEFAULT 'PENDING', -- NotificationStatus: PENDING|RETRY|DELIVERED|FAILED
  delivery_attempts   INTEGER NOT NULL DEFAULT 0,
  max_attempts        INTEGER NOT NULL DEFAULT 5,
  created_at          TEXT NOT NULL,
  delivered_at        TEXT,
  next_retry_at       TEXT
);
CREATE INDEX idx_cn_payee ON CreditNotifications(payee_bank_id, status);
CREATE INDEX idx_cn_txid  ON CreditNotifications(txid);

EdiRecords(ZEDI統合 — 全銀EDIリッチデータ)

CREATE TABLE EdiRecords (
  edi_ref             TEXT PRIMARY KEY,
  txid                TEXT,
  format_version      TEXT NOT NULL DEFAULT '1.0',
  invoice_number      TEXT,
  invoice_date        TEXT,
  payment_due_date    TEXT,
  tax_amount          INTEGER,
  tax_rate            REAL,
  discount_amount     INTEGER,
  note                TEXT,
  sender_ref          TEXT,
  receiver_ref        TEXT,
  line_items_json     TEXT,                        -- JSON配列
  created_by_bank_id  TEXT NOT NULL,
  created_at          TEXT NOT NULL
);
CREATE INDEX idx_edi_txid    ON EdiRecords(txid);
CREATE INDEX idx_edi_invoice ON EdiRecords(invoice_number);

ProxyDirectory(エイリアス送金)

CREATE TABLE ProxyDirectory (
  proxy_id            TEXT PRIMARY KEY,
  proxy_type          TEXT NOT NULL,               -- PHONE|EMAIL|NATIONAL_ID
  proxy_value         TEXT NOT NULL,
  bank_id             TEXT NOT NULL,
  account_id          TEXT NOT NULL,
  account_holder_name TEXT NOT NULL,
  is_active           INTEGER NOT NULL DEFAULT 1,
  registered_at       TEXT NOT NULL,
  updated_at          TEXT NOT NULL,
  UNIQUE(proxy_type, proxy_value)
);
CREATE INDEX idx_proxy_lookup ON ProxyDirectory(proxy_type, proxy_value, is_active);
CREATE INDEX idx_proxy_bank   ON ProxyDirectory(bank_id, account_id);

QrCodes(QRコード送金)

CREATE TABLE QrCodes (
  qr_ref              TEXT PRIMARY KEY,
  qr_type             TEXT NOT NULL,               -- STATIC|DYNAMIC
  payee_bank_id       TEXT NOT NULL,
  payee_account_id    TEXT NOT NULL,
  payee_name          TEXT NOT NULL,
  amount_value        INTEGER,                     -- NULL=任意額(Static QR)
  amount_currency     TEXT NOT NULL DEFAULT 'JPY',
  purpose             TEXT,
  edi_ref             TEXT,
  signature           TEXT NOT NULL,               -- HMAC署名
  is_used             INTEGER NOT NULL DEFAULT 0,   -- DYNAMIC は単一使用(消費はCAS)
  expires_at          TEXT,
  created_at          TEXT NOT NULL
);
CREATE INDEX idx_qr_payee ON QrCodes(payee_bank_id);

規範(DYNAMIC QR の単一使用):DYNAMIC QR の消費は UPDATE QrCodes SET is_used = 1 WHERE qr_ref = ? AND is_used = 0 の CAS で行い、changes() == 0

の敗者は QR_ALREADY_USED で拒否する。読取り時の is_used チェックだけでは、

読取り(ガード)と書込み(消費)が別文のため同一 QR への並行決済が双方ともガードを

通過し(TOCTOU)二重使用が成立しうる。単一使用を担保するのは行述語 is_used = 0

である(実装 src/zc/directory/qr.ts#processQrPayment、回帰 test/zc/qr.test.ts)。

RichDataStore(リッチデータストレージ)

CREATE TABLE RichDataStore (
  data_ref            TEXT PRIMARY KEY,
  data_type           TEXT NOT NULL,               -- RichDataType: EDI|INVOICE|ATTACHMENT_META|REMITTANCE
  txid                TEXT,
  content_json        TEXT NOT NULL,
  content_hash        TEXT NOT NULL,
  r2_key              TEXT,                        -- R2バケットキー(大容量データ用)
  created_by_bank_id  TEXT NOT NULL,
  retention_days      INTEGER NOT NULL DEFAULT 2555, -- 約7年
  created_at          TEXT NOT NULL,
  expires_at          TEXT
);
CREATE INDEX idx_rds_txid ON RichDataStore(txid);
CREATE INDEX idx_rds_type ON RichDataStore(data_type);

CrossBorderTransactions(クロスボーダー送金)

CREATE TABLE CrossBorderTransactions (
  cb_txid             TEXT PRIMARY KEY,
  domestic_txid       TEXT,                        -- FK → Transactions
  direction           TEXT NOT NULL,               -- OUTBOUND|INBOUND
  foreign_fps_id      TEXT NOT NULL,               -- 外国FPS識別子
  foreign_bank_bic    TEXT NOT NULL,               -- 相手行BIC
  foreign_account_id  TEXT NOT NULL,
  foreign_currency    TEXT NOT NULL,
  foreign_amount      INTEGER NOT NULL,
  exchange_rate       REAL,
  domestic_amount     INTEGER NOT NULL,
  status              TEXT NOT NULL DEFAULT 'INITIATED', -- INITIATED|ROUTED|FOREIGN_ACCEPTED|SETTLED|FAILED|RETURNED
  settlement_bank_id  TEXT,
  nostro_account_ref  TEXT,
  fatf_data_json      TEXT NOT NULL,               -- FATF R16準拠送金人・受取人データ
  created_at          TEXT NOT NULL,
  updated_at          TEXT NOT NULL
);
CREATE INDEX idx_cb_domestic ON CrossBorderTransactions(domestic_txid);
CREATE INDEX idx_cb_status   ON CrossBorderTransactions(status);

EventStream(双方向通信 — SSEイベントキュー)

CREATE TABLE EventStream (
  event_id            TEXT PRIMARY KEY,
  target_bank_id      TEXT NOT NULL,
  event_type          TEXT NOT NULL,
  payload_json        TEXT NOT NULL,
  is_delivered        INTEGER NOT NULL DEFAULT 0,
  created_at          TEXT NOT NULL
);
CREATE INDEX idx_es_bank ON EventStream(target_bank_id, is_delivered, created_at);

ZC テーブル(Circuit Breaker / Reversal)

CircuitBreakerState(参加行疎通監視)

参加行ごとのサーキットブレーカー状態と運用観測メトリクスを保持する。
状態遷移は CLOSED → OPEN → HALF_OPEN → CLOSED の標準パターン。

CREATE TABLE CircuitBreakerState (
  bank_id               TEXT PRIMARY KEY,
  state                 TEXT NOT NULL DEFAULT 'CLOSED',  -- CLOSED|OPEN|HALF_OPEN
  consecutive_failures  INTEGER NOT NULL DEFAULT 0,
  last_failure_at       TEXT,
  opened_at             TEXT,
  half_open_at          TEXT,
  updated_at            TEXT NOT NULL,
  -- 観測メトリクス
  total_requests        INTEGER NOT NULL DEFAULT 0,      -- 累計呼び出し数
  total_successes       INTEGER NOT NULL DEFAULT 0,      -- 累計成功
  total_failures        INTEGER NOT NULL DEFAULT 0,      -- 累計失敗
  total_denied          INTEGER NOT NULL DEFAULT 0,      -- OPEN 状態で拒否した数
  half_open_inflight    INTEGER NOT NULL DEFAULT 0,      -- HALF_OPEN 中の進行中呼び出し
  last_success_at       TEXT                             -- 直近成功時刻
);

GET /api/circuit-breaker[/:bank_id] で全行 / 特定行のメトリクスを照会、
POST /api/circuit-breaker/:bank_id/reset で運用上の強制 CLOSED が可能。
詳細は 32_api_contracts.md § Circuit Breaker。

ReversalRecords(救済取引)

SETTLED 後に発生した苦情・誤送金等を救済するための補償取引メタデータ。
reversal_txid は実際の補償送金 TX を指す(lane=STANDARD, purpose='REFUND'
で生成)。

CREATE TABLE ReversalRecords (
  reversal_id    TEXT PRIMARY KEY,
  original_txid  TEXT NOT NULL,                     -- 元の SETTLED な txid
  reversal_txid  TEXT,                              -- 補償送金の txid(生成後に埋まる)
  amount         INTEGER NOT NULL,
  reason         TEXT NOT NULL,                     -- ReversalReason: CUSTOMER_DISPUTE|DUPLICATE_PAYMENT|INCORRECT_AMOUNT|INCORRECT_PAYEE|FRAUD|OPERATIONAL_ERROR
  status         TEXT NOT NULL DEFAULT 'REQUESTED', -- ReversalStatus: REQUESTED|APPROVED|TX_CREATED|COMPLETED|REJECTED
  requested_by   TEXT NOT NULL,                     -- bank_id | 'OPS'
  description    TEXT,
  -- 一部の reason は事前承認 ref が必須
  approval_ref   TEXT,                              -- 内部統制系チケット参照
  -- リバーサル要求の冪等キー(at-least-once 再配信のリプレイ用)
  idempotency_key TEXT,                             -- UNIQUE; 未設定は NULL
  created_at     TEXT NOT NULL,
  updated_at     TEXT NOT NULL
);
CREATE INDEX idx_rev_original    ON ReversalRecords(original_txid);
CREATE INDEX idx_rev_reversal_tx ON ReversalRecords(reversal_txid);
CREATE UNIQUE INDEX idx_rev_idempotency ON ReversalRecords(idempotency_key);

要求の冪等性: requestReversal は idempotency_key を ReversalRecords に
保存し、再配信された要求(同一キー)を自身の台帳で検知して元のリバーサルを
そのままリプレイ返却する。UNIQUE インデックスは並行二重要求のバックストップ
でもある(敗者の INSERT はバッチごとロールバックされ phantom 行を残さない)。

reversal_txid 経由のカスケード: onPayeeExecConfirmed(補償送金が
SETTLED に到達)は ReversalRecords WHERE reversal_txid = ? を引いて該当
すれば completeReversal を呼ぶ。


ZC テーブル(KeyRegistry)

KeyRegistry(外部主体の検証用公開鍵レジストリ)

SettlementProofRef・Attestation・Mandate・FinalityCosign(副署)が
共有する信頼アンカー。ZC は 検証
専用の公開鍵のみ
を保持し、秘密鍵は持たない。ZC→外部(egress)の署名は引き
続き既存の HMAC 共有秘密(src/shared/hmac.ts)を用い、本テーブルの対象外。

CREATE TABLE KeyRegistry (
  key_id      TEXT    PRIMARY KEY,           -- 外部主体が指定する不透明な識別子
  owner_type  TEXT    NOT NULL,              -- 'PARTICIPANT'|'ATTESTER'|'AGENT'|'EXTERNAL_RAIL'
  owner_ref   TEXT    NOT NULL,              -- bank_id / participant_id / attester id 等
  public_key  TEXT    NOT NULL,              -- 公開鍵の raw バイト列(base64)
  algo        TEXT    NOT NULL,              -- 'ECDSA_P256'|'ED25519'
  valid_from  TEXT    NOT NULL,              -- RFC3339
  valid_to    TEXT,                          -- RFC3339, NULL=無期限
  revoked_at  TEXT,                          -- RFC3339, NULL=未失効
  status      TEXT    NOT NULL DEFAULT 'ACTIVE', -- 'ACTIVE'|'REVOKED'|'EXPIRED'
  created_at  TEXT    NOT NULL
);
CREATE INDEX idx_key_registry_owner ON KeyRegistry(owner_type, owner_ref);

検証モデル(src/shared/external_signature.ts):

  • 署名は occurred_at(署名者が主張する時刻)が [valid_from, valid_to) の
    範囲内、かつ revoked_at が設定されている場合は occurred_at < revoked_at
    のときのみ有効(失効は遡及しない)。
  • (key_id, nonce) のリプレイ防止は既存 IdempotencyKeys に
    sig:<key_id>:<nonce> という合成キーで相乗りする(新規テーブルなし)。
  • 鍵の登録・失効は ZC 運営の制度行為として 4 眼承認の対象とする
    (10_requirements.md(制度・ガバナンス要件)のブレークグラスと同じ統制思想)。

ZC テーブル(ConditionTemplate / Attestation)

ConditionTemplate(条件テンプレートのホワイトリスト)

HTLC の「preimage 提示=条件成立」を一般化した、署名付き「条件成立」表明
(アテステーション)の whitelist 本体。HtlcAuthWhitelist と同じ統制思想で、
許可制だが第三者が提案できる。

CREATE TABLE ConditionTemplate (
  template_id            TEXT    PRIMARY KEY,        -- 例: 'TPL-INSPECTION-COMPLETE'
  predicate_kind         TEXT    NOT NULL,           -- ドメインラベル(例: 'INSPECTION_COMPLETE')
  allowed_attester_scope TEXT    NOT NULL,           -- JSON: {key_ids?, owner_refs?, owner_types?}
  status                 TEXT    NOT NULL DEFAULT 'ACTIVE', -- 'ACTIVE'|'SUSPENDED'|'REVOKED'
  description            TEXT,
  registered_at          TEXT    NOT NULL
  -- プログラマビリティ拡張(統合スキーマに収録)
  , min_attester_quorum  INTEGER NOT NULL DEFAULT 1  -- 条件成立に要する distinct attester operator 数(k-of-n)。1=従来の単一表明
  , ledger_predicate_json TEXT                       -- 非NULL時、外部表明ではなく ZC 自身の確定済み FinalityLog で解決する述語(LedgerPredicate)
);

プログラマビリティ拡張の 2 列(いずれも既定値ありで後方互換):

  • min_attester_quorum: そのテンプレートの Attestation が条件リーフを満たすために必要な distinct な attester operator(KeyRegistry.owner_ref)数。Watcher の onchain_min_watchers と同型の k-of-n 定足数で、同一 operator が複数鍵を持っても 1 と数える。既定 1 は従来挙動。判定は src/shared/attestation_quorum.ts(純粋な定足数・equivocation 判定は src/shared/operator_quorum.ts に集約し Watcher 経路と共有)。
  • ledger_predicate_json: 非 NULL のとき、このテンプレートは外部 attester ではなく ZC が自分の確定済み FinalityLog に対して決定的に解決する(src/zc/platform/ledger_predicate.ts、例 {"kind":"TX_REACHED_STATE","txid":"TX-…","states":["SETTLED"]})。この種のテンプレートは allowed_attester_scope='{}'(誰も表明できない=fail-closed)とする。

Attestation(署名付き「条件成立」表明)

ZC は表明の真偽を判断せず、「誰が・いつ・どの条件根拠で表明したか」の証跡
のみを持つ。原文は保持せず statement_hash(sha256 hex)と検証結果
(verified_result)のみ保持する(30_internal_design.md §11.2-b)。

CREATE TABLE Attestation (
  attestation_id   TEXT    PRIMARY KEY,              -- 'ATT-<uuid>'
  template_id      TEXT    NOT NULL,
  subject_ref      TEXT    NOT NULL,                 -- txid / gtid / leg_id
  attester_key_id  TEXT    NOT NULL,                 -- KeyRegistry.key_id
  statement_hash   TEXT    NOT NULL,                 -- sha256 hex
  signature        TEXT    NOT NULL,                 -- base64
  nonce            TEXT    NOT NULL,
  occurred_at      TEXT    NOT NULL,                 -- RFC3339(表明者主張)
  verified_result  TEXT    NOT NULL,                 -- 'PASS'|'FAIL'
  created_at       TEXT    NOT NULL,
  FOREIGN KEY (template_id) REFERENCES ConditionTemplate(template_id)
);
CREATE INDEX idx_attestation_subject  ON Attestation(subject_ref);
CREATE INDEX idx_attestation_template ON Attestation(template_id);

検証フロー(src/shared/attestation.ts の recordAttestation()):

  1. ConditionTemplate が status='ACTIVE' であること(TEMPLATE_NOT_WHITELISTED)。
  2. statement_hash が sha256 hex 形式であること(ATTESTATION_INVALID)。
  3. {template_id, subject_ref, statement_hash, verified_result} への署名を
    KeyRegistry(§K)で検証(KEY_* / EXTERNAL_SIGNATURE_INVALID /
    SIGNATURE_REPLAYED)。
  4. 検証済み鍵が allowed_attester_scope の範囲内であること
    (ATTESTER_UNAUTHORIZED。空スコープは fail-closed=誰も許可されない)。

ZC テーブル(Mandate)

Mandate(委任チェーン — 「誰の権限で指示されたか」)

すべての指図に「どの権限根拠で発生したか」の参照を持たせる委任エンティティ。
新しいレーン/状態は追加しない。
受理時に mandate_id を解決し、parent_mandate_id を遡って委任チェーンを
辿り、各リンクの amount/purpose/lane が許可範囲内であることを検証する
(assertMandateValid)。委任は親のスコープを狭める方向にのみ働く。

CREATE TABLE Mandate (
  mandate_id               TEXT    PRIMARY KEY,        -- 'MANDATE-<uuid>'
  principal_participant_id TEXT    NOT NULL,           -- 権限の最終的な保持者
  grantee_ref              TEXT    NOT NULL,           -- 委任先(エージェント等)
  parent_mandate_id        TEXT,                       -- 自己参照FK。NULL=root mandate
  max_amount               INTEGER,                    -- NULL=このリンクでは無制限(金額は整数の最小単位)
  allowed_purposes         TEXT,                       -- JSON配列。NULL=このリンクでは無制限
  allowed_lanes            TEXT,                       -- JSON配列。NULL=このリンクでは無制限
  valid_from               TEXT    NOT NULL,           -- RFC3339
  valid_to                 TEXT    NOT NULL,           -- RFC3339
  principal_key_id         TEXT    NOT NULL,           -- KeyRegistry.key_id(principalの鍵)
  signature                TEXT    NOT NULL,           -- base64
  nonce                    TEXT    NOT NULL,
  occurred_at              TEXT    NOT NULL,           -- RFC3339(principal主張)
  revoked_at               TEXT,                       -- RFC3339, NULL=未失効
  created_at               TEXT    NOT NULL,
  FOREIGN KEY (parent_mandate_id) REFERENCES Mandate(mandate_id)
);
CREATE INDEX idx_mandate_principal ON Mandate(principal_participant_id);
CREATE INDEX idx_mandate_grantee   ON Mandate(grantee_ref);
CREATE INDEX idx_mandate_parent    ON Mandate(parent_mandate_id);

登録フロー(src/shared/mandate.ts の registerMandate()):

  • principal が buildMandatePayload() の正規ペイロード
    (principal_participant_id, grantee_ref, parent_mandate_id, max_amount,
    allowed_purposes, allowed_lanes, valid_from, valid_to)に署名し、
    KeyRegistry(§K)で検証する(KEY_* / EXTERNAL_SIGNATURE_INVALID /
    SIGNATURE_REPLAYED)。parent_mandate_id の存在確認は呼び出し側の責務。

検証フロー(assertMandateValid(db, mandate_id, check, now)):

  1. mandate_id(またはその祖先)が存在しない → MANDATE_NOT_FOUND
  2. revoked_at が設定済みかつ now >= revoked_at → MANDATE_REVOKED
    (失効は遡及しない)
  3. now が [valid_from, valid_to) の範囲外 → MANDATE_EXPIRED
  4. check.amount/purpose/lane がいずれかのリンクの
    max_amount/allowed_purposes/allowed_lanes を超える → MANDATE_BREACH
  5. parent_mandate_id を辿って 1〜4 をルートまで繰り返す
    (MAX_CHAIN_DEPTH=10 を超えると MANDATE_BREACH、循環防止)。

返り値は参照された mandate(index 0)からルートまでのチェーン。


ZC テーブル(継続収納)

要件=10_requirements.md §3.2.8、処理方式=20_method_design.md §2.2.7。

3 層で構成する。各層の担う仕事は重ならない。

層 表 守るもの
契約 DebitMandate + MandateBudget 誰が誰に、何について、どこまでの回収を許したか
予告 ScheduledCollection + CollectionAttempt いつ・いくら回収するかの事前開示と、変更の統制
実行 Transactions(lane='DIRECT_DEBIT') 資金移動そのもの

DebitMandate(継続収納契約)

顧客が署名した Mandate を根拠に、受取人が反復的に回収できる範囲を固定する。

上限(*_cap)は本表に一元的に保持し、MandateBudget 側に写しを置かない。 上限は契約の途中で変更されうる(10_requirements.md §3.2.8.4-10:引き下げは顧客署名不要、引き上げは必要)ため、写しを持つと伝播漏れが「宣言された上限と実際に効いている上限の食い違い」を生む。枠の判定は本表への相関副問合せで行う。

CREATE TABLE DebitMandate (
  dd_mandate_id        TEXT    PRIMARY KEY,        -- 'DDM-<uuid>'
  mandate_id           TEXT    NOT NULL,           -- FK → Mandate(顧客署名の根拠)
  payer_bank_id        TEXT    NOT NULL,
  payer_account_alias  TEXT    NOT NULL,           -- ALS/Proxy 解決対象。口座番号を直書きしない
  payee_bank_id        TEXT    NOT NULL,
  payee_account_hash   TEXT    NOT NULL,
  product_ref          TEXT    NOT NULL,           -- 商材参照。ZC は意味を解釈しない
  charge_mode          TEXT    NOT NULL,           -- 'PERIODIC' | 'ITEMIZED'
  period_cycle         TEXT,                       -- PERIODIC 時 'MONTHLY'|'YEARLY'
  collection_mode      TEXT    NOT NULL,           -- 'REALTIME'|'SCHEDULED'|'SCHEDULED_LONG'
  notice_days_min      INTEGER NOT NULL DEFAULT 0,
  amend_freeze_hours   INTEGER NOT NULL,           -- 振替日から遡る凍結時刻(時間)
  ladder_max           INTEGER NOT NULL,
  nonbusiness_day_rule TEXT    NOT NULL DEFAULT 'NEXT_BUSINESS',
  per_collection_cap   INTEGER,                    -- 1 回あたり金額
  month_amount_cap     INTEGER,                    -- 暦月の累計金額(リセット型)
  month_count_cap      INTEGER,                    -- 暦月の回数(リセット型)
  two_month_amount_cap INTEGER,                    -- 連続 2 暦月の合計(境界攻撃の防止)
  day_count_cap        INTEGER,                    -- 1 日あたりの回数(リセット型)
  lifetime_amount_cap  INTEGER,                    -- 全期間の累計金額(消尽型)
  lifetime_count_cap   INTEGER,                    -- 全期間の回数(消尽型)
  pending_amount_cap   INTEGER,                    -- 未確定枠
  pending_count_cap    INTEGER,
  latefee_month_cap    INTEGER,                    -- 遅延損害金の独立枠
  latefee_rate_max     REAL,
  realtime_month_count_cap INTEGER,                -- モード別枠
  variance_ratio_max   REAL,                       -- 前回比の変動幅上限
  eligibility_attestation_id TEXT,                 -- 受取行が署名して主張した適格性
  state                TEXT    NOT NULL DEFAULT 'ACTIVE', -- ACTIVE|EXHAUSTED|REVOKED
  revoked_at           TEXT,
  created_at           TEXT    NOT NULL,
  updated_at           TEXT    NOT NULL,
  version              INTEGER NOT NULL DEFAULT 0,
  FOREIGN KEY (mandate_id) REFERENCES Mandate(mandate_id)
);
CREATE INDEX idx_ddm_payer   ON DebitMandate(payer_bank_id, payer_account_alias, state);
CREATE INDEX idx_ddm_payee   ON DebitMandate(payee_bank_id, payee_account_hash, state);
CREATE INDEX idx_ddm_mandate ON DebitMandate(mandate_id);
  • NULL の *_cap は「この枠は制約として働かない」を意味する。
  • 認可期限は必須ではない(10_requirements.md §3.2.8.1-4)。無期限の契約は Mandate.valid_to を遠い将来の番兵値で表現する(assertMandateValid の検証意味論を変更しないため)。
  • payer_account_alias に口座番号を直書きしないのは、口座変更・行内番号変更・合併に耐えるため。

MandateBudget(累計枠のカウンタ)

契約あたり 1 行に全カウンタを集約する。 収納 1 件は暦月枠・消尽枠・未確定枠を同時に消費するため、窓ごとに行を分けると複数行の原子的取得が必要になり、取得順序の管理(=デッドロック回避)を要求してしまう。全カウンタを 1 行に置けば、単一行 CAS がそのまま直列化点になる(transitionWithLog / H 予約 / FxTransfers.status と同じ作法)。

CREATE TABLE MandateBudget (
  dd_mandate_id        TEXT    PRIMARY KEY,
  month_key            TEXT    NOT NULL,           -- 'YYYY-MM'
  month_amount         INTEGER NOT NULL DEFAULT 0,
  month_count          INTEGER NOT NULL DEFAULT 0,
  prev_month_key       TEXT,                       -- 連続 2 暦月合計の算定用
  prev_month_amount    INTEGER NOT NULL DEFAULT 0,
  day_key              TEXT    NOT NULL,           -- 'YYYY-MM-DD'
  day_count            INTEGER NOT NULL DEFAULT 0,
  latefee_month_amount INTEGER NOT NULL DEFAULT 0,
  realtime_month_count INTEGER NOT NULL DEFAULT 0,
  lifetime_amount      INTEGER NOT NULL DEFAULT 0, -- 消尽型(回復しない)
  lifetime_count       INTEGER NOT NULL DEFAULT 0, -- 消尽型(回復しない)
  pending_amount       INTEGER NOT NULL DEFAULT 0, -- 未確定枠(確定時に解放)
  pending_count        INTEGER NOT NULL DEFAULT 0,
  last_amount          INTEGER,                    -- 前回比の変動幅判定用
  updated_at           TEXT    NOT NULL,
  version              INTEGER NOT NULL DEFAULT 0,
  FOREIGN KEY (dd_mandate_id) REFERENCES DebitMandate(dd_mandate_id)
);

窓のロールオーバーは予約と同じ UPDATE の中で行う。 month_key が現在の暦月と異なれば、その UPDATE の中で CASE 式によりカウンタをリセットし、旧値を prev_month_* へ送る。別ジョブでリセットすると、リセットと予約のあいだに競合窓が生じる。

  • リセット型(month_* / day_* / latefee_month_* / realtime_month_*)は窓が変われば自動的に回復する。意味はレート制限。
  • 消尽型(lifetime_*)は回復しない。意味は契約の総量であり、使い切りをもって契約が終了する(「12 回払い」はこれで表現する)。違反時の理由コードを分ける(BUDGET_RATE_EXCEEDED / BUDGET_EXHAUSTED)のは、両者が「取りすぎ」と「契約範囲外」という別の事象だからである。
  • 上限変更はカウンタをリセットしない(引き上げ→引き下げの往復による消費の洗浄を防ぐ)。

ScheduledCollection(収納予告)

Transactions ではない。振替日に発火したものだけが Transactions として実体化する。ラダーの各段も本表の行であり、同一 charge_ref を共有する。

CREATE TABLE ScheduledCollection (
  collection_id     TEXT    PRIMARY KEY,           -- 'COL-<uuid>'
  dd_mandate_id     TEXT    NOT NULL,
  charge_ref        TEXT    NOT NULL,              -- 請求費目(正規化済み・顧客に表示される)
  ladder_seq        INTEGER NOT NULL DEFAULT 1,    -- 段番号(1 = 基本段)
  amount_value      INTEGER NOT NULL,              -- 元本
  latefee_value     INTEGER NOT NULL DEFAULT 0,    -- 遅延損害金(元本と分離)
  amount_currency   TEXT    NOT NULL DEFAULT 'JPY',
  due_date          TEXT    NOT NULL,              -- 振替日 'YYYY-MM-DD'
  confirm_deadline_at TEXT  NOT NULL,              -- 失敗が確定する時刻(モードごとの確定点)
  amend_freeze_at   TEXT    NOT NULL,              -- RFC3339。REALTIME は生成時刻と等しい
  mode              TEXT    NOT NULL,              -- 実効モード(降格後)
  requested_mode    TEXT    NOT NULL,              -- 受取行が要求したモード(降格の説明用)
  state             TEXT    NOT NULL DEFAULT 'SCHEDULED',
  result            TEXT,                          -- NULL(未確定)|'CONFIRMED_OK'|'CONFIRMED_NG'
  reason_code       TEXT,
  retriable_today   INTEGER NOT NULL DEFAULT 0,
  vault_ref         TEXT,                          -- 認可証明 preimage の Vault 参照
  hashlock          TEXT,
  extra_mandate_id  TEXT,                          -- 単発認可(追加認可)
  edi_ref           TEXT,
  priority_hint     INTEGER,                       -- 顧客の優先指定(充当順序 第1項)
  budget_reserved   INTEGER NOT NULL DEFAULT 0,
  txid              TEXT,                          -- FIRED 後
  notified_at       TEXT,
  frozen_at         TEXT,
  fired_at          TEXT,
  confirmed_at      TEXT,
  idempotency_key   TEXT    NOT NULL UNIQUE,
  created_at        TEXT    NOT NULL,
  updated_at        TEXT    NOT NULL,
  version           INTEGER NOT NULL DEFAULT 0,
  UNIQUE (dd_mandate_id, charge_ref, ladder_seq),
  FOREIGN KEY (dd_mandate_id) REFERENCES DebitMandate(dd_mandate_id),
  FOREIGN KEY (extra_mandate_id) REFERENCES Mandate(mandate_id)
);

CREATE UNIQUE INDEX uq_collection_charge_ok
  ON ScheduledCollection(dd_mandate_id, charge_ref)
  WHERE result = 'CONFIRMED_OK';

CREATE INDEX idx_collection_due    ON ScheduledCollection(due_date, state);
CREATE INDEX idx_collection_ddm    ON ScheduledCollection(dd_mandate_id, charge_ref);
CREATE INDEX idx_collection_txid   ON ScheduledCollection(txid);
CREATE INDEX idx_collection_freeze ON ScheduledCollection(amend_freeze_at, state);

uq_collection_charge_ok が二重収納の防止とラダーの排他(OCO)を同時に担う唯一の制約である。 1 費目について CONFIRMED_OK に到達できる収納は高々ひとつなので、先行段が成功すれば後続段は構造的に成立しえない。両者に別々の機構を設けてはならない——同じ不変条件だからである。部分ユニークインデックス(WHERE result = 'CONFIRMED_OK')としているのは、失敗した段や未確定の段は同一 charge_ref に複数並立してよいためである。

状態遷移は 20_method_design.md §2.2.7.3 の図を正とする。SUPERSEDED / LAPSED / WITHDRAWN は行削除ではなく状態である——「5月13日の再請求は 4月27日に成功したため取り消された」と説明できなければならない。

CollectionAttempt(試行の系列)

追記型。収納の現在状態は、この系列と窓の開閉から導出する(設計思想 1)。中間結果は確定ではないが、受取人にとっては行動の根拠となる(0 時に未成功が判れば当日中に督促できる)ため記録する。

CREATE TABLE CollectionAttempt (
  attempt_id      TEXT    PRIMARY KEY,             -- 'CATT-<uuid>'
  collection_id   TEXT    NOT NULL,
  attempt_no      INTEGER NOT NULL,
  observed_at     TEXT    NOT NULL,                -- RFC3339
  result          TEXT    NOT NULL,                -- 'OK' | 'NG'
  reason_code     TEXT,
  retriable_today INTEGER NOT NULL DEFAULT 0,
  bank_proof_ref  TEXT,
  created_at      TEXT    NOT NULL,
  UNIQUE (collection_id, attempt_no),
  FOREIGN KEY (collection_id) REFERENCES ScheduledCollection(collection_id)
);
CREATE INDEX idx_cattempt_collection ON CollectionAttempt(collection_id, attempt_no);

確定は成功と失敗で非対称である。 成功した試行を観測した時点で result='CONFIRMED_OK' が確定する(資金が動いた以上それ以上変わらない)。失敗は振替日 24:00 をもって初めて CONFIRMED_NG に確定する——日中の残高不足は「まだ b に到達していない」だけであり、顧客が日中に入金すれば後続のセンターカットで成功しうる。


PDFを作成

フォントは初回だけ読み込むため、1回目は時間がかかります。

用紙
組み方向
表紙
本文