Rebounder Tech Blog

Written by the people who actually run these systems in production.

listTotal Skips the units Multiplier, Preview Price Drifts

Published About 3 min readBy the Rebounder engineering team — the people who operate these systems

This article may contain affiliate links. Its content is not affected by advertising.

In short

When a bundle discount preview adds termFee without multiplying by units, a request reserving several slots still shows the price for a single slot, never matching the amount actually charged.

Conclusion

A bundle discount preview must multiply its total by the number of slots being reserved (units) before computing the discounted price. Simply adding the slot’s own termFee to the termFee of the bundled members does not account for the units query parameter, which reserves several slots in one request. Without the multiplier, the screen keeps showing the price for a single slot no matter how many are actually being reserved.

Symptom

The application form has a preview that shows “list total → discounted price” to make it clear that bundling slots together triggers an automatic discount.

That preview always stayed at the price for one slot, even for a request like ?units=3 that reserves several slots in a single submission. The server correctly computed the contracted price for the full number of slots, so the amount shown on screen and the amount ultimately charged no longer matched.

Cause

The preview’s price calculation looked like this.

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>
);

It only adds termFee and memberTotal together. units, which represents how many slots this request is for, is not multiplied in anywhere. units drives the reservation count for the whole form and is used in the server’s contracted-price calculation — but this preview calculation was the one place that never read it.

The fix

Multiply the total by units, and fix two display gaps along the way.

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>
);
  • Multiply the list total by units. This is the core fix.
  • When a monthly-billed slot with no termFee is mixed into the bundle, the total cannot be computed, so instead of adding it as 0 yen as before, the price is not shown at all. Adding it as 0 yen quietly turned “actually unknown” into “a total that looks smaller than it is.”
  • The discounted price is clamped to a minimum of 0 yen with Math.max(0, ...), matching the server.

A note was also added at the bottom of the confirmation screen stating that the “amount to pay” shown there is only for this one slot. Fixing the arithmetic alone was not enough: on a screen that reserves multiple bundled slots in one request, leaving “what this amount refers to” ambiguous would just reproduce the same kind of misunderstanding.

Preventing a repeat

This preview had the same calculation written a second time, separately from the server’s contracted-price logic. Forgetting the units multiplier, and treating a monthly-billed slot as 0 yen, are both the kind of drift that would not happen if the server logic were simply called from here. Even for a display-only amount, keeping the calculation itself in one place — and letting the screen only format the result it receives — makes this kind of “fixed one side, forgot the other” mismatch much less likely.

Frequently asked questions

Q1Why did the displayed price not match the contracted price?

The preview only summed the termFee of the slot itself and the termFee of the bundled members. It never multiplied that sum by units, the parameter that represents how many slots the request was reserving. A request like ?units=3 for multiple slots still always showed the price for a single slot.

Q2Was there a problem besides the multiplier?

Yes. When a monthly-billed slot with no termFee was mixed into the bundle, the total could not be computed correctly. Before the fix, that slot was added as 0 yen, which made the total look lower than it actually was. After the fix, no price is shown at all in that case.

Q3Can the discounted price go negative?

When the discount exceeds the list total, the server clamps the contracted price to a minimum of 0 yen. The preview did not apply that clamp before the fix and could show a different value than the server. After the fix, the display follows the same rule and never goes below 0 yen.

Q4Is the shown amount the total for every slot in the bundle?

No. After the fix, the bottom of the confirmation screen states that the price shown is only for the slot being reserved right now. Additional bundled slots are communicated separately, so this note was added to prevent the amount from being read as the full total.

Environment verified

  • React (kimiteras-portal, src/app/apply/[id]/ApplyForm.tsx)
  • Fixed following review comment S2 on 2026-07-24

What this article is based on

  • TypeScript file lines 288-305commit 4314479
  • TypeScript file lines 288-308commit 8d1878b

Every claim in this article comes from the records above. The repositories we operate are private so we cannot link to them, but which file, which lines, and at which commit we read them is recorded for every article. Nothing here is written from guesswork.