契約月数を延長すると、term_monthsを見た帳票表示だけ金額とズレる
※本記事にはアフィリエイトリンクを含む場合があります。内容は広告の有無に影響されません。
結論
帳票の契約期間表示はfirst.term_months(枠に固定された単位)を直接参照しており、金額の計算に使うperiod_start/period_end(選択された実効期間)とは別の値を見ていたため、契約月数を延長すると金額と表示が食い違った。
結論
契約金額は申込時に選んだ実効期間(period_start/period_end)から計算されるのに、申込書・請求書の「契約期間」表示は、枠にあらかじめ固定された単位月数(term_months)を直接参照する別のコードのままだった。契約月数をその場で延長して申し込めるようになったことで、金額は延長後の期間を反映するが、表示だけが元の単位月数のままという矛盾が書面上に現れるようになった。
症状
このシステムは単位制で、掲載枠には基本の単位月数(term_months)があらかじめ決まっている。申込時にその月数を延長して契約できる機能が入ると、契約テーブルには実際に選んだ期間がperiod_start/period_endとして保存され、金額もその実効期間で計算される。
ところが請求書・見積書を組み立てるInvoiceDoc.tsxの表示ロジックは、この機能が入る前からあったコードのままだった(修正前・736a3be時点)。
const period =
first.start_month && first.term_months
? `${monthJp(first.start_month)}〜${monthJp(endMonthStr(first.start_month, first.term_months))}(${first.term_months}ヶ月一括)`
: "";
first.term_monthsは枠側の固定値であり、契約側が延長した実効期間ではない。申込書を組み立てるapplication/page.tsxも同じ形の値を別々に参照していた(同時点)。
const termMonths = first.term_months ?? 1;
if (first.start_month && first.term_months) {
const endMon = endMonthStr(first.start_month, first.term_months);
displayPeriod = `${monthJp(first.start_month)} 〜 ${monthJp(endMon)}(契約期間 ${first.term_months}ヶ月)`;
}
延長前はterm_monthsと実効期間が常に一致していたためこれで問題なかったが、延長機能が入った時点でこの前提が崩れた。金額欄は延長後の月数分の金額を示し、契約期間欄は延長前の単位月数のまま——という矛盾が、同じ書面の中に同時に出るようになった。
原因
原因は「契約期間」を文字列に組み立てる計算が、契約の実効期間(period_start/period_end)ではなく、枠テンプレート側の固定値(start_month/term_months)を直接参照するコードとして、少なくともInvoiceDoc.tsxとapplication/page.tsxの2箇所に個別に書かれていたことにある。契約月数を延長できる機能自体はこの2箇所のどちらも変更していないため、機能追加のレビューでは見えにくく、実際に「レビュー🔴対応」として指摘・修正されたのは機能追加コミットから12分後だった(736a3be → 66977ce、いずれも2026-06-18)。
直す
修正はInvoiceDoc.tsxにmonthSpan/docPeriodという2つの関数を新設し、期間の決め方を1箇所に集約する形で行われた(修正後・66977ce時点)。
export function monthSpan(
startDate: string | null | undefined,
endDate: string | null | undefined
): number | null {
if (!startDate || !endDate) return null;
const [sy, sm] = startDate.slice(0, 7).split("-").map(Number);
const [ey, em] = endDate.slice(0, 7).split("-").map(Number);
if (!sy || !sm || !ey || !em) return null;
const months = (ey - sy) * 12 + (em - sm) + 1;
return months >= 1 ? months : null;
}
export function docPeriod(
periodStart: string | null | undefined,
periodEnd: string | null | undefined,
fallbackStartMonth: string | null | undefined,
fallbackTermMonths: number | null | undefined
): { startYm: string; term: number } | null {
const term = monthSpan(periodStart, periodEnd) ?? fallbackTermMonths ?? null;
const startYm = (periodStart ? periodStart.slice(0, 7) : fallbackStartMonth) ?? null;
if (!startYm || !term) return null;
return { startYm, term };
}
docPeriodは契約のperiod_start/period_end(=実際に選ばれた実効期間)を最優先し、それが無い契約(延長していない従来の契約)でだけ枠側のstart_month/term_monthsにフォールバックする。first.term_monthsの参照自体は消していない——消すと延長していない契約の表示が壊れるため、優先順位を逆にしただけだ。
application/page.tsx側も同じ関数を呼ぶように変更されている。
const dp = docPeriod(
contract.period_start,
contract.period_end,
first.start_month,
first.term_months
);
const termMonths = dp?.term ?? first.term_months ?? 1;
displayPeriod = dp
? `${monthJp(dp.startYm)} 〜 ${monthJp(endMonthStr(dp.startYm, dp.term))}(契約期間 ${dp.term}ヶ月)`
: `契約期間 ${termMonths}ヶ月`;
コミットメッセージによれば、同じdocPeriodはアーカイブ版の帳票・同意ページ・契約一覧の表示にも適用されている。
再発防止
この不整合が起きたのは、「契約期間をどう文字列にするか」という同じ計算が、帳票を組み立てるファイルの数だけ個別に書かれていたためだ。金額の計算ロジック側は実効期間ベースに正しく作られていたのに、表示側だけが枠テンプレートの固定値を見る古いコードのまま複数箇所に残っていた。契約月数の延長のように「一部の契約だけ実効値と枠のテンプレ値がズレうる」機能を足すときは、その値を参照している箇所を1つのファイルの中だけで探すのではなく、同じ計算を独自に組み立て直している別ファイルが無いかを確認する必要がある。
よくある質問
Q1なぜ金額と契約期間の表示だけがズレたのですか?
金額は契約のperiod_start/period_end(申込時に選んだ実効期間)から計算されるが、帳票の「契約期間」表示はfirst.term_months(枠にあらかじめ固定された単位月数)を直接参照するコードのままだったため。値の出どころが2つに分かれていた。
Q2影響したのは請求書だけですか?
いいえ。同じ計算ロジックがInvoiceDoc.tsx(請求書・見積書)とapplication/page.tsx(申込書)の両方に別々に書かれており、どちらも同じ理由でズレていた。コミットメッセージによれば同意ページや契約一覧の表示にも同種の修正が入っている。
Q3本番で顧客に矛盾した書面が渡ったのですか?
根拠にある範囲では、契約月数を延長できる機能の導入コミットと、このズレをレビューで指摘して直した修正コミットは同じ日(2026-06-18)で、コミット間隔は12分。実際に矛盾した書面が顧客に渡ったかどうかは根拠に記載が無く、ここでは判断しない。
Q4直し方はfirst.term_monthsの参照をやめることですか?
いいえ、削除していない。新設したdocPeriod()はperiod_start/period_endを優先し、値が無い契約(=延長していない従来の契約)では従来どおりfirst.start_month/first.term_monthsにフォールバックする。既存契約の表示は変わらない。
Q5なぜ1箇所ではなく複数ファイルを直す必要があったのですか?
「契約期間」を文字列に組み立てる同じ計算が、InvoiceDoc.tsxとapplication/page.tsxにそれぞれ個別に書かれていたため。docPeriod()という単一の関数に集約し、両方から呼ぶ形に変更した。
確認した環境
- Next.js 16.2.7 / @supabase/supabase-js ^2.106.2 / TypeScript ^5
- 契約月数の延長機能とその修正はいずれも2026-06-18
この記事の根拠
- TypeScriptファイル 93〜99行目コミット 736a3be
- TypeScriptファイル 31〜60行目コミット 66977ce
- TypeScriptファイル 137〜142行目コミット 736a3be
- TypeScriptファイル 137〜146行目コミット 66977ce
本文の主張は、上の記録に書かれていることだけです。運用しているリポジトリは非公開のため リンクは張れませんが、どのファイルの何行目を、どのコミット時点で見て書いたかは 記事ごとに残しています。推測で書いた箇所はありません。