Skip to main content

Shipping Address & Sales Tax Calculation

Overview​

Gully Sports includes a comprehensive shipping address management system with automatic sales tax calculation powered by Intuit QuickBooks. This feature provides an Amazon-like checkout experience with saved addresses, real-time tax calculation, and seamless integration with both payment providers.

Key Features​

  • 🏠 Saved Addresses - Store multiple shipping addresses per user
  • ⚡ Real-Time Tax Calculation - Automatic tax calculation based on destination
  • 🎯 Smart Default Selection - Auto-select default or most recent address
  • 📍 California Tax Integration - Accurate tax rates via Intuit QuickBooks API
  • 🌎 Multi-State Support - Handles tax-free states automatically
  • 🔄 Seamless UX - Change addresses without losing cart or progress
  • 👤 Guest Checkout - Works for both authenticated and guest users

How It Works​

For Logged-In Users​

  1. Automatic Loading: Saved addresses are fetched on checkout page load
  2. Default Selection: Default or first address is auto-selected
  3. Tax Calculation: Tax is calculated immediately based on selected address
  4. Easy Switching: Click "Change" to select a different address
  5. Add New: Create additional addresses with a single click

For Guest Users​

  1. Manual Entry: Fill out shipping address form
  2. Real-Time Tax: Tax calculates as you type (when zip code is complete)
  3. Optional Save: Can save address by creating an account

Sales Tax Calculation​

Tax Calculation Logic​

The system uses intelligent logic to determine sales tax:

1. Non-California Addresses​

  • Tax Rate: 0%
  • Processing: Instant (no API call)
  • Reason: Business operates in California, only charges CA tax

2. California Addresses​

  • Primary Method: Intuit QuickBooks GraphQL API
  • Fallback: Zip code lookup table
  • Tax Rates: 7.25% - 10.25% depending on jurisdiction
  • Accuracy: City/county-specific rates

Tax Calculation Flow​

User enters address
↓
Is California? (by state or zip)
↓
┌──NO──→ Return 0% tax
│
YES
↓
Call Intuit QuickBooks API
↓
Calculate tax per line item
↓
Return total tax + jurisdiction
↓
API failed?
↓
Fallback to zip code table

Supported Jurisdictions (California)​

The system accurately calculates tax for all California jurisdictions:

County/RegionZip RangeTypical Rate
Los Angeles County900-9189.75%
San Diego County919-9217.75%
Orange County926-9277.75%
San Bernardino917, 9237.75%
San Francisco9418.625%
Santa Clara County950-9519.375%
Sacramento County956-9588.75%

Full rates for all California ZIP prefixes (900–961) are in src/config/salesTaxRates.ts using CDTFA 2026 data.

Shipping Address Management​

Saved Addresses​

Authenticated users can save multiple shipping addresses:

interface ShippingAddress {
id: string;
firstName: string;
lastName: string;
address1: string;
address2?: string;
city: string;
state: string;
postalCode: string;
country: string;
phone?: string;
isDefault: boolean;
}

Address Operations​

Create Address​

POST /api/addresses
Body: { firstName, lastName, address1, city, state, postalCode, ... }
Response: { address: {...}, message: "Address saved successfully" }

List Addresses​

GET /api/addresses
Response: { addresses: [...] }

Update Address​

PATCH /api/addresses/:id
Body: { isDefault: true }
Response: { address: {...}, message: "Address updated" }

Delete Address​

DELETE /api/addresses/:id
Response: { message: "Address deleted successfully" }

Implementation Details​

Components​

ShippingAddressForm​

Main component handling address selection and tax calculation:

Props:

interface ShippingAddressFormProps {
onAddressSelected: (address: ShippingAddress) => void;
onTaxCalculated?: (
taxAmount: number,
taxRate: number,
jurisdiction: string
) => void;
onTaxCalculating?: (isCalculating: boolean) => void;
initialAddress?: ShippingAddress | null;
subtotal: number;
cartItems?: TaxLineItem[];
}

