diff --git a/comebackhere-backend/src/routes/invoice-actions.ts b/comebackhere-backend/src/routes/invoice-actions.ts index c5533fc..0c26f08 100644 --- a/comebackhere-backend/src/routes/invoice-actions.ts +++ b/comebackhere-backend/src/routes/invoice-actions.ts @@ -5,6 +5,7 @@ import { requireEnv } from "../lib/env.js" import { asyncHandler, ContractError, parseContractErrorCode } from "../lib/errors.js" import { validateParams } from "../middleware/validate.js" import { invoiceActionIdParamSchema } from "../schemas/index.js" +import { connectMongo, getInvoicesCollection } from "../db/mongo.js" const router = Router() @@ -43,6 +44,14 @@ export async function getInvoiceStatus( } } +export async function isInvoiceExpired(invoiceId: string): Promise { + const db = await connectMongo() + const invoice = await getInvoicesCollection(db).findOne({ invoice_id: invoiceId }) + if (!invoice) return false + const now = Math.floor(Date.now() / 1000) + return now > invoice.due_date +} + export async function applyInvoiceAction( action: InvoiceAction, invoiceId: string, @@ -52,6 +61,24 @@ export async function applyInvoiceAction( const signer = Keypair.fromSecret(env.signerSecret) const currentStatus = await getInvoiceStatus(invoiceId, env, client) + // Prevent paid invoices from reverting to pending (#872) + if (action === "cancel_invoiced" && currentStatus === "Paid") { + throw new ContractError(3, "Cannot cancel a paid invoice", 409) + } + + // Prevent payment on cancelled invoices (#871) + if (action === "request_refund" && currentStatus === "Cancelled") { + throw new ContractError(3, "Cannot pay a cancelled invoice", 409) + } + + // Prevent payment on expired invoices (#873) + if (action === "request_refund") { + const isExpired = await isInvoiceExpired(invoiceId) + if (isExpired) { + throw new ContractError(3, "Cannot pay an expired invoice", 409) + } + } + if (action === "request_refund" && currentStatus !== "Paid") { if (currentStatus === "RefundRequested") { throw new ContractError(11, "A refund has already been requested", 409) diff --git a/comebackhere-backend/src/routes/invoices.ts b/comebackhere-backend/src/routes/invoices.ts index d728079..ab6c1ac 100644 --- a/comebackhere-backend/src/routes/invoices.ts +++ b/comebackhere-backend/src/routes/invoices.ts @@ -7,6 +7,7 @@ import { asyncHandler, NotFoundError } from "../lib/errors.js" import { cacheGet, cacheSet } from "../lib/cache.js" import { validateBody, validateParams, validateQuery } from "../middleware/validate.js" import { createInvoiceSchema, invoiceIdParamSchema, invoiceListQuerySchema } from "../schemas/index.js" +import { getInvoiceStatus } from "./invoice-actions.js" const router = Router() @@ -527,31 +528,23 @@ router.get("/", validateQuery(invoiceListQuerySchema), asyncHandler(async (req: router.get("/:id", validateParams(invoiceIdParamSchema), asyncHandler(async (req: Request, res: Response) => { const { id } = req.params - const env = requireEnv({ invoiceContractId: "INVOICE_CONTRACT_ID" }) + const env = requireEnv({ invoiceContractId: "INVOICE_CONTRACT_ID", rpcUrl: "SOROBAN_RPC_URL", signerSecret: "SIGNER_SECRET_KEY", networkPassphrase: "NETWORK_PASSPHRASE" }) - const server = new SorobanRpc.Server(env.rpcUrl) - const contract = new Contract(env.invoiceContractId) - - // Build a read-only ledger entry query for the invoice - const ledgerKey = contract.getFootprint() - void ledgerKey // used below via getLedgerEntries - - // Query the contract's ledger entry directly - const entries = await server.getLedgerEntries( - xdr.LedgerKey.contractData( - new xdr.LedgerKeyContractData({ - contract: new Contract(env.invoiceContractId).address().toScAddress(), - key: nativeToScVal(BigInt(id), { type: "u64" }), - durability: xdr.ContractDataDurability.persistent(), - }) - ) - ) + const db = await connectMongo() + const invoice = await getInvoicesCollection(db).findOne({ invoice_id: id }) - if (!entries.entries.length) { + if (!invoice) { throw new NotFoundError("Invoice not found") } - res.json({ invoice_id: id, status: "Pending" }) + const status = await getInvoiceStatus(id, { + rpcUrl: env.rpcUrl, + invoiceContractId: env.invoiceContractId, + signerSecret: env.signerSecret, + networkPassphrase: env.networkPassphrase, + }) + + res.json({ invoice_id: id, status, due_date: invoice.due_date }) })) /**