feat(shift): cash drawer balance carried across shifts + admin cash movements
New signed cash_movement event (admin-only): load/remove drawer float, signed + attributed. ShiftService folds cash payments + movements by time into a drawer balance; shift open auto-inherits the prior shift's expected closing drawer as its opening float; the Z-report reports opening/taken/added/removed/expected (= next shift's opening float). Card payments excluded (settle to bank). Routes: POST /api/cash-movement, drawer in GET /api/shift/current. ShiftControl shows the live drawer + admin load/remove form + Z-report drawer block. Wiki: shift.md.
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { requireRole } from "../auth.js";
|
||||
import {
|
||||
InvalidCashMovementError,
|
||||
NoOpenShiftError,
|
||||
ShiftAlreadyOpenError,
|
||||
type ShiftService,
|
||||
} from "../shift-service.js";
|
||||
|
||||
interface CashMovementBody {
|
||||
/** Signed minor units: positive = load INTO drawer, negative = remove FROM drawer. */
|
||||
amountMinor: number;
|
||||
reason?: string;
|
||||
currency?: string;
|
||||
}
|
||||
|
||||
// Shift endpoints (manned mode). The operator is the logged-in user; a shift is
|
||||
// opened/closed explicitly (not time-based — see wiki/concepts/shift.md and
|
||||
// local-jwt-auth.md "until logout"). End Shift signs a shift_z_report + prints it.
|
||||
@@ -15,12 +23,36 @@ export async function shiftRoutes(app: FastifyInstance, shift: ShiftService): Pr
|
||||
const guard = requireRole("admin", "operator", "cashier");
|
||||
|
||||
// Is the current operator's shift open? (For the UI to show Start vs. End.)
|
||||
// Also returns the live drawer balance so the UI can show what's in the till.
|
||||
app.get("/api/shift/current", { preHandler: guard }, async (req) => {
|
||||
const operator = req.user.username;
|
||||
const open = shift.openShiftFor(operator);
|
||||
return { operator, open: open ? { startedAt: open.occurredAt } : null };
|
||||
const drawer = shift.drawerBalance();
|
||||
return {
|
||||
operator,
|
||||
open: open ? { startedAt: open.occurredAt } : null,
|
||||
drawerMinor: drawer.balanceMinor,
|
||||
currency: drawer.currency,
|
||||
};
|
||||
});
|
||||
|
||||
// Admin loads/removes physical drawer cash (the float). Signed cash_movement
|
||||
// event. ADMIN ONLY — an operator takes payments but cannot move the float.
|
||||
// amountMinor is signed: + load IN, − remove OUT. See wiki/concepts/shift.md.
|
||||
app.post<{ Body: CashMovementBody }>(
|
||||
"/api/cash-movement",
|
||||
{ preHandler: requireRole("admin") },
|
||||
async (req, reply) => {
|
||||
const { amountMinor, reason, currency } = req.body ?? ({} as CashMovementBody);
|
||||
try {
|
||||
return await shift.recordCashMovement(req.user.username, amountMinor, reason ?? "", currency);
|
||||
} catch (err) {
|
||||
if (err instanceof InvalidCashMovementError) return reply.code(400).send({ error: err.message });
|
||||
return reply.code(500).send({ error: (err as Error).message });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
app.post("/api/shift/open", { preHandler: guard }, async (req, reply) => {
|
||||
try {
|
||||
return await shift.open(req.user.username);
|
||||
|
||||
Reference in New Issue
Block a user