feat(onboarding): show recurring monthly fee in the wizard cost summary

This commit is contained in:
2026-05-29 23:38:22 +02:00
parent 521398b0fc
commit 3c2cb696c7
4 changed files with 54 additions and 18 deletions

View File

@@ -81,6 +81,7 @@ export default async function NewInstancePage() {
hasOrgBilling={hasOrgBilling} hasOrgBilling={hasOrgBilling}
existingOrgBilling={orgBilling} existingOrgBilling={orgBilling}
setupFeeChf={pricing.tenantSetupFeeChf} setupFeeChf={pricing.tenantSetupFeeChf}
monthlyFeeChf={pricing.tenantMonthlyFeeChf}
/> />
</div> </div>
</div> </div>

View File

@@ -326,6 +326,7 @@ export default async function DashboardPage() {
hasOrgBilling={hasOrgBilling} hasOrgBilling={hasOrgBilling}
existingOrgBilling={orgBilling} existingOrgBilling={orgBilling}
setupFeeChf={platformPricing.tenantSetupFeeChf} setupFeeChf={platformPricing.tenantSetupFeeChf}
monthlyFeeChf={platformPricing.tenantMonthlyFeeChf}
/> />
</div> </div>
</div> </div>

View File

@@ -31,6 +31,12 @@ interface OnboardingFlowProps {
* step. Forwarded straight to the wizard. * step. Forwarded straight to the wizard.
*/ */
setupFeeChf?: number | null; setupFeeChf?: number | null;
/**
* Recurring per-tenant monthly fee (net CHF). Forwarded to the
* wizard's review-step cost summary so the customer sees the ongoing
* commitment, not just the one-time setup fee.
*/
monthlyFeeChf?: number | null;
/** /**
* Bug 6: when present, the wizard is rendered in edit mode against * Bug 6: when present, the wizard is rendered in edit mode against
* the given pending request. See `OnboardingWizard` for the full * the given pending request. See `OnboardingWizard` for the full
@@ -59,6 +65,7 @@ export function OnboardingFlow({
hasOrgBilling, hasOrgBilling,
existingOrgBilling, existingOrgBilling,
setupFeeChf, setupFeeChf,
monthlyFeeChf,
editingRequest, editingRequest,
}: OnboardingFlowProps) { }: OnboardingFlowProps) {
const router = useRouter(); const router = useRouter();
@@ -71,6 +78,7 @@ export function OnboardingFlow({
hasOrgBilling={hasOrgBilling} hasOrgBilling={hasOrgBilling}
existingOrgBilling={existingOrgBilling} existingOrgBilling={existingOrgBilling}
setupFeeChf={setupFeeChf} setupFeeChf={setupFeeChf}
monthlyFeeChf={monthlyFeeChf}
editingRequest={editingRequest} editingRequest={editingRequest}
onComplete={() => { onComplete={() => {
// Navigate back to /dashboard and re-fetch on the server. The // Navigate back to /dashboard and re-fetch on the server. The

View File

@@ -117,6 +117,13 @@ interface WizardProps {
* the order skips the Checkout redirect (handled server-side). * the order skips the Checkout redirect (handled server-side).
*/ */
setupFeeChf?: number | null; setupFeeChf?: number | null;
/**
* The platform's recurring per-tenant monthly fee (net CHF, before
* VAT). Shown on the review step alongside the setup fee so the
* customer sees the ongoing commitment — not just the one-time
* charge — before submitting. Null/0 hides the monthly line.
*/
monthlyFeeChf?: number | null;
/** /**
* Bug 6: when present, the wizard renders in "edit" mode — fields * Bug 6: when present, the wizard renders in "edit" mode — fields
* are pre-populated from the request, the SOUL.md auto-fetch is * are pre-populated from the request, the SOUL.md auto-fetch is
@@ -157,6 +164,7 @@ export function OnboardingWizard({
hasOrgBilling, hasOrgBilling,
existingOrgBilling, existingOrgBilling,
setupFeeChf, setupFeeChf,
monthlyFeeChf,
editingRequest, editingRequest,
onComplete, onComplete,
}: WizardProps) { }: WizardProps) {
@@ -1382,28 +1390,46 @@ export function OnboardingWizard({
<p className="text-xs text-text-muted">{t("confirmNote")}</p> <p className="text-xs text-text-muted">{t("confirmNote")}</p>
{/* Phase 9b: order-time setup-fee notice + amount. The {/* Cost summary. Surfaces the full commitment before
figure shown is the net platform fee (before VAT); submitting — not just the one-time setup fee but the
VAT is added server-side based on the billing recurring monthly per-assistant fee and the fact that
country. We show "+ VAT" rather than a computed AI usage is billed by consumption (with the budget-cap
gross to avoid mis-displaying a country-dependent control as the reassurance). All figures are net (before
total. If setupFeeChf is null/0, no charge happens VAT); VAT is added server-side per billing country, so
and the whole block is suppressed. */} we show "+ VAT" rather than a country-dependent gross.
{typeof setupFeeChf === "number" && setupFeeChf > 0 && ( The block is suppressed only when there are no fixed
fees at all. */}
{((typeof setupFeeChf === "number" && setupFeeChf > 0) ||
(typeof monthlyFeeChf === "number" && monthlyFeeChf > 0)) && (
<div className="text-xs rounded-md border border-accent/30 bg-accent/10 text-text-secondary px-3 py-3 mt-4"> <div className="text-xs rounded-md border border-accent/30 bg-accent/10 text-text-secondary px-3 py-3 mt-4">
<strong className="block text-text-primary mb-1"> <strong className="block text-text-primary mb-2">
{t("setupFeeNoticeHeading")} {t("costSummaryHeading")}
</strong> </strong>
<div className="flex items-baseline justify-between mb-2 pb-2 border-b border-accent/20"> {typeof setupFeeChf === "number" && setupFeeChf > 0 && (
<span>{t("setupFeeAmountLabel")}</span> <div className="flex items-baseline justify-between mb-1.5">
<span className="text-sm font-semibold text-text-primary"> <span>{t("costSetupLabel")}</span>
CHF {setupFeeChf.toFixed(2)}{" "} <span className="text-sm font-semibold text-text-primary">
<span className="text-[10px] font-normal text-text-muted"> CHF {setupFeeChf.toFixed(2)}{" "}
{t("setupFeePlusVat")} <span className="text-[10px] font-normal text-text-muted">
{t("setupFeePlusVat")}
</span>
</span> </span>
</span> </div>
)}
{typeof monthlyFeeChf === "number" && monthlyFeeChf > 0 && (
<div className="flex items-baseline justify-between mb-1.5">
<span>{t("costMonthlyLabel")}</span>
<span className="text-sm font-semibold text-text-primary">
CHF {monthlyFeeChf.toFixed(2)}{" "}
<span className="text-[10px] font-normal text-text-muted">
{t("setupFeePlusVat")}
</span>
</span>
</div>
)}
<div className="mt-2 pt-2 border-t border-accent/20 leading-relaxed">
{t("costUsageNote")}
</div> </div>
{t("setupFeeNoticeBody")}
</div> </div>
)} )}
</div> </div>