Features:

  • Fetches saved addresses on mount
  • Auto-selects default address
  • Calculates tax on address change
  • Validates address fields
  • Shows loading states
  • Handles errors gracefully

Tax Calculation API​

Endpoint: POST /api/tax/calculate​

Request:

{
"amount": 100.0,
"postalCode": "95120",
"address": {
"address1": "123 Main St",
"city": "San Jose",
"state": "CA",
"country": "US"
},
"lineItems": [
{
"productVariantId": "1",
"quantity": 2,
"pricePerUnit": 50.0
}
]
}

Response:

{
"success": true,
"taxAmount": 9.38,
"taxRate": 9.375,
"jurisdiction": "San Jose, CA"
}

Intuit QuickBooks Integration​

The sales tax calculation uses Intuit's GraphQL API:

GraphQL Mutation:

mutation IndirectTaxCalculateSaleTransactionTax(
$input: IndirectTax_TaxCalculationInput!
) {
indirectTaxCalculateSaleTransactionTax(input: $input) {
taxCalculation {
transactionDate
taxTotals {
totalTaxAmountExcludingShipping {
value
}
}
# ... additional fields
}
}
}

Required Variables:

  • transactionDate: Current date (YYYY-MM-DD)
  • subject.qbCustomerId: Customer ID (default: "1")
  • shipping.shipFromAddress: Business address (San Jose, CA)
  • shipping.shipToAddress: Customer's shipping address
  • lineItems: Array of products with quantities and prices

Configuration​

Environment Variables​

Add these to your .env.local:

# Required for Intuit tax calculation
NEXT_PUBLIC_INTUIT_COMPANY_ID=your_company_id
INTUIT_CLIENT_ID=your_client_id
INTUIT_CLIENT_SECRET=your_client_secret

# OAuth tokens (managed automatically after OAuth flow)
INTUIT_ACCESS_TOKEN=encrypted_token
INTUIT_REFRESH_TOKEN=encrypted_token

# Encryption for token storage
INTUIT_ENCRYPTION_KEY=your-32-character-encryption-key
Token Encryption

The Intuit tokens are encrypted before storage. Ensure INTUIT_ENCRYPTION_KEY is exactly 32 characters long.

User Experience​

Checkout Flow​

  1. Initial Load

    User lands on checkout page
    ↓
    Is user logged in?
    ↓
    ┌──NO──→ Show address form
    │
    YES
    ↓
    Fetch saved addresses
    ↓
    Auto-select default address
    ↓
    Calculate tax for selected address
    ↓
    Show address + tax + total
  2. Address Change

    User clicks "Change"
    ↓
    Show list of saved addresses
    ↓
    User selects different address
    ↓
    Calculate new tax amount
    ↓
    Update total
    ↓
    Mark as new default
  3. Add New Address

    User clicks "Add new address"
    ↓
    Show address form
    ↓
    User fills in details
    ↓
    Real-time tax calculation as user types
    ↓
    User clicks "Use this address"
    ↓
    Save address (if logged in)
    ↓
    Calculate final tax
    ↓
    Update total

Visual States​

Selected Address Display​

┌─────────────────────────────┐
│ John Doe │
│ 123 Main Street │
│ San Jose, CA 95120 │
│ Phone: (555) 123-4567 │
│ [Change] │
└─────────────────────────────┘

Address Selector​

┌─────────────────────────────┐
│ Choose a delivery address │
│ │
│ ○ John Doe │
│ 123 Main Street │
│ San Jose, CA 95120 │
│ │
│ ● Jane Smith │
│ 456 Oak Avenue │
│ Los Angeles, CA 90001 │
│ │
│ [+ Add a new address] │
└─────────────────────────────┘

Testing​

Test Tax Calculation​

  1. Non-California Address

    Address: 123 Main St, Seattle, WA 98101
    Expected: 0% tax
  2. San Jose, CA Address

    Address: 123 Main St, San Jose, CA 95120
    Expected: ~9.375% tax (via Intuit API)
  3. Los Angeles, CA Address

    Address: 456 Oak Ave, Los Angeles, CA 90001
    Expected: ~9.5% tax (via Intuit API or fallback)

