feat: add core POS module — transactions, discounts, drawer, tax

Phase 3a backend API for point-of-sale. Includes:

Schema (packages/backend/src/db/schema/pos.ts):
- pgEnums: transaction_type, transaction_status, payment_method,
  discount_type, discount_applies_to, drawer_status
- Tables: transaction, transaction_line_item, discount,
  discount_audit, drawer_session
- Transaction links to accounts, repair_tickets, repair_batches
- Line items link to products and inventory_units

Tax system:
- tax_rate + service_tax_rate columns on location
- tax_category enum (goods/service/exempt) on product
- Tax resolves per line item: goods→tax_rate, service→service_tax_rate,
  exempt→0. Repair line items map: part→goods, labor→service
- GET /tax/lookup/:zip stubbed for future API integration (TAX_API_KEY)

Services (export const pattern, matching existing codebase):
- TransactionService: create, addLineItem, removeLineItem, applyDiscount,
  recalculateTotals, complete (decrements inventory), void, getReceipt
- DiscountService: CRUD + listAll for dropdowns
- DrawerService: open/close with expected balance + over/short calc
- TaxService: getRateForLocation (by tax category), calculateTax

Routes:
- POST/GET /transactions, GET /transactions/:id, GET /transactions/:id/receipt
- POST /transactions/:id/line-items, DELETE /transactions/:id/line-items/:id
- POST /transactions/:id/discounts, /complete, /void
- POST /drawer/open, POST /drawer/:id/close, GET /drawer/current, GET /drawer
- CRUD /discounts + GET /discounts/all
- GET /products/lookup/upc/:upc (barcode scanner support)

All routes gated by pos.view/pos.edit/pos.admin + withModule('pos').
POS module already seeded in migration 0026.

Still needed: bun install, drizzle-kit generate + migrate, tests, lint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ryan
2026-04-04 16:26:38 +00:00
parent 7aa81c4e7c
commit 7b15f18e59
17 changed files with 1247 additions and 1 deletions

View File

@@ -21,6 +21,10 @@ import { fileRoutes } from './routes/v1/files.js'
import { rbacRoutes } from './routes/v1/rbac.js'
import { repairRoutes } from './routes/v1/repairs.js'
import { lessonRoutes } from './routes/v1/lessons.js'
import { transactionRoutes } from './routes/v1/transactions.js'
import { drawerRoutes } from './routes/v1/drawer.js'
import { discountRoutes } from './routes/v1/discounts.js'
import { taxRoutes } from './routes/v1/tax.js'
import { storageRoutes } from './routes/v1/storage.js'
import { storeRoutes } from './routes/v1/store.js'
import { vaultRoutes } from './routes/v1/vault.js'
@@ -111,6 +115,10 @@ export async function buildApp() {
await app.register(withModule('files', storageRoutes), { prefix: '/v1' })
await app.register(withModule('repairs', repairRoutes), { prefix: '/v1' })
await app.register(withModule('lessons', lessonRoutes), { prefix: '/v1' })
await app.register(withModule('pos', transactionRoutes), { prefix: '/v1' })
await app.register(withModule('pos', drawerRoutes), { prefix: '/v1' })
await app.register(withModule('pos', discountRoutes), { prefix: '/v1' })
await app.register(withModule('pos', taxRoutes), { prefix: '/v1' })
await app.register(withModule('vault', vaultRoutes), { prefix: '/v1' })
// Register WebDAV custom HTTP methods before routes
app.addHttpMethod('PROPFIND', { hasBody: true })