コンテンツにスキップ

ValueOS 業務フロー

位置づけ: クライアントERP(ValueOS)

ValueOSは案件(cases)に対して1つの決済区分(case_settlements.settlement_type)を紐付け、決済区分ごとに宣言された条件(SETTLEMENT_RULES)をWorkflowEngineが解釈して発注可否(canOrder)・請求可否(canInvoice)・現在の状態(currentState)・次アクション(nextAction)・担当(assignee)を機械的に導出する。判定ロジックはすべてlib/workflow配下の純関数に集約し、画面はこの判定結果を表示するレイヤーに徹する。if/elseの分岐をUIや各フォームに直接書かず、決済区分を追加・変更する場合はSETTLEMENT_RULESという設定オブジェクトを増やすことで対応する設計である。

決済区分が「3社間決済」の案件は、発注・請求とは別に、信販会社からの入金(finance_receipts)・販売店への仕切精算(dealer_settlements)・仕入先への支払(supplier_payments)という3つの金銭実体を持つ。これらはexecute_three_party_moneyという冪等なRPC経路を通じてのみ書き込まれ、three_party_money_requestsテーブルが実行結果を記録する。販売店発注フォーム(app/dealer/orders/new)による案件起票は、この金銭フローとは別の独立した登録経路であり、本ページで両者を区別して扱う。

項目内容
対象WorkflowEngine(canOrder/canInvoice/現在状態の判定)、決済区分別の工程進行、三社間金銭フロー(信販入金・仕切精算・仕入先支払)、販売店発注フォームの登録経路、各テーブルの全フィールド仕様と権限(RLS/GRANT)
対象外ダッシュボード集計基準(valueos/overviewを参照)、cases/orders/invoices本体のフィールド網羅(valueos/data-modelを参照)
情報源ValueOS正本(github.com/stanaka-blip/valueos)のlib/workflow・lib/threeParty・app/dealer/orders/new・supabase/migrations
関連ページfeatures/wholesale-settlement、redesign/valueos-gap、glossary

データ(エンティティ)— 全フィールド仕様

Section titled “データ(エンティティ)— 全フィールド仕様”

case_settlements(案件決済条件・1案件1行)

Section titled “case_settlements(案件決済条件・1案件1行)”
フィールド型必須既定値制約・備考
iduuid○gen_random_uuid()PK
case_iduuid○—cases.id参照(ON DELETE CASCADE)。UNIQUE制約により1案件1行
settlement_typetext○—DB側にCHECK制約なし。正式値・エイリアスの妥当性はSETTLEMENT_RULES(アプリ側)のみで担保する
fee_ratenumeric–NULL決済手数料率(%)
fee_amountnumeric○0決済手数料額
deposit_ratenumeric–NULL前金率(%)
deposit_amountnumeric–NULL前金額。前金のcanOrder判定(入金確認)の比較基準
payment_termstext–NULL支払条件の自由記述
card_brandtext–NULLカード会社名(カード区分で使用)
finance_companytext–NULL信販会社名(3社間決済で使用)。create_case_registrationRPC(Ver1.2)は3社間決済で必須とする
approval_numbertext–NULL承認番号(3社間決済で使用)。同RPCで3社間決済必須
loan_statustext–NULLローン進捗。DB側にCHECK制約なし。値の妥当性はアプリ側LOAN_STATUSES(未申請/申請中/承認済/否認)のみ
loan_status_updated_attimestamptz–NULL
card_statustext–NULLカード決済状況。DB側にCHECK制約なし。値の妥当性はアプリ側CARD_STATUSES(未決済/処理中/決済成功/決済失敗/取消)のみ
card_status_updated_attimestamptz–NULL
memotext–NULL自由記述。DDL未適用環境向けの__workflow_v1__:{...}フォールバック格納先も兼ねる(後述)
created_at/updated_attimestamptz○now()updated_atはトリガーで自動更新(決済区分正規化などの一括UPDATE時は一時的に無効化して実行された実績あり)

cases(本ページで参照する列のみ)

Section titled “cases(本ページで参照する列のみ)”
フィールド型必須既定値制約・備考
construction_completed_datedate–NULL完工日(実績)。3社間決済のcanInvoice判定に使用

finance_receipts(信販会社からの入金)

Section titled “finance_receipts(信販会社からの入金)”
フィールド型必須既定値制約・備考
iduuid○gen_random_uuid()PK
case_iduuid○—cases.id参照(ON DELETE RESTRICT)
finance_companytext○—非空チェック
scheduled_datedate–NULL
scheduled_amountnumeric○0>= 0
actual_datedate–NULL確定(入金済)時に設定
actual_amountnumeric–NULLNULLまたは>= 0
statustext○'予定'CHECK: 予定/入金済/取消
memotext–NULL
cancelled_at/cancel_reasontimestamptz/text–NULL取消時に設定
corrects_iduuid–NULL自テーブル参照(ON DELETE RESTRICT)。訂正チェーンの元レコードを指す

dealer_settlements(仕切清算書兼販売店支払)

Section titled “dealer_settlements(仕切清算書兼販売店支払)”
フィールド型必須既定値制約・備考
iduuid○gen_random_uuid()PK
case_iduuid○—cases.id参照(RESTRICT)
dealer_iduuid○—dealers.id参照(RESTRICT)。案件のdealer_idと一致必須(RPCで検証。案件側のdealer_idがNULLの場合のみ検証をスキップ)
statement_notext–NULL
issue_datedate–NULL
finance_receipt_iduuid–NULLfinance_receipts.id参照(RESTRICT)。同一案件・取消以外のみ許可
invoice_iduuid–NULLinvoices.id参照(RESTRICT)。同一案件のみ許可
credit_received_amountnumeric○0>= 0。信販入金額スナップショット
ve_share_amountnumeric○0>= 0。当社取り分スナップショット
adjustment_total_amountnumeric○0>= 0。調整明細(控除)の合計スナップショット
payout_amountnumeric○0credit_received_amount − ve_share_amount − adjustment_total_amount。負値は作成・訂正時にAPI層・RPC層の両方で拒否
scheduled_payout_datedate–NULL
actual_payout_datedate–NULL
actual_payout_amountnumeric–NULLNULLまたは>= 0
contract_date/delivery_datedate–NULL
statustext○'下書き'CHECK: 下書き/確定/支払済/取消
memotext–NULL
cancelled_at/cancel_reasontimestamptz/text–NULL
corrects_iduuid–NULL自テーブル参照(RESTRICT)

