メインコンテンツまでスキップ

初期データ移行

本手順書は、既存の顧客マスタと取引履歴を、基幹系や勘定系からエクスポートした ファイルから、新規構築した Merlon へ一度だけ投入する作業を扱う。

現在サポートされる方法

Merlon にバルクインポートコマンドは存在しない。サポートされる方法は、エクスポート ファイルを読み取って REST API 経由で投入し、その後スコアリングとモニタリングの バックフィルを実行することである。参照実装 scripts/migrate-initial-data.py は以下の手順をそのまま実装しており、改変の出発点として利用できる。

ネイティブなバルクローダーは設計済みだが未実装である。そのスコープと、デモ用の seed ローダーをそのまま流用できない理由は ADR-0015(バルクデータインポート)を参照。

顧客レコードを SQL で書き込んではならない

直接 PII 属性(full_nameaddressdate_of_birthphoneemailaccount_numberid_document_number)は、api/internal/store/customer_pii.go においてアプリケーション側で書き込み時に暗号化される。customers テーブルへの COPYINSERTpsql \copy は、読み出し時に復号が試みられるカラムへ平文のまま データを格納することになる。結果としてデータ保護上の違反であると同時に、Merlon が 読み戻せないデータベースになる。

顧客レコードは必ず API 経由で書き込むこと。

開始前の準備

  1. マイグレーションを適用し、API が正常であることを確認する。 デプロイ手順を参照。
  2. 暗号鍵が設定済みであることを確認する。暗号化設定前に顧客を投入すると、 後から鍵を展開しても遡って保護されない平文 PII が書き込まれる。
  3. 空のデータベースのバックアップを取得する。失敗した投入を部分的に取り消すのではなく、 きれいに初期化し直せるようにするため。バックアップとリストア手順を参照。
  4. 投入に必要な権限を持つ API キーを発行し、移行完了時に失効させる計画を立てる。

まず本番相当のコピーに対して一連の手順を予行すること。投入はトランザクションで 保護されない(失敗時の扱いを参照)ため、データ上の問題を安価に 発見する手段はリハーサルしかない。

手順1: 顧客の投入

POST /api/v1/customers は1リクエストにつき1顧客を作成する。

{
"external_id": "CIF-000123",
"customer_type": "individual",
"country_code": "JP",
"product_types": ["deposit"],
"attributes": {
"full_name": "…",
"date_of_birth": "1980-01-01",
"occupation": "…"
}
}
  • external_id は必須で、自社システムにおける顧客のキーである。UNIQUE 制約 (migrations/001_init.sql)が付いており、これが投入の再実行を可能にしている。 失敗時の扱いを参照。
  • customer_typeindividualcorporate_domesticcorporate_foreign のいずれかでなければならない。
  • id は Merlon が採番する。 指定はできない。レスポンスボディに採番された id が含まれるので、external_idid の対応を必ず保持すること。取引の投入では 自社キーではなく Merlon の id を使うためである。
  • 直接 PII の属性キーは書き込み時に暗号化される。それ以外の attributes (職業・業種・国籍・PEP フラグ等)はスコアリングで索引可能なまま維持するため 平文で格納される。

連携元システムの項目をこの形式へ対応付ける方法は アダプタガイドを参照。

手順2: 取引履歴の投入

POST /api/v1/transactions を、取引1件につき1リクエストで実行する。

{
"external_id": "TXN-2026-0000001",
"customer_id": "<手順1で Merlon が返した id>",
"amount": 1500000,
"currency": "JPY",
"direction": "outbound",
"channel": "atm",
"executed_at": "2026-04-01T09:30:00Z",
"counterparty_id": "CP-000987",
"counterparty_country": "KP"
}
  • customer_id は Merlon 内部の ID である。手順1で保持した対応表から解決する。 未知の ID は 400 で拒否される。
  • external_id は顧客と同様に必須かつ UNIQUE である。
  • amount は正の数、directioninboundoutboundinternal のいずれか。
  • executed_at は連携元システムにおける取引日時である。正確に投入すること。 TM シナリオはこの項目を基準とした期間で評価するため、既定値や取込時刻を入れると 手順4のバックフィルが暗黙のうちに無意味になる。
  • counterparty_country は API 上は任意だが、連携元が保持しているなら必ず投入すること。 ハイリスク国送金シナリオはまさにこの項目を比較しているため、省略すると投入済みの 送金に対してこのコントロールが一切発火できなくなる。それは「ハイリスク国への送金は 無かった」という結果と見分けがつかない。
  • currency は当該環境の TM 基準通貨MERLON_TM_BASE_CURRENCY、既定 JPYでなければならない。 Merlon は換算を行わない。エンジンは名目額をそのまま合算する ため、基準通貨以外の行は基準通貨建ての閾値と比較されることになる。リアルタイム経路も 手順4のバックフィルも、誤った結果を出す代わりに当該顧客を PENDING_REVIEW へ退避 させる。エクスポート側で正規化しておくこと。

