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:
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user