dealer_settlement_lines(仕切調整明細)

Section titled “dealer_settlement_lines(仕切調整明細)”
フィールド型必須既定値制約・備考
iduuid○gen_random_uuid()PK
dealer_settlement_iduuid○—dealer_settlements.id参照(RESTRICT)
sort_orderint○1
line_kindtext○—CHECK: credit_in/ve_share/transfer_fee/discount/offset/other
descriptiontext○—非空チェック
amountnumeric○0>= 0。調整種別(transfer_fee/discount/offset/other)は控除額を正数で保持し、payout_amount計算に合算する。credit_in/ve_shareは表示用行で合算対象外
memotext–NULL

明細はアプリ層で最大50件(MAX_LINES)に制限される(DB側に件数上限のCHECKはない)。品名・摘要(description)は最大200文字、memoは最大2000文字。

フィールド型必須既定値制約・備考
iduuid○gen_random_uuid()PK
case_iduuid○—cases.id参照(RESTRICT)
supplier_iduuid○—suppliers.id参照(RESTRICT)
order_iduuid–NULLorders.id参照(RESTRICT)。同一案件の発注のみ、かつ発注側supplier_idと一致必須(発注に仕入先が設定されている場合)。NULL可=案件単位の支払も許容し、同一order_idへの複数行も許容する
due_datedate–NULL
scheduled_amountnumeric○0>= 0
paid_datedate–NULL
paid_amountnumeric–NULLNULLまたは>= 0
statustext○'予定'CHECK: 予定/支払済/取消
memotext–NULL
cancelled_at/cancel_reasontimestamptz/text–NULL
corrects_iduuid–NULL自テーブル参照(RESTRICT)

信販入金が完了していることは仕入先支払登録の前提条件としない(supplier_paymentsはfinance_receiptsのステータスを参照しない独立系統)。

three_party_money_requests(3社間金銭APIの冪等リクエスト台帳)

Section titled “three_party_money_requests(3社間金銭APIの冪等リクエスト台帳)”
フィールド型必須既定値制約・備考
request_iduuid○—PK。クライアントのIdempotency-Keyヘッダとセッション情報からderiveThreePartyMoneyRequestIdが導出
actiontext○—MoneyActionのいずれか(下表)。非空チェック
case_iduuid–NULLcases.id参照(ON DELETE SET NULL)
resource_iduuid–NULL完了時に対象金銭レコードのIDを格納
statustext○—CHECK: PROCESSING/COMPLETED/FAILED
payload_hashtext○—md5(payload::text)。同一request_idで異なる内容が来た場合はREQUEST_ID_CONFLICT
error_code/error_messagetext–NULL失敗時のみ設定
responsejsonb–NULL完了・失敗時のレスポンス本体
created_at/completed_attimestamptz○/–now()/NULL

case_registration_requests(案件登録RPCの冪等リクエスト台帳)

Section titled “case_registration_requests(案件登録RPCの冪等リクエスト台帳)”
フィールド型必須既定値制約・備考
request_iduuid○—PK
case_iduuid–NULLcases.id参照(ON DELETE SET NULL)
statustext○—CHECK: PROCESSING/COMPLETED/FAILED
payload_hashtext○—md5(payload::text)
error_code/error_messagetext–NULL
responsejsonb–NULL
created_at/completed_attimestamptz○/–now()/NULL

create_case_registrationRPCから使用されるが、現行のapp/dealer/orders/new画面はこのRPCを呼び出していない(後述)。

case_settlements.settlement_typeの正式値は4つ。旧値は読み取り互換のエイリアスとしてresolveSettlementRuleが吸収する。DB側にはsettlement_typeの値を制限するCHECK制約は存在せず、正式値・エイリアスの判定はすべてアプリ側SETTLEMENT_RULESの設定に委ねられている。

正式値エイリアス(読取互換の旧値)
前金前金
売掛売掛、掛売
3社間決済3社間決済、ローン、三社間決済、3社間、三社間
カードカード

「その他」はレガシー値として保持されるが正式区分には含まれない。settlementTypeがいずれの正式値・エイリアスにも一致しない場合、WorkflowEngineはruleKey: null・currentState: "決済条件未設定"・canOrder: false・canInvoice: falseを返し、警告に「未対応の決済区分です」または「決済区分が未設定です」を積む。

当方のWholesaleCaseは決済区分を前金/掛売/三社間/カード/その他の5値で管理しており、ValueOSの正式値(前金/売掛/3社間決済/カード)とは表記が異なる。対応は本ページ末尾の対応表を参照。

evaluateWorkflow(ctx: WorkflowContext)は決済区分ごとの設定(SETTLEMENT_RULES)から次を導出し、WorkflowResultとして返す。

フィールド内容
canOrder発注可否。canOrderWhen条件配列がすべて真のとき真
canInvoice請求可否。canInvoiceWhen条件配列がすべて真のとき真
currentState / assignee / nextAction現在の工程表示。phases配列を先頭から走査し、completeWhenが未達成の最初のフェーズを返す(canOrder/canInvoiceとは別ロジック)
warnings発注・請求がブロックされている理由の文言(複数可、重複排除)
billingClosingDate / paymentDueDate売掛のみ算出する請求締め日・支払期日

canOrder/canInvoiceとcurrentStateは同じ条件セットを参照するが、評価の入口が異なる独立したロジックである。settlement_confirmed条件は決済区分が正式値に解決できた時点で常に真になる(別途「確定」操作を要しない)。

