Files
jorgecuadros-platform/apps/api/src/cash-receipts/cash-receipts.spec.ts
T
rmancinasandClaude Opus 5.5 ca76f4d57a
Build and Push Images / Build jorgecuadros-web (push) Successful in 1m47s
Build and Push Images / Build jorgecuadros-api (push) Successful in 2m17s
feat(efectivo): the EFE cash receipt book, folio and posting in one step
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>
2026-10-12 19:24:43 -07:00

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");
});
});