feat(orders): issued-orders service (CRUD + totals + transitions)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 11:14:39 +02:00
parent f00fcf95fd
commit b1f48df30c
2 changed files with 459 additions and 0 deletions

View File

@@ -6,6 +6,13 @@ import {
releaseIssuedOrderNumber,
} from "../services/numbering.service";
import { CreateIssuedOrderSchema } from "../schemas/issued-orders.schema";
import {
computeIssuedOrderTotals,
createIssuedOrder,
getIssuedOrder,
updateIssuedOrder,
deleteIssuedOrder,
} from "../services/issued-orders.service";
afterEach(async () => {
await prisma.number_sequences.deleteMany({ where: { type: "issued_order" } });
@@ -55,3 +62,137 @@ describe("CreateIssuedOrderSchema", () => {
expect(bad.success).toBe(false);
});
});
const createdIds: number[] = [];
const createdCustomerIds: number[] = [];
afterEach(async () => {
for (const id of createdIds)
await prisma.issued_orders.deleteMany({ where: { id } });
for (const id of createdCustomerIds)
await prisma.customers.deleteMany({ where: { id } });
createdIds.length = 0;
createdCustomerIds.length = 0;
});
async function makeCustomer() {
const c = await prisma.customers.create({
data: { name: "Dodavatel s.r.o." },
});
createdCustomerIds.push(c.id);
return c;
}
describe("computeIssuedOrderTotals (NET + VAT-on-top)", () => {
it("adds VAT on top of net, rounded per line", () => {
const t = computeIssuedOrderTotals(
[
{ quantity: 2, unit_price: 100, vat_rate: 21 },
{ quantity: 1, unit_price: 50, vat_rate: 12 },
],
true,
21,
);
expect(t.subtotal).toBe(250);
expect(t.vat_amount).toBe(48);
expect(t.total).toBe(298);
});
it("zeroes VAT when apply_vat is false but keeps the net subtotal", () => {
const t = computeIssuedOrderTotals(
[{ quantity: 3, unit_price: 100, vat_rate: 21 }],
false,
21,
);
expect(t).toEqual({ subtotal: 300, vat_amount: 0, total: 300 });
});
it("falls back to the document rate when a line rate is null", () => {
const t = computeIssuedOrderTotals(
[{ quantity: 1, unit_price: 100, vat_rate: null }],
true,
15,
);
expect(t.vat_amount).toBe(15);
});
});
describe("createIssuedOrder", () => {
it("assigns a PO number, defaults to draft, stores items", async () => {
const c = await makeCustomer();
const order = await createIssuedOrder({
customer_id: c.id,
items: [
{ description: "Materiál", quantity: 2, unit_price: 100, vat_rate: 21 },
],
});
createdIds.push(order.id);
expect(order.po_number).toBeTruthy();
expect(order.status).toBe("draft");
const items = await prisma.issued_order_items.findMany({
where: { issued_order_id: order.id },
});
expect(items.length).toBe(1);
expect(Number(items[0].unit_price)).toBe(100);
});
});
describe("updateIssuedOrder status transitions", () => {
it("allows draft -> sent and rejects draft -> completed", async () => {
const order = await createIssuedOrder({});
createdIds.push(order.id);
const ok = await updateIssuedOrder(order.id, { status: "sent" });
expect("error" in ok).toBe(false);
const bad = await updateIssuedOrder(order.id, { status: "completed" });
expect("error" in bad && bad.error).toBe("invalid_transition");
});
it("locks items once confirmed", async () => {
const order = await createIssuedOrder({
items: [{ description: "A", quantity: 1, unit_price: 10 }],
});
createdIds.push(order.id);
await updateIssuedOrder(order.id, { status: "sent" });
await updateIssuedOrder(order.id, { status: "confirmed" });
await updateIssuedOrder(order.id, {
items: [{ description: "B", quantity: 9, unit_price: 99 }],
});
const items = await prisma.issued_order_items.findMany({
where: { issued_order_id: order.id },
});
expect(items.length).toBe(1);
expect(items[0].description).toBe("A");
});
});
describe("getIssuedOrder", () => {
it("returns customer_name and valid_transitions", async () => {
const c = await makeCustomer();
const order = await createIssuedOrder({ customer_id: c.id });
createdIds.push(order.id);
const detail = await getIssuedOrder(order.id);
expect(detail?.customer_name).toBe("Dodavatel s.r.o.");
expect(detail?.valid_transitions).toContain("sent");
});
});
describe("deleteIssuedOrder", () => {
it("deletes, cascades items, frees the latest number", async () => {
const order = await createIssuedOrder({
items: [{ description: "X", quantity: 1, unit_price: 1 }],
});
const before = order.po_number;
await deleteIssuedOrder(order.id);
expect(
await prisma.issued_orders.findUnique({ where: { id: order.id } }),
).toBeNull();
expect(
(
await prisma.issued_order_items.findMany({
where: { issued_order_id: order.id },
})
).length,
).toBe(0);
expect((await previewIssuedOrderNumber()).number).toBe(before);
});
});