- Add thermal/full-page receipt format toggle (per-device, localStorage) - Full-page receipt uses clean invoice layout matching repair PDF style - Settings page reorganized into tabbed sections (Store, Locations, Modules, Receipt, POS Security, Advanced) - Manager override system: configurable PIN prompt for void, refund, discount, cash in/out - Discount threshold setting: require manager approval above X% - Consumable product type: tracked for internal job costing, excluded from POS search, receipts, and customer-facing totals - Repair line item dialog: product picker dropdown for parts/consumables from inventory - Repair → POS checkout: load ready-for-pickup tickets into repair_payment transactions with proper tax categories (labor=service, parts=goods) - Transaction completion auto-updates repair ticket status to picked_up - POS Repairs dialog with Pickup and New Intake tabs, customer account lookup - Inline price adjustment on cart items: % off, $ off, or set price with live preview - Order-level discount button with same three input modes - Backend: migration 0043 (consumable enum + is_consumable flag), createFromRepairTicket service, ready-for-pickup endpoint - Fix: backend dev script uses --env-file for turbo compatibility Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
89 lines
2.7 KiB
TypeScript
89 lines
2.7 KiB
TypeScript
import { eq } from 'drizzle-orm'
|
|
import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js'
|
|
import { locations } from '../db/schema/stores.js'
|
|
import { AppError } from '../lib/errors.js'
|
|
|
|
export type TaxCategory = 'goods' | 'service' | 'exempt'
|
|
|
|
export const TaxService = {
|
|
/**
|
|
* Get the tax rate for a location, resolved by tax category:
|
|
* - "goods" → location.taxRate (default)
|
|
* - "service" → location.serviceTaxRate, falls back to taxRate
|
|
* - "exempt" → 0
|
|
*
|
|
* Returns 0 with no warning for exempt items.
|
|
* Returns 0 with warning if no rate is configured on the location.
|
|
*/
|
|
async getRateForLocation(
|
|
db: PostgresJsDatabase<any>,
|
|
locationId: string,
|
|
taxCategory: TaxCategory = 'goods',
|
|
): Promise<number> {
|
|
if (taxCategory === 'exempt') return 0
|
|
|
|
const [location] = await db
|
|
.select({ taxRate: locations.taxRate, serviceTaxRate: locations.serviceTaxRate })
|
|
.from(locations)
|
|
.where(eq(locations.id, locationId))
|
|
.limit(1)
|
|
|
|
if (!location) return 0
|
|
|
|
if (taxCategory === 'service') {
|
|
// Use service rate if set, otherwise fall back to goods rate
|
|
const rate = location.serviceTaxRate ?? location.taxRate
|
|
return rate ? parseFloat(rate) : 0
|
|
}
|
|
|
|
// Default: goods rate
|
|
return location.taxRate ? parseFloat(location.taxRate) : 0
|
|
},
|
|
|
|
calculateTax(amount: number, rate: number): number {
|
|
return Math.round(amount * rate * 100) / 100
|
|
},
|
|
|
|
/**
|
|
* Swedish rounding: round to nearest $0.05 for cash payments.
|
|
* Only affects the final total — tax and line items stay exact.
|
|
*/
|
|
roundToNickel(amount: number): number {
|
|
return Math.round(amount * 20) / 20
|
|
},
|
|
|
|
/**
|
|
* Map repair line item types to tax categories:
|
|
* - "part" → goods (taxable)
|
|
* - "labor" → service (may be taxed differently)
|
|
* - "flat_rate" → goods (conservative — includes parts)
|
|
* - "misc" → goods (default)
|
|
*/
|
|
repairItemTypeToTaxCategory(itemType: string): TaxCategory {
|
|
switch (itemType) {
|
|
case 'labor':
|
|
return 'service'
|
|
case 'consumable':
|
|
return 'exempt'
|
|
case 'part':
|
|
case 'flat_rate':
|
|
case 'misc':
|
|
default:
|
|
return 'goods'
|
|
}
|
|
},
|
|
|
|
// TODO: Integrate with a real tax rate API (TaxJar, Avalara, etc.)
|
|
// Set TAX_API_KEY env var when ready.
|
|
async lookupByZip(
|
|
_zip: string,
|
|
): Promise<{ zip: string; rate: number; state_rate: number; county_rate: number; city_rate: number }> {
|
|
if (!process.env.TAX_API_KEY) {
|
|
throw new AppError('Tax rate lookup is not configured. Set TAX_API_KEY to enable automatic lookup.', 501)
|
|
}
|
|
|
|
// Placeholder — replace with real API call
|
|
throw new AppError('Tax rate lookup not yet implemented', 501)
|
|
},
|
|
}
|