flowchart TD
    A["evaluateWorkflow(ctx)"] --> B{"resolveSettlementRule<br/>正式値/エイリアスに一致?"}
    B -- 不一致 --> B1["ruleKey=null<br/>currentState=決済条件未設定<br/>canOrder=false / canInvoice=false<br/>warning: 未対応の決済区分です / 決済区分が未設定です"]
    B -- 一致 --> C["settlementConfirmed = true"]
    C --> D["canOrder = canOrderWhen 全条件を評価"]
    C --> E["canInvoice = canInvoiceWhen 全条件を評価"]
    C --> F["resolveCurrentPhase:<br/>phases を先頭から走査し<br/>completeWhen 未達成の最初の phase を返す"]
    D --> G{"rule.datePolicy.trigger<br/>= all_orders_delivered?(売掛のみ)"}
    G -- Yes --> H["computeCreditDates:<br/>最終納品日 起点に<br/>billingClosingDate/paymentDueDate を算出"]
    G -- No --> I["billingClosingDate/paymentDueDate = null"]
    D --> J{"canOrder = false?"}
    J -- Yes --> K["orderBlockReason で警告文言を1件追加"]
    E --> L{"canInvoice = false かつ rule.key ≠ 売掛?"}
    L -- Yes --> M["invoiceBlockReason で警告文言を1件追加"]
    H --> N{"canInvoice = false?"}
    N -- Yes --> O["納品日欠損 or 未納品の個別警告を追加(売掛専用文言)"]
    K --> P["warnings を重複排除して返却"]
    M --> P
    O --> P
    I --> P
    F --> P
    P --> Q["WorkflowResult"]

決済区分別の発注可否・請求可否条件

Section titled “決済区分別の発注可否・請求可否条件”
決済区分canOrder条件canInvoice条件
前金入金確認(payment_confirmed_from_billing)決済区分の解決(実質常時true)
売掛決済区分の解決(実質常時true)案件内の全発注が納品済(all_orders_delivered)
3社間決済loan_statusが「承認済」construction_completed_date(完工日)が設定済
カードcard_statusが「決済成功」card_statusが「決済成功」(発注と同一条件)

前金は請求可否が実質常時trueになる点、カードは発注と請求が同一条件で同時に解放される点が、区分間で構造が異なる箇所である。

入金確認(payment_confirmed_from_billing)は、案件に紐づく入金(payments)のうち確認済みステータスの金額を合算し、deposit_amount(頭金予定額)が設定されていれば合計がそれ以上か、未設定なら合計が0円超かで判定する。全発注納品済(all_orders_delivered)は、キャンセル等を除いたアクティブな発注(lib/status/activeRecords.tsと同一の判定)が1件以上あり、かつ全件のstatusが「納品済」かつdelivered_dateが入力済みであることを要求する。発注が0件の案件は未達とみなす。

決済区分STEP1STEP2STEP3STEP4
前金決済条件未設定(経理)入金確認待ち(経理)発注可能(発注担当)—
売掛決済条件未設定(経理)発注可能(発注担当、全納品まで)請求可能(経理)—
3社間決済決済条件未設定(経理)ローン承認待ち(経理)完工報告待ち(営業事務)請求可能(経理)
カード決済条件未設定(経理)カード決済待ち(経理)発注・請求可能(発注担当)—

「決済条件未設定」フェーズはsettlement_confirmedが常時真であるため実質的に即時通過し、UI上で長く滞在することはない。

工程がもっとも長い3社間決済を例に、案件が辿る状態遷移を示す。

stateDiagram-v2
    [*] --> 決済条件未設定
    決済条件未設定 --> ローン承認待ち: settlement_confirmed(常時true・即通過)
    ローン承認待ち --> 完工報告待ち: loan_status = 承認済
    完工報告待ち --> 請求可能: construction_completed_date 設定
    請求可能 --> [*]

    note right of ローン承認待ち
        canOrder = true になるのも
        loan_status = 承認済 の時点
    end note
    note right of 完工報告待ち
        canInvoice = true になるのも
        completion_date_set の時点
    end note

発注・請求ブロック時の警告文言

Section titled “発注・請求ブロック時の警告文言”
状況警告文言
前金・入金未確認で発注不可前金の入金確認前のため発注できません(請求・入金データで判定)
3社間決済・ローン未承認で発注不可ローン未承認のため発注できません
カード・決済未成立で発注不可カード決済前のため発注できません
カード・決済未成立で請求不可カード決済前のため請求できません
3社間決済・完工日未登録で請求不可完工日登録前のため請求できません
売掛・納品済だがdelivered_date欠損納品日が登録されていません
売掛・未納品の発注が残っている売掛の請求は、案件内の全発注が納品済になってから可能です

売掛の請求不可は上記の個別メッセージのみを出し、汎用メッセージ(「現在の決済区分ルールでは請求できません」)は出さない実装になっている。

売掛(datePolicy.trigger: all_orders_delivered)は、案件内の全発注が納品済となった時点の最終納品日(アクティブな発注のdelivered_dateの最大値)を起点に、月末締め・翌月末払いでbillingClosingDate・paymentDueDateを自動算出する(endOfMonth/endOfNextMonth。UTC正午基準でタイムゾーン境界の日跨ぎを避ける)。他の決済区分にはこの日付ポリシーはない。

請求後の画面表示は原則invoices.due_date(保存値)を優先し、resolvePaymentDueDisplayが次のいずれかを返す。

kind条件表示ラベル
confirmed有効請求(取消以外)のdue_dateが存在(複数あれば最も早い日を採用)支払期限
planned有効請求のdue_dateがなく、ルール計算値(paymentDueDate)が存在入金予定日(予定)
noneいずれも存在しない、または売掛以外の決済区分表示なし

