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>
177 lines
4.9 KiB
TypeScript
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),
|
|
};
|
|
}
|