diff --git a/cli/src/commands/invoices.test.ts b/cli/src/commands/invoices.test.ts index b70340b0..c67f901e 100644 --- a/cli/src/commands/invoices.test.ts +++ b/cli/src/commands/invoices.test.ts @@ -143,4 +143,58 @@ describe("invoices create", () => { pr_links: [searchUrl, "https://github.com/org/repo/pull/42"], }); }); + + it("rejects invalid amount values before posting", async () => { + const { handleError } = await import("../helpers.js"); + + await run([ + "invoices", "create", "gig-123", + "--application-id", "app-456", + "--amount", "not-a-number", + "--payment-currency", "usdc_pol", + "--wallet-address", "0xabc", + ]); + + expect(mockClient.post).not.toHaveBeenCalled(); + expect(handleError).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("Invalid --amount") }), + expect.any(Object) + ); + }); + + it("rejects partially numeric amount values before posting", async () => { + const { handleError } = await import("../helpers.js"); + + await run([ + "invoices", "create", "gig-123", + "--application-id", "app-456", + "--amount", "10usd", + "--payment-currency", "usdc_pol", + "--wallet-address", "0xabc", + ]); + + expect(mockClient.post).not.toHaveBeenCalled(); + expect(handleError).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("Invalid --amount") }), + expect.any(Object) + ); + }); + + it("rejects non-positive line item quantities before posting", async () => { + const { handleError } = await import("../helpers.js"); + + await run([ + "invoices", "create", "gig-123", + "--application-id", "app-456", + "--item", "Bug fix|-2|1", + "--payment-currency", "usdc_pol", + "--wallet-address", "0xabc", + ]); + + expect(mockClient.post).not.toHaveBeenCalled(); + expect(handleError).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("positive quantity") }), + expect.any(Object) + ); + }); }); diff --git a/cli/src/commands/invoices.ts b/cli/src/commands/invoices.ts index d361eba6..ef9609d5 100644 --- a/cli/src/commands/invoices.ts +++ b/cli/src/commands/invoices.ts @@ -134,12 +134,19 @@ export function registerInvoicesCommands(program: Command): void { try { const items = cmdOpts.item.map((spec) => { const [description = "", qty = "1", unitPrice = "", link = ""] = spec.split("|"); + const quantity = qty.trim() === "" ? 1 : Number(qty); + const unitPriceValue = Number(unitPrice); const item: Record = { description: description.trim(), - quantity: parseFloat(qty) || 1, - unit_price: parseFloat(unitPrice), + quantity: Number.isFinite(quantity) ? quantity : 1, + unit_price: unitPriceValue, }; if (link.trim()) item.link = link.trim(); + if (!Number.isFinite(item.quantity as number) || (item.quantity as number) <= 0) { + throw new Error( + `Invalid --item "${spec}" — expected "description|qty|unit_price[|link]" with a positive quantity` + ); + } if (!Number.isFinite(item.unit_price as number) || (item.unit_price as number) <= 0) { throw new Error( `Invalid --item "${spec}" — expected "description|qty|unit_price[|link]" with a positive unit price` @@ -158,7 +165,13 @@ export function registerInvoicesCommands(program: Command): void { payment_currency: cmdOpts.paymentCurrency, merchant_wallet_address: cmdOpts.walletAddress, }; - if (cmdOpts.amount) body.amount = parseFloat(cmdOpts.amount); + if (cmdOpts.amount) { + const amount = Number(cmdOpts.amount); + if (!Number.isFinite(amount) || amount <= 0) { + throw new Error(`Invalid --amount "${cmdOpts.amount}" — expected a positive number`); + } + body.amount = amount; + } if (items.length > 0) body.items = items; if (cmdOpts.prLinks) { const prLinks = cmdOpts.prLinks