保存値がルール計算値と異なる場合はisMismatch: trueを返すのみで、自動UPDATEは行わない。案件横断でこの不一致を検出するfindCreditDueDateMismatchesも同じ比較ロジックを使うSELECT専用関数であり、backfillは提供しない。3社間決済にはこの表示ロジックを適用しない(ruleKey !== "売掛"で即座に空値を返す)。

決済条件の読み取りフォールバック(memo埋め込み)

Section titled “決済条件の読み取りフォールバック(memo埋め込み)”

case_settlementsにloan_status/card_status/construction_completed_date(cases側)の正式カラムが未適用な環境向けに、workflowMeta.tsがcase_settlements.memoへ__workflow_v1__:{"loan_status":...,"card_status":...,"construction_completed_date":...}という形式でメタデータを埋め込むフォールバックを持つ。buildWorkflowContextは正式カラムを優先し、nullのときのみmemo内のメタデータを参照する。書き込み時はwriteWorkflowMetaが既存メモ本文(メタ部分を除いた前半)を保持したまま末尾に追記する。

決済の読取方法が異なる2つの評価入口がある。

  • loadCaseWorkflow:クライアント/サーバー双方から使える汎用版。cases/orders/invoices/payments/決済を通常のSupabaseクライアント(publishable key)で読む
  • loadCaseWorkflowAdmin:サーバー専用(server-only)。決済(case_settlements)のみservice_role(admin)で読み、orders/invoices/paymentsは通常クライアントのまま読む。案件詳細画面と同じ読み方に揃えるための実装であり、case_settlementsのRLSがanon/authenticatedへ開放されている(後述)こととは独立に、サーバー経路では意図的にadmin読みへ寄せている

evaluateCaseWorkflowFromSettlementは決済行の「取得失敗」と「本当の未設定(行が存在しない)」を区別する。取得失敗(ok: false)はWorkflow評価そのものを行わずエラーを返し、「決済条件未設定」と誤表示しない。

sequenceDiagram
    participant UI as 案件詳細画面(サーバー)
    participant LCA as loadCaseWorkflowAdmin
    participant Admin as getCaseSettlementByCaseIdAdmin(service_role)
    participant DB as Supabase(publishable client)
    participant WF as evaluateCaseWorkflowFromSettlement / evaluateWorkflow

    UI->>LCA: caseId
    par 決済行の取得
        LCA->>Admin: case_settlements 取得(service_role)
    and 関連データの取得
        LCA->>DB: cases.construction_completed_date
        LCA->>DB: orders(id,status,delivered_date)
        LCA->>DB: invoices(id,status,invoice_amount)
        LCA->>DB: payments(id,invoice_id,status,payment_amount)
    end
    Admin-->>LCA: settlementResult(ok/エラー種別)
    DB-->>LCA: related データ or エラー
    LCA->>WF: settlementResult, related, relatedError
    alt relatedError あり
        WF-->>UI: ok=false, RELATED_READ_FAILED
    else settlementResult.ok = false
        WF-->>UI: ok=false, error_code(SETTLEMENT_READ_FAILED等)
    else 正常
        WF->>WF: buildWorkflowContext → evaluateWorkflow
        WF-->>UI: ok=true, result, settlementMissing(true/false)
    end

販売店発注(app/dealer/orders/new)の登録経路

Section titled “販売店発注(app/dealer/orders/new)の登録経路”

販売店発注フォームは4ステップ(案件情報→商品選択→決済区分選択→確認)で入力を受け付け、送信時にsaveDealerOrder.tsが次の順でSupabaseへ直接INSERTする。

  1. casesへ案件本体を作成する。ステータスは常に「新規受付」固定、case_noはVE-{timestamp}形式で発番する
  2. パッケージ発注ならcase_packages+case_package_items(パッケージ構成のスナップショット・仕入価格)、部材のみ発注ならcase_productsを作成する
  3. case_settlementsに選択された決済区分(settlement_type)を保存する
  4. いずれかのステップが失敗した場合はcleanupCaseが作成済みの行を逆順で削除し、中間状態の案件を残さない

この経路はブラウザから直接複数テーブルへ書き込む非トランザクションな実装であり、失敗時のクリーンアップで整合性を保っている。

sequenceDiagram
    participant Form as 販売店発注フォーム(STEP1-4)
    participant Save as saveDealerOrder
    participant DB as Supabase(publishable client)

    Form->>Save: caseForm, productForm, settlementType
    Save->>DB: dealers から dealer_id 解決(resolveDealerId)
    alt 入力名に一致する有効な販売店なし
        DB-->>Save: 最初に作成された有効な販売店へフォールバック
    end
    Save->>DB: cases へ INSERT(status=新規受付, case_no=VE-{timestamp})
    alt パッケージ発注
        Save->>DB: case_packages へ INSERT
        Save->>DB: package_items 取得 → case_package_items へ INSERT(仕入価格スナップショット)
    else 部材のみ発注
        Save->>DB: case_products へ INSERT(仕入価格スナップショット)
    end
    Save->>DB: case_settlements へ INSERT(settlement_type そのまま保存)
    alt いずれかのINSERT失敗
        Save->>DB: cleanupCase(作成済み行を逆順DELETE)
        Save-->>Form: ok=false, errorMessage
    else 全件成功
        Save-->>Form: ok=true, caseId, caseNo
    end

販売店解決のフォールバック挙動

Section titled “販売店解決のフォールバック挙動”

resolveDealerIdは入力されたdealer_name(画面初期値はダミー文字列)と完全一致する有効な販売店を検索し、見つからない場合は作成日時が最も古い有効な販売店へ黙って(エラーなしに)フォールバックする。販売店名の候補入力・選択UIがないため、この経路で作成された案件のdealer_idは入力内容と一致しない可能性がある。仕入価格(supplierId)の解決も同様に、案件のdealer_idに紐づく既定仕入先(resolveDealerDefaultSupplierId)が未設定の場合は価格スナップショットを0円のまま保存し、案件作成自体は継続する(コンソール警告のみ)。

STEP3(決済区分選択)の入力値とWorkflowEngine正式値の対応

