Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions comebackhere-backend/src/routes/invoice-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -43,6 +44,14 @@ export async function getInvoiceStatus(
}
}

export async function isInvoiceExpired(invoiceId: string): Promise<boolean> {
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,
Expand All @@ -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)
Expand Down
33 changes: 13 additions & 20 deletions comebackhere-backend/src/routes/invoices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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 })
}))

/**
Expand Down
Loading