第13巻 DBスキーマ定義(2) ― トレーサビリティ・清算決済・継続収納
目次
- ZC テーブル(トレーサビリティ・着金フィルタ・HTLC Auth)
- TxEventLog(ZC側 詳細処理イベントログ:INSERT ONLY)
- HtlcAuthWhitelist(HTLC受取側起点ロック ホワイトリスト)
- HtlcAuthRequests(HTLC受取側起点オーソリリクエスト)
- ZC テーブル(清算・決済機能)
- IgsRequests(IGS連携 — 日銀ネット即時グロス清算)
- AccountVerifications(事前口座確認)
- CreditNotifications(入金結果通知)
- EdiRecords(ZEDI統合 — 全銀EDIリッチデータ)
- ProxyDirectory(エイリアス送金)
- QrCodes(QRコード送金)
- RichDataStore(リッチデータストレージ)
- CrossBorderTransactions(クロスボーダー送金)
- EventStream(双方向通信 — SSEイベントキュー)
- ZC テーブル(Circuit Breaker / Reversal)
- CircuitBreakerState(参加行疎通監視)
- ReversalRecords(救済取引)
- ZC テーブル(KeyRegistry)
- KeyRegistry(外部主体の検証用公開鍵レジストリ)
- ZC テーブル(ConditionTemplate / Attestation)
- ConditionTemplate(条件テンプレートのホワイトリスト)
- Attestation(署名付き「条件成立」表明)
- ZC テーブル(Mandate)
- Mandate(委任チェーン — 「誰の権限で指示されたか」)
- ZC テーブル(継続収納)
- DebitMandate(継続収納契約)
- MandateBudget(累計枠のカウンタ)
- ScheduledCollection(収納予告)
- CollectionAttempt(試行の系列)
第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()):
ConditionTemplateがstatus='ACTIVE'であること(TEMPLATE_NOT_WHITELISTED)。statement_hashが sha256 hex 形式であること(ATTESTATION_INVALID)。{template_id, subject_ref, statement_hash, verified_result}への署名をKeyRegistry(§K)で検証(KEY_*/EXTERNAL_SIGNATURE_INVALID/SIGNATURE_REPLAYED)。- 検証済み鍵が
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)):
mandate_id(またはその祖先)が存在しない →MANDATE_NOT_FOUNDrevoked_atが設定済みかつnow >= revoked_at→MANDATE_REVOKED
(失効は遡及しない)nowが[valid_from, valid_to)の範囲外 →MANDATE_EXPIREDcheck.amount/purpose/laneがいずれかのリンクのmax_amount/allowed_purposes/allowed_lanesを超える →MANDATE_BREACHparent_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 に到達していない」だけであり、顧客が日中に入金すれば後続のセンターカットで成功しうる。