月次請求 (AR)
本書は AGENTS.md の分冊(正典・AGENTS.md と同格)。優先順位は「実コード → AGENTS.md(本書を含む)→ DESIGN.md → PRODUCT.md」。 節は AGENTS.md から逐語で移設した(PR #nnn・2026-09-16)。対象パス:
apps/web/src/lib/billing/**` `packages/shared/src/lib/invoice-payment-status.ts` `supabase/migrations/*billing*。この領域に触るワーカー / レビュアは本書を読む。
AR Stage 2 月次請求書プレビュー(不変条件ラチェット・mig202)¶
月次請求書プレビューは、締め日・段別課金・二次搬出運搬費・掛売持ち込みをライブ導出して画面と 下書き Excel(.xlsx) に表示する Stage 2 である。本 Stage 2 は表示専用で、請求書の確定・スナップショット・ 採番は Stage 3(mig203)、入金の記帳は Stage 4(mig217)が担う。送付(メール)は未実装(通知基盤待ち)。
- 無保存: プレビューと下書き Excel(.xlsx) の生成は DB に請求書・明細・調整行を保存しない。
collector_billing_settingsは発行者設定専用であり、請求結果を保存する表ではない。tenantsへ登録番号や発行者情報の列を追加してはならない (越境 SELECT があり、将来の非公開請求情報を露出させるため)。 - 表示と出力の列: プレビュー画面は日付と明細を列分けして表示する(
CollectorBillingPreviewPage→buildInvoiceExcel)。 - 税抜と税丸め: 持ち込み金額、段別課金単価、二次搬出固定運搬費はすべて税抜として扱う。税額は現状 10% を
税率単位で一度だけ計算し、取引先の
partners.tax_rounding(既定floor)で丸める。負の小計も 符号規則を変えず、floor はゼロ方向、round は half-away-from-zero、ceil はゼロから遠い方向である。 - 請求帰属: 二次搬出運搬費は排出元
collection_sites.partner_idの取引先へ帰属し、期間判定はcompleted_atの JST 業務日で行う。route は既存のresolveSecondaryFeeState/resolveActiveSecondaryRouteを再利用し、未登録は警告として合計から隠さない。 - 重量帰属: 段別課金 usage の Σkg は
fetchAggregationForPeriod→aggregateByPartnerAndItem系を 必ず経由する。run の item-level partner/site 優先という mig101 CR14 の規則を請求側で再実装してはならない。 取引先ごとに締め期間が異なるため、集計 range も請求期間単位で分離する。 - 取得上限: 請求プレビューの一覧取得はすべて
fetchAllPagesとidtiebreaker を使う。主キー 1 件取得は.maybeSingle()で終端(ratchet のSAFE_TERMINALS)・マーカー不要(孤立マーカーは違反)。 -
権限:
/collector/billing/previewは collector の admin/normal、発行者設定の編集は admin のみ。collector_billing_settingsは自テナント staff の SELECT、admin の INSERT/UPDATE のみを許可し、DELETE policy と service-role policy を作らない。登録番号はT+ 13 桁の CHECK を維持する。 -
請求 usage の重量集計: usage は
weighing_kind in ('single','run')に限定する。walk-in の重量を混ぜると伝票行と二重請求になり、spot 現金精算済みの重量も再計上される。 - 明細行の金額: 金額は円整数へ丸める(mig111 と同規約)。小数単価 × 小数 kg の計算途中の端数を小計へ持ち込まない。
- SaaS 課金 API/DB の admin 限定(mig265): SaaS 課金 API(
GET /billing/subscription・POST /billing/checkout)とsubscriptionsの SELECT は、Web / API / DB の三層とも collector|discharger の admin だけ(DB はcurrent_tenant_type() in ('collector','discharger')を policy に含む・role / tenant_type の NULL は fail-closed で除外)。POST /billing/webhookは認証を付けず Stripe 署名検証のみ。subscriptionsの SELECT policy はtenant can read own subscription1 本を再定義し 2 本目を足さない(permissive OR)。API は service-role で読むため RLS は API の執行点ではない。
買取精算(不変条件ラチェット・mig242/mig250)¶
- 取消は admin 限定の soft-delete、支払対象は active かつ finalized の精算だけとする。
- 同一 partner/period の取消済み finalized を再確定するときは、recompute が
supersedes_settlement_idを自動設定し、後継帳票へ旧 statement_no の取消を表示する。 - 切替下限は recompute・finalize・restore・adjustment の4経路で同一の締め日由来
period_fromoracle を使い、2026-10-01 より前を fail-closed にする。
AR Stage 3 請求書確定スナップショット(不変条件ラチェット・mig203)¶
確定帳票は billing_invoices / billing_invoice_lines、設定と算出エンジンは既存の
collector_billing_settings / lib/billing/invoice-preview.ts とする。AP の invoice-ocr、
external_partner_settlements、walk-in の支払 timing とは別概念であり、新表・新 RPC は
billing_invoice(s) 命名へ統一する。
- 信頼モデル: 確定は admin の自テナント操作で、web の
buildInvoicePreviewが作った snapshot をfinalize_billing_invoice(SECURITY DEFINER)へ渡す。RPC はΣ(lines.amount)=subtotal_ex_tax、 snapshot のtax_roundingによる税の丸め、total=subtotal+taxを SQL 側でis distinct fromにより厳密再検証し、NaN / ±Infinity と警告行を拒否する。将来の server-authoritative 算出は別スライス。 DEFINER 化は必須(INVOKER 化禁止): mig094(settlement finalize)と同思想で、確定/明細の書込は DEFINER RPC が RLS をバイパスして行い、billing_invoices/billing_invoice_linesは INSERT/UPDATE の書込 RLS policy を持たない(SELECT のみ staff に開ける)。旧版は 「INVOKER +app.bypass_*GUC ゲートの INSERT policy」だったが、client に請求書の書込経路を残す 設計そのものが RPC の Σ/税/採番検証を迂回し得るため、DEFINER + 書込 policy 無しへ変更して経路を 完全に封鎖した。app.*GUC が client から任意設定できることを前提にしてはならない。既存の bypass-GUC guard(walk_in / vdl / jwnet など)は別の不変条件を守っており、安全なので削除・弱体化してはならない。 GUC ゲートの書込 policy を再導入してはならない。 - 凍結:
closing_day/issuer_name/invoice_registration_number/tax_rounding/ 振込先5列(bank_name/branch_name/account_type/account_number/account_holder・mig213) と金額・明細は確定時の snapshot として保存し、mig202 の設定変更に追随させない。振込先は mig213 でcollector_billing_settingsとbilling_invoicesの両方へ加え、finalize_billing_invoiceがissuer_nameと同じ snapshot 経路で copy し、billing_invoices_guard_finalizedの immutable 検査に 5列を並置して確定後の改変を raise する。account_typeは settings 側(mig213)と snapshot 側billing_invoices(mig214)の両方に名前付き CHECK('普通','当座')を張る(issuer_name は 自由 text だが account_type は enum ドメインなので snapshot も domain を保つ多層防御・ table CHECK なので finalize DEFINER 含む全書込経路に効く。歯はscripts/proofs/mig214-billing-invoice-account-type-check.sql)。 歯はscripts/proofs/mig213-invoice-bank-transfer.sql(5列 copy・guard の exact message + ROW_COUNT・ CHECK は別制約が先に落ちない値で constraint_name 照合)。 - 器の不変性:
billing_invoices/billing_invoice_linesは UPDATE/INSERT 書込 policy が無いので client(authenticated)の直書きは RLS で不可(UPDATE は 0 行・値不変)。多層防御として finalized guard(authenticated の finalized 列変更を raise)と明細 client 直 INSERT の guard (current_user in ('authenticated','anon')で常時 raise・GUC carve-out なし)も残す。ヘッダ/明細の 物理 DELETE はblock_physical_delete_legalで拒否する。取消・復元は admin 限定 DEFINER RPC のsoft_delete_billing_invoice/restore_billing_invoice(owner=postgres で書込むので guard 非発火)のみ。 - 重複・採番・越境:
(tenant_id, partner_id, period_from, period_to) where deleted_at is nullの partial unique と RPC 先回りチェックで重複確定を防ぎ、next_billing_invoice_seqが tenant/月の行ロックでYYYYMM-0001形式を採番する。採番関数のp_tenantはcurrent_tenant_id()とIS DISTINCT FROMで比較し、NULL と越境を fail-closed にする。billing_invoices の tenant と finalized_by は fill trigger が server authority として上書きする。 - web: 警告が残るプレビューは確定ボタンを無効化し、admin の ConfirmDialog を通す。確定後は
一覧とプレビューを invalidate し、確定済み一覧・明細取得は
fetchAllPages+idtiebreaker を使う。 確定請求書はbuildInvoiceExcel(exceljs 動的 import)で編集可能な .xlsx を出力し、社判画像をinvoice-seal-imagesバケットから埋め込む(mig212)。PDF 経路(invoicePreviewPdf)は撤去。invoice_no/finalized_at/ 凍結発行者情報の印字要件は Excel 側で維持する。 - 回帰の権威:
scripts/proofs/mig203-billing-invoice-finalize.sqlは admin allow、normal/driver/ role-less deny、越境、Σ/税不一致、警告、重複、確定後改竄(UPDATE 0 行・値不変)、物理削除、 soft-delete/restore、採番越境を固定する。歯: case 18/18b が「admin が両app.bypass_*GUC を立てて 直接 INSERT しても header は RLS で、明細は guard で拒否される」を実証する(INVOKER + GUC ゲート policy へ戻す変異体で case 18 が赤化することを確認済み)。
AR Stage 4 請求書入金記録(不変条件ラチェット・mig217)¶
確定済み billing_invoices へ複数回・一部入金を追記し、未収残高と状態を表示する。入金行は
billing_invoice_payments に限定し、mig203 の billing_invoices / billing_invoice_lines、finalized guard、
physical-delete block、確定 RPC は変更しない。メール送付は別スライスである。
- 追記型・権限: 入金の記帳は
admin+normalの staff に許可する。訂正は admin 限定の DELETE → 再記録に 限定し、billing_invoice_paymentsに UPDATE policy を作らない。driver / anon / 越境 tenant は deny する。 - authority fill:
BEFORE INSERT OR UPDATE全列の DEFINER fill trigger がinvoice_idの変更を拒否し、親が 同一テナント・status='finalized'・deleted_at is nullであることを fail-closed に検証する。tenant_idは 親 invoice から再導出し、client 供給値を上書きする。created_byは profiles FK に対応するfill_audit_created_by()で server-owned とし、created_atとともに UPDATE では OLD を保持する。 - 金額:
amountは正の有限numericのみ許可し、NaNと ±Infinityを named CHECK で拒否する。過入金は 金額行の合計が請求合計を超える状態として表現する。 - shared authority:
packages/shared/src/lib/invoice-payment-status.tsのpaymentTotals(有限値のみ合算)、paymentStatus(円単位の厳密比較)、outstandingBalanceを入金状態判定の単一権威とする。UI や DB hook に 判定式を複製しない。 - web: 確定済み一覧は
fetchAllPages+(paid_on, id)tiebreaker で入金を取得し、未収残高を右寄せ・tabular-nums・formatYenで表示する。入金 Dialog の追加と即時 invalidate、状態フィルタ、方法ラベルを提供し、 削除ボタンは admin にだけ表示する。DialogContent 自体にmax-h/overflowを付けない。 - fill の DEFINER は意図的な厳格化: parent 検証 fill は SECURITY DEFINER で RLS を跨いで親 invoice を読むが、
service_role / postgres の carve-out を持たないため backfill・移行スクリプトからの直接 INSERT も raise する
(claims 不在 →
current_tenant_id()NULL → fail-closed)。将来の入金 backfill はこの trigger を一時 disable するか、bypass を設計してから行う(黙って carve-out を足さない)。 - 回帰の権威:
scripts/proofs/mig217-billing-invoice-payments.sqlは catalog/sentinel/index(UPDATE policy 不在はcmd in ('UPDATE','ALL')で FOR ALL も検出)、admin/normal allow、fill による tenant/created_by 上書き、 越境・soft-delete 親 deny、0/負/NaN/Infinity CHECK の constraint_name、UPDATE 0 行・値不変(fixture 消失の 沈黙はv_before is not nullで防止)、越境 admin / 同一テナント driver の SELECT 0 行、invoice_idimmutable、normal/admin DELETE を固定する。全 proof 実行は[NG]=0かつ psql exit 0 を要求する。
請求書 Excel + 社判ストレージ(不変条件ラチェット・mig212)¶
invoice-seal-images は private bucket とし、mig130 パターンの tenant-first-folder RLS で collector staff の
SELECT/INSERT だけを許可する。service-role policy と public access は作らない。発行者設定の
collector_billing_settings.seal_image_path に <tenant_id>/invoice-seal/... の path を保存し、請求書出力時に
社判を取得する。CollectorBillingPreviewPage / CollectorBillingInvoicesPage は buildInvoiceExcel を動的 import
し、編集可能な .xlsx を出力する。二次搬出セクションはサンプル形式(日付・排出元・排出先・数量・単価・金額)を維持する。
proof は scripts/proofs/mig212-invoice-seal.sql(越境 M212-CROSS-SELECT / M212-CROSS-INSERT deny)。
排出事業者向け 請求書ダウンロード(不変条件ラチェット・mig231)¶
請求先がアプリにログインして自社宛の確定済み請求書を取得する導線。設計正典は
docs/superpowers/specs/2026-08-16-discharger-invoice-portal-design.md。
- 越境開示は RLS ではなく DEFINER RPC。
billing_invoicesに discharger 向けの SELECT policy を 足してはならない。RLS は列マスクを持たないので、表に越境 SELECT を許すと将来追加される全列が 開示される(mig166 の車両台帳と同じ事故形)。get_discharger_invoices()/get_discharger_invoice_detail(uuid)が唯一の経路で、RETURNS TABLEに開示列を明示列挙する。security definer/search_path=''/ 全参照public.完全修飾 /revoke all from public+grant execute to authenticated。 - 口座情報・適格請求書発行事業者登録番号は詳細 RPC にのみ載せる。請求先が支払うために必要で、
適格請求書の法定記載事項でもある。一覧の戻り列に出してはならない。
finalized_by/deleted_at/deleted_byはどちらにも出さない。 - 認可は AND で、すべて満たすときだけ開示する:
status='finalized'かつdeleted_at is null/partners.discharger_tenant_id = current_tenant_id()/current_tenant_type()が discharger 以外は raise(claims 欠落の NULL も拒否)/current_tenant_id() is nullの孤児ユーザーを拒否 (mig166 で実際に見つかった穴。tenant_type だけ見ると誤プロビジョニングのユーザーが越境閲覧できる)。 詳細は ID を推測されても他テナント宛なら raise する。 - Excel はクライアントで
buildInvoiceExcelを再利用し、サーバーにファイルを置かない (保存・署名 URL・失効管理の設計を持ち込まない)。社判が取れないときは社判なしで出す。 - 排出側は読取専用。支払・入金の操作を作らない。
- 回帰は
scripts/proofs/mig231-discharger-invoice-portal.sql。戻り列はpg_get_function_resultで pin してあり、開示列を足すと proof が落ちる(mig166 の V166-4 と同じ歯)。越境拒否は 一覧側・詳細側を個別にミューテーションで実証すること(検査は detail の拒否を先に見て打ち切る構造なので、 両方を同時に壊す 1 本の変異では一覧側の判定に到達しない)。
mig237 で塞いだ穴(QA 2026-08-19・再導入禁止)¶
- 帳票の
tenantNameは発行元(collector テナント名)。受領者名を渡してはならない。 Excel はB7 会社名 = tenantName/B6 発行者 = issuerName || tenantNameで、排出側がcustomer_name(=呼出元自身)を渡していたため請求先が自分の会社名の請求書を受け取っていた。issuer_nameが NULL だと発行者欄まで受領者名になる。開示列issuer_tenant_nameが権威。 このバグは「引数を検証しないモック」(toHaveBeenCalledTimesのみ)で素通りした。 - DEFINER RPC は
partners.deleted_at is nullを明示する(mig086 の規約)。 欠落すると取引先を法定ソフトデリートしても確定請求書が排出側に出続ける。 - 排出側の明細は全件取得する(
fetchAllPages相当)。単発 RPC 呼出だとmax-rowsで 黙って切れ、合計はヘッダ列由来で正しいまま明細合計と請求額が合わない Excel が出る。
マスタコードの 23505 文言(mig232/235 の付随・再導入禁止)¶
- 一意違反の人向け文言は制約名で分岐する。 SQLSTATE 23505 だけで判定すると、工場名・
ナンバー・車台番号・回収先名の重複がすべて「このコードは既に使われています」になり、
利用者は原因に辿り着けない。写経元は
collection-site-name.ts(そのコメントが 「SQLSTATE だけを条件にすると別の一意違反まで偽装する」と警告している)。 CSV 取込も同じ(逆にコード重複を「名称が重複」と報告していた)。 - 採番 RPC の role / tenant-type 拒否は proof で検査する。 guard を書いただけでは 「消しても緑のまま通る」状態になる(mig232/235 の初版がそうだった)。