fix(buyer/billing/comms/a11y): Codex review pass round 2

Tuxedo buyer path (subagent 2):
- src/app/tuxedo/page.tsx: remove duplicate CinematicShowcase render
- src/components/storefront/CinematicShowcase.tsx: wire up useCart, Add to Cart button, brand-aware fulfillment
- src/app/tuxedo/stops/TuxedoStopsList.tsx: improved empty state with calendar icon + CTAs
- src/app/cart/CartClient.tsx: guard against empty cart checkout; 'Cart is Empty' state

Billing reconciliation (subagent 3):
- src/actions/billing/billing-overview.ts: NEW — single source of truth
- src/app/admin/settings/billing/page.tsx: use getBillingOverview
- src/app/admin/settings/billing/BillingClientPage.tsx: rewritten to consume BillingOverview (status pill, addons state, removable flags, derived invoice amounts, usage footer)
- src/app/admin/page.tsx: use getBillingOverview (aligns dashboard with billing)
- src/components/admin/DashboardClient.tsx: 'Active Products' now reads from getBillingOverview
- supabase/migrations/203_plan_usage_active_products.sql: get_brand_plan_info counts products as active=true AND deleted_at IS NULL

Harvest Reach dedup + audience preview (manual, subagent 4 didn't complete):
- src/components/admin/CommunicationsPage.tsx: add initialTab prop
- src/app/admin/communications/compose/page.tsx: now renders with initialTab='compose' (single compose experience, no duplicate edit panel)
- src/components/admin/HarvestReach/CampaignComposerPage.tsx: always-visible audience preview panel (count + sample emails), loads via previewCampaignAudience action

Layout/content consistency + a11y sweep (subagent 5):
- src/components/layout/SiteHeader.tsx: Admin link only shows for authenticated admin users
- src/components/Providers.tsx: suppress public SiteHeader/Footer for /admin, /cart, /checkout, /wholesale, /water (fixes duplicate headers)
- src/app/contact/ContactClientPage.tsx: Phone/Email now use tel:/mailto: links; dynamic year
- src/app/blog/page.tsx, changelog, privacy-policy, roadmap, security, terms-and-conditions, waitlist: dynamic year
- src/app/admin/wholesale/WholesaleClient.tsx: proper htmlFor/id, type=email/tel, autoComplete
- src/app/admin/settings/ai/AIClient.tsx: proper htmlFor/id, required + aria-required
- src/app/admin/settings/integrations/IntegrationsClient.tsx: only mask secret fields; add required + aria-required + CredentialField.required type
- src/app/admin/water-log/headgates/HeadgatesManager.tsx: htmlFor/id, aria-required
- src/components/admin/CreateUserModal.tsx: htmlFor/id, required, aria-required, aria-describedby, autoComplete
- src/app/admin/me/AdminMeClient.tsx, products/import, sales/import, water-log/settings, login, brands, tuxedo: a11y polish (ids/required/aria)
This commit is contained in:
2026-06-03 16:39:19 +00:00
parent 03ae372509
commit 0245aa29cc
34 changed files with 1122 additions and 295 deletions
+236
View File
@@ -0,0 +1,236 @@
"use server";
/**
* Centralized Billing Overview
*
* Single source of truth for everything the billing page + dashboard usage
* bars need to render. Replaces ad-hoc calls to `getBrandPlanInfo`,
* `getEnabledAddons`, brand-row subscription columns, and hard-coded invoice
* data with one consistent, denormalized payload.
*
* Why a single action:
* - Before this, the billing page combined `getBrandPlanInfo` +
* `getEnabledAddons` + a brands-table read of subscription fields +
* hard-coded mock invoices. The dashboard's product count came from
* `getDashboardStats` which used a different filter than `get_brand_plan_info`
* (`active=true` vs `deleted_at IS NULL`), causing "Active Products 1" vs
* "Products 0/25" in the same UI.
* - Add-ons could be "Active" in `brand_features` while no Stripe
* subscription existed, with a Remove button that would fail at Stripe.
*
* What this returns:
* - planTier + planCycle (we default to "monthly" because the brand row
* doesn't store a cycle — the UI toggle controls display, but a real
* cycle would be detected from a `stripe_subscription.items.data[0].price
* .recurring.interval` when fetched)
* - hasActiveSubscription + subscriptionStatus (so the UI can hide
* misleading "Active" add-on badges / Remove buttons)
* - usage: { users, stops_this_month, products } — products counted
* consistently with the dashboard (`active=true AND deleted_at IS NULL`).
* - enabledAddons with a derived `removable` flag — only true when
* there is an active subscription AND the feature is enabled AND the
* current user can manage settings.
* - displayedInvoiceAmount — monthly or annual equivalent matching the
* billingCycle toggle, summed across plan + active add-ons. Used by the
* invoice table so amounts always match the displayed plan.
*/
import { getAdminUser } from "@/lib/admin-permissions";
import { svcHeaders } from "@/lib/svc-headers";
import { ADDONS, PLAN_TIERS, type AddonKey, type PlanTierKey } from "@/lib/pricing";
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
const supabaseKey = process.env.SUPABASE_SERVICE_ROLE_KEY!;
export type BillingSubscriptionStatus =
| "active"
| "trialing"
| "past_due"
| "canceled"
| "incomplete"
| "unpaid"
| "inactive"
| "none";
export type BillingOverview = {
brandId: string;
brandName: string | null;
planTier: PlanTierKey;
planCycle: "monthly" | "annual";
planMonthlyPrice: number;
planAnnualPrice: number;
hasStripeCustomer: boolean;
hasActiveSubscription: boolean;
subscriptionStatus: BillingSubscriptionStatus | null;
currentPeriodEnd: string | null;
limits: { max_users: number; max_stops_monthly: number; max_products: number };
usage: { users: number; stops_this_month: number; products: number };
enabledAddons: Record<string, boolean>;
// Denormalized add-on list (for direct UI rendering)
addons: Array<{
key: AddonKey;
label: string;
icon: string;
description: string;
enabled: boolean;
removable: boolean;
monthlyPrice: number;
annualPrice: number;
}>;
// Pre-computed totals so the billing page never disagrees with itself
displayedInvoiceAmount: number; // current cycle total
monthlyTotal: number; // always monthly equivalent
annualTotal: number; // always annual equivalent
isPlatformAdmin: boolean;
canManageBilling: boolean;
};
/**
* Fetch a single, consistent billing summary for a brand.
* Safe to call from a server component (uses service-role key).
*/
export async function getBillingOverview(
brandId: string,
options?: { planCycle?: "monthly" | "annual" }
): Promise<{ success: boolean; data?: BillingOverview; error?: string }> {
try {
if (!brandId) return { success: false, error: "brandId required" };
// 1) Plan info (plan_tier + limits + usage via get_brand_plan_info)
const planRes = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_brand_plan_info`,
{
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({ p_brand_id: brandId }),
}
);
const planData = planRes.ok ? await planRes.json() : null;
// 2) Brand row: subscription state, name, stripe_customer_id
const brandRes = await fetch(
`${supabaseUrl}/rest/v1/brands?id=eq.${brandId}&select=name,stripe_customer_id,stripe_subscription_id,stripe_subscription_status,stripe_current_period_end`,
{ headers: svcHeaders(supabaseKey) }
);
const brandRows = brandRes.ok ? await brandRes.json() : [];
const brand = Array.isArray(brandRows) ? brandRows[0] : null;
// 3) Enabled add-ons (feature flags)
const addonsRes = await fetch(
`${supabaseUrl}/rest/v1/rpc/get_brand_features`,
{
method: "POST",
headers: { ...svcHeaders(supabaseKey), "Content-Type": "application/json" },
body: JSON.stringify({ p_brand_id: brandId }),
}
);
const enabledAddons = addonsRes.ok ? await addonsRes.json() : {};
// 4) Active product count — same semantics as the dashboard
// (active=true AND deleted_at IS NULL). Keeps the billing page in
// sync with /admin "Active Products" stat.
const productRes = await fetch(
`${supabaseUrl}/rest/v1/products?select=id&brand_id=eq.${brandId}&active=eq.true&deleted_at=is.null&limit=1000`,
{ headers: { ...svcHeaders(supabaseKey), Prefer: "count=exact" } }
);
const productCountHeader = productRes.headers.get("Content-Range");
let activeProductCount = 0;
if (productCountHeader && productCountHeader.includes("/")) {
const total = productCountHeader.split("/").pop();
activeProductCount = parseInt(total ?? "0", 10) || 0;
}
// 5) Admin context (so we know if the user can manage / is platform)
const adminUser = await getAdminUser();
const isPlatformAdmin = adminUser?.role === "platform_admin";
const canManageBilling = isPlatformAdmin || !!adminUser?.can_manage_settings;
// ── Normalize plan data ──────────────────────────────────────────────────
const planTier = (planData?.plan_tier ?? "starter") as PlanTierKey;
const limits = {
max_users: planData?.max_users ?? 1,
max_stops_monthly: planData?.max_stops_monthly ?? 10,
max_products: planData?.max_products ?? 25,
};
const planInfo = PLAN_TIERS[planTier] ?? PLAN_TIERS.starter;
const planMonthlyPrice = planInfo.monthlyPrice;
const planAnnualPrice = planInfo.annualPrice;
// ── Normalize subscription state ─────────────────────────────────────────
const rawStatus = (brand?.stripe_subscription_status ?? null) as BillingSubscriptionStatus | null;
const hasStripeCustomer = !!brand?.stripe_customer_id;
const hasActiveSubscription =
rawStatus === "active" || rawStatus === "trialing";
// ── Build add-on list with derived `removable` ──────────────────────────
const addons: BillingOverview["addons"] = (Object.keys(ADDONS) as AddonKey[]).map((key) => {
const cfg = ADDONS[key];
const enabled = !!enabledAddons?.[key];
return {
key,
label: cfg.label,
icon: cfg.icon,
description: cfg.description,
enabled,
removable: enabled && hasActiveSubscription && canManageBilling,
monthlyPrice: cfg.monthlyPrice,
annualPrice: cfg.annualPrice,
};
});
// ── Usage (products override from active=true+not-deleted count) ───────
const usage = {
users: planData?.usage?.users ?? 0,
stops_this_month: planData?.usage?.stops_this_month ?? 0,
// Prefer the active+non-deleted count for cross-UI consistency.
products: activeProductCount || (planData?.usage?.products ?? 0),
};
// ── Totals ───────────────────────────────────────────────────────────────
const addonsMonthlyTotal = addons
.filter((a) => a.enabled && hasActiveSubscription)
.reduce((s, a) => s + a.monthlyPrice, 0);
const addonsAnnualTotal = addons
.filter((a) => a.enabled && hasActiveSubscription)
.reduce((s, a) => s + a.annualPrice, 0);
const monthlyTotal = planMonthlyPrice + addonsMonthlyTotal;
const annualTotal = planAnnualPrice + addonsAnnualTotal;
const planCycle: "monthly" | "annual" =
options?.planCycle ?? "monthly"; // safer default — see header comment
const displayedInvoiceAmount =
planCycle === "annual"
? planAnnualPrice + addonsAnnualTotal
: planMonthlyPrice + addonsMonthlyTotal;
return {
success: true,
data: {
brandId,
brandName: brand?.name ?? planData?.brand_name ?? null,
planTier,
planCycle,
planMonthlyPrice,
planAnnualPrice,
hasStripeCustomer,
hasActiveSubscription,
subscriptionStatus: rawStatus,
currentPeriodEnd: brand?.stripe_current_period_end ?? null,
limits,
usage,
enabledAddons: (enabledAddons ?? {}) as Record<string, boolean>,
addons,
displayedInvoiceAmount,
monthlyTotal,
annualTotal,
isPlatformAdmin,
canManageBilling,
},
};
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
return { success: false, error: `Failed to build billing overview: ${message}` };
}
}