feat(zoho): WebToLead forwarding mode, veteran-owned certified badge, Docker/env/CI updates (batch 0.9.1)

This commit is contained in:
null 2026-06-14 16:08:29 -05:00
parent 05b27d216a
commit d074e597b2
12 changed files with 187 additions and 31 deletions

View File

@ -5,8 +5,17 @@ NODE_ENV=production
SERVER_PORT=3001 SERVER_PORT=3001
# Zoho CRM Integration # Zoho CRM Integration
# Set ZOHO_ENABLED=true to forward leads/support to Zoho CRM # Preferred current setup: webtolead for contact leads using the legacy Zoho form tokens.
# Get credentials from https://api-console.zoho.com → Self Client ZOHO_FORWARDING_MODE=webtolead
ZOHO_WEBTOLEAD_ENABLED=false
ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
ZOHO_WEBTOLEAD_XNQSJSDP=
ZOHO_WEBTOLEAD_XMIWTLD=
ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
ZOHO_WEBTOLEAD_RETURN_URL=null
ZOHO_WEBTOLEAD_ZC_GAD=
# Standby REST API/OAuth setup. Set ZOHO_FORWARDING_MODE=api and ZOHO_ENABLED=true to use it.
ZOHO_ENABLED=false ZOHO_ENABLED=false
ZOHO_API_DOMAIN=https://www.zohoapis.com ZOHO_API_DOMAIN=https://www.zohoapis.com
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com

4
.gitignore vendored
View File

@ -6,6 +6,9 @@ FUTURE.md
HISTORY.md HISTORY.md
BUILD_SUMMARY.md BUILD_SUMMARY.md
SCRIPTS.md SCRIPTS.md
.drop/
zoho.md
# Dependencies # Dependencies
node_modules/ node_modules/
@ -36,3 +39,4 @@ pnpm-debug.log*
.learnings/ .learnings/
Levi.md Levi.md
Queue-North-Website.code-workspace Queue-North-Website.code-workspace
Working Site.zip

View File

