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
- Automatic Loading: Saved addresses are fetched on checkout page load
- Default Selection: Default or first address is auto-selected
- Tax Calculation: Tax is calculated immediately based on selected address
- Easy Switching: Click "Change" to select a different address
- Add New: Create additional addresses with a single click
For Guest Users
- Manual Entry: Fill out shipping address form
- Real-Time Tax: Tax calculates as you type (when zip code is complete)
- 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/Region | Zip Range | Typical Rate |
|---|---|---|
| Los Angeles County | 900-908 | 9.5% |
| San Diego County | 920-924 | 7.75% |
| San Francisco | 941 | 8.625% |
| Santa Clara County | 940-955 | 9.375% |
| Sacramento County | 942-958 | 8.75% |
| Orange County | 926-927 | 7.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 addresslineItems: 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
The Intuit tokens are encrypted before storage. Ensure INTUIT_ENCRYPTION_KEY is exactly 32 characters long.
User Experience
Checkout Flow
-
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 -
Address Change
User clicks "Change"
↓
Show list of saved addresses
↓
User selects different address
↓
Calculate new tax amount
↓
Update total
↓
Mark as new default -
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
-
Non-California Address
Address: 123 Main St, Seattle, WA 98101
Expected: 0% tax -
San Jose, CA Address
Address: 123 Main St, San Jose, CA 95120
Expected: ~9.375% tax (via Intuit API) -
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:
- Check that Intuit OAuth is complete
- Verify
NEXT_PUBLIC_INTUIT_COMPANY_IDis set - Check that access token is valid (not expired)
- Look for API errors in server logs
- Fallback should still provide ~9.375% for most CA zip codes
Problem: Tax calculation is slow
Solutions:
- Intuit API typically responds in
<500ms - Check network connectivity
- Verify API endpoint is reachable
- Consider caching tax rates by zip code
Address Management Issues
Problem: Saved addresses not loading
Solutions:
- Check user is authenticated
- Verify
/api/addressesendpoint is working - Check browser console for errors
- Verify database connection
Problem: Can't delete address
Solutions:
- Check user has permission (owns the address)
- Verify DELETE endpoint is working
- 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
- Always validate address server-side - Never trust client data
- Cache tax rates - Consider caching by zip code to reduce API calls
- Handle API failures gracefully - Use fallback tax calculation
- Test edge cases - Test border zips, military addresses (APO/FPO)
- Monitor API usage - Track Intuit API call volume
For Users
- Keep addresses up to date - Review saved addresses periodically
- Set accurate default - Set most-used address as default
- Include phone number - Helps with delivery issues
- Double-check zip code - Tax rates depend on accurate zip codes
API Reference
Complete Endpoint List
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/addresses | List user's addresses |
| POST | /api/addresses | Create new address |
| PATCH | /api/addresses/:id | Update address |
| DELETE | /api/addresses/:id | Delete address |
| POST | /api/tax/calculate | Calculate 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
Related Documentation
Need help? Check the troubleshooting section or open an issue on GitHub.