Add invoice edit/delete with EDI modification tracking
- PUT /invoices/:id now accepts lineItems, recalculates totals, and adjusts PO billed amounts when totals change - Invoices edited after EDI submission get ediStatus "modified_after_send" - DELETE relaxed from draft-only to any non-paid invoice - Update README to reflect new endpoint behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
59127d5ab8
commit
2ed6cb1cb5
2 changed files with 57 additions and 4 deletions
|
|
@ -32,9 +32,9 @@ API Gateway (HTTP API)
|
||||||
│ ├── GET /invoices list
|
│ ├── GET /invoices list
|
||||||
│ ├── POST /invoices create (bills against PO)
|
│ ├── POST /invoices create (bills against PO)
|
||||||
│ ├── GET /invoices/:id get one
|
│ ├── GET /invoices/:id get one
|
||||||
│ ├── PUT /invoices/:id update
|
│ ├── PUT /invoices/:id update (full edit with line items; marks modified_after_send if already EDI-submitted)
|
||||||
│ ├── PATCH /invoices/:id/status status transition
|
│ ├── PATCH /invoices/:id/status status transition
|
||||||
│ └── DELETE /invoices/:id delete (drafts only)
|
│ └── DELETE /invoices/:id delete (any non-paid invoice; reverses PO billed amount)
|
||||||
│
|
│
|
||||||
├── /settings/** ← Settings Lambda
|
├── /settings/** ← Settings Lambda
|
||||||
│ ├── GET /settings get user settings
|
│ ├── GET /settings get user settings
|
||||||
|
|
|
||||||
|
|
@ -199,6 +199,59 @@ async function updateInvoice(id, event, user) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Handle line items update (recalculate totals)
|
||||||
|
if (body.lineItems && Array.isArray(body.lineItems) && body.lineItems.length > 0) {
|
||||||
|
const lineItems = body.lineItems.map((li, i) => {
|
||||||
|
if (!li.description) throw { statusCode: 400, message: `lineItems[${i}].description required` };
|
||||||
|
const qty = parseFloat(li.qty || li.quantity || 1);
|
||||||
|
const unitPrice = parseFloat(li.unitPrice || li.price || 0);
|
||||||
|
return { description: li.description, qty, unitPrice, total: qty * unitPrice };
|
||||||
|
});
|
||||||
|
|
||||||
|
const subtotal = lineItems.reduce((s, li) => s + li.total, 0);
|
||||||
|
const taxRate = parseFloat(body.taxRate ?? existing.Item.taxRate ?? 0);
|
||||||
|
const tax = subtotal * (taxRate / 100);
|
||||||
|
const total = subtotal + tax;
|
||||||
|
|
||||||
|
// If linked to a PO, check remaining balance (excluding current invoice's amount)
|
||||||
|
if (existing.Item.poId) {
|
||||||
|
const poResult = await db.send(new GetCommand({ TableName: POS_TABLE, Key: { id: existing.Item.poId } }));
|
||||||
|
if (poResult.Item) {
|
||||||
|
const billedWithoutThis = (poResult.Item.billed || 0) - existing.Item.total;
|
||||||
|
const remaining = poResult.Item.amount - billedWithoutThis;
|
||||||
|
if (total > remaining + 0.01) {
|
||||||
|
return badRequest(
|
||||||
|
`Invoice total ${fmt(total)} exceeds PO remaining balance ${fmt(remaining)} for ${poResult.Item.poNumber}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Update PO billed amount: remove old total, add new total
|
||||||
|
const diff = total - existing.Item.total;
|
||||||
|
if (Math.abs(diff) > 0.001) {
|
||||||
|
await db.send(new UpdateCommand({
|
||||||
|
TableName: POS_TABLE,
|
||||||
|
Key: { id: existing.Item.poId },
|
||||||
|
UpdateExpression: "SET billed = billed + :diff, updatedAt = :now",
|
||||||
|
ExpressionAttributeValues: { ":diff": diff, ":now": now },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expressions.push("#lineItems = :lineItems", "#subtotal = :subtotal", "#taxRate = :taxRate", "#tax = :tax", "#total = :total");
|
||||||
|
names["#lineItems"] = "lineItems"; values[":lineItems"] = lineItems;
|
||||||
|
names["#subtotal"] = "subtotal"; values[":subtotal"] = subtotal;
|
||||||
|
names["#taxRate"] = "taxRate"; values[":taxRate"] = taxRate;
|
||||||
|
names["#tax"] = "tax"; values[":tax"] = tax;
|
||||||
|
names["#total"] = "total"; values[":total"] = total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If invoice was already submitted via EDI, mark as modified after send
|
||||||
|
if (existing.Item.ediStatus === "submitted") {
|
||||||
|
expressions.push("#ediStatus = :ediStatus");
|
||||||
|
names["#ediStatus"] = "ediStatus";
|
||||||
|
values[":ediStatus"] = "modified_after_send";
|
||||||
|
}
|
||||||
|
|
||||||
const result = await db.send(new UpdateCommand({
|
const result = await db.send(new UpdateCommand({
|
||||||
TableName: INV_TABLE,
|
TableName: INV_TABLE,
|
||||||
Key: { id },
|
Key: { id },
|
||||||
|
|
@ -263,8 +316,8 @@ async function deleteInvoice(id, user) {
|
||||||
const db = getDocClient();
|
const db = getDocClient();
|
||||||
const existing = await db.send(new GetCommand({ TableName: INV_TABLE, Key: { id } }));
|
const existing = await db.send(new GetCommand({ TableName: INV_TABLE, Key: { id } }));
|
||||||
if (!existing.Item) return notFound("Invoice");
|
if (!existing.Item) return notFound("Invoice");
|
||||||
if (existing.Item.status !== "draft") {
|
if (existing.Item.status === "paid") {
|
||||||
return forbidden("Only draft invoices can be deleted. Cancel it first.");
|
return forbidden("Cannot delete a paid invoice");
|
||||||
}
|
}
|
||||||
|
|
||||||
await db.send(new DeleteCommand({ TableName: INV_TABLE, Key: { id } }));
|
await db.send(new DeleteCommand({ TableName: INV_TABLE, Key: { id } }));
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue