feat(pos): receipt VAT breakdown — per-line rate + subtotals by type
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import type pg from 'pg';
|
||||
import type { PosPaymentKind, PosReceipt, PosReceiptPayment } from '../domain/pos-sale.js';
|
||||
import type {
|
||||
PosPaymentKind,
|
||||
PosReceipt,
|
||||
PosReceiptPayment,
|
||||
PosReceiptTaxBreakdown,
|
||||
} from '../domain/pos-sale.js';
|
||||
import { AppError } from '../../../shared/errors.js';
|
||||
|
||||
type Queryable = Pick<pg.Pool, 'query'> | Pick<pg.PoolClient, 'query'>;
|
||||
@@ -146,12 +151,14 @@ export async function buildPosReceipt(queryable: Queryable, orderId: string): Pr
|
||||
taxCents,
|
||||
totalCents,
|
||||
freeItem: item.is_free_item,
|
||||
vatRate: item.vat_rate,
|
||||
};
|
||||
}),
|
||||
subtotalCents: Number(order.subtotal_cents),
|
||||
discountCents: Number(order.discount_cents),
|
||||
taxCents: Number(order.tax_cents),
|
||||
totalCents: Number(order.total_cents),
|
||||
taxBreakdown: computeTaxBreakdown(itemResult.rows),
|
||||
payments,
|
||||
changeCents: payments.reduce((sum, payment) => sum + payment.changeCents, 0),
|
||||
header: order.receipt_header,
|
||||
@@ -170,6 +177,42 @@ function grossFromNet(
|
||||
return Math.round(netUnitAmountCents * (1 + basisPoints / 10_000));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the per-VAT-rate breakdown for a set of receipt item rows.
|
||||
* Items with unknown VAT (null) or marked as free items are ignored.
|
||||
*/
|
||||
function computeTaxBreakdown(
|
||||
rows: ReceiptItemRow[],
|
||||
): PosReceiptTaxBreakdown {
|
||||
const result: PosReceiptTaxBreakdown = {
|
||||
generalBaseCents: 0,
|
||||
generalTaxCents: 0,
|
||||
reducedBaseCents: 0,
|
||||
reducedTaxCents: 0,
|
||||
superReducedBaseCents: 0,
|
||||
superReducedTaxCents: 0,
|
||||
};
|
||||
for (const row of rows) {
|
||||
if (row.is_free_item || !row.vat_rate) continue;
|
||||
const quantity = Number(row.quantity);
|
||||
const unitNet = Number(row.unit_price_cents);
|
||||
const unitTax = Number(row.tax_cents);
|
||||
const base = unitNet * quantity;
|
||||
const tax = unitTax * quantity;
|
||||
if (row.vat_rate === 'general') {
|
||||
result.generalBaseCents += base;
|
||||
result.generalTaxCents += tax;
|
||||
} else if (row.vat_rate === 'reduced') {
|
||||
result.reducedBaseCents += base;
|
||||
result.reducedTaxCents += tax;
|
||||
} else if (row.vat_rate === 'super-reduced') {
|
||||
result.superReducedBaseCents += base;
|
||||
result.superReducedTaxCents += tax;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPaymentKind(value: unknown): value is PosPaymentKind {
|
||||
return value === 'cash' || value === 'card' || value === 'other';
|
||||
}
|
||||
@@ -241,6 +284,7 @@ export async function buildPosReturnReceipt(
|
||||
taxCents,
|
||||
totalCents: -total,
|
||||
freeItem: row.is_free_item,
|
||||
vatRate: row.vat_rate,
|
||||
};
|
||||
});
|
||||
const subtotal = items.reduce((sum, item) => sum + item.subtotalCents, 0);
|
||||
@@ -263,6 +307,14 @@ export async function buildPosReturnReceipt(
|
||||
discountCents: -discount,
|
||||
taxCents: -tax,
|
||||
totalCents: -refundedCents,
|
||||
taxBreakdown: {
|
||||
generalBaseCents: -original.taxBreakdown.generalBaseCents,
|
||||
generalTaxCents: -original.taxBreakdown.generalTaxCents,
|
||||
reducedBaseCents: -original.taxBreakdown.reducedBaseCents,
|
||||
reducedTaxCents: -original.taxBreakdown.reducedTaxCents,
|
||||
superReducedBaseCents: -original.taxBreakdown.superReducedBaseCents,
|
||||
superReducedTaxCents: -original.taxBreakdown.superReducedTaxCents,
|
||||
},
|
||||
payments: [],
|
||||
changeCents: 0,
|
||||
header: original.header,
|
||||
|
||||
@@ -73,6 +73,20 @@ export interface PosReceiptItem {
|
||||
taxCents: number;
|
||||
totalCents: number;
|
||||
freeItem: boolean;
|
||||
/** Type of VAT applied to this line. null when unknown (free items, legacy). */
|
||||
vatRate: 'general' | 'reduced' | 'super-reduced' | null;
|
||||
}
|
||||
|
||||
export interface PosReceiptTaxBreakdown {
|
||||
/** Sum of net (pre-VAT) base for items subject to the general rate (21%). */
|
||||
generalBaseCents: number;
|
||||
generalTaxCents: number;
|
||||
/** Sum of net base for items subject to the reduced rate (10%). */
|
||||
reducedBaseCents: number;
|
||||
reducedTaxCents: number;
|
||||
/** Sum of net base for items subject to the super-reduced rate (4%). */
|
||||
superReducedBaseCents: number;
|
||||
superReducedTaxCents: number;
|
||||
}
|
||||
|
||||
export interface PosReceiptPayment {
|
||||
@@ -106,6 +120,8 @@ export interface PosReceipt {
|
||||
discountCents: number;
|
||||
taxCents: number;
|
||||
totalCents: number;
|
||||
/** Breakdown of the tax total by VAT rate. */
|
||||
taxBreakdown: PosReceiptTaxBreakdown;
|
||||
payments: PosReceiptPayment[];
|
||||
changeCents: number;
|
||||
header: string | null;
|
||||
|
||||
Reference in New Issue
Block a user