Test Address Management​

Scenario 1: First-time User

  • Checkout → No saved addresses
  • Shows address form
  • Fill in details
  • Tax calculates automatically
  • Can proceed to payment

Scenario 2: Returning User

  • Checkout → Loads saved addresses
  • Default address auto-selected
  • Tax pre-calculated
  • Can change address easily

Scenario 3: Multiple Addresses

  • Checkout → Shows default
  • Click "Change" → See all addresses
  • Select different address
  • Tax recalculates
  • New address marked as default

Troubleshooting​

Tax Calculation Issues​

Problem: Tax showing as 0% for California address

Solutions:

  1. Check that Intuit OAuth is complete
  2. Verify NEXT_PUBLIC_INTUIT_COMPANY_ID is set
  3. Check that access token is valid (not expired)
  4. Look for API errors in server logs
  5. Fallback ZIP table (src/config/salesTaxRates.ts) covers all CA prefixes 900–961; missing prefixes default to 7.25% state minimum

Problem: Tax calculation is slow

Solutions:

  1. Intuit API typically responds in <500ms
  2. Check network connectivity
  3. Verify API endpoint is reachable
  4. Consider caching tax rates by zip code

Address Management Issues​

Problem: Saved addresses not loading

Solutions:

  1. Check user is authenticated
  2. Verify /api/addresses endpoint is working
  3. Check browser console for errors
  4. Verify database connection

Problem: Can't delete address

Solutions:

  1. Check user has permission (owns the address)
  2. Verify DELETE endpoint is working
  3. Check for database constraints

Database Schema​

ShippingAddress Model​

model ShippingAddress {
id String @id @default(cuid())
userId String
firstName String
lastName String
address1 String
address2 String?
city String
state String
postalCode String
country String @default("US")
phone String?
isDefault Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@index([userId])
@@index([userId, isDefault])
}

Best Practices​

For Developers​

  1. Always validate address server-side - Never trust client data
  2. Cache tax rates - Consider caching by zip code to reduce API calls
  3. Handle API failures gracefully - Use fallback tax calculation
  4. Test edge cases - Test border zips, military addresses (APO/FPO)
  5. Monitor API usage - Track Intuit API call volume

For Users​

  1. Keep addresses up to date - Review saved addresses periodically
  2. Set accurate default - Set most-used address as default
  3. Include phone number - Helps with delivery issues
  4. Double-check zip code - Tax rates depend on accurate zip codes

API Reference​

Complete Endpoint List​

MethodEndpointDescription
GET/api/addressesList user's addresses
POST/api/addressesCreate new address
PATCH/api/addresses/:idUpdate address
DELETE/api/addresses/:idDelete address
POST/api/tax/calculateCalculate sales tax

Performance Optimization​

Tax Calculation Caching​

Consider implementing caching for tax rates:

// Example: Cache tax rates by zip code for 24 hours
const taxCache = new Map<string, { rate: number; timestamp: number }>();

function getCachedTaxRate(zipCode: string): number | null {
const cached = taxCache.get(zipCode);
if (cached && Date.now() - cached.timestamp < 24 * 60 * 60 * 1000) {
return cached.rate;
}
return null;
}

Address Prefetching​

Prefetch addresses on app load for smoother checkout:

// In root layout or navigation component
useEffect(() => {
if (isAuthenticated) {
fetch("/api/addresses").then(/* cache response */);
}
}, [isAuthenticated]);

Future Enhancements​

Potential improvements for this feature:

  • Address validation/verification API (USPS, UPS)
  • Address autocomplete (Google Places API)
  • International shipping support
  • Multiple tax jurisdictions (nexus in multiple states)
  • Tax exemption certificates
  • Shipping cost calculation
  • Save addresses during registration flow
  • Import addresses from third-party services

Need help? Check the troubleshooting section or open an issue on GitHub.