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>
This commit is contained in:
poduck
2025-12-27 11:31:19 -05:00
parent da508da398
commit 1aa5b76e3b
156 changed files with 61604 additions and 4 deletions

View File

@@ -0,0 +1,868 @@
/**
* Tests for ShiftReportBuilder
*
* ShiftReportBuilder generates shift summary reports for thermal printers.
* It shows opening/closing times, cash drawer balances, sales breakdown,
* variance calculations, and optional signature lines.
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ShiftReportBuilder, ShiftReportConfig } from '../ShiftReportBuilder';
import type { BusinessInfo, ShiftSummary } from '../../types';
// ESC/POS command constants for verification
const ESC = 0x1b;
const GS = 0x1d;
const LF = 0x0a;
/**
* Helper to create a minimal business info object
*/
function createBusinessInfo(overrides: Partial<BusinessInfo> = {}): BusinessInfo {
return {
name: 'Test Business',
address: '123 Main St',
city: 'Testville',
state: 'TS',
zip: '12345',
phone: '5551234567',
...overrides,
};
}
/**
* Helper to create a minimal shift summary object
*/
function createShiftSummary(overrides: Partial<ShiftSummary> = {}): ShiftSummary {
return {
id: 1,
openedAt: '2024-12-26T09:00:00Z',
closedAt: '2024-12-26T17:00:00Z',
openedByName: 'Alice Smith',
closedByName: 'Alice Smith',
openingBalanceCents: 20000,
expectedBalanceCents: 35000,
actualBalanceCents: 35000,
varianceCents: 0,
cashSalesCents: 15000,
cardSalesCents: 25000,
giftCardSalesCents: 5000,
totalSalesCents: 45000,
transactionCount: 25,
refundsCents: 0,
...overrides,
};
}
/**
* Helper to create shift report config
*/
function createConfig(overrides: Partial<ShiftReportConfig> = {}): ShiftReportConfig {
return {
includeSignatureLine: true,
...overrides,
};
}
describe('ShiftReportBuilder', () => {
let businessInfo: BusinessInfo;
let shift: ShiftSummary;
let config: ShiftReportConfig;
beforeEach(() => {
businessInfo = createBusinessInfo();
shift = createShiftSummary();
config = createConfig();
});
describe('constructor', () => {
it('should create instance with default config', () => {
const builder = new ShiftReportBuilder(businessInfo, shift);
expect(builder).toBeInstanceOf(ShiftReportBuilder);
});
it('should accept custom config', () => {
const customConfig: ShiftReportConfig = {
includeSignatureLine: false,
managerName: 'Manager Bob',
locationName: 'Downtown Store',
printedByName: 'Alice',
};
const builder = new ShiftReportBuilder(businessInfo, shift, customConfig);
expect(builder).toBeInstanceOf(ShiftReportBuilder);
});
it('should default includeSignatureLine to true', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, {});
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Signature:');
});
});
describe('build()', () => {
it('should return Uint8Array', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
expect(result).toBeInstanceOf(Uint8Array);
expect(result.length).toBeGreaterThan(0);
});
it('should start with init command (ESC @)', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
expect(result[0]).toBe(ESC);
expect(result[1]).toBe(0x40);
});
it('should end with partial cut command', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
// Partial cut is GS V 1
const lastThreeBytes = Array.from(result.slice(-3));
expect(lastThreeBytes).toContain(GS);
expect(lastThreeBytes).toContain(0x56);
});
it('should contain line feeds', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const lfCount = Array.from(result).filter((b) => b === LF).length;
expect(lfCount).toBeGreaterThan(10);
});
});
describe('buildHeader()', () => {
it('should include business name in uppercase', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain(businessInfo.name.toUpperCase());
});
it('should include location name when provided', () => {
const configWithLocation = createConfig({
locationName: 'Main Street Location',
});
const builder = new ShiftReportBuilder(businessInfo, shift, configWithLocation);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Main Street Location');
});
it('should skip location when not provided', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
// Should complete without error
expect(result.length).toBeGreaterThan(0);
});
it('should include SHIFT REPORT title', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('SHIFT REPORT');
});
});
describe('buildShiftTimes()', () => {
it('should include shift ID', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Shift #:');
expect(text).toContain('1');
});
it('should include opened time', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Opened:');
});
it('should include opened by name', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Opened By:');
expect(text).toContain('Alice Smith');
});
it('should show Unknown when opened by is not provided', () => {
const shiftNoOpener = createShiftSummary({ openedByName: '' });
const builder = new ShiftReportBuilder(businessInfo, shiftNoOpener, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Opened By:');
expect(text).toContain('Unknown');
});
it('should include closed time when shift is closed', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Closed:');
expect(text).toContain('Closed By:');
});
it('should include duration for closed shifts', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Duration:');
expect(text).toContain('8h'); // 9:00 to 17:00 = 8 hours
});
it('should show SHIFT STILL OPEN for open shifts', () => {
const openShift = createShiftSummary({
closedAt: null,
closedByName: undefined,
actualBalanceCents: null,
varianceCents: null,
});
const builder = new ShiftReportBuilder(businessInfo, openShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('*** SHIFT STILL OPEN ***');
});
it('should format time in business timezone when provided', () => {
const shiftWithTz = createShiftSummary({
business_timezone: 'America/New_York',
});
const builder = new ShiftReportBuilder(businessInfo, shiftWithTz, config);
const result = builder.build();
expect(result.length).toBeGreaterThan(0);
});
it('should show Unknown for closed by when not provided', () => {
const shiftNoCloser = createShiftSummary({ closedByName: '' });
const builder = new ShiftReportBuilder(businessInfo, shiftNoCloser, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Closed By:');
expect(text).toContain('Unknown');
});
});
describe('buildOpeningBalance()', () => {
it('should include CASH DRAWER section header', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('CASH DRAWER');
});
it('should show opening balance', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Opening Balance:');
expect(text).toContain('$200.00');
});
});
describe('buildSalesBreakdown()', () => {
it('should include SALES SUMMARY section header', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('SALES SUMMARY');
});
it('should show transaction count', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Transactions:');
expect(text).toContain('25');
});
it('should show cash sales', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Cash Sales:');
expect(text).toContain('$150.00');
});
it('should show card sales', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Card Sales:');
expect(text).toContain('$250.00');
});
it('should show gift card sales', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Gift Card Sales:');
expect(text).toContain('$50.00');
});
it('should show total sales', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Total Sales:');
expect(text).toContain('$450.00');
});
it('should show refunds when present', () => {
const shiftWithRefunds = createShiftSummary({
refundsCents: 2500,
});
const builder = new ShiftReportBuilder(businessInfo, shiftWithRefunds, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Refunds:');
expect(text).toContain('-$25.00');
});
it('should skip refunds when zero', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
// Refunds should not appear when zero
const refundMatches = text.match(/Refunds:/g) || [];
expect(refundMatches.length).toBe(0);
});
});
describe('buildCashBalance()', () => {
it('should include CASH BALANCE section header', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('CASH BALANCE');
});
it('should show opening balance in calculation', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
// Opening balance should appear in cash balance section
const openingMatches = text.match(/Opening Balance:/g) || [];
expect(openingMatches.length).toBeGreaterThanOrEqual(2); // Once in drawer, once in balance
});
it('should show cash sales in calculation', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('+ Cash Sales:');
});
it('should show expected balance', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Expected Balance:');
expect(text).toContain('$350.00');
});
it('should show actual balance when shift is closed', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Actual Balance:');
});
it('should show (Not yet counted) when actual balance is null', () => {
const openShift = createShiftSummary({
closedAt: null,
actualBalanceCents: null,
varianceCents: null,
});
const builder = new ShiftReportBuilder(businessInfo, openShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('(Not yet counted)');
});
describe('variance display', () => {
it('should show zero variance correctly', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Variance:');
expect(text).toContain('$0.00');
});
it('should show Over for positive variance', () => {
const shiftOver = createShiftSummary({
actualBalanceCents: 36000,
varianceCents: 1000,
});
const builder = new ShiftReportBuilder(businessInfo, shiftOver, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Over:');
expect(text).toContain('+$10.00');
});
it('should show Short for negative variance', () => {
const shiftShort = createShiftSummary({
actualBalanceCents: 33000,
varianceCents: -2000,
});
const builder = new ShiftReportBuilder(businessInfo, shiftShort, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Short:');
expect(text).toContain('-$20.00');
});
it('should skip variance when null', () => {
const openShift = createShiftSummary({
closedAt: null,
actualBalanceCents: null,
varianceCents: null,
});
const builder = new ShiftReportBuilder(businessInfo, openShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).not.toContain('Variance:');
expect(text).not.toContain('Over:');
expect(text).not.toContain('Short:');
});
});
});
describe('buildSignatureLine()', () => {
it('should include signature line when enabled', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Signature:');
expect(text).toContain('_____'); // Underline
});
it('should skip signature line when disabled', () => {
const noSigConfig = createConfig({ includeSignatureLine: false });
const builder = new ShiftReportBuilder(businessInfo, shift, noSigConfig);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).not.toContain('Signature:');
});
it('should include manager name when provided', () => {
const configWithManager = createConfig({
managerName: 'Manager Carol',
});
const builder = new ShiftReportBuilder(
businessInfo,
shift,
configWithManager
);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Manager: Manager Carol');
});
it('should skip manager name when not provided', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).not.toContain('Manager:');
});
it('should include date line', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Date:');
});
});
describe('buildFooter()', () => {
it('should include printed timestamp', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Printed:');
});
it('should include printed by name when provided', () => {
const configWithPrinter = createConfig({
printedByName: 'Alice',
});
const builder = new ShiftReportBuilder(
businessInfo,
shift,
configWithPrinter
);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('By: Alice');
});
it('should skip printed by when not provided', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
// "By: " as a standalone line (for printed by) should not appear
// Note: "Opened By:" and "Closed By:" will still be present
const lines = text.split('\n');
const printedByLine = lines.find(line => line.startsWith('By:'));
expect(printedByLine).toBeUndefined();
});
it('should include confidentiality notice', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('CONFIDENTIAL - FOR INTERNAL USE ONLY');
});
});
describe('duration calculation', () => {
it('should show hours only when no minutes', () => {
const exactHours = createShiftSummary({
openedAt: '2024-12-26T09:00:00Z',
closedAt: '2024-12-26T12:00:00Z',
});
const builder = new ShiftReportBuilder(businessInfo, exactHours, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Duration:');
expect(text).toContain('3h');
});
it('should show minutes only when less than 1 hour', () => {
const shortShift = createShiftSummary({
openedAt: '2024-12-26T09:00:00Z',
closedAt: '2024-12-26T09:45:00Z',
});
const builder = new ShiftReportBuilder(businessInfo, shortShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Duration:');
expect(text).toContain('45m');
});
it('should show hours and minutes', () => {
const mixedShift = createShiftSummary({
openedAt: '2024-12-26T09:00:00Z',
closedAt: '2024-12-26T14:30:00Z',
});
const builder = new ShiftReportBuilder(businessInfo, mixedShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Duration:');
expect(text).toContain('5h 30m');
});
it('should handle invalid dates gracefully', () => {
const badDates = createShiftSummary({
openedAt: 'invalid',
closedAt: 'also-invalid',
});
const builder = new ShiftReportBuilder(businessInfo, badDates, config);
const result = builder.build();
// Should not throw
expect(result.length).toBeGreaterThan(0);
});
it('should show Invalid for negative duration', () => {
const backwards = createShiftSummary({
openedAt: '2024-12-26T17:00:00Z',
closedAt: '2024-12-26T09:00:00Z',
});
const builder = new ShiftReportBuilder(businessInfo, backwards, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Invalid');
});
});
describe('currency formatting', () => {
it('should format zero correctly', () => {
const zeroShift = createShiftSummary({
cashSalesCents: 0,
cardSalesCents: 0,
giftCardSalesCents: 0,
totalSalesCents: 0,
transactionCount: 0,
});
const builder = new ShiftReportBuilder(businessInfo, zeroShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('$0.00');
});
it('should format large amounts correctly', () => {
const bigShift = createShiftSummary({
totalSalesCents: 1000000,
cashSalesCents: 500000,
});
const builder = new ShiftReportBuilder(businessInfo, bigShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('$10,000.00');
expect(text).toContain('$5,000.00');
});
});
describe('chaining', () => {
it('should support method chaining for all section builders', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
// Each method should return `this`
const result = builder
.buildHeader()
.buildShiftTimes()
.buildOpeningBalance()
.buildSalesBreakdown()
.buildCashBalance()
.buildSignatureLine()
.buildFooter();
expect(result).toBe(builder);
});
});
describe('custom width', () => {
it('should respect custom width from config', () => {
const configWithWidth = createConfig({ width: 32 });
const builder = new ShiftReportBuilder(businessInfo, shift, configWithWidth);
const result = builder.build();
expect(result.length).toBeGreaterThan(0);
});
});
describe('complete report generation', () => {
it('should generate a complete closed shift report', () => {
const fullShift = createShiftSummary({
id: 42,
openedAt: '2024-12-26T08:30:00Z',
closedAt: '2024-12-26T17:45:00Z',
openedByName: 'Bob Johnson',
closedByName: 'Carol Williams',
openingBalanceCents: 15000,
expectedBalanceCents: 48750,
actualBalanceCents: 49000,
varianceCents: 250,
cashSalesCents: 33750,
cardSalesCents: 45000,
giftCardSalesCents: 12500,
totalSalesCents: 91250,
transactionCount: 47,
refundsCents: 1500,
business_timezone: 'America/Chicago',
});
const fullConfig: ShiftReportConfig = {
includeSignatureLine: true,
managerName: 'David Manager',
locationName: 'Downtown Store',
printedByName: 'Carol Williams',
};
const builder = new ShiftReportBuilder(businessInfo, fullShift, fullConfig);
const result = builder.build();
const text = new TextDecoder().decode(result);
// Verify key sections
expect(text).toContain('TEST BUSINESS');
expect(text).toContain('Downtown Store');
expect(text).toContain('SHIFT REPORT');
expect(text).toContain('Shift #:');
expect(text).toContain('42');
expect(text).toContain('Bob Johnson');
expect(text).toContain('Carol Williams');
expect(text).toContain('Duration:');
expect(text).toContain('CASH DRAWER');
expect(text).toContain('$150.00'); // Opening balance
expect(text).toContain('SALES SUMMARY');
expect(text).toContain('47'); // Transaction count
expect(text).toContain('$337.50'); // Cash sales
expect(text).toContain('$450.00'); // Card sales
expect(text).toContain('$125.00'); // Gift card sales
expect(text).toContain('$912.50'); // Total sales
expect(text).toContain('Refunds:');
expect(text).toContain('-$15.00');
expect(text).toContain('CASH BALANCE');
expect(text).toContain('Expected Balance:');
expect(text).toContain('Actual Balance:');
expect(text).toContain('Over:');
expect(text).toContain('+$2.50');
expect(text).toContain('Manager: David Manager');
expect(text).toContain('Signature:');
expect(text).toContain('By: Carol Williams');
expect(text).toContain('CONFIDENTIAL - FOR INTERNAL USE ONLY');
});
it('should generate a complete open shift report', () => {
const openShift = createShiftSummary({
id: 43,
openedAt: '2024-12-26T08:00:00Z',
closedAt: null,
openedByName: 'Eve Adams',
closedByName: undefined,
openingBalanceCents: 20000,
expectedBalanceCents: 27500,
actualBalanceCents: null,
varianceCents: null,
cashSalesCents: 7500,
cardSalesCents: 15000,
giftCardSalesCents: 2500,
totalSalesCents: 25000,
transactionCount: 12,
refundsCents: 0,
});
const openConfig: ShiftReportConfig = {
includeSignatureLine: false,
locationName: 'Mall Kiosk',
};
const builder = new ShiftReportBuilder(businessInfo, openShift, openConfig);
const result = builder.build();
const text = new TextDecoder().decode(result);
// Verify open shift specifics
expect(text).toContain('*** SHIFT STILL OPEN ***');
expect(text).toContain('(Not yet counted)');
expect(text).not.toContain('Variance:');
expect(text).not.toContain('Over:');
expect(text).not.toContain('Short:');
expect(text).not.toContain('Signature:');
});
it('should generate report with short variance', () => {
const shortShift = createShiftSummary({
actualBalanceCents: 30000,
expectedBalanceCents: 35000,
varianceCents: -5000,
});
const builder = new ShiftReportBuilder(businessInfo, shortShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('Short:');
expect(text).toContain('-$50.00');
});
it('should handle minimal shift data', () => {
const minimalShift: ShiftSummary = {
id: 1,
openedAt: '2024-12-26T09:00:00Z',
closedAt: null,
openedByName: '',
openingBalanceCents: 0,
expectedBalanceCents: 0,
actualBalanceCents: null,
varianceCents: null,
cashSalesCents: 0,
cardSalesCents: 0,
giftCardSalesCents: 0,
totalSalesCents: 0,
transactionCount: 0,
refundsCents: 0,
};
const builder = new ShiftReportBuilder(businessInfo, minimalShift, {});
const result = builder.build();
expect(result.length).toBeGreaterThan(0);
});
});
describe('variance formatting', () => {
it('should format positive variance with plus sign', () => {
const overShift = createShiftSummary({
varianceCents: 100,
actualBalanceCents: 35100,
});
const builder = new ShiftReportBuilder(businessInfo, overShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('+$1.00');
});
it('should format negative variance with minus sign', () => {
const shortShift = createShiftSummary({
varianceCents: -100,
actualBalanceCents: 34900,
});
const builder = new ShiftReportBuilder(businessInfo, shortShift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
expect(text).toContain('-$1.00');
});
it('should format exactly zero variance without sign', () => {
const builder = new ShiftReportBuilder(businessInfo, shift, config);
const result = builder.build();
const text = new TextDecoder().decode(result);
// Zero variance should be displayed as simple $0.00
expect(text).toContain('$0.00');
});
});
});