Section titled “STEP3(決済区分選択)の入力値とWorkflowEngine正式値の対応”

STEP3で選択できる決済区分はDEALER_ORDER_SETTLEMENT_TYPES(掛売/ローン/現金/カード/その他の5値)であり、WorkflowEngineの正式値(前金/売掛/3社間決済/カード)とは選択肢の構成そのものが異なる。入力値はそのままcase_settlements.settlement_typeにINSERTされ、以降resolveSettlementRuleが正式値へ解決する。

STEP3の選択肢resolveSettlementRuleでの解決
掛売売掛(エイリアス一致)
ローン3社間決済(エイリアス一致)
カードカード(正式値)
現金未対応(ruleKey: null、canOrder/canInvoiceとも常時false)
その他未対応(ruleKey: null、canOrder/canInvoiceとも常時false)

「前金」はSTEP3の選択肢に存在せず、この販売店発注フォームからは前金の案件を作成できない。「現金」「その他」を選択して作成した案件はresolveSettlementRuleがいずれの正式値・エイリアスにも一致せずruleKey: nullを返すため、発注・請求ともにブロックされた状態(警告「未対応の決済区分です: 現金」等)で登録される。案件詳細側で決済区分を正式値へ手動修正しない限りこの状態は解消しない。

create_case_registration RPC(未接続の冪等登録経路)

Section titled “create_case_registration RPC(未接続の冪等登録経路)”

これとは別に、DB側には冪等な案件登録用のcreate_case_registrationRPC(SECURITY INVOKER・service_role専用実行権限・request_idとpayload_hashによる冪等性)と、その結果を記録するcase_registration_requestsテーブルが用意されている。この経路はservice_role経由のトランザクション登録用として設計されており、現行のapp/dealer/orders/new画面からは呼び出されていない。案件起票の正本は現時点で「フォーム→直接INSERT」であり、RPC経路は未接続のバックエンド機能として存在する。

このRPCは複数バージョンを経て仕様が変わっており、フォーム側との差分に注意が必要である。

  • 決済区分の検証がフォームより厳格:Ver1.2(20260801160000)以降、settlement_typeは前金/売掛/3社間決済/カードの4値のみ受理し、それ以外(その他や旧エイリアス)はINVALID_INPUTで登録自体を拒否する。STEP3の掛売/ローン/現金/カード/その他という5値をそのまま渡すとエイリアス変換なしに弾かれるため、仮に将来この画面をRPC経由へ切り替える場合はSTEP3の選択肢自体を正式値へ合わせる必要がある
  • 決済区分別の必須項目:3社間決済はfinance_company(信販会社名)とapproval_number(承認番号)を必須、カードはcard_brand(カード会社名)を必須とし、区分に応じた項目のみ保存する(該当しない項目はNULLで保存)
  • 登録時は価格を持たない:明細(lines、1〜100件)はproduct_idまたはpackage_id(XOR)とquantity(1〜9999の整数)のみを受け取り、クライアントからの単価入力は受け付けない。旧バージョン(Ver1.0/1.1)はsales_prices/purchase_pricesをサーバー側でorder_received_date基準に取得してスナップショットしていたが、現行版(Ver1.2)はマスタ価格を参照せず(「supplier/価格は常にNULL。マスタ価格は参照しない」)、case_productsの価格関連列をNULLのまま保存し、価格確定は後続の発注フローに委ねる設計へ変わっている
  • PACKAGE明細の構成0件は拒否:package_itemsが1件も無い(かつis_hidden=false)パッケージはPACKAGE_ITEMS_NOT_FOUNDで登録全体を失敗させる
  • 手動価格は不可:is_manual_priceがtrueのペイロードはINVALID_INPUTで即座に拒否する
  • 内部のUUID・SQLエラー・スタックトレースはレスポンスに含めず、error_codeはINVALID_INPUT/PACKAGE_ITEMS_NOT_FOUND/REQUEST_ID_CONFLICT/REGISTRATION_FAILEDのいずれかに正規化される

三社間の金銭フロー(信販入金・仕切精算・仕入先支払)

Section titled “三社間の金銭フロー(信販入金・仕切精算・仕入先支払)”

3社間決済の案件は、発注・請求可否のワークフローとは独立に、次の3系統の金銭実体を持つ。いずれもexecute_three_party_moneyRPCへのアクションとして実行され、moneyActionsLogic.tsが入力検証・正規化を行う。

アクション対象テーブル主な操作
finance_receipt.*finance_receiptscreate(入金予定登録)→confirm(実入金確認:実績日・実績額)。cancel/correctで取消・訂正
dealer_settlement.*dealer_settlements + dealer_settlement_linescreate(仕切清算書作成)→confirm(確定)→pay(実績支払日・実績支払額)。cancel/correctで取消・訂正
supplier_payment.*supplier_paymentscreate(支払予定登録)→pay(実績支払日・実績支払額)。cancel/correctで取消・訂正

想定される進行順序は「ローン承認(loan_status=承認済)→発注→工事完了(construction_completed_date)→請求→信販会社からの入金(finance_receipt)→販売店への仕切精算(dealer_settlement)/仕入先への支払(supplier_payment)」である。信販入金・仕切精算・仕入先支払の3系統は互いに独立したアクション列であり、dealer_settlements.finance_receipt_id・invoice_idで該当する信販入金・請求と任意に紐付けられる。

アクションごとの遷移前提とエラー

Section titled “アクションごとの遷移前提とエラー”

RPC(execute_three_party_money)は各アクションの実行前に対象行をFOR UPDATEでロックし、状態遷移が不正なら例外(APP:<code>:<message>形式)を投げてロールバックする。主なガードは次のとおり。