投入する履歴の期間は意図的に決めること。取引モニタリングのシナリオは遡及期間 (ルックバックウィンドウ)にわたって評価するため、最長のシナリオ期間より短い履歴 しか投入しないと、稼働開始後しばらくの間それらのシナリオが正しく発火できない。

手順3: CDD スコアのバックフィル

手順1で作成した顧客は未スコアリング状態である。TM の閾値は CDD リスクティアから 導出される(ADR-0004、スコア駆動アーキテクチャ)ため、モニタリングの前に スコアリングを行うこと。

POST /api/v1/batch/score1リクエストあたり最大1000件の顧客 IDapi/internal/server/batch.gomaxBatchCustomers)を受け付け、 これを超えるリストは 400 で拒否される。ID リストはこれに合わせて分割すること。

{ "customer_ids": ["…", "…"] }

レスポンスは totalsucceededfailed と、顧客ごとの results 配列を返す。 failed が 0 でない場合は中断条件として扱い、顧客ごとの error を確認すること。 一部しかスコアリングされていない状態でモニタリングに進んではならない。

failed だけでなく totalsucceeded も確認すること。サーバが認識しない ID は 評価前にスキップされ results にも現れないため、failed を増やさずに total だけを 減らす。チャンクごとに total == succeeded == チャンク件数 を必須とすること。

手順4: 取引モニタリングのバックフィル

POST /api/v1/batch/monitor も同じく1リクエストあたり1000件の制限があり、 投入済み履歴に対して TM シナリオを実行してアラートを生成する。評価対象は顧客ごとの 投入済み履歴の全件であり、直近の一部ではない。

mode を変えて2回実行すること。 シナリオはどちらのパスに属するかを宣言しており、 エンジンは一致するものだけを適用する。

{ "customer_ids": ["…", "…"], "mode": "realtime" }
{ "customer_ids": ["…", "…"], "mode": "batch" }
  • mode は任意で、既定は realtime。これは本エンドポイントの従来の挙動である。
  • realtime パスは evaluation_mode: batch のシナリオを一切適用しない。同梱サンプルで 言えば休眠口座急活性化と高頻度少額取引がこれにあたる。逆に batch パスは ハイリスク国送金のような realtime 専用シナリオを適用しない。つまり片方だけの実行は、 投入した全履歴に対してルールセットの一部を未適用のまま残し、それでも成功と報告する。
  • both を宣言したシナリオは両方のパスで実行されるが、Merlon が (顧客・シナリオ・集計ウィンドウ)でアラートを重複排除するため二重計上にはならない。
  • mode: "batch"dormant 顧客をスキップする。これは定期 TM バッチジョブと同じ 扱いである(データモデル §1.1.2: 休眠顧客の評価は取引発生時のみ)。closed 顧客は 両モードでスキップされる。

レスポンスには上記に加えて queued_for_review が含まれる。これはエンジンが評価 できなかった顧客——エンジン停止中、あるいは混在通貨のように Merlon が集計を拒否する 履歴——の件数であり、その取引は PENDING_REVIEW へ退避されている。これらの顧客は モニタリングされていない。 failed と同様に中断条件として扱うこと。

この手順では大量のアラート滞留が発生することを想定しておくこと。自社ポートフォリオ 向けにチューニングされていない閾値に対して、数年分の履歴を一度に評価するためである。 トリアージ体制を計画し、必要であれば先に候補ルールセットに対して バックテストのエンドポイントを実行し、アラート量を見積もってから 本実行に進むこと。

レートリミットとスループット

