Skip to content

Commit fdffb8d

Browse files
committed
feat(pricing, nav): regulate executive connect pricing dynamically and surface sales inquiries in navigation
1 parent 885fada commit fdffb8d

5 files changed

Lines changed: 108 additions & 18 deletions

File tree

src/app/(public)/checkout/page.tsx

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ function computePrice(
3434
tier: ExperienceKey,
3535
cur: 'INR' | 'USD',
3636
customSlugs: string[] = [],
37-
pricingConfig: PricingConfig = DEFAULT_PRICING
37+
pricingConfig: PricingConfig & { executiveConnectPricing?: Record<string, number> } = DEFAULT_PRICING
3838
) {
3939
const prices = pricingConfig.basePrices[cur];
4040
const sym = cur === 'INR' ? '₹' : '$';
@@ -48,12 +48,27 @@ function computePrice(
4848
}
4949

5050
const complementarySet = new Set(PACKAGE_COMPLEMENTARY[pkg as PkgSlug] ?? []);
51-
const services = slugs.map(slug => ({
52-
slug,
53-
label: SERVICE_LABELS[slug],
54-
price: complementarySet.has(slug) ? 0 : (prices[slug]?.[tier] ?? 0),
55-
complimentary: complementarySet.has(slug),
56-
}));
51+
const services = slugs.map(slug => {
52+
let price = 0;
53+
if (!complementarySet.has(slug)) {
54+
if (slug === 'EXECUTIVE_CONNECT') {
55+
const ecMap = pricingConfig.executiveConnectPricing;
56+
if (ecMap && typeof ecMap[cur] === 'number') {
57+
price = ecMap[cur];
58+
} else {
59+
price = prices[slug]?.[tier] ?? (cur === 'INR' ? 4999 : 100);
60+
}
61+
} else {
62+
price = prices[slug]?.[tier] ?? 0;
63+
}
64+
}
65+
return {
66+
slug,
67+
label: SERVICE_LABELS[slug],
68+
price,
69+
complimentary: complementarySet.has(slug),
70+
};
71+
});
5772
let discountableSubtotal = 0;
5873
let nonDiscountableSubtotal = 0;
5974
services.forEach(x => {
@@ -118,7 +133,7 @@ function CheckoutPageInner() {
118133
subtotalAfterDiscount: number; taxRate: number; taxAmount: number;
119134
finalPayable: number; isIndia: boolean; gateway: string;
120135
} | null>(null);
121-
const [pricingConfig, setPricingConfig] = useState<PricingConfig>(DEFAULT_PRICING);
136+
const [pricingConfig, setPricingConfig] = useState<PricingConfig & { executiveConnectPricing?: Record<string, number> }>(DEFAULT_PRICING);
122137
const [addExecutiveConnect, setAddExecutiveConnect] = useState(false);
123138
const [whatsapp, setWhatsapp] = useState('');
124139
const [website] = useState('');
@@ -695,16 +710,31 @@ function CheckoutPageInner() {
695710
if (live.services.length === 0) return null;
696711
// International clients see their local currency as the headline; USD is the reference.
697712
const showLocal = !!(localRate && countryCode !== 'IN');
698-
const toLocal = (n: number) => (showLocal ? Math.round(n * localRate!.rate) : n);
713+
const toLocal = (n: number, slug?: string) => {
714+
if (!showLocal) return n;
715+
if (slug === 'EXECUTIVE_CONNECT' && pricingConfig.executiveConnectPricing?.[localRate!.code]) {
716+
return pricingConfig.executiveConnectPricing[localRate!.code];
717+
}
718+
return Math.round(n * localRate!.rate);
719+
};
699720
const priSym = showLocal ? localRate!.symbol : live.sym;
700721
const priCode = showLocal ? localRate!.code : (cur === 'INR' ? 'INR' : 'USD');
722+
const ecPrice = toLocal(
723+
pricingConfig.executiveConnectPricing?.[cur] ?? (cur === 'INR' ? 4999 : 100),
724+
'EXECUTIVE_CONNECT'
725+
);
701726
return (
702727
<div className="space-y-6">
703728
{(experienceLevel === 'EXECUTIVE' || experienceLevel === 'EXECUTIVE_PLUS') && (
704729
<div className="p-5 border border-brand-gold/30 bg-brand-gold/5 flex flex-col gap-3">
705730
<div className="flex items-start justify-between gap-4">
706731
<div>
707-
<h3 className="font-semibold text-brand-obsidian text-sm uppercase tracking-wider mb-1">Add Executive Connect</h3>
732+
<div className="flex items-center gap-2 mb-1">
733+
<h3 className="font-semibold text-brand-obsidian text-sm uppercase tracking-wider">Add Executive Connect</h3>
734+
<span className="text-xs font-bold text-brand-gold bg-brand-gold/10 px-2 py-0.5 rounded">
735+
+{priSym}{ecPrice.toLocaleString()}
736+
</span>
737+
</div>
708738
<p className="text-xs text-brand-obsidian/70 leading-relaxed">
709739
A 45-minute 1-on-1 strategy session with our senior executive team to align your narrative before we start writing. Highly recommended for Director and C-suite roles.
710740
</p>
@@ -727,7 +757,7 @@ function CheckoutPageInner() {
727757
{s.complimentary ? (
728758
<span className="text-[10px] font-bold text-emerald-700 bg-emerald-50 border border-emerald-100 px-2 py-0.5 rounded-full leading-none">INCLUDED FREE</span>
729759
) : (
730-
<span className="font-medium text-brand-obsidian tabular-nums">{priSym}{toLocal(s.price).toLocaleString()}</span>
760+
<span className="font-medium text-brand-obsidian tabular-nums">{priSym}{toLocal(s.price, s.slug).toLocaleString()}</span>
731761
)}
732762
</div>
733763
))}

src/app/api/public/pricing/route.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { NextResponse } from 'next/server';
22
import { getGlobalPricing } from '@/lib/pricing-v2';
3+
import { getExecutiveConnectPricingMap } from '@/lib/systemSettings';
34

45
export const dynamic = 'force-dynamic';
56

67
export async function GET() {
78
try {
89
const config = await getGlobalPricing();
9-
return NextResponse.json(config);
10+
const executiveConnectPricing = await getExecutiveConnectPricingMap();
11+
return NextResponse.json({
12+
...config,
13+
executiveConnectPricing,
14+
});
1015
} catch (error) {
1116
return NextResponse.json({ error: 'Failed to fetch pricing' }, { status: 500 });
1217
}

src/components/AppShell.tsx

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@ import Link from 'next/link';
66
import { usePathname } from 'next/navigation';
77
import {
88
IconGrid, IconPlus, IconList, IconLogout, IconTarget, IconUser,
9-
IconTrendUp, IconMail, IconChevronDown,
9+
IconTrendUp, IconMail, IconChevronDown, IconDocument,
1010
} from '@/components/Icons';
1111
import { Logo } from '@/components/Logo';
1212
import { useAdmin } from '@/components/AdminProvider';
1313
import NotificationBell from '@/components/NotificationBell';
1414

1515
// ── Inline icons ──────────────────────────────────────────────────
16+
function IconInquiry({ size = 16 }: { size?: number }) {
17+
return (
18+
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" aria-hidden>
19+
<path
20+
stroke="currentColor"
21+
strokeWidth="2"
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
d="M21 11.5a8.38 8.38 0 01-.9 3.8 8.5 8.5 0 01-7.6 4.7 8.38 8.38 0 01-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 01-.9-3.8 8.5 8.5 0 014.7-7.6 8.38 8.38 0 013.8-.9h.5a8.48 8.48 0 018 8v.5z"
25+
/>
26+
</svg>
27+
);
28+
}
29+
1630
function IconBug({ size = 16 }: { size?: number }) {
1731
return (
1832
<svg width={size} height={size} fill="none" viewBox="0 0 24 24" aria-hidden>
@@ -123,7 +137,8 @@ function isActive(href: string, pathname: string) {
123137
if (href === '/invoices') return pathname.startsWith('/invoices') && pathname !== '/invoices/new';
124138
if (href === '/career') return pathname === '/career' || (pathname.startsWith('/career/') && !pathname.startsWith('/career/kanban') && !pathname.startsWith('/career/email-logs') && !pathname.startsWith('/career/calendar'));
125139
if (href === '/flywheel') return pathname === '/flywheel';
126-
if (href === '/sales/inquiries') return pathname.startsWith('/sales');
140+
if (href === '/sales/inquiries') return pathname.startsWith('/sales/inquiries');
141+
if (href === '/sales/proposals') return pathname.startsWith('/sales/proposals');
127142
return pathname.startsWith(href);
128143
}
129144

@@ -432,6 +447,22 @@ function SidebarContent({
432447
{/* Deliverables & Growth */}
433448
{hasCatalystAccess && (
434449
<NavGroup label="Deliverables & Growth" collapsed={collapsed}>
450+
<NavLink
451+
href="/sales/inquiries"
452+
icon={<IconInquiry size={16} />}
453+
label="Sales Inquiries"
454+
active={isActive('/sales/inquiries', pathname)}
455+
onClick={onNavigate}
456+
collapsed={collapsed}
457+
/>
458+
<NavLink
459+
href="/sales/proposals"
460+
icon={<IconDocument size={16} />}
461+
label="Proposals"
462+
active={isActive('/sales/proposals', pathname)}
463+
onClick={onNavigate}
464+
collapsed={collapsed}
465+
/>
435466
<NavLink
436467
href="/career/calendar"
437468
icon={<IconCalendar size={16} />}

src/lib/catalog/self-service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ export function resolveSelfServiceServices(
7070
if (packageSlug === 'CUSTOM') {
7171
return customServices;
7272
}
73-
return SELF_SERVICE_PACKAGES[packageSlug].services;
73+
const base = [...SELF_SERVICE_PACKAGES[packageSlug].services];
74+
if (customServices.includes('EXECUTIVE_CONNECT') && !base.includes('EXECUTIVE_CONNECT')) {
75+
base.push('EXECUTIVE_CONNECT');
76+
}
77+
return base;
7478
}
7579

7680
export function validateSelfServiceCheckout(input: {
@@ -92,7 +96,7 @@ export function validateSelfServiceCheckout(input: {
9296
}
9397

9498
if (input.packageSlug === 'CUSTOM') {
95-
const allowed = new Set(pkg.services);
99+
const allowed = new Set([...pkg.services, 'EXECUTIVE_CONNECT' as ServiceSlug]);
96100
for (const s of resolved) {
97101
if (!allowed.has(s)) {
98102
return { valid: false, error: `Service ${s} is not available for self-service checkout.` };

src/lib/pricing-v2.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface PricingConfig {
1616
packageDiscounts: Record<PackageSlug, number>; // Percentage off (e.g. 0.10 for 10% off)
1717
}
1818

19-
import { getSetting } from './systemSettings';
19+
import { getSetting, getExecutiveConnectPricingMap } from './systemSettings';
2020

2121
// These are base prices per currency.
2222
// Having static USD prices allows for clean numbers (e.g. $149) rather than weird exchange rate fractions.
@@ -108,7 +108,27 @@ export const PRICING = DEFAULT_PRICING;
108108

109109
export async function getGlobalPricing(): Promise<PricingConfig> {
110110
const config = await getSetting<PricingConfig>('GLOBAL_PRICING_V2');
111-
return config ?? DEFAULT_PRICING;
111+
const base: PricingConfig = config
112+
? JSON.parse(JSON.stringify(config))
113+
: JSON.parse(JSON.stringify(DEFAULT_PRICING));
114+
115+
try {
116+
const execPricing = await getExecutiveConnectPricingMap();
117+
if (execPricing) {
118+
if (typeof execPricing.INR === 'number' && base.basePrices?.INR?.EXECUTIVE_CONNECT) {
119+
base.basePrices.INR.EXECUTIVE_CONNECT.EXECUTIVE = execPricing.INR;
120+
base.basePrices.INR.EXECUTIVE_CONNECT.EXECUTIVE_PLUS = execPricing.INR;
121+
}
122+
if (typeof execPricing.USD === 'number' && base.basePrices?.USD?.EXECUTIVE_CONNECT) {
123+
base.basePrices.USD.EXECUTIVE_CONNECT.EXECUTIVE = execPricing.USD;
124+
base.basePrices.USD.EXECUTIVE_CONNECT.EXECUTIVE_PLUS = execPricing.USD;
125+
}
126+
}
127+
} catch (err) {
128+
console.error('Failed to sync executive connect pricing in getGlobalPricing:', err);
129+
}
130+
131+
return base;
112132
}
113133

114134
/**

0 commit comments

Comments
 (0)