Appearance
多段承認(承認フロー)
対象バージョン: ロードマップ「多段承認」(2026-08-24 実装) 関連: 要件定義 §4 承認・権限・監査、権限カタログ §1.15、休憩の扱い
KIZAMI の申請フローは v0.1 以来ずっと単段だった — その種別の承認権限をスコープ内で 持っている人が1回 approve すれば、その場で勤怠に反映される。この文書は、テナントが希望する 場合に二段承認(一次承認 + 二次承認)へ切り替えられるようにした設計と、その判断点を記録する。
対象は「権限ベースの単段承認」だった次の3種別:
| 種別 | テーブル | 承認権限 |
|---|---|---|
| 打刻修正申請 | correction_requests | attendance.correction.approve |
| 休暇申請 | leave_requests | leave.request.approve |
| 休憩自動控除の打ち消し申請 | auto_break_waivers | attendance.correction.approve |
有給付与の予告(leave_grant_proposals)は元々「機械の提案 → 人の承認」という別性質のフローで、 承認者が1人であることに意味がある(§11)ため対象外とする。
1. 既定は単段。二段は種別ごとの opt-in
既定を変えないことを最優先にした。既存テナントは何も設定しなければこれまでどおり単段で動き、 API のレスポンスも(新しいフィールドが増えるだけで)意味が変わらない。
設定はテナント単位・種別ごとに 1(単段)か 2(二段)。テーブルは approval_flow_settings (1テナント1行、マイグレーション 0026)。
| 列 | 意味 |
|---|---|
correction_steps | 打刻修正申請の承認段数(既定 1) |
leave_steps | 休暇申請の承認段数(既定 1) |
auto_break_waiver_steps | 休憩自動控除の打ち消し申請の承認段数(既定 1) |
なぜ tenant_setting_versions に相乗りしないのか
tenant_setting_versions は集計結果に影響する設定(日界・法定休日・休憩ルール・GPS)を、 「いつ時点の設定で計算したか」を後から再現できるように実効日付きで持つテーブルである。
承認フローの段数は集計に一切影響しない。承認された結果が同じなら、何段を経ていようと勤怠の 数字は1分も変わらない。実効日で遡って引き直す必要が無いため、tenant_oidc_settings / tenant_slack_settings と同じ「1テナント1行・丸ごと置き換え・変更は監査ログに残す」という 単純な形にそろえた。
種別ごとに行を作る形(type + steps の縦持ち)ではなく1行3列にしたのも同じ理由で、対象の 3種別は権限カタログ上も固定であり、増減するのは KIZAMI 自身のリリース時だけ(テナントが種別を 増やせるわけではない)。1行なら「このテナントの承認フロー」を1回の SELECT で読め、既定値の扱いも 列の DEFAULT だけで済む。
設定の変更は approval_flow_settings.update として監査ログに残す。変更前と変更後の両方を 記録する — 「いつ誰が二段を一段に緩めたか」を後から追えることが、この設定を監査対象にする主目的 だからである。
2. 段の意味
- 一次承認(step 1): 従来どおり。その種別の承認権限を、申請対象者を含むスコープで 持っている人(部署マネージャ等)。単段のときと判定は完全に同一。
- 二次承認(step 2): 同じ権限を
tenantスコープで持っている人(人事・本部を想定)。 「部署の外から見て問題ないか」を確認する段なので、部署スコープの承認者では押せない(403)。
二次承認者を「別の権限キー」にしなかったのは、権限カタログが「非エンジニアが業務タスク単位で 理解できる」ことを設計原則にしている(要件 §4)ため。「休暇申請を承認できる(二次)」という チェックボックスが増えるより、「休暇申請を承認できる」をテナント全体スコープで持っている人が 二次承認者、という説明のほうが、既にあるスコープの概念だけで完結して分かりやすい。
不変条件
- 同一人物が同じ申請の一次と二次を両方行うことはできない。 できてしまえば段を分けた意味が 無い。違反した場合は HTTP 409
{ "error": "same_approver_as_step1" }を返す(権限の話ではなく 業務上の競合なので、403 ではなく 409 にしている)。 - 申請者本人による承認(自己承認)の可否は従来のまま変えない。 KIZAMI では「自分の申請を 承認する」こと自体は禁止しておらず、その種別の承認権限を持っているかどうかで決まる (権限を持たない一般従業員は自分の申請も承認できない)。二段でも同じで、一次と二次が別人で ありさえすればよい。監査ログには従来どおり
selfApprovedを残す。
3. 状態遷移
単段 (required_steps = 1)
pending ──approve──> approved
──reject───> rejected
──withdraw─> withdrawn
二段 (required_steps = 2)
pending ──approve(1)──> approved_step1 ──approve(2)──> approved
──reject──────> rejected ──reject─────> rejected
──withdraw────> withdrawn ──withdraw───> withdrawnapproved_step1 は3種別すべてに共通の新しい状態値で、**「一次承認済み・二次承認待ち。まだ何も 反映されていない」**を意味する。
中間状態の扱い
approved_step1 は「承認済み」ではない。したがって:
- 打刻修正:
punch_eventsへの追記は行わない(対象打刻は supersede されないまま)。 - 休暇: 残高・年5日義務の計算に入らない(
listAllApprovedLeaveRequests/listApprovedLeaveRequestsInRangeはstatus = 'approved'だけを拾う)。ただし 同日重複申請のチェックには数える(listActiveLeaveRequestsForDate) — まだ反映されて いないだけでこれから承認されうるため、同じ日に重ねて申請できると二重取得になる。 - 休憩自動控除の打ち消し: 集計エンジンへ渡る
autoBreakWaivedDatesに入らない (listApprovedWaiverDatesInRangeはstatus = 'approved'だけを拾う)。 - 締め済み月ガード(
assertAmendAllowed)は一次承認では通さない。締めに影響する変更が 起きるのは二次承認で実際に反映する時だからで、一次承認の時点でclosing.unlockを要求すると 「まだ何も変えていないのに拒否される」ことになる。
decided_by / decided_at / decision_note は最終決裁の欄であり、一次承認では埋めない。 一次承認者は専用列 step1_decided_by / step1_decided_at に記録する。承認済み表示や監査で 「誰が決裁したか」を読むときに、一次承認者を最終決裁者と取り違えないためである。
却下
どちらの段からでも却下できる。 二次承認者が内容に問題を見つけたら、一次で承認済みでも 差し戻せるべきだからである。ただし approved_step1 からの却下は、承認と同じく tenant スコープの承認権限を要求する(部署スコープの承認者が、上位の判断を待っている申請を独断で 消せてはいけない)。
どの段で却下されたかは監査ログの detail.step に残す。専用の列は増やしていない — 一次承認済み かどうかは step1_decided_by の有無から一意に決まるため、列を足しても情報は増えない。
取り下げ
最終承認が下りるまで、申請者本人が取り下げられる(approved_step1 からも可)。まだ何も 反映されていない以上、本人の意思表示を尊重しない理由が無い。
4. 仕掛かり中の申請(グランドファザリング)
申請は作成時点の段数で最後まで進む。 各申請テーブルの required_steps 列に、作成時の テナント設定を凍結して保存する(承認のたびに設定を読み直さない)。
これは今回の設計でいちばん重要な決定である。承認のたびに設定を読む実装にすると、仕掛かり中の 申請が設定変更で壊れる:
- 1 → 2 に変えた瞬間、既に一次承認だけ済んでいた申請が「二次承認待ち」に化ける。誰も そのことを知らされていないので、申請は静かに滞留する。
- 2 → 1 に変えた瞬間、二次承認を経ずに反映できてしまう。二段承認を敷いた意味が、設定を 戻すだけで遡って消える。
申請は「その時点のルールで出したもの」なので、ルールの変更はその後に出される申請から効く。 これは労務のワークフロー一般の感覚とも一致する(申請書の様式を変えても、提出済みの書類は 古い様式のまま処理される)。
設定画面にもこの旨を明示する — 管理者が「設定を戻せば滞留も解消する」と誤解しないため。
5. 通知
新しいカテゴリは作らず、既存の approval_request カテゴリ(個人の通知設定で ON/OFF できる)を 再利用する。通知タイプは approval_request_* の接頭辞規約に従う (apps/api/src/lib/notification-preferences.ts がこの接頭辞でカテゴリを判定する)。
| タイミング | 宛先 | タイプ |
|---|---|---|
| 申請作成時(従来どおり) | 申請対象者をスコープに含む承認権限保持者(申請者本人を除く) | approval_request_correction / _leave / _waiver |
| 一次承認時(新規) | tenant スコープの承認権限保持者(一次承認者・申請者本人を除く) | approval_request_correction_step2 / _leave_step2 / _waiver_step2 |
文面は「二次承認をお願いします」。既に決裁に関与した人(一次承認者)には再通知しない。 申請者への最終決定通知(承認された / 却下されました)は従来のまま — 二段でも通知は最終決定の 1回だけで、途中経過は本人に送らない(進捗は申請一覧の状態チップで分かる)。
一次承認時はテナント共有 Webhook には送らない。共有チャネルへは「申請が出た」ことだけを1度流す 方針(共有チャネルに他人の勤怠の詳細を流さない、 apps/api/src/lib/notification-channels.ts)で、段が進んだことまで全社チャネルに流す必要は 無いと判断した。
運用上の注意
二段に設定したのに tenant スコープでその承認権限を持つ人が1人もいないと、申請は approved_step1 のまま誰も進められず滞留する。API はこれをエラーにしない(通知先が0人でも 一次承認自体は成功させる)代わりに、設定画面で注意喚起する。
6. API
承認・却下・取り下げのパスは変えていない(POST /corrections/:id/approve 等)。段の判定は サーバー側が申請行の required_steps と現在の status から決める。
一覧・詳細のレスポンスには、3種別すべてに次のフィールドが増える(単段でも常に返す — UI が 「段数によって分岐する」のではなく「返ってきた状態をそのまま描く」形にするため):
| フィールド | 意味 |
|---|---|
requiredSteps | 作成時に凍結された必要段数(1 or 2) |
currentStep | 次に必要な承認が何段目か。決裁・取り下げ済みなら null |
step1DecidedBy | 一次承認者。単段では常に null |
step1DecidedAt | 一次承認時刻(UTC エポック分)。単段では常に null |
GET ...?status=pending は「まだ決裁が終わっていない申請」の意味に整理し、pending に加えて approved_step1 も返すようにした。承認者の画面から「二次承認待ち」が消えてしまうと誰も 気づけないためである(status=all は従来どおり全件)。
設定 API は GET / PUT /settings/approval-flow。権限は専用キー approval_flow.manage (テナント全体のみ・危険フラグなし)。PUT は3種別すべての値を常に受け取る(部分更新はしない)。
新しい権限キーを足した理由
既存キーの転用を検討したが、どれも意味が合わなかった:
permission.preset.manage— 「誰が何をできるか」の設定であって、承認が何段必要かの設定では ない(段数を変えても誰の権限も増減しない)。tenant_settings.*— このグループは集計の入力になるテナント設定(日界・法定休日・ フレックス・GPS)を指す。承認フローは集計に影響しない。「日界・法定休日カレンダーを 設定できる」のチェックボックスに承認フローが隠れるのは、カタログの設計原則に反する。attendance.correction.approve/leave.request.approve— 承認する権限であって、承認フローを 決める権限ではない(承認者が自分の承認を1段に減らせてはいけない)。
危険フラグは立てていない。段数の変更は「承認を厳しくする/緩める」という運用判断であって、 権限の付与・個人情報の露出・データの不可逆な変更のいずれにも当たらない。緩める方向の変更も、 グランドファザリングにより仕掛かり中の申請には影響しない。
7. 実装の所在
| 役割 | ファイル |
|---|---|
| 段の判定・不変条件・二次承認者への通知 | apps/api/src/lib/approval-flow.ts |
| 二次承認者候補(tenant スコープ保持者)の解決 | apps/api/src/lib/approvers.ts の resolveTenantScopeApprovers |
| 設定 API | apps/api/src/routes/settings/approval-flow.ts |
| 各種別の承認処理 | apps/api/src/routes/{corrections,leave,auto-break-waivers}.ts |
| 設定テーブル | packages/db/src/schema/approval-flows.ts |
| 設定画面 | apps/web/src/components/SettingsApprovalFlowView.tsx |
| テスト | apps/api/test/approval-flow-two-step.test.ts |
段の判定(何段目か・確定してよいか・同一人物でないか)は3種別で同じ答えを出さねばならないため lib/approval-flow.ts に集約した。一方で承認そのものの副作用 — 打刻の追記・残高の再検証・ 締め後修正 — は種別ごとに全く違うので、各ルートに残してある。