Rebounder Tech Blog

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

セット割引プレビューのlistTotalはunitsを掛け忘れ、表示額と契約額が食い違う

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

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

結論

セット割引の確認画面で定価合計と割引後の金額を計算する際にunitsを掛けていないと、複数枠をまとめて申し込むリクエストでも画面には常に1枠分の金額が表示され続け、実際に契約される金額と一致しなくなる。

結論

セット割引の確認画面に出す金額は、申込み枠数(units)を掛けてから計算する。 対象枠自体のtermFeeとセット対象メンバーのtermFeeを単純に足すだけでは、units クエリで複数枠をまとめて申し込むリクエストに対応できず、画面には常に1枠分の金額が表示され続ける。

症状

申込みフォームには、セット対象の枠を一緒に申し込むと自動で割引されることを示すため、「定価合計 → 割引後」という形で金額をプレビュー表示する箇所がある。

このプレビューは、?units=3 のように1回の申込みで複数枠をまとめて確保するリクエストでも、常に1枠分の金額のままだった。実際の契約金額はサーバー側で枠数ぶん正しく計算されるため、画面の表示額と、最終的に請求される契約額が一致しない状態になっていた。

原因

プレビューの金額計算は、こうなっていた。

const memberTotal = bundleOffer.members.reduce(
  (a, m) => a + (m.termFee ?? 0),
  0
);
const listTotal = (termFee ?? 0) + memberTotal;
if (!listTotal) return null;
return (
  <span className="mt-2 block text-xs font-bold text-gray-900">
    {formatYen(listTotal)} →{" "}
    <span className="text-amber-700">
      {formatYen(listTotal - bundleOffer.discountYen)}
    </span>
    (税別)
  </span>
);

termFee と memberTotal を足しているだけで、この申込みが何枠分なのかを表す units がどこにも掛かっていない。 units はフォーム全体の確保数に効くパラメータで、サーバー側の契約金額計算では使われているが、このプレビュー計算だけが参照していなかった。

直す

units を合計に掛け、あわせて2つの表示ズレも直した。

const missing = bundleOffer.members.some((m) => m.termFee == null);
if (termFee == null || missing) return null;
const listTotal =
  (termFee + bundleOffer.members.reduce((a, m) => a + (m.termFee ?? 0), 0)) *
  units;
if (!listTotal) return null;
const after = Math.max(0, listTotal - bundleOffer.discountYen);
return (
  <span className="mt-2 block text-xs font-bold text-gray-900">
    {formatYen(listTotal)} →{" "}
    <span className="text-amber-700">{formatYen(after)}</span>
    (税別{units > 1 ? `・${units}枠ぶん` : ""})
  </span>
);
  • **units を定価合計に掛ける。**これが本題の修正。
  • 期間額(termFee)を持たない月額制の枠が対象に混ざると合計が出せないため、**以前のように0円として足さず、金額自体を表示しない。**0円として足すと「本当は不明」が「少なく見える合計」にすり替わってしまう。
  • 割引後の金額はサーバー側と同じく Math.max(0, ...) で0円未満にクランプする。

あわせて、確認画面の下に「このページ下部の『お支払い額』はこの枠のみの金額です」という注記を足した。計算を直しても、複数枠をまとめて申し込む画面で「どの金額が何を指しているか」が曖昧なままでは、同じ種類の誤解がまた起きる。

再発しない形にする

このプレビューは、サーバー側の契約金額計算とは別の場所に同じ計算式をもう一度書いていた。 units を掛け忘れたのも、月額制の枠を0円として扱ったのも、サーバー側のロジックをそのまま呼び出していれば起きなかった種類のズレだった。表示用の金額であっても、計算そのものは一箇所に持たせ、画面側は結果を受け取って整形するだけにするほうが、この手の片方だけ直し忘れる不一致は起きにくい。

よくある質問

Q1なぜ表示金額と契約金額が食い違ったのですか?

確認画面の金額計算は、申込み枠自体のtermFeeとセット対象メンバーのtermFeeを合算するだけで、申込み枠数を表すunitsパラメータを掛けていませんでした。そのため?units=3のように複数枠をまとめて申し込むリクエストでも、画面には常に1枠分の金額が表示されていました。

Q2金額以外にも問題はありましたか?

はい。期間額(termFee)を持たない月額制の枠が対象に混ざると合計を正しく出せないため、修正前はその枠分を0円として合算しており、合計が実際より少なく見える過少表示になっていました。修正後は、そのようなケースでは金額自体を表示しない形にしています。

Q3割引後の金額がマイナスになることはありますか?

割引額が定価合計を上回るケースでは、サーバー側で確定する契約金額は0円未満にクランプされます。修正前の画面はこのクランプをしておらず、サーバー側と異なる値を表示しうる状態でした。修正後は表示側も同じ規則で0円未満にならないようにしています。

Q4表示している金額はセット対象の全部の合計ですか?

いいえ。修正後はチェック画面の下部に、表示している「お支払い額」は今回確保するこの枠のみの金額である旨を明示しています。セット対象の追加枠ぶんは別の案内でまとめて伝える運用のため、合計額だと誤解されないよう注記を加えました。

確認した環境

  • React(kimiteras-portal, src/app/apply/[id]/ApplyForm.tsx)
  • 2026-07-24 のレビュー指摘S2で修正

この記事の根拠

  • TypeScriptファイル 288〜305行目コミット 4314479
  • TypeScriptファイル 288〜308行目コミット 8d1878b

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