Closed12

100日チャレンジ day15 (ステートマシンをつかったクレジットカード発行ワークフロー)

riddle_tecriddle_tec

昨日
https://zenn.dev/gin_nazo/scraps/a1f3424135b058


https://blog.framinal.life/entry/2025/04/14/154104

100日チャレンジに感化されたので、アレンジして自分でもやってみます。

やりたいこと

  • 世の中のさまざまなドメインの簡易実装をつくり、バックエンドの実装に慣れる(dbスキーマ設計や、関数の分割、使いやすいインターフェイスの切り方に慣れる
  • 設計力(これはシステムのオーバービューを先に自分で作ってaiに依頼できるようにする
  • 生成aiをつかったバイブコーティングになれる
  • 実際にやったことはzennのスクラップにまとめ、成果はzennのブログにまとめる(アプリ自体の公開は必須ではないかコードはgithubにおく)

できたもの

https://github.com/lirlia/100day_challenge_backend/tree/main/day15_credit_card_workflow

riddle_tecriddle_tec

day15_ebpf_dashboard 仕様書

概要

eBPF(Extended Berkeley Packet Filter)を用いてLinuxシステムの低レベル情報(例:ネットワークパケット数、システムコール統計など)を取得し、そのデータをWeb UIで可視化・管理するダッシュボードアプリ。

目的

  • eBPFによるシステム観測データの取得・保存・可視化の一連の流れを体験・学習する
  • Webバックエンド(Next.js, Prisma, SQLite)と外部プログラム(eBPF)の連携を実践する

主な機能

  1. eBPFプログラムの実行・データ取得(サーバー側で外部プロセスとして実行)
  2. 取得データのSQLite保存・Prismaによる管理
  3. データのAPI化(Next.js Route Handlerで提供)
  4. Web UIでのデータ可視化(グラフ・リスト表示)
  5. eBPFプログラムの再実行・パラメータ変更(将来的な拡張)

技術構成

  • フロントエンド: Next.js (App Router), TypeScript, Tailwind CSS
  • バックエンド: Next.js API Route, Prisma, SQLite
  • eBPFプログラム: GoまたはPython等で実装し、Node.jsから子プロセス実行
  • データ保存: SQLite(prisma/dev.db
  • 可視化: シンプルなグラフテーブル(将来的にChart.js等も検討)

想定データ例

  • タイムスタンプ
  • 取得したメトリクス(例:受信/送信パケット数、プロセスID、システムコール回数など)

画面イメージ

  • ダッシュボード(最新データのグラフリスト表示)
  • データ取得履歴一覧

制約・注意事項

  • eBPFプログラムの実行にはLinux環境が必要
  • eBPF(低レベル操作)により十分な安全と安定性を考慮する設計とする
  • まずは最小構成で実装し、拡張性を持たせる
riddle_tecriddle_tec

「day15_ebpf_dashboard」アプリ作成のための作業工程を再掲します。

  1. ディレクトリ作成  - templateディレクトリをコピーし、day15_ebpf_dashboardディレクトリを作成  - package.jsonのnameフィールドをday15_ebpf_dashboardに修正
  2. 仕様書作成  - day15_ebpf_dashboard/README.mdに仕様・目的・機能・技術構成を記載
  3. Prismaセットアップ  - prisma/schema.prismaの初期設計(例:ebpf_metricsテーブル)  - マイグレーション実行
  4. eBPFデータ取得スクリプト準備  - bpftraceやbccツールを使ったワンライナーを用意  - サーバーからdocker runコマンドでLinuxコンテナ上で実行できるようにする
  5. サーバー側実装  - Next.js API Routeでdockerコマンドを実行し、eBPFデータを取得・DB保存するエンドポイントを作成  - データ取得用APIの実装
  6. フロントエンド実装  - データ取得APIを呼び出し、グラフやリストで可視化するページを作成
  7. 動作確認・README追記  - 全体の動作確認  - READMEにセットアップ・使い方・注意事項を追記

この流れで進めます。

riddle_tecriddle_tec

day15_ebpf_dashboard プロジェクト指示まとめ

概要

  • eBPF(Extended Berkeley Packet Filter)を使い、システムコールやネットワーク通信などの低レベル情報を取得し、Web UIで可視化するダッシュボードアプリを作成する。
  • macOS上のDockerコンテナでeBPFワンライナー(bpftraceやbccツール)を実行し、その結果をNext.jsアプリで取得・保存・可視化する。

これまでの流れ・要件

  1. ディレクトリ作成

    • template ディレクトリをコピーし、day15_ebpf_dashboard ディレクトリを作成
    • package.jsonname フィールドを day15_ebpf_dashboard に修正
  2. 仕様書作成

    • README.md に仕様・目的・機能・技術構成を記載
  3. Prismaセットアップ

    • prisma/schema.prismaebpf_metrics テーブル等を設計
    • マイグレーション実行
  4. eBPFデータ取得スクリプト準備

    • bpftraceやbccツールを使ったワンライナーを用意
    • サーバーから docker run コマンドでLinuxコンテナ上で実行できるようにする
    • この際、システムコールやネットワーク通信が発生する処理(例: ls, curl など)を先に実行し、その直後にeBPFで観測データを取得する仕組みを組み込む
    • 4/5の段階で、docker runによるワンライナー実行で本当にデータが取得できるかテストを行う
  5. サーバー側実装

    • Next.js API Routeでdockerコマンドを実行し、eBPFデータを取得・DB保存するエンドポイントを作成
    • データ取得用APIの実装
  6. フロントエンド実装

    • データ取得APIを呼び出し、グラフやリストで可視化するページを作成
  7. 動作確認・README追記

    • 全体の動作確認
    • READMEにセットアップ・使い方・注意事項を追記

注意事項

  • eBPFプログラムの実行にはLinuxカーネルが必要なため、macOS上ではDockerコンテナ(Linux)で実行する
  • docker run時は --privileged オプション等、eBPFが動作する権限設定に注意
  • まずは最小構成で実装し、拡張性を持たせる

例:docker run コマンド例

docker run --rm --privileged bpftrace/bpftrace:latest \
  sh -c "ls > /dev/null; bpftrace -e 'tracepoint:syscalls:sys_enter_execve { @[comm] = count(); }' -c 'ls'"
riddle_tecriddle_tec

Mac だときつかったので諦めてステートマシンをつかったクレカシステムにする

riddle_tecriddle_tec

承知しました。ここにクレジットカード発行ワークフローシステムの仕様を記載します。


クレジットカード発行ワークフローシステム 仕様

1. 概要

クレジットカードの申し込みを受け付け、審査、発行、有効化に至るまでのワークフローを管理するシステムです。ステートマシンを用いて申請の状態遷移を管理し、各段階で実行可能なアクションを制御します。

2. 機能要件

  • 申請管理:
    • 新規クレジットカード申請を受け付けます。
    • 申請情報(申請者名、メールアドレスなど)と現在のステータスを記録します。
    • 各申請の状態遷移履歴(いつ、誰が、どの状態からどの状態へ遷移したか)を記録します。
  • ステートマシン:
    • 定義された状態と遷移ルールに基づき、申請の状態を管理します(独自実装、ライブラリ不使用)。
    • 現在の状態に応じて、次に実行可能なアクション(遷移)を決定します。
    • 状態遷移は許可された操作によってのみ行われます。
  • 管理UI (/admin):
    • 左ペイン:
      • 新規申請フォーム(申請者名、メールアドレス)。
      • 申請一覧をテーブル表示(ID、申請者名、現在の状態)。
      • 一覧から申請を選択すると、その詳細情報(ID、申請者、状態、履歴)を表示。
      • 選択された申請の現在の状態に基づき、実行可能なアクションボタン(例: 「初期審査開始」「承認」)を動的に表示し、クリックで状態遷移を実行。
    • 右ペイン:
      • 選択された申請が現在どのワークフロー段階にあるかをグラフィカルに表示
      • 状態を表すボックスと遷移を示す矢印で構成されたシンプルな図を描画し、現在の状態に対応するボックスをハイライト表示します。

3. データモデル

  • CreditCardApplication:
    • id: String (UUID, Primary Key)
    • applicantName: String
    • applicantEmail: String
    • status: ApplicationStatus (Enum) - 現在の状態
    • createdAt: DateTime (自動設定)
    • updatedAt: DateTime (自動更新)
    • histories: Relation to ApplicationHistory (1対多)
  • ApplicationHistory:
    • id: String (UUID, Primary Key)
    • applicationId: String (Foreign Key to CreditCardApplication)
    • fromStatus: ApplicationStatus (Enum) - 遷移前の状態
    • toStatus: ApplicationStatus (Enum) - 遷移後の状態
    • timestamp: DateTime (自動設定) - 遷移が発生した日時
    • notes: String? - 遷移に関するメモ(例: 否決理由、担当者コメントなど)
    • application: Relation to CreditCardApplication (多対1)
  • ApplicationStatus (Enum):
    • APPLIED (申込受付)
    • SCREENING (初期審査中)
    • IDENTITY_VERIFICATION_PENDING (本人確認待ち)
    • CREDIT_CHECK (信用情報照会中)
    • MANUAL_REVIEW (手動審査中)
    • APPROVED (承認済み)
    • CARD_ISSUING (カード発行準備中)
    • CARD_SHIPPED (カード発送済み)
    • ACTIVE (有効化済み)
    • REJECTED (否決済み)
    • CANCELLED (申込キャンセル)

4. 状態と遷移ルール

遷移アクション (Action Name) 遷移元状態 (From Status) 遷移先状態 (To Status) トリガー/条件
SubmitApplication (初期状態) APPLIED ユーザー申請
StartScreening APPLIED SCREENING システム/担当者操作
RequestIdentityVerification SCREENING IDENTITY_VERIFICATION_PENDING システム/担当者操作
CompleteIdentityVerification IDENTITY_VERIFICATION_PENDING CREDIT_CHECK システム/担当者操作 (確認OK)
FailIdentityVerification IDENTITY_VERIFICATION_PENDING REJECTED システム/担当者操作 (確認NG)
StartCreditCheck SCREENING CREDIT_CHECK システム/担当者操作 (本人確認不要時)
PassCreditCheck CREDIT_CHECK APPROVED システム判断 (自動承認)
RequireManualReview CREDIT_CHECK MANUAL_REVIEW システム判断 (自動判断不可)
FailCreditCheck CREDIT_CHECK REJECTED システム判断 (与信NG)
ApproveManually MANUAL_REVIEW APPROVED 担当者操作
RejectManually MANUAL_REVIEW REJECTED 担当者操作
StartCardIssuing APPROVED CARD_ISSUING システム
CompleteCardIssuing CARD_ISSUING CARD_SHIPPED システム
ActivateCard CARD_SHIPPED ACTIVE ユーザー操作
CancelApplication APPLIED, SCREENING, IDENTITY_VERIFICATION_PENDING, CREDIT_CHECK, MANUAL_REVIEW CANCELLED ユーザー操作
RejectScreening (否決追加) SCREENING REJECTED 担当者操作 (初期審査で否決)
BackToScreening (手動審査差戻し追加) MANUAL_REVIEW SCREENING 担当者操作 (再確認等)

5. APIエンドポイント

  • POST /api/applications: 新規申請を作成
    • Request Body: { applicantName: string, applicantEmail: string }
    • Response: 作成された申請データ (CreditCardApplication)
  • GET /api/applications: 全申請一覧を取得
    • Response: CreditCardApplication[] (関連履歴は含まない)
  • GET /api/applications/[id]: 特定の申請詳細を取得 (状態履歴も含む)
    • Response: CreditCardApplication (with histories sorted by timestamp)
  • PATCH /api/applications/[id]: 申請の状態を遷移させる
    • Request Body: { action: string, notes?: string } (例: { action: "StartScreening" })
    • Response: 更新された申請データ (CreditCardApplication)
    • 内部でステートマシンロジックを呼び出し、遷移可能かチェック。可能であれば状態を更新し、履歴を記録。

6. 技術スタック

  • フレームワーク: Next.js (App Router)
  • 言語: TypeScript
  • データベース: SQLite
  • ORM: Prisma
  • スタイリング: Tailwind CSS
  • パッケージ管理: npm

7. 実装スコープ外

  • ユーザー認証・認可(簡易的なユーザー識別で代替)
  • 複雑なアクセス制御(誰でも全操作可能とする)
  • 外部システム(信用情報機関、本人確認サービス、カード発行システム、発送システム)との実際の連携(API呼び出しはシミュレート)
  • メール通知などの副作用
  • 高度なエラーハンドリング・バリデーション
  • レスポンシブデザインの詳細な調整
riddle_tecriddle_tec

承知しました。クレジットカード発行ワークフローシステムの開発作業工程を以下に示します。

  1. プロジェクト初期化: (一部実施済み)

    • day15_credit_card_workflow ディレクトリ作成 (済)
    • テンプレートコピー (要再実行)
    • package.jsonname 更新 (要再実行)
    • day15_credit_card_workflow/README.md に先ほどの仕様を書き込み。
  2. データモデリングとDB設定:

    • prisma/schema.prisma を開き、CreditCardApplication モデル、ApplicationHistory モデル、および ApplicationStatus Enum を定義します。リレーションも設定します。
    • ターミナルで cd day15_credit_card_workflow してから npx prisma migrate deploy を実行し、データベーススキーマを SQLite ファイル (prisma/dev.db) に反映させます。
  3. ステートマシンロジック実装:

    • day15_credit_card_workflow/app/_lib/stateMachine.ts ファイルを作成します。
    • 仕様書に記載された状態遷移ルールを TypeScript のデータ構造(例: Map<ApplicationStatus, Map<string, ApplicationStatus>> のような形式)で定義します。
    • 状態遷移を試行する関数 canTransition(currentStatus: ApplicationStatus, action: string): ApplicationStatus | null を実装します。この関数は、指定されたアクションが現在の状態から可能であれば遷移先の状態を返し、不可能であれば null を返します。
  4. APIエンドポイント実装:

    • lib/db.ts の Prisma Client インスタンス設定を確認します(テンプレートに含まれているはず)。
    • app/api/applications/route.ts を作成し、POST (新規申請作成) と GET (申請一覧取得) の Route Handler を実装します。
    • app/api/applications/[id]/route.ts を作成し、GET (特定申請の詳細取得、履歴含む) と PATCH (状態遷移実行) の Route Handler を実装します。
      • PATCH ハンドラ内:
        • リクエストボディから action を受け取ります。
        • 現在の申請の状態を取得します。
        • stateMachine.tscanTransition を呼び出して遷移可能か検証します。
        • 遷移可能な場合、prisma.$transaction を使用して以下の処理をアトミックに行います:
          1. CreditCardApplicationstatus を新しい状態に更新します。
          2. ApplicationHistory に新しい遷移履歴レコードを作成します。
        • 更新後の申請データをレスポンスとして返します。
  5. UIコンポーネント実装 (app/(pages)/admin/page.tsx):

    • app/(pages)/admin ディレクトリと page.tsx ファイルを作成します。
    • "use client" ディレクティブを追加します。
    • Tailwind CSS を使用して、左右2ペインのレイアウトコンポーネントを作成します。
    • 左ペイン:
      • useState を使用して、新規申請フォームの入力状態、申請一覧データ、選択中の申請ID、選択中の申請詳細データを管理します。
      • useEffect を使用して、初期表示時および申請更新時に /api/applications から一覧データを取得・更新します。
      • 新規申請フォームを実装し、送信時に POST /api/applications を呼び出します。
      • 申請一覧テーブルを実装し、行クリックで申請IDを state にセットします。
      • 選択中の申請IDが変わったら useEffectGET /api/applications/[id] を呼び出し、詳細データを取得します。
      • 取得した詳細データの現在の状態 (status) とステートマシンルールに基づき、実行可能なアクションボタンを動的にレンダリングします。
      • 各アクションボタンのクリック時に PATCH /api/applications/[id] を適切な action と共に呼び出し、成功したら申請一覧と詳細を再取得します。
      • 詳細データから状態履歴を表示します。
    • 右ペイン:
      • 選択された申請の status を props として受け取るシンプルなコンポーネントを作成します。
      • ApplicationStatus の各状態に対応するボックスと、主要な遷移を示す矢印を HTML と Tailwind CSS で描画します。
      • 現在の status に対応するボックスのスタイルを変更してハイライト表示します。
  6. 動作確認とデバッグ:

    • 管理画面 (/admin) をブラウザで開きます。
    • 新規申請を作成します。
    • 作成された申請を一覧から選択します。
    • 左ペインのアクションボタンをクリックし、状態が遷移すること、右ペインの図のハイライトが変わること、左ペインの履歴が更新されることを確認します。
    • 意図しない遷移(例:APPLIED から直接 APPROVED)のボタンが表示されていないことを確認します。
    • curl を使って API エンドポイントを直接叩き、期待通りのレスポンスが返ってくるか確認します。
    • 開発中に埋め込んだ console.log などを削除します。
  7. ドキュメント更新:

    • day15_credit_card_workflow/README.md に最終的な使い方や注意点を追記します。
    • ルートの rules/knowledge.md に今回のアプリケーションの概要を追加します。
riddle_tecriddle_tec

ステートマシンの実装

import { ApplicationStatus } from '../generated/prisma';

// アクション名を定義(APIリクエストの action と一致させる)
export type ActionName =
  | 'SubmitApplication'
  | 'StartScreening'
  | 'RequestIdentityVerification'
  | 'CompleteIdentityVerification'
  | 'FailIdentityVerification'
  | 'StartCreditCheck'
  | 'PassCreditCheck'
  | 'RequireManualReview'
  | 'FailCreditCheck'
  | 'ApproveManually'
  | 'RejectManually'
  | 'StartCardIssuing'
  | 'CompleteCardIssuing'
  | 'ActivateCard'
  | 'CancelApplication'
  | 'RejectScreening'
  | 'BackToScreening'
  | 'DeleteApplication';

// 状態遷移ルール: Map<現在の状態, Map<アクション名, 次の状態>>
const transitions = new Map<ApplicationStatus, Map<ActionName, ApplicationStatus>>();

// 各状態からの遷移を定義
transitions.set(ApplicationStatus.APPLIED, new Map([
  ['StartScreening', ApplicationStatus.SCREENING],
  ['CancelApplication', ApplicationStatus.CANCELLED],
]));

transitions.set(ApplicationStatus.SCREENING, new Map([
  ['RequestIdentityVerification', ApplicationStatus.IDENTITY_VERIFICATION_PENDING],
  ['StartCreditCheck', ApplicationStatus.CREDIT_CHECK],
  ['RejectScreening', ApplicationStatus.REJECTED],
  ['CancelApplication', ApplicationStatus.CANCELLED],
]));

transitions.set(ApplicationStatus.IDENTITY_VERIFICATION_PENDING, new Map([
  ['CompleteIdentityVerification', ApplicationStatus.CREDIT_CHECK],
  ['FailIdentityVerification', ApplicationStatus.REJECTED],
  ['CancelApplication', ApplicationStatus.CANCELLED],
]));

transitions.set(ApplicationStatus.CREDIT_CHECK, new Map([
  ['PassCreditCheck', ApplicationStatus.APPROVED],
  ['RequireManualReview', ApplicationStatus.MANUAL_REVIEW],
  ['FailCreditCheck', ApplicationStatus.REJECTED],
  ['CancelApplication', ApplicationStatus.CANCELLED],
]));

transitions.set(ApplicationStatus.MANUAL_REVIEW, new Map([
  ['ApproveManually', ApplicationStatus.APPROVED],
  ['RejectManually', ApplicationStatus.REJECTED],
  ['BackToScreening', ApplicationStatus.SCREENING],
  ['CancelApplication', ApplicationStatus.CANCELLED],
]));

transitions.set(ApplicationStatus.APPROVED, new Map([
  ['StartCardIssuing', ApplicationStatus.CARD_ISSUING],
]));

transitions.set(ApplicationStatus.CARD_ISSUING, new Map([
  ['CompleteCardIssuing', ApplicationStatus.CARD_SHIPPED],
]));

transitions.set(ApplicationStatus.CARD_SHIPPED, new Map([
  ['ActivateCard', ApplicationStatus.ACTIVE],
]));

// ACTIVE, REJECTED, CANCELLED からの遷移は定義しない(終端状態)
// Resetもこれらの状態からは許可しない (addResetTransition で制御)

/**
 * 指定されたアクションが現在の状態から可能か検証し、可能であれば次の状態を返す
 * @param currentStatus 現在の状態
 * @param action 実行しようとしているアクション
 * @returns 遷移可能な場合は次の状態、不可能な場合は null
 */
export function canTransition(
  currentStatus: ApplicationStatus,
  action: string // APIからは文字列で渡される想定
): ApplicationStatus | null {
  const possibleActions = transitions.get(currentStatus);
  if (!possibleActions) {
    // 現在の状態からの遷移ルールが存在しない (終端状態など)
    return null;
  }

  // 型安全性を確保するために ActionName 型にキャストしようとする
  // 不正な action 文字列が渡された場合も考慮
  const validAction = action as ActionName;
  if (possibleActions.has(validAction)) {
    return possibleActions.get(validAction) ?? null;
  }

  // 指定されたアクションが現在の状態から許可されていない
  return null;
}

/**
 * 指定された状態から実行可能な「状態遷移」アクション名のリストを取得する
 * (削除アクションは含まない)
 */
export function getAllowedActions(currentStatus: ApplicationStatus): ActionName[] {
    const possibleStateTransitions = transitions.get(currentStatus);
    const allowed = possibleStateTransitions ? Array.from(possibleStateTransitions.keys()) : [];

    // Filter out DeleteApplication just in case it was accidentally added to transitions map
    return allowed.filter(action => action !== 'DeleteApplication');
}
このスクラップは4ヶ月前にクローズされました