From 1bc6378f2cc2ce0bfcef910e7e8385343fe873f0 Mon Sep 17 00:00:00 2001 From: default Date: Tue, 13 Oct 2026 12:58:55 +0000 Subject: [PATCH] billing: deactivate metering and the verify gate (dormant) Billing is off the product path: the pipeline no longer deducts or restores credits and never blocks an AI reply on a balance, the wizard no longer requires card verification to finish, and the dashboard drops the Credits tab. The billing modules, D1 schema and /api/billing routes are left in place (dormant) so this is easy to reverse. --- frontend/src/Page/Dashboard.elm | 1 - frontend/src/Page/Wizard.elm | 43 +++++++++------------------------ public/features.html | 2 +- public/index.html | 2 +- public/pricing.html | 2 +- public/privacy.html | 2 +- public/terms.html | 2 +- src/api/wizard.rs | 11 +++------ src/billing/mod.rs | 7 ++++++ src/pipeline.rs | 28 +++------------------ 10 files changed, 30 insertions(+), 70 deletions(-) diff --git a/frontend/src/Page/Dashboard.elm b/frontend/src/Page/Dashboard.elm index 8054e03..a2fdf39 100644 --- a/frontend/src/Page/Dashboard.elm +++ b/frontend/src/Page/Dashboard.elm @@ -819,7 +819,6 @@ tabs current = [ ( Overview, Route.Dashboard, "Overview" ) , ( Channels, Route.DashboardChannels, "Channels" ) , ( Persona, Route.DashboardPersona, "Voice" ) - , ( Billing, Route.DashboardBilling, "Credits" ) , ( Settings, Route.DashboardSettings, "Settings" ) ] ) diff --git a/frontend/src/Page/Wizard.elm b/frontend/src/Page/Wizard.elm index c82355c..1bd9cd5 100644 --- a/frontend/src/Page/Wizard.elm +++ b/frontend/src/Page/Wizard.elm @@ -14,7 +14,6 @@ local until you press the button that advances the step. -} import Api -import Format import Html exposing (Html, div, h1, h2, li, ol, p, section, span, text, ul) import Html.Attributes exposing (attribute, class, classList) import Html.Events @@ -752,39 +751,19 @@ safetyNote status = launchStep : Model -> Api.Wizard -> Html Msg launchStep model wizard = - let - l = - wizard.launch - in section [ class "wizard-step" ] - [ h1 [] [ text "One last thing" ] - , Ui.card - [ h2 [] [ text "What you'll pay" ] - , p [] - [ text "Every AI reply costs " - , span [ class "price" ] - [ text (Format.milliRate "en-IN" l.currency l.unitPriceMilli) ] - , text ". Canned replies are free, and you're never charged for a message Concierge decides not to answer." - ] + [ h1 [] [ text "You're set" ] + , p [ class "step-sub" ] + [ text + (if String.isEmpty wizard.business.name then + "Concierge will start answering on the channels you connected." + + else + "Concierge will start answering for " + ++ wizard.business.name + ++ " on the channels you connected." + ) ] - , if l.metered && not l.verified then - Ui.card - [ h2 [] [ text "Verify your card" ] - , p [] - [ text "We charge " - , span [ class "price" ] - [ text (Format.money "en-IN" l.currency l.verificationAmount) ] - , text " and refund it straight away. It's how we keep throwaway signups out." - ] - , Ui.linkButton - { label = "Verify now" - , url = "/dashboard/billing" - , primary = True - } - ] - - else - text "" , div [ class "wizard-actions" ] [ Ui.button { label = "Back" diff --git a/public/features.html b/public/features.html index 2396f38..ae880cd 100644 --- a/public/features.html +++ b/public/features.html @@ -15,7 +15,7 @@ - +

What Concierge does

Answers WhatsApp for you

Messages to your business number get a reply in your own voice, within seconds, at any hour. Your number and your customers' threads stay exactly where they are.

Knows when to stop

Anything that quotes a price, promises a date or strays outside what you said it should discuss is never sent. Concierge tells the customer a person is picking it up, and emails you.

Waits for people to finish typing

A burst of three short messages gets one considered answer, not three. You choose how long it waits.

Remembers the conversation

Replies use the thread's recent history. After a few hours of silence the conversation is over and the next message starts fresh.

A prompt you can actually read

Pick a voice, answer a few questions, and Concierge writes the prompt. The fixed guardrails around it are visible on the page — nothing about what the model is told is hidden from you.

Checked before it speaks

Every prompt goes past a safety classifier before it's allowed to answer a customer. Change the prompt and AI replies pause until the new one clears.

Keeps almost nothing

Message bodies pass through and are never written down. Only channel, direction, sender, recipient and timestamp are stored.

diff --git a/public/index.html b/public/index.html index a35a11b..57ef102 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,7 @@ - +

Answer every customer, even when you're closed.

Concierge replies to your WhatsApp messages in your own voice, and hands the conversation to you the moment it matters — a price, a promise, anything it shouldn't decide alone.

How it works

  • Connect your WhatsApp number

    Through Meta's own signup flow. No new number, no app to install.

  • Describe your business once

    Pick a voice, tell it what you do and when you're open. It writes the prompt for you.

  • It answers; you get the hard ones

    Anything about money or a commitment is never sent on its own — Concierge holds the conversation and emails you.

diff --git a/public/pricing.html b/public/pricing.html index 4e50a49..34490be 100644 --- a/public/pricing.html +++ b/public/pricing.html @@ -15,7 +15,7 @@ - +

Pay for replies, nothing else

No subscription, no per-seat fee, no minimum. Buy a pile of replies and use them whenever.

What counts as a reply

  • An AI-written answer costs one reply.
  • A fixed message you wrote costs nothing, however many go out.
  • A message Concierge decides not to answer — because it's about money, or it's been handed to you — costs nothing.
  • If the AI fails mid-reply, the credit goes back.

Buying

Any quantity, upwards of the minimum shown above. Credits you buy don't expire.

diff --git a/public/privacy.html b/public/privacy.html index d6d4836..46a4873 100644 --- a/public/privacy.html +++ b/public/privacy.html @@ -15,7 +15,7 @@ - +
diff --git a/public/terms.html b/public/terms.html index 1abd142..51b5d80 100644 --- a/public/terms.html +++ b/public/terms.html @@ -15,7 +15,7 @@ - +
diff --git a/src/api/wizard.rs b/src/api/wizard.rs index 5d42bb3..818dfa9 100644 --- a/src/api/wizard.rs +++ b/src/api/wizard.rs @@ -238,15 +238,10 @@ pub async fn handle(mut req: Request, env: Env, rest: &[&str]) -> Result { - let tenant = get_tenant(&db, &tenant_id).await?.unwrap_or_default(); - if tenant.plan.is_metered() && tenant.verified_at.is_none() { - return super::error( - "unverified", - "Verify your account before finishing setup.", - 402, - ); - } let has_wa = !list_whatsapp_accounts(&kv, &tenant_id).await?.is_empty(); let has_email = !list_email_accounts(&kv, &tenant_id).await?.is_empty(); if !has_wa && !has_email { diff --git a/src/billing/mod.rs b/src/billing/mod.rs index ef8a4cc..3c34fd6 100644 --- a/src/billing/mod.rs +++ b/src/billing/mod.rs @@ -35,6 +35,10 @@ pub fn calculate_total(credits: i64, milli_price: i64) -> i64 { /// balance returns `false` and the reply is skipped. When `false` (free /// / complimentary), we still bump `replies_used` and consume from any /// existing balance, but always return `true` so the reply ships. +/// +/// Dormant: the pipeline no longer calls this (billing is deactivated), so +/// the item is retained for a possible re-enable rather than deleted. +#[allow(dead_code)] pub async fn try_deduct(db: &D1Database, tenant_id: &str, metered: bool) -> Result { let mut billing = storage::get_tenant_billing(db, tenant_id).await?; prune_expired(&mut billing); @@ -62,6 +66,9 @@ pub async fn try_deduct(db: &D1Database, tenant_id: &str, metered: bool) -> Resu /// Restore one reply credit after a failed send. /// Adds as a non-expiring purchase credit for simplicity. +/// +/// Dormant: see [`try_deduct`]. Retained for a possible re-enable. +#[allow(dead_code)] pub async fn restore_credit(db: &D1Database, tenant_id: &str) -> Result<()> { let mut billing = storage::get_tenant_billing(db, tenant_id).await?; billing.credits.push(CreditEntry { diff --git a/src/pipeline.rs b/src/pipeline.rs index 968fd21..31fca2c 100644 --- a/src/pipeline.rs +++ b/src/pipeline.rs @@ -4,7 +4,6 @@ use worker::*; use crate::ai; -use crate::billing; use crate::channel; use crate::helpers::generate_id; use crate::storage::*; @@ -293,16 +292,10 @@ async fn handle_auto_reply( return Ok(()); } - if is_ai { - let metered = crate::storage::get_tenant(db, &msg.tenant_id) - .await? - .map(|t| t.plan.is_metered()) - .unwrap_or(true); - if !billing::try_deduct(db, &msg.tenant_id, metered).await? { - console_log!("Tenant {} out of AI-reply credits, skipping", msg.tenant_id); - return Ok(()); - } - } + // Billing is deactivated (dormant): AI replies are never metered and + // never blocked on a credit balance. `src/billing` and the credit ledger + // remain in the tree for a possible re-enable, but the pipeline no longer + // touches them. // Build the chat history we'll hand to the model: prior turns // (capped) plus the new inbound as the latest user turn. The same @@ -379,9 +372,6 @@ async fn handle_auto_reply( Ok(r) => r, Err(e) => { console_log!("AI auto-reply error: {:?}", e); - if let Err(re) = billing::restore_credit(db, &msg.tenant_id).await { - console_log!("Failed to restore credit: {:?}", re); - } return Ok(()); } } @@ -396,11 +386,6 @@ async fn handle_auto_reply( let new_handoff = stripped.handoff && matches!(handoff_mode, HandoffMode::None); if reply.is_empty() { - if is_ai { - if let Err(e) = billing::restore_credit(db, &msg.tenant_id).await { - console_log!("Failed to restore credit: {:?}", e); - } - } return Ok(()); } @@ -452,11 +437,6 @@ async fn handle_auto_reply( channel::send_reply(&msg.channel, env, &msg.raw_metadata, &msg.sender, &reply).await { console_log!("Auto-reply send error: {:?}", e); - if is_ai { - if let Err(re) = billing::restore_credit(db, &msg.tenant_id).await { - console_log!("Failed to restore credit: {:?}", re); - } - } return Ok(()); }