API にはグローバルなレートリミッタ(api/internal/server/ratelimit.go)があり、 超過時には X-RateLimit-LimitX-RateLimit-Remaining ヘッダを伴う 429 を返す。 手順1・2はレコード1件につき1リクエストであるため、大規模なポートフォリオでは 相応の時間を要する。

これは回避するのではなく、織り込んで計画すること。

  • クライアント側は 429 を失敗として扱わず、バックオフして再試行する。
  • レートリミッタの枠を実トラフィックと共有しないよう、メンテナンスウィンドウ中に 移行を実行する。
  • それでも現実的な時間に収まらない場合は、移行中に限りレート上限を引き上げ、 終了後に戻す。この変更は必ず記録すること。一時的なレート上限の緩和は統制の変更である。

失敗時の扱い

全体を包むトランザクションは存在しない。 各レコードはそれぞれのリクエストで コミットされるため、中断した投入はデータベースを部分的に埋めた状態で残る。 ロールバックコマンドは存在しない。

再開の安全性は external_idUNIQUE 制約に依存する。再投入されたレコードは 重複ではなくデータベース側で拒否される。ただし、クライアントの実装方針を左右する 2つの粗さがあることに注意すること。

  • 重複による拒否は 409 ではなく 500 として現れる。作成ハンドラがリポジトリの エラーをすべて内部エラーに写像しているためである。したがって再実行中の 500 は それ単体では判別できない。
  • external_id による検索手段がない。GET /api/v1/customers はカーソルおよび オフセットによるページングのみを提供しており、「このレコードは投入済みか」を 安価に問い合わせることはできない。

実務上の帰結として、クライアント側が独自のチェックポイントを保持しなければならない。 投入に成功した external_id と Merlon が返した id を記録し、サーバ側の状態ではなく そのファイルから再開すること。参照スクリプトはレコードごとに成功時点でチェックポイントを 書き出し、再実行時には記録済みの ID をスキップする。

初回移行においては、移行前バックアップをリストアしてやり直す方が、部分的な投入を 突き合わせるより通常はきれいである。

投入内容を記録すること。エクスポートファイル名とチェックサム、手順ごとの件数、 開始・終了時刻、実施者。レコード自体については API が監査エントリを書き込むが、 エクスポートファイルの出所は Merlon の外部にあり、利用者側で記録する必要がある。

検証

移行完了を宣言する前に、以下を確認すること。

  1. 連携元のエクスポートと件数を突き合わせる。GET /api/v1/dashboardtotal_customerscustomers_by_risk_tier の内訳を返す。 注意: ダッシュボードが数える顧客は最大10,000件である (api/internal/server/dashboard.go)。これを超える規模では、 突合ではなくヘルスシグナルとして扱うこと。正確な件数が必要な場合は GET /api/v1/customers をカーソルでページングするか、ローダーが書き出した チェックポイントファイルと突き合わせる。

  2. customers_by_risk_tierunscored が空または 0 であることを確認する。 未スコアリングの顧客は正しい閾値でモニタリングされないため、これが手順3の 完了を確かめるチェックである。

  3. 直接 PII が実際に暗号化されたことを、API 読み出しではなく格納値そのものを 検査して確認する。

    SELECT attributes->>'full_name' FROM customers LIMIT 5;

    暗号化された値は base64(鍵バージョンバイト + nonce + AES-GCM 暗号文)であり 判読できない。氏名がそのまま読める場合、そのレコードは API を経由せずに データベースへ到達しており、PII が平文で格納されている。

    このチェックに API 読み出しを使ってはならない。decryptDirectPIIapi/internal/store/customer_pii.go)は、暗号化を後から有効化しても既存行の 読み出しを壊さないよう、復号できない値を意図的にそのまま返す。その帰結として、 直接 INSERT で書き込まれた平文は API 経由では何の問題もなく読み戻せてしまう。 API は暗号化された書き込みと迂回された書き込みを区別できない。

  4. 複数の顧客をエンドツーエンドで抜き取り確認し、PII 以外の属性・商品種別・国コードが 連携元のエクスポートと一致することを確認する。

  5. 取引のタイムスタンプが保たれていることを確認する。最古と最新のレコードを取り出し、 executed_at が投入時刻ではなく連携元のエクスポートと一致することを確認する。

  6. 手順4で生じたアラート滞留量を、自社のトリアージ体制と照らして確認してから アナリストに公開する。

  7. 移行用の API キーを失効させる。