feat(drafts): DB drafts with deferred numbering — create-as-draft (no number), assign number on finalize (offers/invoices/issued-orders)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
generateIssuedOrderNumber,
|
||||
previewIssuedOrderNumber,
|
||||
releaseIssuedOrderNumber,
|
||||
assignIssuedOrderNumber,
|
||||
} from "./numbering.service";
|
||||
|
||||
export interface IssuedOrderItemInput {
|
||||
@@ -185,20 +186,29 @@ export async function getIssuedOrder(id: number) {
|
||||
|
||||
export async function createIssuedOrder(body: IssuedOrderInput) {
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const poNumber =
|
||||
const status = body.status ? String(body.status) : "draft";
|
||||
|
||||
// Deferred numbering: a draft carries NO po_number (the column is
|
||||
// nullable-unique so many drafts coexist). The official number is consumed
|
||||
// only when the draft is finalized (assignIssuedOrderNumber on draft->sent).
|
||||
// A caller-supplied number, or a doc created already-finalized, numbers now.
|
||||
const explicit =
|
||||
body.po_number !== undefined &&
|
||||
body.po_number !== null &&
|
||||
body.po_number !== ""
|
||||
? String(body.po_number)
|
||||
: (await generateIssuedOrderNumber(undefined, tx)).number;
|
||||
: null;
|
||||
const poNumber =
|
||||
explicit ??
|
||||
(status === "draft"
|
||||
? null
|
||||
: (await generateIssuedOrderNumber(undefined, tx)).number);
|
||||
|
||||
const order = await tx.issued_orders.create({
|
||||
data: {
|
||||
po_number: poNumber,
|
||||
customer_id: body.customer_id ? Number(body.customer_id) : null,
|
||||
status: (body.status
|
||||
? String(body.status)
|
||||
: "draft") as $Enums.issued_orders_status,
|
||||
status: status as $Enums.issued_orders_status,
|
||||
currency: body.currency ? String(body.currency) : "CZK",
|
||||
vat_rate: body.vat_rate != null ? Number(body.vat_rate) : 21.0,
|
||||
apply_vat: body.apply_vat !== false,
|
||||
@@ -299,7 +309,23 @@ export async function updateIssuedOrder(id: number, body: IssuedOrderInput) {
|
||||
|
||||
if (body.status !== undefined) data.status = String(body.status);
|
||||
|
||||
await prisma.issued_orders.update({ where: { id }, data });
|
||||
// Finalize transition draft -> sent: consume + write the official number
|
||||
// ATOMICALLY with the status change, so there is never a window where the
|
||||
// order is "sent" but still has a null po_number. assignIssuedOrderNumber is
|
||||
// idempotent (won't re-number an order that already has a number).
|
||||
const finalizing =
|
||||
currentStatus === "draft" &&
|
||||
body.status !== undefined &&
|
||||
String(body.status) === "sent";
|
||||
|
||||
if (finalizing) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.issued_orders.update({ where: { id }, data });
|
||||
await assignIssuedOrderNumber(id, tx);
|
||||
});
|
||||
} else {
|
||||
await prisma.issued_orders.update({ where: { id }, data });
|
||||
}
|
||||
|
||||
if (editable && Array.isArray(body.items)) {
|
||||
await prisma.$transaction(async (tx) => {
|
||||
@@ -321,7 +347,18 @@ export async function updateIssuedOrder(id: number, body: IssuedOrderInput) {
|
||||
});
|
||||
}
|
||||
|
||||
return { id, po_number: existing.po_number };
|
||||
// Reflect the number the finalize transition just assigned (existing was read
|
||||
// before the assign, so its po_number is still the pre-finalize null).
|
||||
const poNumber = finalizing
|
||||
? ((
|
||||
await prisma.issued_orders.findUnique({
|
||||
where: { id },
|
||||
select: { po_number: true },
|
||||
})
|
||||
)?.po_number ?? existing.po_number)
|
||||
: existing.po_number;
|
||||
|
||||
return { id, po_number: poNumber };
|
||||
}
|
||||
|
||||
export async function deleteIssuedOrder(id: number) {
|
||||
|
||||
Reference in New Issue
Block a user