feat: repair-POS integration, receipt formats, manager overrides, price adjustments
- 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>
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
drawerSessions,
|
||||
} from '../db/schema/pos.js'
|
||||
import { products, inventoryUnits } from '../db/schema/inventory.js'
|
||||
import { repairTickets, repairLineItems } from '../db/schema/repairs.js'
|
||||
import { companies, locations } from '../db/schema/stores.js'
|
||||
import { NotFoundError, ValidationError, ConflictError } from '../lib/errors.js'
|
||||
import { TaxService } from './tax.service.js'
|
||||
@@ -47,6 +48,78 @@ export const TransactionService = {
|
||||
return txn
|
||||
},
|
||||
|
||||
async createFromRepairTicket(db: PostgresJsDatabase<any>, ticketId: string, locationId: string | undefined, processedBy: string) {
|
||||
// Fetch ticket
|
||||
const [ticket] = await db
|
||||
.select()
|
||||
.from(repairTickets)
|
||||
.where(eq(repairTickets.id, ticketId))
|
||||
.limit(1)
|
||||
|
||||
if (!ticket) throw new NotFoundError('Repair ticket')
|
||||
if (!['ready', 'approved', 'in_progress'].includes(ticket.status)) {
|
||||
throw new ValidationError('Ticket must be in ready, approved, or in_progress status to check out')
|
||||
}
|
||||
|
||||
// Check for existing pending repair_payment for this ticket
|
||||
const [existing] = await db
|
||||
.select({ id: transactions.id })
|
||||
.from(transactions)
|
||||
.where(and(
|
||||
eq(transactions.repairTicketId, ticketId),
|
||||
eq(transactions.status, 'pending'),
|
||||
))
|
||||
.limit(1)
|
||||
if (existing) throw new ConflictError('A pending transaction already exists for this ticket')
|
||||
|
||||
// Fetch non-consumable line items
|
||||
const items = await db
|
||||
.select()
|
||||
.from(repairLineItems)
|
||||
.where(eq(repairLineItems.repairTicketId, ticketId))
|
||||
|
||||
const billableItems = items.filter((i) => i.itemType !== 'consumable')
|
||||
if (billableItems.length === 0) throw new ValidationError('No billable line items on this ticket')
|
||||
|
||||
// Create transaction
|
||||
const txn = await this.create(db, {
|
||||
transactionType: 'repair_payment',
|
||||
locationId: locationId ?? ticket.locationId ?? undefined,
|
||||
accountId: ticket.accountId ?? undefined,
|
||||
repairTicketId: ticketId,
|
||||
}, processedBy)
|
||||
|
||||
// Add each billable line item
|
||||
for (const item of billableItems) {
|
||||
const taxCategory = TaxService.repairItemTypeToTaxCategory(item.itemType)
|
||||
let taxRate = 0
|
||||
const txnLocationId = locationId ?? ticket.locationId
|
||||
if (txnLocationId) {
|
||||
taxRate = await TaxService.getRateForLocation(db, txnLocationId, taxCategory)
|
||||
}
|
||||
|
||||
const unitPrice = parseFloat(item.unitPrice)
|
||||
const qty = Math.round(parseFloat(item.qty))
|
||||
const lineSubtotal = unitPrice * qty
|
||||
const taxAmount = TaxService.calculateTax(lineSubtotal, taxRate)
|
||||
const lineTotal = lineSubtotal + taxAmount
|
||||
|
||||
await db.insert(transactionLineItems).values({
|
||||
transactionId: txn.id,
|
||||
productId: item.productId,
|
||||
description: item.description,
|
||||
qty,
|
||||
unitPrice: unitPrice.toString(),
|
||||
taxRate: taxRate.toString(),
|
||||
taxAmount: taxAmount.toString(),
|
||||
lineTotal: lineTotal.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
await this.recalculateTotals(db, txn.id)
|
||||
return this.getById(db, txn.id)
|
||||
},
|
||||
|
||||
async addLineItem(db: PostgresJsDatabase<any>, transactionId: string, input: TransactionLineItemCreateInput) {
|
||||
const txn = await this.getById(db, transactionId)
|
||||
if (!txn) throw new NotFoundError('Transaction')
|
||||
@@ -309,6 +382,15 @@ export const TransactionService = {
|
||||
})
|
||||
.where(eq(transactions.id, transactionId))
|
||||
.returning()
|
||||
|
||||
// If this is a repair payment, update ticket status to picked_up
|
||||
if (completed.transactionType === 'repair_payment' && completed.repairTicketId) {
|
||||
await db
|
||||
.update(repairTickets)
|
||||
.set({ status: 'picked_up', completedDate: new Date(), updatedAt: new Date() })
|
||||
.where(eq(repairTickets.id, completed.repairTicketId))
|
||||
}
|
||||
|
||||
return completed
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user