アクション遷移前提主なエラー
finance_receipt.confirm対象が予定であること取消済→CONFLICT、入金済済→IMMUTABLE(訂正へ誘導)
finance_receipt.correct対象が取消でないこと取消済→CONFLICT
dealer_settlement.confirm対象が下書きであること取消済→CONFLICT、確定/支払済→IMMUTABLE
dealer_settlement.pay対象が確定であること下書き→CONFLICT(先に確定が必要)、支払済→IMMUTABLE、取消→CONFLICT
dealer_settlement.create/correctdealer_idが案件のdealer_idと一致、finance_receipt_id/invoice_id(指定時)が同一案件かつ取消以外、payout_amount ≥ 0不一致・負値はいずれもINVALID_INPUT
supplier_payment.pay対象が予定であること取消→CONFLICT、支払済→IMMUTABLE
supplier_payment.create/correctorder_id(指定時)が同一案件の発注、かつ発注側supplier_idが設定されていれば一致必須不一致はINVALID_INPUT
stateDiagram-v2
    [*] --> 予定: finance_receipt.create
    予定 --> 入金済: confirm
    予定 --> 取消: cancel
    入金済 --> 取消: cancel
    予定 --> 予定: correct(元を取消し新規予定を作成)
    入金済 --> 予定: correct(元を取消し新規予定を作成)
    取消 --> [*]
stateDiagram-v2
    [*] --> 下書き: dealer_settlement.create
    下書き --> 確定: confirm
    確定 --> 支払済: pay
    下書き --> 取消: cancel
    確定 --> 取消: cancel
    支払済 --> 取消: cancel
    下書き --> 下書き: correct(元を取消し新規下書きを作成)
    確定 --> 下書き: correct
    支払済 --> 下書き: correct
    取消 --> [*]
stateDiagram-v2
    [*] --> 予定: supplier_payment.create
    予定 --> 支払済: pay
    予定 --> 取消: cancel
    支払済 --> 取消: cancel
    予定 --> 予定: correct(元を取消し新規予定を作成)
    支払済 --> 予定: correct
    取消 --> [*]

いずれのテーブルも物理DELETEを行う権限をservice_roleに付与していない(GRANTはSELECT, INSERT, UPDATEのみ)。取消・訂正は状態値の更新と新規行の追加で表現し、履歴を必ず残す。

dealer_settlement.create時、払戻額(payout_amount)は次の式で自動計算される。

payout_amount = credit_received_amount − ve_share_amount − Σ(調整明細のamount)

調整明細(dealer_settlement_lines.line_kind)はtransfer_fee(振込手数料)・discount(値引)・offset(相殺)・other(その他控除)の4種で、控除額を正数で保持する。credit_in(信販入金額行)・ve_share(当社取り分行)は明細に計上できるが調整合計には含めない。金額はすべて円単位の整数として扱い、入力値はfloor(切り捨て)で丸めてから演算する(アプリ層のfloorMoneyとRPC側のfloor()が同じ丸め規則)。計算結果payout_amountが負になる登録・訂正は、アプリ層のバリデーション(parseDealerCreateBody)とRPC側の両方で拒否される(二重ガード)。

参考として、仕切初期額(調整なし)はsuggestedDealerPayoutFromFinanceが「信販入金額 − 有効請求額合計(取消請求を除く)」で算出する初期値提案のロジックを持つが、これは画面入力の初期値補助であり、確定計算式(credit_received_amount − ve_share_amount − Σ調整)そのものではない。振込手数料はこの初期提案には含めない。

すべての3社間金銭アクションはrequest_id(クライアント発行のUUID、Idempotency-Keyヘッダとセッションから導出)を伴い、three_party_money_requestsにpayload_hash(キー順正規化したJSONのMD5ハッシュ)とともに記録される。RPC内部ではpg_advisory_xact_lockで同一request_idのリクエストを直列化したうえで、次の分岐で冪等性を担保する。

  • 既存レコードなし → PROCESSINGで新規INSERTし、本処理へ進む
  • 既存レコードのpayload_hashが今回と異なる → REQUEST_ID_CONFLICT(同じキーで違う内容は拒否)
  • 既存レコードがCOMPLETED → 本処理を実行せず、保存済みレスポンスをidempotent_replay: trueで返す(再送安全)
  • 既存レコードがPROCESSING → REQUEST_IN_PROGRESS(同時実行中の重複送信を拒否)
  • 既存レコードがFAILED → PROCESSINGへ戻して再実行を許可する(同一request_idでの再試行が可能)

確定済み金額の直接UPDATEは扱わず、訂正はcorrectアクション(新しい内容で作り直し、corrects_idで元レコードを参照し、元レコードを同一トランザクション内で取消にする)、取消はcancelアクション(cancel_reasonを記録)という形で、常に監査可能な履歴として残す設計である。訂正時にcancel_reason未指定の場合はRPCが既定値「訂正のため取消」を自動設定する。ledgerの更新(COMPLETED/FAILED確定)と業務テーブルの更新は同一トランザクションで行われ、PROCESSINGのまま取り残される状態は発生しない。

金銭アクション実行のシーケンス

Section titled “金銭アクション実行のシーケンス”
sequenceDiagram
    participant UI as 画面
    participant GW as API Route(runMoneyGateway)
    participant Val as validateMoneyActionInput
    participant Exec as executeMoneyAction(service_role)
    participant RPC as execute_three_party_money(RPC)
    participant DB as finance_receipts/dealer_settlements/...

    UI->>GW: POST(Idempotency-Key ヘッダ必須)
    GW->>GW: オリジン検証 / セッション検証 / CSRF検証 / Content-Type検証
    alt いずれか失敗
        GW-->>UI: 401/403/415(安全なエラーDTO)
    end
    GW->>GW: deriveThreePartyMoneyRequestId(session, Idempotency-Key)
    GW->>Val: action, caseId, resourceId, body
    alt 入力不正
        Val-->>GW: INVALID_INPUT + field_errors
        GW-->>UI: 400
    end
    GW->>Exec: requestId, ValidatedMoneyAction
    alt finance_receipt.create
        Exec->>DB: 有効な予定/入金済の既存行チェック(二重登録ガード)
        alt 既存あり
            Exec-->>GW: CONFLICT
        end
    end
    Exec->>RPC: execute_three_party_money(payload)
    RPC->>RPC: advisory lock → ledger冪等判定 → 業務更新 → ledger COMPLETED/FAILED(同一tx)
    RPC-->>Exec: ok / error_code
    Exec-->>GW: ExecuteMoneyActionResult
    GW->>GW: toSafeMoneyError/toSafeMoneySuccess(内部情報のサニタイズ)
    GW-->>UI: 200 or エラーステータス