@ -47,6 +47,14 @@ ENV SERVER_PORT=3001
ENV RATE_LIMIT_PER_MINUTE=5 ENV RATE_LIMIT_PER_MINUTE=5
ENV CORS_ORIGIN=* ENV CORS_ORIGIN=*
ENV LOG_LEVEL=info ENV LOG_LEVEL=info
ENV ZOHO_FORWARDING_MODE=webtolead
ENV ZOHO_WEBTOLEAD_ENABLED=false
ENV ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
ENV ZOHO_WEBTOLEAD_XNQSJSDP=
ENV ZOHO_WEBTOLEAD_XMIWTLD=
ENV ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
ENV ZOHO_WEBTOLEAD_RETURN_URL=null
ENV ZOHO_WEBTOLEAD_ZC_GAD=
ENV ZOHO_ENABLED=false ENV ZOHO_ENABLED=false
ENV ZOHO_API_DOMAIN=https://www.zohoapis.com ENV ZOHO_API_DOMAIN=https://www.zohoapis.com
ENV ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com ENV ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -22,12 +22,21 @@ services:
- RATE_LIMIT_PER_MINUTE=5 - RATE_LIMIT_PER_MINUTE=5
- CORS_ORIGIN=https://queuenorth.com - CORS_ORIGIN=https://queuenorth.com
- LOG_LEVEL=info - LOG_LEVEL=info
- ZOHO_ENABLED=false - ZOHO_FORWARDING_MODE=${ZOHO_FORWARDING_MODE:-webtolead}
- ZOHO_API_DOMAIN=https://www.zohoapis.com - ZOHO_WEBTOLEAD_ENABLED=${ZOHO_WEBTOLEAD_ENABLED:-false}
- ZOHO_CLIENT_ID= - ZOHO_WEBTOLEAD_URL=${ZOHO_WEBTOLEAD_URL:-https://crm.zoho.com/crm/WebToLeadForm}
- ZOHO_CLIENT_SECRET= - ZOHO_WEBTOLEAD_XNQSJSDP=${ZOHO_WEBTOLEAD_XNQSJSDP:-}
- ZOHO_REFRESH_TOKEN= - ZOHO_WEBTOLEAD_XMIWTLD=${ZOHO_WEBTOLEAD_XMIWTLD:-}
- ZOHO_REDIRECT_URI= - ZOHO_WEBTOLEAD_ACTION_TYPE=${ZOHO_WEBTOLEAD_ACTION_TYPE:-TGVhZHM=}
- ZOHO_WEBTOLEAD_RETURN_URL=${ZOHO_WEBTOLEAD_RETURN_URL:-null}
- ZOHO_WEBTOLEAD_ZC_GAD=${ZOHO_WEBTOLEAD_ZC_GAD:-}
- ZOHO_ENABLED=${ZOHO_ENABLED:-false}
- ZOHO_API_DOMAIN=${ZOHO_API_DOMAIN:-https://www.zohoapis.com}
- ZOHO_ACCOUNTS_DOMAIN=${ZOHO_ACCOUNTS_DOMAIN:-https://accounts.zoho.com}
- ZOHO_CLIENT_ID=${ZOHO_CLIENT_ID:-}
- ZOHO_CLIENT_SECRET=${ZOHO_CLIENT_SECRET:-}
- ZOHO_REFRESH_TOKEN=${ZOHO_REFRESH_TOKEN:-}
- ZOHO_CASES_ENABLED=${ZOHO_CASES_ENABLED:-false}
- RECAPTCHA_ENABLED=${RECAPTCHA_ENABLED:-false} - RECAPTCHA_ENABLED=${RECAPTCHA_ENABLED:-false}
- RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY:-} - RECAPTCHA_SECRET_KEY=${RECAPTCHA_SECRET_KEY:-}
- RECAPTCHA_MIN_SCORE=${RECAPTCHA_MIN_SCORE:-0.5} - RECAPTCHA_MIN_SCORE=${RECAPTCHA_MIN_SCORE:-0.5}

View File

@ -1,18 +1,36 @@
# Zoho CRM Setup Guide for Queue North Admins # Zoho CRM Setup Guide for Queue North Admins
This guide walks you through setting up Zoho CRM integration to automatically capture leads and support cases from Queue North's website. This guide walks you through the current Zoho CRM integration. Contact leads use the legacy Zoho WebToLead form tokens, while the OAuth/API integration remains available as a standby option for future lead upserts or support cases.
--- ---
## 🔒 Prerequisites ## Prerequisites
Before you begin, ensure you have: Before you begin, ensure you have:
- A Zoho CRM account (admin access required) - A Zoho CRM account (admin access required)
- Access to Zoho API Console: https://api-console.zoho.com - The WebToLead hidden field values from the old Zoho form
--- ---
## Step 1: Create a Zoho Self-Client App ## Step 1: Gather WebToLead Values
The current integration needs the old Zoho form's hidden fields:
```text
xnQsjsdp
xmIwtLD
actionType
returnURL
zc_gad
```
These values are stored locally in `zoho.md`, which is ignored by git.
---
## Optional Standby: Create a Zoho Self-Client App
Only use these OAuth steps if switching `ZOHO_FORWARDING_MODE=api`.
1. Go to **https://api-console.zoho.com** 1. Go to **https://api-console.zoho.com**
2. Click **"Create Self Client"** 2. Click **"Create Self Client"**
@ -29,7 +47,7 @@ Before you begin, ensure you have:
--- ---
## Step 2: Generate an Authorization Code ## Optional Standby: Generate an Authorization Code
1. In the Self Client tab, click **"Generate Code"** 1. In the Self Client tab, click **"Generate Code"**
2. Set the **Scope** to: 2. Set the **Scope** to:
@ -42,7 +60,7 @@ Before you begin, ensure you have:
--- ---
## Step 3: Exchange Auth Code for Tokens ## Optional Standby: Exchange Auth Code for Tokens
Run this `curl` command (replace placeholders): Run this `curl` command (replace placeholders):
@ -69,12 +87,27 @@ curl -X POST https://accounts.zoho.com/oauth/v2/token \
--- ---
## Step 4: Configure Environment Variables ## Step 2: Configure Environment Variables
Add these to your `.env` file: The current production-friendly setup uses the legacy Zoho WebToLead form tokens for contact leads while keeping the OAuth API integration available as a standby option.
Add these to your `.env` file for WebToLead lead forwarding:
```env ```env
# Zoho integration is OFF by default — set to true to enable ZOHO_FORWARDING_MODE=webtolead
ZOHO_WEBTOLEAD_ENABLED=true
ZOHO_WEBTOLEAD_URL=https://crm.zoho.com/crm/WebToLeadForm
ZOHO_WEBTOLEAD_XNQSJSDP=<from Zoho WebToLead hidden field>
ZOHO_WEBTOLEAD_XMIWTLD=<from Zoho WebToLead hidden field>
ZOHO_WEBTOLEAD_ACTION_TYPE=TGVhZHM=
ZOHO_WEBTOLEAD_RETURN_URL=null
ZOHO_WEBTOLEAD_ZC_GAD=
```
Use these only if switching back to the Zoho CRM REST API/OAuth integration:
```env
ZOHO_FORWARDING_MODE=api
ZOHO_ENABLED=false ZOHO_ENABLED=false
ZOHO_API_DOMAIN=https://www.zohoapis.com ZOHO_API_DOMAIN=https://www.zohoapis.com
ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com ZOHO_ACCOUNTS_DOMAIN=https://accounts.zoho.com
@ -85,7 +118,7 @@ ZOHO_REFRESH_TOKEN=<from Step 3>
ZOHO_CASES_ENABLED=false ZOHO_CASES_ENABLED=false
``` ```
> **Note:** Both `ZOHO_ENABLED` and `ZOHO_CASES_ENABLED` default to `false`. Set them to `true` only after completing Steps 13 and verifying your credentials. > **Note:** `ZOHO_CASES_ENABLED` only applies to the OAuth/API path. The WebToLead values found in the old site are for lead capture only.
### Datacenter Variants ### Datacenter Variants
@ -100,7 +133,7 @@ If your Zoho datacenter is **outside the US**, adjust the domains:
--- ---
## Step 5: Test the Integration ## Step 3: Test the Integration
### Test Lead Capture ### Test Lead Capture
1. Submit a lead on the contact form (name, email, phone, message) 1. Submit a lead on the contact form (name, email, phone, message)
@ -156,7 +189,7 @@ Website Contact Form → SQLite (always saved)
3. `refresh_token` never expires — store it securely 3. `refresh_token` never expires — store it securely
### Upsert Logic ### Upsert Logic
- **Leads**: Email-based upsert (update if exists, create if new) - **Leads**: WebToLead creates leads through the legacy Zoho form endpoint. API mode uses email-based upsert.
- **Cases**: Always insert (new case per submission) - **Cases**: Always insert (new case per submission)
### Fire-and-Forget Design ### Fire-and-Forget Design
@ -171,7 +204,7 @@ Website Contact Form → SQLite (always saved)
After configuration: After configuration:
1. Deploy the environment variables to production 1. Deploy the environment variables to production
2. Set `ZOHO_ENABLED=true` and `ZOHO_CASES_ENABLED=true` in production `.env` 2. Set `ZOHO_WEBTOLEAD_ENABLED=true` in production `.env`
3. Restart the application 3. Restart the application
4. Submit a test lead and support case to verify data flows to Zoho CRM 4. Submit a test lead and support case to verify data flows to Zoho CRM
5. Check Zoho CRM Leads and Cases tabs to confirm both appear 5. Check Zoho CRM Leads and Cases tabs to confirm both appear

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -374,6 +374,14 @@ const ZOHO_ACCOUNTS_DOMAIN = process.env.ZOHO_ACCOUNTS_DOMAIN || 'https://accoun
const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || null const ZOHO_CLIENT_ID = process.env.ZOHO_CLIENT_ID || null
const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null const ZOHO_CLIENT_SECRET = process.env.ZOHO_CLIENT_SECRET || null
const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null const ZOHO_REFRESH_TOKEN = process.env.ZOHO_REFRESH_TOKEN || null
const ZOHO_FORWARDING_MODE = (process.env.ZOHO_FORWARDING_MODE || 'api').toLowerCase()
const ZOHO_WEBTOLEAD_ENABLED = process.env.ZOHO_WEBTOLEAD_ENABLED === 'true'
const ZOHO_WEBTOLEAD_URL = process.env.ZOHO_WEBTOLEAD_URL || 'https://crm.zoho.com/crm/WebToLeadForm'
const ZOHO_WEBTOLEAD_XNQSJSDP = process.env.ZOHO_WEBTOLEAD_XNQSJSDP || null
const ZOHO_WEBTOLEAD_XMIWTLD = process.env.ZOHO_WEBTOLEAD_XMIWTLD || null
const ZOHO_WEBTOLEAD_ACTION_TYPE = process.env.ZOHO_WEBTOLEAD_ACTION_TYPE || 'TGVhZHM='
const ZOHO_WEBTOLEAD_RETURN_URL = process.env.ZOHO_WEBTOLEAD_RETURN_URL || 'null'
const ZOHO_WEBTOLEAD_ZC_GAD = process.env.ZOHO_WEBTOLEAD_ZC_GAD || ''
// In-memory access token cache // In-memory access token cache
let zohoAccessToken = null let zohoAccessToken = null
@ -536,6 +544,70 @@ async function forwardToZoho(leadData) {
} }
} }
async function forwardToZohoWebToLead(leadData) {
if (!ZOHO_WEBTOLEAD_ENABLED) return
if (!ZOHO_WEBTOLEAD_XNQSJSDP || !ZOHO_WEBTOLEAD_XMIWTLD) {
log.warn('[Zoho WebToLead] Skipping forwarding - hidden form tokens are not configured')
return
}
const descriptionParts = []
if (leadData.message) descriptionParts.push(leadData.message)
if (leadData.service_interest) descriptionParts.push(`Service Interest: ${leadData.service_interest}`)
const payload = new URLSearchParams({
xnQsjsdp: ZOHO_WEBTOLEAD_XNQSJSDP,
zc_gad: ZOHO_WEBTOLEAD_ZC_GAD,
xmIwtLD: ZOHO_WEBTOLEAD_XMIWTLD,
actionType: ZOHO_WEBTOLEAD_ACTION_TYPE,
returnURL: ZOHO_WEBTOLEAD_RETURN_URL,
aG9uZXlwb3Q: '',
Company: leadData.company || '',
'Last Name': leadData.name || 'Unknown',
Email: leadData.email || '',
Phone: leadData.phone || '',
'Zip Code': leadData.zip || '',
Description: descriptionParts.join('\n\n'),
})
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), ZOHO_TIMEOUT_MS)
try {
const response = await fetch(ZOHO_WEBTOLEAD_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: payload.toString(),
signal: controller.signal,
})
if (!response.ok) {
const text = await response.text()
log.error(`[Zoho WebToLead] Forwarding failed (${response.status}):`, text.slice(0, 500))
return
}
log.info('[Zoho WebToLead] Lead forwarded successfully')
} catch (fetchErr) {
if (fetchErr.name === 'AbortError') {
log.warn('[Zoho WebToLead] Lead forwarding timed out after', ZOHO_TIMEOUT_MS, 'ms')
} else {
log.error('[Zoho WebToLead] Forwarding error:', fetchErr.message)
}
} finally {
clearTimeout(timeoutId)
}
}
function forwardLeadToZoho(leadData) {
if (ZOHO_FORWARDING_MODE === 'webtolead') {
return forwardToZohoWebToLead(leadData)
}
return forwardToZoho(leadData)
}
// --- Zoho Cases Forwarding (best-effort, fire-and-forget) --- // --- Zoho Cases Forwarding (best-effort, fire-and-forget) ---
async function forwardSupportToZoho(supportData) { async function forwardSupportToZoho(supportData) {
if (!ZOHO_CASES_ENABLED) return if (!ZOHO_CASES_ENABLED) return
@ -703,7 +775,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), async (req, res) => {
log.info(`Lead submitted: ${sanitized.email} from ${sanitized.company} (id: ${result.lastInsertRowid})`) log.info(`Lead submitted: ${sanitized.email} from ${sanitized.company} (id: ${result.lastInsertRowid})`)
// Fire-and-forget Zoho forwarding (best-effort, non-blocking) // Fire-and-forget Zoho forwarding (best-effort, non-blocking)
forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message)) forwardLeadToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
res.json({ success: true, message: "Thanks! We'll be in touch shortly." }) res.json({ success: true, message: "Thanks! We'll be in touch shortly." })
} catch (err) { } catch (err) {
@ -713,7 +785,7 @@ app.post('/api/leads', express.json({ limit: '1mb' }), async (req, res) => {
log.warn(`Duplicate lead email: ${sanitized.email}`) log.warn(`Duplicate lead email: ${sanitized.email}`)
// Still forward to Zoho (non-blocking) for existing leads // Still forward to Zoho (non-blocking) for existing leads
forwardToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message)) forwardLeadToZoho(sanitized).catch(err => log.error('[Zoho] Forwarding error:', err.message))
return res.status(409).json({ return res.status(409).json({
error: 'Duplicate lead', error: 'Duplicate lead',
@ -857,13 +929,16 @@ app.get('*', (req, res, next) => {
app.listen(PORT, () => { app.listen(PORT, () => {
log.info(`Server running on http://localhost:${PORT}`) log.info(`Server running on http://localhost:${PORT}`)
log.info(`Health check: http://localhost:${PORT}/api/health`) log.info(`Health check: http://localhost:${PORT}/api/health`)
if (ZOHO_ENABLED) { log.info(`Zoho lead forwarding mode: ${ZOHO_FORWARDING_MODE}`)
log.info(`Zoho CRM forwarding: ENABLED`) if (ZOHO_FORWARDING_MODE === 'webtolead') {
log.info(`Zoho WebToLead forwarding: ${ZOHO_WEBTOLEAD_ENABLED ? 'ENABLED' : 'DISABLED'}`)
} else if (ZOHO_ENABLED) {
log.info(`Zoho CRM API forwarding: ENABLED`)
log.info(`Zoho API domain: ${ZOHO_API_DOMAIN}`) log.info(`Zoho API domain: ${ZOHO_API_DOMAIN}`)
log.info(`Zoho Accounts domain: ${ZOHO_ACCOUNTS_DOMAIN}`) log.info(`Zoho Accounts domain: ${ZOHO_ACCOUNTS_DOMAIN}`)
log.info(`Zoho Cases forwarding: ${process.env.ZOHO_CASES_ENABLED === 'true' ? 'ENABLED' : 'DISABLED'}`) log.info(`Zoho Cases forwarding: ${process.env.ZOHO_CASES_ENABLED === 'true' ? 'ENABLED' : 'DISABLED'}`)
} else { } else {
log.info('Zoho CRM forwarding: DISABLED (set ZOHO_ENABLED=true to enable)') log.info('Zoho CRM API forwarding: DISABLED (set ZOHO_ENABLED=true to enable)')
} }
log.info(`Rate limiting: ${rateLimitMax} requests per ${rateLimitWindowMs / 1000} seconds`) log.info(`Rate limiting: ${rateLimitMax} requests per ${rateLimitWindowMs / 1000} seconds`)
log.info(`Security headers: Helmet enabled with CSP configured`) log.info(`Security headers: Helmet enabled with CSP configured`)

View File

@ -141,6 +141,13 @@ const Footer = () => {
{/* Veteran Owned & Operated */} {/* Veteran Owned & Operated */}
<div className="border-t border-white/10 mt-10 py-6"> <div className="border-t border-white/10 mt-10 py-6">
<div className="flex flex-col items-center gap-4 text-center"> <div className="flex flex-col items-center gap-4 text-center">
<span className="flex h-24 w-20 items-center justify-center rounded-md border border-white/10 bg-white p-1 shadow-sm">
<img
src="/assets/brand/veteran-owned-certified.webp"
alt="SBA Veteran-Owned Certified badge"
className="h-full w-full object-contain"
/>
</span>
<p className="text-primary-cyan text-xs font-semibold uppercase tracking-[0.18em]"> <p className="text-primary-cyan text-xs font-semibold uppercase tracking-[0.18em]">
Veteran Owned &amp; Operated Veteran Owned &amp; Operated
</p> </p>

View File

@ -1,6 +1,6 @@
import SEO from '@/components/SEO' import SEO from '@/components/SEO'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ArrowRight, Award, CheckCircle2, Compass, Cpu, Handshake, Headphones, Route, Users, Wrench } from 'lucide-react' import { ArrowRight, Award, CheckCircle2, Compass, Cpu, Handshake, Headphones, Route, Wrench } from 'lucide-react'
const proofPoints = [ const proofPoints = [
{ label: '25+ years', detail: 'Communications and infrastructure experience', icon: Award, containerClass: '' }, { label: '25+ years', detail: 'Communications and infrastructure experience', icon: Award, containerClass: '' },
@ -20,7 +20,14 @@ const proofPoints = [
logoClassName: 'h-full w-full scale-[2]', logoClassName: 'h-full w-full scale-[2]',
containerClass: 'p-1 overflow-hidden', containerClass: 'p-1 overflow-hidden',
}, },
{ label: 'Veteran owned', detail: 'Disciplined delivery and direct accountability', icon: Users, containerClass: '' }, {
label: 'Veteran-Owned Certified',
detail: 'Disciplined delivery and direct accountability',
logo: '/assets/brand/veteran-owned-certified.webp',
logoAlt: 'SBA Veteran-Owned Certified badge',
logoClassName: 'h-full w-full',
containerClass: 'p-1',
},
] ]
const operatingPrinciples = [ const operatingPrinciples = [

View File

@ -190,10 +190,14 @@ const Home = () => {
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Cisco Certified Partner</span> <span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Cisco Certified Partner</span>
</div> </div>
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start"> <div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
<span className="flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white"> <span className="flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white p-1">
<ShieldCheck className="h-8 w-8 text-primary-blue" aria-hidden="true" /> <img
src="/assets/brand/veteran-owned-certified.webp"
alt="SBA Veteran-Owned Certified badge"
className="h-full w-full object-contain"
/>
</span> </span>
<span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Veteran Owned</span> <span className="text-sm font-semibold leading-tight text-primary-navy text-center lg:text-left">Veteran-Owned Certified</span>
</div> </div>
<div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start"> <div className="flex flex-col items-center gap-2 lg:flex-row lg:gap-3 lg:justify-start">
<span className="font-numeric flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white text-2xl font-semibold text-primary-navy"> <span className="font-numeric flex h-16 w-20 shrink-0 items-center justify-center rounded-md border border-border bg-white text-2xl font-semibold text-primary-navy">