Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions app/api/signup/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,10 @@ async function mintAccount(
*/
/**
* The founder's new-lead mail lists the quote, and under `commission` that total
* EXCLUDES the online-payments module — so the number alone reads as a cheaper
* plan with no visible reason for it. This row is what explains it, and it lives
* outside POST so the handler stays under its cognitive-complexity limit.
* prices the online-payments module at its reduced €9 floor rather than at the
* full list price — so the number alone reads as a cheaper plan with no visible
* reason for it. This row is what explains it, and it lives outside POST so the
* handler stays under its cognitive-complexity limit.
*/
function paymentsRow(config: StoredSignupConfiguration): string {
if (config.paymentsMode === "commission") {
Expand Down
20 changes: 13 additions & 7 deletions components/PaymentsModeChoice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useTranslations } from "next-intl";
import {
COMMISSION_FLOOR_CENTS,
DEFAULT_COMMISSION_BPS,
ONLINE_PAYMENTS_PRICE_CENTS,
crossoverCentsPerMonth,
Expand All @@ -11,9 +12,8 @@ import {
import { eur } from "@/lib/format";

/**
* How to be charged for `online-payments`, offered on /signup
* (SOFRA-PAYMENTS-PRICING-MODE-PLAN S3) — a flat monthly fee, or €0/mo plus a
* per-transaction rate.
* How to be charged for `online-payments`, offered on /signup — the full flat
* monthly fee, or a REDUCED €9/mo floor plus a per-transaction rate.
*
* Extracted out of `SignupConfigurator`, which sits at the CLAUDE.md §4
* component limit and cannot grow — the same split `PaymentsModePanel` /
Expand Down Expand Up @@ -59,8 +59,14 @@ export default function PaymentsModeChoice({
// high" by returning null. DEFAULT_COMMISSION_BPS is never 0, so this is
// reached in practice, but the guard stays the same shape as every other
// caller of this function.
const crossover = crossoverCentsPerMonth(DEFAULT_COMMISSION_BPS, ONLINE_PAYMENTS_PRICE_CENTS);
// No price argument: the break-even is against what commission SAVES on the
// module (€19 - €9 = €10), which is the function's own default — the full list
// price would promise a buyer they are cheaper up to 1.9x more turnover than
// they actually are, on the page where they choose.
const crossover = crossoverCentsPerMonth(DEFAULT_COMMISSION_BPS);
const percent = formatCommissionPercent(DEFAULT_COMMISSION_BPS);
const floor = eur(COMMISSION_FLOOR_CENTS);
const flatPrice = eur(ONLINE_PAYMENTS_PRICE_CENTS);

return (
<fieldset className="hand-drawn-border bg-card p-4">
Expand All @@ -84,7 +90,7 @@ export default function PaymentsModeChoice({
/>
<span>
<label htmlFor="paymentsMode-flat" className="font-bold">
{t("flatLabel", { price: eur(ONLINE_PAYMENTS_PRICE_CENTS) })}
{t("flatLabel", { price: flatPrice })}
</label>
<br />
<span id="paymentsMode-flat-hint" className="text-muted-foreground">
Expand All @@ -105,7 +111,7 @@ export default function PaymentsModeChoice({
/>
<span>
<label htmlFor="paymentsMode-commission" className="font-bold">
{t("commissionLabel", { percent })}
{t("commissionLabel", { percent, floor })}
</label>
<br />
<span id="paymentsMode-commission-hint" className="text-muted-foreground">
Expand All @@ -116,7 +122,7 @@ export default function PaymentsModeChoice({
</div>
{crossover !== null && (
<p className="font-label text-xs text-muted-foreground mt-2">
{t("crossover", { percent, amount: eur(crossover) })}
{t("crossover", { percent, amount: eur(crossover), floor, price: flatPrice })}
</p>
)}
<p className="font-label text-xs text-muted-foreground mt-2">{t("deferredNote")}</p>
Expand Down
19 changes: 15 additions & 4 deletions components/control/PaymentsModeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import type {
PaymentsModeTarget,
} from "@/lib/actions/payments-mode-change";
import {
COMMISSION_FLOOR_CENTS,
COMMISSION_MODE_SAVING_CENTS,
DEFAULT_COMMISSION_BPS,
MAX_COMMISSION_BPS,
ONLINE_PAYMENTS_PRICE_CENTS,
Expand Down Expand Up @@ -83,7 +85,10 @@ export default function PaymentsModeForm({
else if (bps === 0) setBps(DEFAULT_COMMISSION_BPS);
};

const preview = mode === "commission" ? crossoverCentsPerMonth(bps, ONLINE_PAYMENTS_PRICE_CENTS) : null;
// No price argument — the live preview is the break-even against what
// `commission` SAVES on the module (€19 - €9), which is the function's own
// default. Passing the full list price here would overstate it by 1.9x.
const preview = mode === "commission" ? crossoverCentsPerMonth(bps) : null;

return (
<form action={action} className="grid gap-3 sm:max-w-md">
Expand All @@ -98,7 +103,7 @@ export default function PaymentsModeForm({
checked={mode === "flat"}
onChange={() => handleModeChange("flat")}
/>
{t("modeFlat")}
{t("modeFlat", { price: eur(ONLINE_PAYMENTS_PRICE_CENTS) })}
</label>
<label className="flex items-center gap-2 font-label text-sm">
<input
Expand All @@ -109,7 +114,7 @@ export default function PaymentsModeForm({
disabled={commissionDisabled}
onChange={() => handleModeChange("commission")}
/>
{t("modeCommission")}
{t("modeCommission", { floor: eur(COMMISSION_FLOOR_CENTS) })}
</label>
{!eligibility.eligible && (
<p className="font-label text-sm text-craft-warning-text dark:text-craft-warning">
Expand Down Expand Up @@ -137,7 +142,13 @@ export default function PaymentsModeForm({
</label>
{preview !== null && (
<p className="font-label text-sm text-muted-foreground">
{t("crossover", { percent: formatCommissionPercent(bps), amount: eur(preview) })}
{t("crossover", {
percent: formatCommissionPercent(bps),
amount: eur(preview),
floor: eur(COMMISSION_FLOOR_CENTS),
price: eur(ONLINE_PAYMENTS_PRICE_CENTS),
saving: eur(COMMISSION_MODE_SAVING_CENTS),
})}
</p>
)}
<div className="flex items-center gap-3">
Expand Down
22 changes: 18 additions & 4 deletions components/control/PaymentsModePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getTranslations } from "next-intl/server";
import { eur } from "@/lib/format";
import {
COMMISSION_FLOOR_CENTS,
COMMISSION_MODE_SAVING_CENTS,
ONLINE_PAYMENTS_PRICE_CENTS,
crossoverCentsPerMonth,
formatCommissionPercent,
Expand Down Expand Up @@ -69,16 +71,22 @@ export default async function PaymentsModePanel({
// `effectivePaymentsMode` itself falls back to the intent when the registry
// cannot be read at all, so this can never disagree with `effective.mode`.
const effectiveBps = registryReadable ? (registryTenant?.payments_commission_bps ?? 0) : billingBps;
const crossover = crossoverCentsPerMonth(effectiveBps, ONLINE_PAYMENTS_PRICE_CENTS);
// No price argument: the break-even is driven by what `commission` SAVES on
// the module (€19 - €9 = €10), never by its full list price — the default
// (COMMISSION_MODE_SAVING_CENTS) is the only basis that is ever correct here.
const crossover = crossoverCentsPerMonth(effectiveBps);
const eligibility = commissionEligibility({ registryReadable, tenant: registryTenant });

return (
<section className="hand-drawn-border bg-card p-5">
<h2 className="font-hand text-2xl font-bold">{t("title")}</h2>
<p className="mt-1 font-label text-sm text-muted-foreground">
{effective.mode === "commission"
? t("commissionSummary", { percent: formatCommissionPercent(effectiveBps) })
: t("flatSummary")}
? t("commissionSummary", {
percent: formatCommissionPercent(effectiveBps),
floor: eur(COMMISSION_FLOOR_CENTS),
})
: t("flatSummary", { price: eur(ONLINE_PAYMENTS_PRICE_CENTS) })}
</p>
{effective.pending && (
// <output>, not <p role="status">: it carries the same implicit ARIA role
Expand All @@ -94,7 +102,13 @@ export default async function PaymentsModePanel({
high" and must never be printed as one. */}
{crossover !== null && (
<p className="mt-2 font-label text-sm text-muted-foreground">
{t("crossover", { percent: formatCommissionPercent(effectiveBps), amount: eur(crossover) })}
{t("crossover", {
percent: formatCommissionPercent(effectiveBps),
amount: eur(crossover),
floor: eur(COMMISSION_FLOOR_CENTS),
price: eur(ONLINE_PAYMENTS_PRICE_CENTS),
saving: eur(COMMISSION_MODE_SAVING_CENTS),
})}
</p>
)}
<div className="mt-4">
Expand Down
Loading
Loading