コンテンツにスキップ

日次締め

Document ID: SRS-REG-002 Parent: SRS-ROOT-001 v0.5 Version: 0.2 Status: Implemented (API + Web UI 最小) Last Updated: 2026-05-06 Depends on: SRS-TEN-001 v0.4, SRS-TEN-002 v0.2, SRS-TEN-003 v0.2, SRS-REG-001 v0.2, SRS-RES-004 v0.3, SRS-WRK-001 v0.2 依存される: SRS-ANL-001

本書は SRS-ROOT-001 v0.5 に従う。日次締めは 1 店舗 1 レジを前提とし、暦日 closing_date を採用する。


当日現金残高を確定し、前日以前の売上集計を daily_sales_summary に固定化する。差額は register_closing.diffcash_movement(category='cash_diff_adjust') に残し、締め直しは void で扱う(物理削除しない)。

assertNotClosedDate(storeId, date) 共通関数で締め後改変を強制ガードする(親 §7.18)。


  • As a receptionist, I want to 1 日の現金残高を締めたい
  • As a manager, I want to 差額が出たら自動で帳尻行を残したい
  • As a owner, I want to 前日分の売上を固定し、当日だけオンザフライ集計したい

  1. オペレーターが closing_date の expected cash を確認
  2. counted cash を入力し POST /api/admin/register/closings
  3. API が advisory lock 取得(pg_advisory_xact_lock(hashtext('register_closing:'||store_id||':'||closing_date))
  4. 対象日が未締めであることを確認
  5. expected cash を二系統で計算し照合(cash_movement 累計 vs visit 集計)
  6. 差額があれば cash_movement(category='cash_diff_adjust') を同 tx で挿入
  7. register_closing を INSERT
  8. 翌日付け cash_movement(category='opening', amount=counted_cash) を同 tx で自動作成
  • 未会計予約あり: warning、admin:register:close + 強行フラグで通せる
  • 誤締め: POST /api/admin/register/closings/:id/voidis_voided=true
  • withdraw / expense: closing 前に手動 cash_movement 登録
  • 同一 store/date 二重締め: 409 REGISTER_CLOSING.ALREADY_CLOSED
  • advisory lock 競合: 409 REGISTER_CLOSING.IN_PROGRESS
  • 二系統 expected cash 不一致: 500 REGISTER_CLOSING.CASH_RECONCILIATION_FAILED + Sentry
  • 未会計予約あり + permission 不足: 422 REGISTER_CLOSING.PENDING_RESERVATIONS_EXIST
  • void 対象 not found / 既に void: 409 REGISTER_CLOSING.ALREADY_VOIDED

  • expected cash 表示
  • counted cash 入力
  • diff 表示
  • 未会計予約 warning
  • cash movement 一覧
  • closing 一覧と void 状態表示
  • counted cash は 0 以上整数
  • void は理由必須

MethodPath用途permission
GET/api/admin/register/closings/:date日次締め状況admin:register:close
POST/api/admin/register/closings締め実行admin:register:close
POST/api/admin/register/closings/:id/void締め直しadmin:register:close
GET/api/admin/cash-movements?from=&to=入出金一覧admin:register:operate
POST/api/admin/cash-movements手動入出金admin:register:operate
POST /register/closings
{
closing_date: 'YYYY-MM-DD',
counted_cash: bigint,
memo?: string,
force_close_with_pending?: boolean
}
POST /register/closings/:id/void
{ reason: string }
POST /cash-movements
{
closing_date: 'YYYY-MM-DD',
category: 'withdraw' | 'expense',
amount: bigint,
memo: string
}
  • REGISTER_CLOSING.ALREADY_CLOSED
  • REGISTER_CLOSING.IN_PROGRESS
  • REGISTER_CLOSING.PENDING_RESERVATIONS_EXIST
  • REGISTER_CLOSING.CASH_RECONCILIATION_FAILED
  • REGISTER_CLOSING.ALREADY_VOIDED
  • CASH_MOVEMENT.INVALID_CATEGORY

カラム制約
iduuidPK
store_iduuidFK
closing_datedateNOT NULL
expected_cashbigintNOT NULL
counted_cashbigintNOT NULL
diffbigintNOT NULL
operator_iduuidNOT NULL(TEN-002 後 FK)
memotextNULL
is_voidedbooleanNOT NULL DEFAULT false
voided_byuuidNULL
voided_reasontextNULL
voided_attimestamptzNULL
closed_attimestamptzNOT NULL
制約UNIQUE(store_id, id)
制約UNIQUE(store_id, closing_date) WHERE is_voided=false
カラム制約
idbigserialPK(親 §7.2.2 例外)
store_iduuidFK
closing_datedateNOT NULL
closing_iduuidNULL FK
visit_iduuidNULL FK(sales_cash / refund
categoryvarchar(32)CHECK IN (opening, sales_cash, refund, withdraw, expense, cash_diff_adjust)
amountbigintCHECK > 0
memotextNULL
operator_iduuidNULL(system カテゴリは NULL)
occurred_attimestamptzNOT NULL
created_attimestamptzNOT NULL
カラム制約
store_iduuidPK part
summary_datedatePK part
service_salesbigintNOT NULL
goods_salesbigintNOT NULL
option_salesbigintNOT NULL
discount_salesbigintNOT NULL
net_salesbigintNOT NULL
cash_salesbigintNOT NULL
customer_countintegerNOT NULL
aggregated_attimestamptzNOT NULL

0020_visit_register_closing.sql で REG-001 と統合:

  • 上記 3 表を追加
  • RLS / GRANT / index / category CHECK
  • assertNotClosedDate 関数(または application 層共通関数として実装)
  • permission backfill (admin:register:close)

closing_datepaid_at AT TIME ZONE store.timezone で求める暦日。Phase 1 は Asia/Tokyo 固定。

二系統で計算し一致確認:

  • movement 系: opening + sales_cash - refund - withdraw - expense + cash_diff_adjust
  • visit 系: 当日 cash 支払 visit_payment 合計 + 当日以前 carry opening - refund - withdraw - expense

不一致は 5xx + Sentry。

diff = counted_cash - expected_cashdiff != 0 なら cash_movement(category='cash_diff_adjust', amount=abs(diff)) を自動挿入、符号は register_closing.diff 列で表現。amount は正数固定。

pg_advisory_xact_lock(hashtext('register_closing:'||store_id||':'||closing_date)) を取得。

締め成功時、同一 tx で翌日付け cash_movement(category='opening', amount=counted_cash) を自動挿入。翌日 0:00 cron は使わない。cash_movement.closing_date 列で「翌日付け」を明示。

対象日に reservations.status != 'paid' がある場合 warning。force_close_with_pending=true で押し通せる(owner / manager のみ)。

assertNotClosedDate(storeId, date) 共通関数:

async function assertNotClosedDate(storeId: string, date: string, db: Tx): Promise<void> {
const exists = await db.execute(
`SELECT 1 FROM register_closing
WHERE store_id = $1 AND closing_date = $2 AND is_voided = false`,
[storeId, date]
);
if (exists.rows.length > 0) {
throw new ApiError(409, 'VISIT.CLOSED_PERIOD_LOCKED', '締め済みの日付の取引は変更できません');
}
}

全 visit / cash_movement mutation の handler で必ず先頭で呼ぶ。Phase 1 はアプリ層強制、Phase 2 で DB trigger 化を検討(OQ-17)。

is_voided=true, voided_by, voided_reason, voided_at を埋める。物理削除なし。void 後のみ対象日の会計再修正と再締めを許可。


  • closing API p95 1s
  • advisory lock 取得待ち 3 秒で打ち切り 409
  • 失敗時 Sentry に storeId, closingDate を付与

key用途初期付与
admin:register:close締め・締め直しowner / manager

RLS は register_closing, cash_movement, daily_sales_summary に適用。


10. 受け入れ基準(Given-When-Then)

Section titled “10. 受け入れ基準(Given-When-Then)”
  • GWT-1 通常締め: expected=50,000, counted=50,000 → diff=0 で成功
  • GWT-2 差額発生: expected=50,000, counted=49,000 → diff=-1000cash_diff_adjust 1 行
  • GWT-3 同日二重締め禁止: 409 REGISTER_CLOSING.ALREADY_CLOSED
  • GWT-4 advisory lock: 2 人同時 close → 片方 IN_PROGRESS
  • GWT-5 未会計予約 warning: 422 PENDING_RESERVATIONS_EXIST(permission 不足時)
  • GWT-6 強行締め: force_close_with_pending=true + 権限あり → 成功、監査記録
  • GWT-7 翌日 opening: 締め成功 → 翌日付け category='opening' movement 1 行
  • GWT-8 category 制約: category='deposit' → 422 / DB CHECK 違反
  • GWT-9 TZ 境界: paid_at='2026-05-05T16:00:00Z' (JST 2026-05-06 01:00) → closing_date='2026-05-06'
  • GWT-10 二系統不一致: 5xx + Sentry
  • GWT-11 void 締め: is_voided=true、物理削除なし
  • GWT-12 void 二重: 409
  • GWT-13 withdraw 反映: expected から差し引かれる
  • GWT-14 summary 参照境界: 前日以前は daily_sales_summary、当日は visit* 直接参照
  • GWT-15 assertNotClosedDate 通過: visit PATCH が register_closing(is_voided=false) 存在で 409

  • integration: closing 正常 / diff / 自動 opening / void
  • db-level: category CHECK / UNIQUE / RLS
  • time-zone: UTC/JST 境界
  • worker integration: aggregate-daily-sales-for-storedaily_sales_summary を作る
  • failure injection: 集計不一致で 5xx

  • aggregate-daily-sales(system scheduler、UTC 18:00 daily)
  • aggregate-daily-sales-for-store(store fan-out)

#内容扱い
OQ-REG-002-01営業日ベース締め (cutoff hour)Phase 2
OQ-REG-002-02締め後改変 DB trigger 化Phase 2 OQ-17
OQ-REG-002-03複数レジ (register_id)Phase 3

VersionDateAuthorChange
0.12026-05-05Codex / yudai初版
0.22026-05-05yudai (with Codex co-design)Round 2 反映: migration 番号 0020、assertNotClosedDate 共通関数明記、advisory lock 規律、is_voided 方式の void、cash_movement の closing_date 列、permission 1 個に集約