お金に触れる機能を、誰の料金も勝手に変えずに出す方法
料金のバグは、カードに請求されたあとでは取り消せません。割引、スタッフ別料金、セールを予約の流れに足しながら、こちらが意図するまで1円も動かさないために使っている決まりごとをご紹介します。
たいていのバグは、謝罪で済みます。料金のバグは、返金とチャージバック、そして画面の数字を信じられなくなったお客様という代償を伴います。損得が釣り合っていないので、お金に触れるものの手順は、ほかの作業よりも意図的に遅くしてあります。その形を、最近の機能を例にご説明します。サロンがフェイシャルを一括でセールにできる、カテゴリー単位のセールです。
既定はオフ。本番にあり、何もしない
この機能の最初の一行はフラグです。環境変数を読み、既定はオフです。つまり、何かをするずっと前にコードを取り込んで配備でき、料金は前日と1バイトも変わりません。緊張する一斉公開はありません。公開は、こちらが自信を持った時点で行う、別の退屈な手順だからです。
export const FEATURE_LIVE = process.env.FEATURE_LIVE === "true"; // Read at the one place it matters. When the flag is off, the branch// is skipped entirely: no extra query, no price change, nothing.if (FEATURE_LIVE) { const pct = await activeCategorySalePercent(orgId, "service", service.category); if (pct > 0) fullPriceCents = applyCategorySaleCents(fullPriceCents, pct);}1つの関数に、1つの仕事
割引の計算は、1つの純粋関数の中にあります。セント単位の金額とパーセントを受け取り、セント単位の金額を返します。データベースも、リクエストも、時計も使いません。だから端の条件を単独で試すのが簡単です。割引ゼロ、通常の割引、上限での頭打ち、起きてはならないマイナス。計算が頭に入る小さな関数1つになっていれば、それについて推測する必要がなくなります。
export function applyCategorySaleCents( baseCents: number, salePercent: number | null | undefined,): number { const base = cents(baseCents); const pct = Math.max(0, Math.min(100, Math.round(salePercent ?? 0))); if (pct === 0) return base; return cents((base * (100 - pct)) / 100);}サーバーで、必ず計算し直す
ブラウザは、お客様が支払う額を知るために料金を表示します。ただしその数字は、あくまで表示にすぎません。予約が届くと、サーバーは本物のメニューを読み込み、スタッフ別の料金を決め、有効なカテゴリーセールを適用し、そのうえで割引を計算します。すべて自分で取ってきたデータからです。途中で誰かがリクエストを書き換えても、何も変わりません。カードに請求される金額は、そもそもブラウザが送ってきた金額ではないからです。お金を扱うコードで最も大事な習慣がこれです。信頼境界はサーバーであり、計算するのはサーバーです。
お客様より先に、自分で壊しにいく
テストが通ることで分かるのは、思いついた場合が動くということだけです。見落とした場合については何も教えてくれません。だから料金のフラグを入れる前に、お金を間違わせる方法を探すことだけを仕事にした意地悪な検査を走らせます。それぞれ別の角度から見る検査を分けてあります。お客様に過大請求する経路を探すもの、お店が損をする経路を探すもの、フラグがオフのときに本当に何もしないかを確かめるもの、そして端数と境界のずれを狙うもの。前払いにクーポン、その上にセールが重なったところは、まさに1セント未満が隠れる場所なので、そこを一番厳しく見ます。
どれも気の利いた工夫ではありません。フラグが1つ、小さな関数が1つ、自分で計算するサーバー、そして出す前に自分の作業を壊しにいく習慣です。合わせると、お金に触れる機能を足すことと、それで眠れなくなることの差になります。