toSafeMoneyErrorは、レスポンスに含めるerror_messageがUUID・SQL文・constraint/pg_/SQLSTATE/permission denied等の内部情報を含んでいないか、200文字以内かを検査し、該当する場合は「処理を完了できませんでした」という汎用メッセージへ差し替える。error_codeも許可リスト外の値はACTION_FAILEDへ正規化する。

finance_receipt.createはRPC本体のCHECK制約ではなく、executeMoneyActionWithClient内のアプリケーションコードが、対象案件のfinance_receiptsに有効な行(入金済または予定)が既に存在するかを事前SELECTで確認し、存在すればCONFLICTで拒否する(マイグレーションレベルのユニーク制約は無い)。「予定」が残っている場合は「入金済にするか取消してから登録してください」、「入金済」の場合は「二重登録はできません」とメッセージが分かれる。

DB上のステータスは最小集合とし、期限超過などの表示状態は登録時の日付から都度導出する。

エンティティDB値表示値
finance_receipts予定 / 入金済 / 取消未入金 / 入金予定 / 入金済 / 期限超過 / 取消
dealer_settlements下書き / 確定 / 支払済 / 取消下書き / 支払予定 / 支払済 / 期限超過 / 取消
supplier_payments予定 / 支払済 / 取消支払予定 / 支払済 / 期限超過 / 取消

期限超過は「予定日(scheduled_date/scheduled_payout_date/due_date)が当日より前」で判定し、当日中は超過扱いにしない。取消済み行は集計から除外する。finance_receiptsのみ、予定日が未設定かつ予定ステータスのとき「未入金」という追加の表示値を持つ(dealer_settlements/supplier_paymentsにこの区分はない)。

実質回収額・未入金残高の算出

Section titled “実質回収額・未入金残高の算出”

案件の資金回収状況はcomputeThreePartyRecoveryAmountsが次式で導出する(finance_receipts/dealer_settlementsを正とし、paymentsとは混ぜない)。

実質回収額 = 入金済の信販実入金額(未登録なら0) − 支払済の販売店支払額
未入金残高 = 有効請求額合計(取消請求を除く) − 実質回収額

販売店支払額は支払済のactual_payout_amount(実績)を優先し、実績が無い場合のみpayout_amount(確定額)にフォールバックする。

対象権限方式実装根拠
case_settlementsRLS無効化+anon/authenticated双方にFOR ALL USING(true) WITH CHECK(true)の開放ポリシー+GRANT SELECT,INSERT,UPDATE,DELETE。ブラウザの publishable key から直接CRUD可能20260724170000_fix_case_settlements_rls.sql
finance_receipts / dealer_settlements / dealer_settlement_lines / supplier_paymentsRLS有効化のうえポリシー0件(anon/authenticatedは明示REVOKE ALL)。service_roleにのみSELECT,INSERT,UPDATE(DELETE権限なし)20260810150000_three_party_money_events.sql
three_party_money_requestsRLS無効化だがanon/authenticatedはREVOKE ALL。service_roleにのみSELECT,INSERT,UPDATE20260810160000_three_party_money_request_ledger.sql
execute_three_party_money(RPC)SECURITY INVOKER・search_path固定。EXECUTEはanon/authenticatedから明示REVOKE、service_roleのみGRANT20260810161000_execute_three_party_money_rpc.sql
case_registration_requestsRLS無効化。anon/authenticatedはREVOKE ALL。service_roleにのみSELECT,INSERT,UPDATE20260726210000_case_registration_requests.sql
create_case_registration(RPC)SECURITY INVOKER・search_path固定。EXECUTEはservice_roleのみ20260726210100_create_case_registration_rpc.sql

3社間金銭系のテーブル・RPCはDB権限だけでなく、Next.jsのAPI Route(runMoneyGateway)でオリジン検証・セッションCookie検証・CSRF検証・Idempotency-Key必須化までを行い、ブラウザへservice roleキーを渡さない構成になっている。一方でcase_settlements(決済区分そのものを含む)はDBレベルではanon/authenticatedに無条件で開放されており、書き込みを制限するアプリケーション側の認可チェック(誰が決済区分やローン/カードステータスを変更してよいか)は本ページで参照した範囲のコードには見当たらない。

