Merge pull request #1 from amoussa1229/feature/invoice-edit-delete

Add invoice edit/delete with EDI modification tracking
This commit is contained in:
Adam Moussa 2026-04-03 11:44:53 -04:00 • committed by GitHub
commit 5efa6fc961
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 4 deletions

View file

@ -32,9 +32,9 @@ API Gateway (HTTP API)
│ ├── GET /invoices list
│ ├── POST /invoices create (bills against PO)
│ ├── 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
│ └── DELETE /invoices/:id delete (drafts only)
│ └── DELETE /invoices/:id delete (any non-paid invoice; reverses PO billed amount)
│
├── /settings/** ← Settings Lambda
│ ├── GET /settings get user settings

View file

@ -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({
TableName: INV_TABLE,
Key: { id },
@ -263,8 +316,8 @@ async function deleteInvoice(id, user) {
const db = getDocClient();
const existing = await db.send(new GetCommand({ TableName: INV_TABLE, Key: { id } }));
if (!existing.Item) return notFound("Invoice");
if (existing.Item.status !== "draft") {
return forbidden("Only draft invoices can be deleted. Cancel it first.");
if (existing.Item.status === "paid") {
return forbidden("Cannot delete a paid invoice");
}
await db.send(new DeleteCommand({ TableName: INV_TABLE, Key: { id } }));