Rebounder Tech Blog

運用している当事者が書く、本番システムの記録。

effectiveStartMonth:表示用の計算とサーバー側の計算がズレる原因

公開 読了時間 約3分執筆: Rebounder 開発チーム(当該システムの運用当事者)

※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。

結論

effectiveStartMonthで開始月を繰り上げる設計では、契約作成側だけでなく表示側にも同じ関数を適用しないと、保存された予定月と実際に開始できる月が画面と契約で食い違う。

掲載開始月を繰り上げる関数を契約作成側だけで呼び、画面には保存値をそのまま渡していた。直し方は、表示用の値を作る経路でも同じ関数を呼ぶことだった。2026年7月23日のレビューで見つかり、修正した実装の話である。

effectiveStartMonthで開始月を繰り上げる設計では、契約作成側だけでなく表示側にも同じ関数を適用しないと、保存された予定月と実際に開始できる月が画面と契約で食い違う。

表示用の値だけがstart_monthの生値だった

掲載枠の開始月を表すstart_monthは、募集を始めた時点の予定である。時間がたてば過去の月になるため、契約作成側はeffectiveStartMonthで「実際に始められる月」に補正していた。

一方、画面に渡す値の組み立ては次の形だった。

startMonth: l.start_month ?? null,

これは未設定をnullにそろえるだけで、開始月の繰り上げは行わない。画面は保存された予定月を示し、契約側は補正後の月を使うという、異なる経路になっていた。

effectiveStartMonthは当月ちょうども翌月へ送る

関数の規則は、開始月が当月以前なら翌月へ繰り上げるというものだ。審査・入稿の時間を確保するため、当月開始も認めない設計だった。開始月が将来ならそのまま返す。

return startMonth! <= currentMonth!
  ? nextMonthOf(currentMonth)
  : startMonth!;

上は根拠コードの改行だけを変えた抜粋である。開始月が未設定・不正なら先にnullを返し、比較に使う当月が不正なら開始月をそのまま返す。現在月は関数内で取得せず、呼び出し側から渡す。

この規則に従えば、開始月が2026-08で当月も2026-08の場合、補正後は翌月になる。表示側が生値のままなら8月、契約側は9月となる。これは関数から導ける例であり、実際に誤った契約を作ったという記録ではない。

表示側にも同じ関数を適用した

修正は、画面へ渡す値にも契約側と同じ補正を通すことだった。

startMonth: effectiveStartMonth(l.start_month, jstMonth()),

jstMonth()は日本時間の当月をYYYY-MM形式で返す。契約作成側も同じ組み合わせで開始月を求め、その結果から期間を組み立てている。

const startYm = effectiveStartMonth(loop.start_month, jstMonth());
const range = periodRange(startYm, effectiveTerm);
const periodStr = publicPeriodLabel(startYm, effectiveTerm);

これで両経路に同じ規則が適用される。ただし、この修正は表示時と契約作成時の現在月を固定するものではない。それぞれがjstMonth()を呼ぶ構造なので、月をまたいでも同じ結果になるという保証までは含まない。

関数の共通化だけでは呼び出し漏れは防げない

今回、開始月を補正する関数は既に存在し、契約作成側でも使われていた。取り残されていたのは、表示用の値を組み立てる経路だった。

表示と処理の計算がズレる原因は、同じ計算を別々に実装することだけではない。一方が補正後の値を使い、もう一方が保存値をそのまま使っていても食い違う。共通関数があるかに加え、表示と実処理の両方がその関数を通るかが、この修正の分かれ目だった。

よくある質問

Q1開始月が当月と同じ場合も翌月に繰り上げますか?

今回の実装では、開始月が当月以前なら翌月へ繰り上げます。審査・入稿の時間が必要という業務上の前提によるものです。将来の開始月はそのまま返し、開始月が未設定・不正ならnullを返します。

Q2同じ関数を用意しているのに画面と契約がズレるのはなぜですか?

関数が存在していても、表示経路が呼ばなければ規則は適用されません。今回、契約側はeffectiveStartMonthを呼ぶ一方、表示側はstart_monthの生値を渡していました。表示側の値の組み立てにも同じ関数を適用しました。

確認した環境

  • TypeScriptの開始月計算・画面表示・契約作成処理
  • 2026-07-23 のレビューで指摘され修正

この記事の根拠

  • TypeScriptファイル 50〜50行目コミット 9cb296c
  • TypeScriptファイル 51〜54行目コミット 5e1c4d6
  • TypeScriptファイル 1065〜1068行目コミット 5e1c4d6
  • TypeScriptファイル 73〜89行目コミット 5e1c4d6
  • TypeScriptファイル 40〜43行目コミット 5e1c4d6

本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。