The office still issues cash receipts from Access (EFE MENU -> EFE). This is that form: cliente, fecha, monto + moneda, concepto, elaboró (the logged-in user), fecha de depósito, and a folio that continues the legacy book. - cash_receipts, folio unique. Next folio = one past the highest of the platform's receipts and the imported utilities EFECTIVO journal, so after the cutover import it lands on the office's next number (13935 on prod today). A typed folio the legacy book already used is refused. - Issuing a receipt posts its credit in the same database transaction, as legacy posted to DATOS2: reference C<folio>, CASH DEPOSIT, pseudo check CASH<MMDD><P|D>. Dollars with a rate credit a pesos ledger (1,500 USD at 16.80 -> 25,200 MXN, as C13830 did). Voiding that row voids the receipt. - /efectivo: issue, search, record the deposit date; /efectivo/<folio> prints the receipt. - Reporte de efectivo now lists platform receipts by receipt, not by check, so each is counted once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
79 lines
2.8 KiB
TypeScript
79 lines
2.8 KiB
TypeScript
import { ConflictException } from "@nestjs/common";
|
|
import { CashReceiptsService, cashCheckNumber } from "./cash-receipts.service";
|
|
|
|
/**
|
|
* A cash receipt is two writes that must agree: the receipt and its C<folio>
|
|
* credit. Checked here: the credit legacy would have posted, and that a folio
|
|
* the legacy book already used is refused. Folio numbering itself runs against
|
|
* real data (it reads the imported EFECTIVO journal).
|
|
*/
|
|
|
|
function build(opts: { legacyTaken?: boolean } = {}) {
|
|
const posted: Record<string, unknown>[] = [];
|
|
const tx = {
|
|
customer: { findUnique: jest.fn().mockResolvedValue({ id: "c1" }) },
|
|
cashReceipt: {
|
|
aggregate: jest.fn().mockResolvedValue({ _max: { folio: 13934 } }),
|
|
count: jest.fn().mockResolvedValue(0),
|
|
create: jest.fn(({ data }) => Promise.resolve(data)),
|
|
},
|
|
transaction: {
|
|
count: jest.fn().mockResolvedValue(opts.legacyTaken ? 1 : 0),
|
|
create: jest.fn(({ data }) => {
|
|
posted.push(data);
|
|
return Promise.resolve({ id: "tx1", ...data });
|
|
}),
|
|
},
|
|
typeTransaction: { findFirst: jest.fn().mockResolvedValue({ id: "cash-deposit" }) },
|
|
$queryRaw: jest.fn().mockResolvedValue([{ mx: 13900 }]),
|
|
};
|
|
const prisma = { $transaction: jest.fn((fn) => fn(tx)) };
|
|
return { service: new CashReceiptsService(prisma as never), posted };
|
|
}
|
|
|
|
const base = {
|
|
customerId: "c1",
|
|
receivedAt: "2026-07-14",
|
|
concept: "RECEIVED $1500 DLLS FOR ACCOUNT.",
|
|
};
|
|
|
|
describe("cash receipts", () => {
|
|
it("takes the next folio and posts a C<folio> credit", async () => {
|
|
const { service, posted } = build();
|
|
const r = await service.create({ ...base, amount: 9000, currency: "MXN" }, "u1");
|
|
|
|
expect(r.folio).toBe(13935);
|
|
expect(posted[0]).toMatchObject({
|
|
reference: "C13935",
|
|
checkNumber: "CASH0714P",
|
|
currency: "MXN",
|
|
typeId: "cash-deposit",
|
|
});
|
|
expect(Number(posted[0].amount)).toBe(9000);
|
|
});
|
|
|
|
it("credits dollars to a pesos ledger at the given rate, as legacy did", async () => {
|
|
// Legacy C13830: 1,500 USD at 16.80 posted as 25,200 MXN.
|
|
const { service, posted } = build();
|
|
await service.create(
|
|
{ ...base, amount: 1500, currency: "USD", exchangeRate: 16.8 },
|
|
"u1",
|
|
);
|
|
|
|
expect(posted[0]).toMatchObject({ currency: "MXN", checkNumber: "CASH0714D" });
|
|
expect(Number(posted[0].amount)).toBe(25200);
|
|
});
|
|
|
|
it("refuses a typed folio the legacy book already used", async () => {
|
|
const { service, posted } = build({ legacyTaken: true });
|
|
await expect(
|
|
service.create({ ...base, folio: 13800, amount: 1, currency: "MXN" }, "u1"),
|
|
).rejects.toBeInstanceOf(ConflictException);
|
|
expect(posted).toHaveLength(0);
|
|
});
|
|
|
|
it("names the pseudo check by day and currency", () => {
|
|
expect(cashCheckNumber(new Date("2026-01-05T00:00:00Z"), "USD")).toBe("CASH0105D");
|
|
});
|
|
});
|