Files
smoothschedule/frontend/src/hooks/useTaxRates.ts
poduck 1aa5b76e3b Add Point of Sale system and tax rate lookup integration
POS System:
- Full POS interface with product grid, cart panel, and payment flow
- Product and category management with barcode scanning support
- Cash drawer operations and shift management
- Order history and receipt generation
- Thermal printer integration (ESC/POS protocol)
- Gift card support with purchase and redemption
- Inventory tracking with low stock alerts
- Customer selection and walk-in support

Tax Rate Integration:
- ZIP-to-state mapping for automatic state detection
- SST boundary data import for 24 member states
- Static rates for uniform-rate states (IN, MA, CT, etc.)
- Statewide jurisdiction fallback for simple lookups
- Tax rate suggestion in location editor with auto-apply
- Multiple data sources: SST, CDTFA, TX Comptroller, Avalara

UI Improvements:
- POS renders full-screen outside BusinessLayout
- Clear cart button prominently in cart header
- Tax rate limited to 2 decimal places
- Location tax rate field with suggestion UI

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 11:31:19 -05:00

177 lines
4.9 KiB
TypeScript

/**
* Hook for looking up tax rates by ZIP code or address.
*
* Supports multiple data sources:
* - SST (Streamlined Sales Tax) for 24 member states - address-level accuracy
* - California CDTFA data
* - Texas Comptroller data
* - ZIP-based fallback for other states
*/
import { useQuery } from '@tanstack/react-query';
import apiClient from '../api/client';
/**
* Tax rate lookup result.
*/
export interface TaxRateLookup {
zip_code: string;
zip_ext?: string;
state: string;
combined_rate: number;
combined_rate_percent: string;
state_rate: number;
county_rate: number;
city_rate: number;
special_rate: number;
// Source and accuracy info
source: 'sst' | 'cdtfa' | 'tx_comptroller' | 'avalara' | 'state_dor' | 'no_sales_tax' | 'not_found';
accuracy: 'zip9' | 'zip5' | 'zip' | 'address' | 'jurisdiction' | 'state' | 'exact' | 'none';
// Jurisdiction details
jurisdiction_code?: string;
jurisdiction_name?: string;
county_name?: string;
city_name?: string;
// SST liability protection
liability_protection: boolean;
effective_date?: string;
// Legacy fields for backwards compatibility
risk_level?: number;
has_multiple_rates?: boolean;
note?: string;
error?: string;
}
/**
* ZIP-based tax rate (fallback data).
*/
export interface TaxRate {
id: number;
state: string;
zip_code: string;
tax_region_name: string;
estimated_combined_rate: string;
combined_rate_percent: string;
state_rate: string;
estimated_county_rate: string;
estimated_city_rate: string;
estimated_special_rate: string;
risk_level: number;
source: string;
effective_date: string;
}
/**
* Parameters for tax rate lookup.
*/
export interface TaxLookupParams {
zipCode: string;
zipExt?: string;
state?: string;
streetAddress?: string;
}
/**
* Look up tax rate by ZIP code or address.
*
* @param params - Lookup parameters or ZIP code string
* @param options - Query options
* @returns Tax rate lookup result
*
* @example
* // Simple ZIP lookup
* const { data } = useTaxRateLookup('84003');
*
* @example
* // Enhanced lookup with ZIP+4 for better accuracy
* const { data } = useTaxRateLookup({
* zipCode: '84003',
* zipExt: '1234',
* state: 'UT'
* });
*/
export function useTaxRateLookup(
params: string | TaxLookupParams | null,
options?: { enabled?: boolean }
) {
// Normalize params
const zipCode = typeof params === 'string' ? params : params?.zipCode || '';
const zipExt = typeof params === 'string' ? '' : params?.zipExt || '';
const state = typeof params === 'string' ? '' : params?.state || '';
const streetAddress = typeof params === 'string' ? '' : params?.streetAddress || '';
const normalizedZip = zipCode.replace(/\D/g, '').slice(0, 5);
const normalizedExt = zipExt.replace(/\D/g, '').slice(0, 4);
const normalizedState = state.replace(/[^A-Za-z]/g, '').slice(0, 2).toUpperCase();
const isValidZip = normalizedZip.length === 5;
// Build query key based on all params
const queryKey = ['tax', 'lookup', normalizedZip, normalizedExt, normalizedState, streetAddress];
return useQuery<TaxRateLookup>({
queryKey,
queryFn: async () => {
const response = await apiClient.get('/tax/lookup/', {
params: {
zip_code: normalizedZip,
...(normalizedExt && { zip_ext: normalizedExt }),
...(normalizedState && { state: normalizedState }),
...(streetAddress && { street_address: streetAddress }),
},
});
return response.data;
},
enabled: isValidZip && (options?.enabled !== false),
staleTime: 1000 * 60 * 60 * 24, // Cache for 24 hours (tax rates don't change often)
retry: false, // Don't retry if ZIP not found
});
}
/**
* Get tax rate as a decimal for a ZIP code.
* Returns null if not found or still loading.
*
* @param zipCode - 5-digit US ZIP code
* @returns Combined tax rate as decimal (e.g., 0.0825) or null
*/
export function useTaxRateForZip(zipCode: string | null): number | null {
const { data, isSuccess } = useTaxRateLookup(zipCode);
if (isSuccess && data && data.source !== 'not_found') {
return data.combined_rate;
}
return null;
}
/**
* Get formatted tax rate info for display.
* Includes source and accuracy information.
*/
export function useTaxRateInfo(params: string | TaxLookupParams | null) {
const { data, isLoading, isError } = useTaxRateLookup(params);
if (isLoading) {
return { loading: true };
}
if (isError || !data || data.source === 'not_found') {
return { loading: false, notFound: true };
}
return {
loading: false,
notFound: false,
rate: data.combined_rate,
ratePercent: data.combined_rate_percent,
source: data.source,
accuracy: data.accuracy,
jurisdictionName: data.jurisdiction_name,
liabilityProtection: data.liability_protection,
note: data.note,
// Helper flags
isSST: data.source === 'sst',
isNoTax: data.source === 'no_sales_tax',
isHighAccuracy: ['zip9', 'address', 'exact'].includes(data.accuracy),
};
}