diff --git a/comebackhere-backend/src/routes/invoices.ts b/comebackhere-backend/src/routes/invoices.ts index d728079..c60b887 100644 --- a/comebackhere-backend/src/routes/invoices.ts +++ b/comebackhere-backend/src/routes/invoices.ts @@ -462,7 +462,7 @@ router.get("/", validateQuery(invoiceListQuerySchema), asyncHandler(async (req: } : filter - let findCursor = collection.find(pageFilter).sort({ created_at: -1, invoice_id: -1 }) + let findCursor = collection.find(pageFilter).sort({ created_at: -1, invoice_id: -1, _id: -1 }) if (legacyPagination) findCursor = findCursor.skip(offset) const rows = await findCursor.limit(limit + 1).toArray() const hasMore = rows.length > limit @@ -524,34 +524,56 @@ router.get("/", validateQuery(invoiceListQuerySchema), asyncHandler(async (req: * schema: * $ref: '#/components/schemas/ErrorResponse' */ -router.get("/:id", validateParams(invoiceIdParamSchema), asyncHandler(async (req: Request, res: Response) => { - const { id } = req.params +/** + * Sync invoice status from contract to database (#874). + * Called before returning invoice details to ensure status reflects current contract state. + */ +async function syncInvoiceStatus( + invoiceId: string, + collection: ReturnType, + env: { invoiceContractId: string; rpcUrl: string; signerSecret: string; networkPassphrase: string } +): Promise { + try { + const { getInvoiceStatus } = await import("./invoice-actions.js") + const contractStatus = await getInvoiceStatus(invoiceId, { + rpcUrl: env.rpcUrl, + invoiceContractId: env.invoiceContractId, + signerSecret: env.signerSecret, + networkPassphrase: env.networkPassphrase, + }) + + // Update the database with the current contract status + await collection.updateOne( + { invoice_id: invoiceId }, + { $set: { status: contractStatus, updated_at: new Date() } } + ) - const env = requireEnv({ invoiceContractId: "INVOICE_CONTRACT_ID" }) + return contractStatus + } catch (err) { + // If we can't fetch from contract, return null to fall back to DB status + return null + } +} - const server = new SorobanRpc.Server(env.rpcUrl) - const contract = new Contract(env.invoiceContractId) +router.get("/:id", validateParams(invoiceIdParamSchema), asyncHandler(async (req: Request, res: Response) => { + const { id } = req.params - // Build a read-only ledger entry query for the invoice - const ledgerKey = contract.getFootprint() - void ledgerKey // used below via getLedgerEntries + const env = requireEnv({ invoiceContractId: "INVOICE_CONTRACT_ID", rpcUrl: "SOROBAN_RPC_URL", signerSecret: "SIGNER_SECRET_KEY", networkPassphrase: "NETWORK_PASSPHRASE" }) - // 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 collection = getInvoicesCollection(db) - if (!entries.entries.length) { + // Get the invoice from database + const invoice = await collection.findOne({ invoice_id: id }) + if (!invoice) { throw new NotFoundError("Invoice not found") } - res.json({ invoice_id: id, status: "Pending" }) + // Sync status from contract to ensure it reflects any settlement (#874) + const contractStatus = await syncInvoiceStatus(id, collection, env) + const status = contractStatus || invoice.status + + res.json({ invoice_id: id, status }) })) /** @@ -725,6 +747,16 @@ router.post("/", requireMerchantApiKey, validateBody(createInvoiceSchema), async const db = await connectMongo() const collection = getInvoicesCollection(db) + + // Check for duplicate invoice ID to prevent conflicts (#877) + const existing = await collection.findOne({ invoice_id: result.invoice_id }) + if (existing) { + throw Object.assign( + new Error(`Invoice ID ${result.invoice_id} already exists; please retry to generate a new ID`), + { status: 409 } + ) + } + const now = new Date() await collection.insertOne({ invoice_id: result.invoice_id, diff --git a/comebackhere-backend/src/schemas/index.ts b/comebackhere-backend/src/schemas/index.ts index 762ad4d..59c63e3 100644 --- a/comebackhere-backend/src/schemas/index.ts +++ b/comebackhere-backend/src/schemas/index.ts @@ -28,6 +28,9 @@ const futureTimestamp = z .refine((val) => val > Math.floor(Date.now() / 1000), { message: "due_date must be in the future", }) + .refine((val) => val <= Math.floor(Date.now() / 1000) + 315_360_000, { + message: "due_date must be within 10 years from now", + }) export const createInvoiceSchema = z.object({ token: z.string().min(1, "token is required"),