SETTLEMENT_RULESのphasesが持つassignee(経理/発注担当/営業事務)は画面表示用のラベルであり、その担当者しか操作できないようにするアクセス制御ではない。ロールベースの権限管理(担当者以外の操作を防ぐ仕組み)は本ページで確認した実装の範囲では未実装である。

  • 未対応の決済区分:settlement_typeが正式値・エイリアスのいずれにも一致しない場合(STEP3の「現金」「その他」を含む)、ruleKey: nullで発注・請求とも恒久的にブロックされる。案件詳細で決済区分を手動修正しない限り解消しない
  • 発注0件:areAllOrdersDelivered/areAllOrderStatusesDeliveredはアクティブな発注が0件のとき「未達」を返す(売掛の請求は永久に不可のまま)
  • delivered_date欠損の納品済発注:status=納品済だがdelivered_dateが空の発注が1件でもあると、売掛の請求は個別警告「納品日が登録されていません」でブロックされる
  • 決済行そのものが存在しない案件:evaluateCaseWorkflowFromSettlementはsettlementMissing: trueとして区別し、DB取得エラー(ok: false)とは異なる扱いにする。呼び出し側はこの2つを混同して「未設定」と即断してはならない
  • 請求期日の保存値とルール計算値の不一致:resolvePaymentDueDisplay/findCreditDueDateMismatchesはSELECTで不一致を検出するのみで自動修正しない。運用上は個別に案件を確認して手動修正する前提
  • 3社間金銭アクションの二重送信:同一Idempotency-Key(→同一request_id)で内容が同じ再送はidempotent_replay: trueで安全に処理される。内容が異なればREQUEST_ID_CONFLICT、処理中の重複送信はREQUEST_IN_PROGRESS
  • 信販入金の二重登録:案件に有効なfinance_receipts(予定/入金済)が既にある状態でのcreateはCONFLICT
  • 仕切清算の負の払戻額:調整額の合計が信販入金額と当社取り分の差を超える入力は、アプリ層・RPC層の双方でINVALID_INPUTとして登録前に拒否される
  • 確定済み金銭イベントの直接変更:confirm/pay済みの行に対する再度のconfirm/payはIMMUTABLE。変更するにはcorrect(新規作成+元取消)を使う
  • dealer_settlement.payの前提未達:下書きのままpayを呼ぶとCONFLICT(先にconfirmが必要)
  • 販売店・信販入金・請求の不一致参照:dealer_settlement.create/correctで指定したdealer_idが案件のdealer_idと異なる、またはfinance_receipt_id/invoice_idが別案件のものを指す場合はINVALID_INPUT
  • 仕入先支払と発注の不一致:supplier_paymentのorder_idが別案件の発注を指す、または発注に設定済みのsupplier_idと異なるsupplier_idを指定した場合はINVALID_INPUT
  • 販売店発注フォームの販売店名フォールバック:入力(初期値はダミー文字列)に完全一致する有効な販売店が見つからない場合、resolveDealerIdはエラーにせず最も古い有効な販売店へ黙って差し替える。案件のdealer_idが意図と異なるまま登録され得る
  • 仕入価格マスタ未設定:販売店発注フォームで既定仕入先(default_supplier_id)が未設定の場合、価格スナップショットは0円のまま保存され、案件作成自体は継続する(後続の仕入発注画面での補完が前提)
  • 販売店発注フォームの途中失敗:STEP4送信中にいずれかのテーブルへのINSERTが失敗すると、cleanupCaseが作成済み行を逆順DELETEする。ただしこれは単一トランザクションではなく、クリーンアップ自体が失敗した場合の後始末は実装上保証されない
  • create_case_registrationとSTEP3選択肢の不整合:RPC(Ver1.2)は正式4値以外をINVALID_INPUTで即拒否するため、現行STEP3の選択肢(掛売/ローン/現金/カード/その他)をそのまま渡すと大半が弾かれる。この画面をRPC接続へ切り替える場合はSTEP3側の見直しが前提になる
観点当方(WholesaleCase)ValueOS
決済区分の値前金 / 掛売 / 三社間 / カード / その他(5値)前金 / 売掛 / 3社間決済 / カード(正式4値、旧値はエイリアス吸収)
発注可否・請求可否の判定wholesaleFlow(工程配列上の位置関係から導出)WorkflowEngine.evaluateWorkflow(決済区分別の条件配列canOrderWhen/canInvoiceWhen)
承認番号・審査情報WholesaleCase.approvalNo(単一・三社間とカードで共用)case_settlements.finance_company/approval_number(3社間決済)、card_brand/card_status(カード)に分離
入金消込案件のpayments[]を残高計算方式で合算前金はpayments確認済み合計とdeposit_amount比較、売掛は納品完了トリガーで締め日算出、3社間決済はfinance_receiptsが信販入金を個別管理
三社間の下流精算未実装(案件メモでの運用)dealer_settlements(販売店への仕切精算)・supplier_payments(仕入先への支払)として構造化。冪等RPC+advisory lockで多重実行を防止
販売店発注の起票DealerIntakeForm送信→WholesaleCaseをクライアント側で生成app/dealer/orders/new→saveDealerOrder.tsが直接INSERT。冪等RPC(create_case_registration)はDB側に存在するが未接続、かつ受理する決済区分の値が現行フォームの選択肢より厳格
権限モデル未確認(本ページ対象外)決済区分(case_settlements)はDBレベルでanon/authenticatedに開放。3社間金銭系はservice_role限定+アプリ側ゲートウェイで多重防御

差分の全体像はredesign/valueos-gapを参照。

  • redesign/valueos-gapの「ValueOSには決済区分でフローを制御するロジックが無い」という記述は、本ページで確認したWorkflowEngineの実装内容(決済区分別のcanOrder/canInvoice宣言的判定)と齟齬がある。記述の更新要否を確認する
  • create_case_registrationRPC(冪等な案件登録経路)をapp/dealer/orders/newから呼び出す設計へ移行する要否。移行する場合、STEP3の決済区分選択肢(掛売/ローン/現金/カード/その他)をRPCが受理する正式4値へ揃える必要がある。現行はsaveDealerOrder.tsによる直接INSERTで運用されている
  • 3社間金銭アクション(finance_receipt/dealer_settlement/supplier_payment)の呼び出し導線(社内のどの画面・どの担当者が実行するか)の確認
  • 販売店発注STEP3で「現金」「その他」を選択した案件が発注・請求ともに永続ブロックされる状態を仕様として許容するか、STEP3の選択肢自体をWorkflowEngine正式値(前金/売掛/3社間決済/カード)に合わせて見直すかの確認
  • case_settlements(決済区分・ローン/カードステータスを含む)がDBレベルでanon/authenticatedに無条件で開放されている点について、アプリケーション側で意図した認可制御(担当者以外は変更不可等)が別途あるのか、それとも運用上のリスクとして許容しているのかの確認
  • 販売店発注フォームのresolveDealerIdフォールバック(入力名に一致する販売店が無い場合に別の販売店へ差し替える挙動)を仕様として許容するか、販売店選択UIを設けて明示選択に変える必要があるかの確認