fix: Activity should log partial payment amount #141
3 changed files with 96 additions and 21 deletions
|
|
@ -10,6 +10,7 @@ import { generateDownloadUrl } from "@/lib/storage";
|
|||
import { groupAttachments } from "@/lib/attachments";
|
||||
import { TC_FIXED_LINE } from "@/lib/validations/po";
|
||||
import { parsePoTerms } from "@/lib/terms";
|
||||
import { actionLabel } from "@/lib/po-activity";
|
||||
import type { LineItemInput } from "@/lib/validations/po";
|
||||
import type { Role } from "@prisma/client";
|
||||
|
||||
|
|
@ -87,26 +88,6 @@ interface Props {
|
|||
vendorEmail?: string | null;
|
||||
}
|
||||
|
||||
const ACTION_LABELS: Record<string, string> = {
|
||||
CREATED: "Created",
|
||||
SUBMITTED: "Submitted for review",
|
||||
APPROVED: "Approved",
|
||||
APPROVED_WITH_NOTE: "Approved with note",
|
||||
REJECTED: "Rejected",
|
||||
EDITS_REQUESTED: "Edits requested",
|
||||
VENDOR_ID_REQUESTED: "Vendor ID requested",
|
||||
VENDOR_ID_PROVIDED: "Vendor ID provided",
|
||||
PAYMENT_SENT: "Payment confirmed",
|
||||
PARTIAL_PAYMENT_CONFIRMED: "Partial payment confirmed",
|
||||
RECEIPT_CONFIRMED: "Receipt confirmed",
|
||||
PARTIAL_RECEIPT_CONFIRMED: "Partial receipt confirmed",
|
||||
CLOSED: "Closed",
|
||||
MANAGER_LINE_EDIT: "Manager amended line items",
|
||||
PRODUCT_PRICE_UPDATED: "Product prices updated",
|
||||
CANCELLED: "Cancelled",
|
||||
SUPERSEDED: "Superseded",
|
||||
};
|
||||
|
||||
export async function PoDetail({ po, currentUserId, currentRole, readOnly = false, vendorEmail = null }: Props) {
|
||||
const lineItemsForEditor = po.lineItems.map((li) => ({
|
||||
name: li.name,
|
||||
|
|
@ -577,7 +558,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
<div className="absolute -left-1.5 mt-1.5 h-3 w-3 rounded-full border-2 border-white bg-neutral-400" />
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-sm font-medium text-neutral-900">
|
||||
{ACTION_LABELS[action.actionType] ?? action.actionType}
|
||||
{actionLabel(action, po.currency)}
|
||||
</span>
|
||||
<span className="text-xs text-neutral-400">by {action.actor.name}</span>
|
||||
<span className="text-xs text-neutral-400 ml-auto">{formatDateTime(action.createdAt)}</span>
|
||||
|
|
|
|||
40
App/lib/po-activity.ts
Normal file
40
App/lib/po-activity.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import type { Prisma } from "@prisma/client";
|
||||
import { formatCurrency } from "@/lib/utils";
|
||||
|
||||
// Human-readable labels for each POAction type, shown in the PO Activity timeline.
|
||||
export const ACTION_LABELS: Record<string, string> = {
|
||||
CREATED: "Created",
|
||||
SUBMITTED: "Submitted for review",
|
||||
APPROVED: "Approved",
|
||||
APPROVED_WITH_NOTE: "Approved with note",
|
||||
REJECTED: "Rejected",
|
||||
EDITS_REQUESTED: "Edits requested",
|
||||
VENDOR_ID_REQUESTED: "Vendor ID requested",
|
||||
VENDOR_ID_PROVIDED: "Vendor ID provided",
|
||||
PAYMENT_SENT: "Payment confirmed",
|
||||
PARTIAL_PAYMENT_CONFIRMED: "Partial payment confirmed",
|
||||
RECEIPT_CONFIRMED: "Receipt confirmed",
|
||||
PARTIAL_RECEIPT_CONFIRMED: "Partial receipt confirmed",
|
||||
CLOSED: "Closed",
|
||||
MANAGER_LINE_EDIT: "Manager amended line items",
|
||||
PRODUCT_PRICE_UPDATED: "Product prices updated",
|
||||
CANCELLED: "Cancelled",
|
||||
SUPERSEDED: "Superseded",
|
||||
};
|
||||
|
||||
// Produce the Activity-timeline label for an action. Most actions use the static
|
||||
// ACTION_LABELS map; PARTIAL_PAYMENT_CONFIRMED interpolates the instalment amount
|
||||
// from the action's metadata (already persisted by markPaid) — issue #140.
|
||||
export function actionLabel(
|
||||
action: { actionType: string; metadata: Prisma.JsonValue },
|
||||
currency: string,
|
||||
): string {
|
||||
const fallback = ACTION_LABELS[action.actionType] ?? action.actionType;
|
||||
if (action.actionType === "PARTIAL_PAYMENT_CONFIRMED") {
|
||||
const amount = (action.metadata as { paymentAmount?: unknown } | null)?.paymentAmount;
|
||||
if (typeof amount === "number" && Number.isFinite(amount)) {
|
||||
return `Partial payment of ${formatCurrency(amount, currency)} confirmed`;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
54
App/tests/unit/po-activity-label.test.ts
Normal file
54
App/tests/unit/po-activity-label.test.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { actionLabel } from "@/lib/po-activity";
|
||||
import { formatCurrency } from "@/lib/utils";
|
||||
|
||||
describe("actionLabel (Activity timeline)", () => {
|
||||
it("interpolates the instalment amount for a partial payment (issue #140)", () => {
|
||||
const label = actionLabel(
|
||||
{ actionType: "PARTIAL_PAYMENT_CONFIRMED", metadata: { paymentAmount: 5000 } },
|
||||
"INR"
|
||||
);
|
||||
expect(label).toBe(`Partial payment of ${formatCurrency(5000, "INR")} confirmed`);
|
||||
expect(label).toContain("5,000");
|
||||
});
|
||||
|
||||
it("respects the PO currency", () => {
|
||||
const label = actionLabel(
|
||||
{ actionType: "PARTIAL_PAYMENT_CONFIRMED", metadata: { paymentAmount: 1200 } },
|
||||
"USD"
|
||||
);
|
||||
expect(label).toBe(`Partial payment of ${formatCurrency(1200, "USD")} confirmed`);
|
||||
});
|
||||
|
||||
it("falls back to the plain label when paymentAmount is missing (older audit rows)", () => {
|
||||
expect(
|
||||
actionLabel({ actionType: "PARTIAL_PAYMENT_CONFIRMED", metadata: null }, "INR")
|
||||
).toBe("Partial payment confirmed");
|
||||
expect(
|
||||
actionLabel(
|
||||
{ actionType: "PARTIAL_PAYMENT_CONFIRMED", metadata: { paymentRef: "TXN-1" } },
|
||||
"INR"
|
||||
)
|
||||
).toBe("Partial payment confirmed");
|
||||
});
|
||||
|
||||
it("falls back when paymentAmount is non-numeric, never rendering NaN", () => {
|
||||
const label = actionLabel(
|
||||
{ actionType: "PARTIAL_PAYMENT_CONFIRMED", metadata: { paymentAmount: "5000" } },
|
||||
"INR"
|
||||
);
|
||||
expect(label).toBe("Partial payment confirmed");
|
||||
expect(label).not.toContain("NaN");
|
||||
});
|
||||
|
||||
it("leaves other action labels unchanged", () => {
|
||||
expect(
|
||||
actionLabel({ actionType: "PAYMENT_SENT", metadata: { paymentAmount: 5000 } }, "INR")
|
||||
).toBe("Payment confirmed");
|
||||
expect(actionLabel({ actionType: "APPROVED", metadata: null }, "INR")).toBe("Approved");
|
||||
});
|
||||
|
||||
it("falls back to the raw action type for unknown actions", () => {
|
||||
expect(actionLabel({ actionType: "MYSTERY", metadata: null }, "INR")).toBe("MYSTERY");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue