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-9089.5%
San Diego County920-9247.75%
San Francisco9418.625%
Santa Clara County940-9559.375%
Sacramento County942-9588.75%
Orange County926-9277.75%

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 should still provide ~9.375% for most CA zip codes

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.