From c432654befcc315964e105d1f81c2785576155ee Mon Sep 17 00:00:00 2001 From: ahmadadebayo78-boop Date: Mon, 19 Oct 2026 09:27:08 +0000 Subject: [PATCH] fix: resolve issues #891, #892, #893, #894 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #893 — compliance.ts: import cacheDelete and call it after both /compliance/allow and /compliance/block succeed so the stale compliance:status:{address} cache entry is evicted immediately. #891 — treasury.ts: after executeSettlementWithBalanceCheck returns a confirmed tx_hash, updateOne the settlement document to status=Executed + executed_tx_hash so it no longer stays Pending. #894 — treasury.ts: remove the try/catch that swallowed errors from executeSettlementWithBalanceCheck and re-emitted them as HTTP 4xx/5xx JSON responses — this masked FAILED contract transactions as apparent successes. Errors now propagate through asyncHandler to the central error middleware. Also replace the broken res.locals.logger reference in the standalone helper function with the module-level logger import. #892 — invoices.ts: before calling createInvoice, look up the most recent compliance audit record for the merchant address; if the latest event is address_blocked, throw a 403 ForbiddenError so compliance- rejected addresses cannot initiate invoice payments. Adds the missing requireMerchantApiKey import and ForbiddenError / compliance collection imports required by this guard. .gitignore: add *.orig, *.bak, *.rej, .node_repl_history, Thumbs.db:encryptable, ehthumbs.db, Desktop.ini, Python __pycache__ and bytecode patterns. --- .gitignore | 16 +++++++++ comebackhere-backend/src/routes/compliance.ts | 5 +++ comebackhere-backend/src/routes/invoices.ts | 16 +++++++-- comebackhere-backend/src/routes/treasury.ts | 33 ++++++++++--------- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index aed438a..da328b7 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,19 @@ playwright/.auth/ # Playwright screenshots (generated during e2e runs) **/e2e/screenshots/ + +# Miscellaneous temporary / backup files +*.orig +*.bak +*.rej +.node_repl_history + +# OS-specific +Thumbs.db:encryptable +ehthumbs.db +Desktop.ini + +# Python bytecode (scripts/) +__pycache__/ +*.py[cod] +*.pyo diff --git a/comebackhere-backend/src/routes/compliance.ts b/comebackhere-backend/src/routes/compliance.ts index 730ca6a..838643b 100644 --- a/comebackhere-backend/src/routes/compliance.ts +++ b/comebackhere-backend/src/routes/compliance.ts @@ -13,6 +13,7 @@ import { asyncHandler } from "../lib/errors.js" import { requireAdmin } from "../middleware/adminAuth.js" import { allowBodySchema, blockBodySchema, complianceAuditQuerySchema } from "../schemas/index.js" import { connectMongo, getComplianceAuditCollection } from "../db/mongo.js" +import { cacheDelete } from "../lib/cache.js" const router = Router() @@ -206,6 +207,8 @@ router.post("/allow", requireAdmin, validateBody(allowBodySchema), asyncHandler( env.signerSecret, env.networkPassphrase ) + // #893 — evict any stale compliance status cache entry for this address + await cacheDelete(`compliance:status:${address}`) res.status(200).json(result) })) @@ -243,6 +246,8 @@ router.post("/block", requireAdmin, validateBody(blockBodySchema), asyncHandler( env.signerSecret, env.networkPassphrase ) + // #893 — evict any stale compliance status cache entry for this address + await cacheDelete(`compliance:status:${address}`) res.status(200).json(result) })) diff --git a/comebackhere-backend/src/routes/invoices.ts b/comebackhere-backend/src/routes/invoices.ts index d728079..4c52280 100644 --- a/comebackhere-backend/src/routes/invoices.ts +++ b/comebackhere-backend/src/routes/invoices.ts @@ -1,11 +1,12 @@ import { Router, type Request, type Response } from "express" import type { FindCursor, WithId } from "mongodb" import { Keypair, TransactionBuilder, BASE_FEE, Contract, nativeToScVal, SorobanRpc, xdr } from "stellar-sdk" -import { connectMongo, getInvoiceEventsCollection, getInvoicesCollection, type InvoiceRecord, type InvoiceStatus } from "../db/mongo.js" +import { connectMongo, getComplianceAuditCollection, getInvoiceEventsCollection, getInvoicesCollection, type InvoiceRecord, type InvoiceStatus } from "../db/mongo.js" import { requireEnv } from "../lib/env.js" -import { asyncHandler, NotFoundError } from "../lib/errors.js" +import { asyncHandler, ForbiddenError, NotFoundError } from "../lib/errors.js" import { cacheGet, cacheSet } from "../lib/cache.js" import { validateBody, validateParams, validateQuery } from "../middleware/validate.js" +import { requireMerchantApiKey } from "../middleware/apiKey.js" import { createInvoiceSchema, invoiceIdParamSchema, invoiceListQuerySchema } from "../schemas/index.js" const router = Router() @@ -715,6 +716,17 @@ router.post("/", requireMerchantApiKey, validateBody(createInvoiceSchema), async const client = buildSorobanClient(env.rpcUrl) const body = { ...req.body, merchant_address: res.locals.merchantAddress } as CreateInvoiceBody + + // #892 — reject invoice creation for compliance-blocked addresses + const complianceDb = await connectMongo() + const latestAudit = await getComplianceAuditCollection(complianceDb).findOne( + { address: body.merchant_address }, + { sort: { ledger: -1, _id: -1 } }, + ) + if (latestAudit && latestAudit.event_type === "address_blocked") { + throw new ForbiddenError("Merchant address has been blocked by compliance") + } + const result = await createInvoice( body, client, diff --git a/comebackhere-backend/src/routes/treasury.ts b/comebackhere-backend/src/routes/treasury.ts index 5929ffe..7da3e0c 100644 --- a/comebackhere-backend/src/routes/treasury.ts +++ b/comebackhere-backend/src/routes/treasury.ts @@ -19,6 +19,7 @@ import { escalateHoldSchema, } from "../schemas/index.js" import { getBalanceCache, setBalanceCache, invalidateBalanceCache } from "../lib/cache.js" +import { logger } from "../lib/logger.js" const router = Router() @@ -211,7 +212,7 @@ export async function executeSettlementWithBalanceCheck( env.networkPassphrase, ) - res.locals.logger.info( + logger.info( { settlementId: body.settlement_id, required: settlement.amount.toString(), @@ -327,20 +328,22 @@ router.post("/execute-settlement", requireAdmin, validateBody(executeSettlementS const { settlement_id: settlementId, token_contract } = req.body as { settlement_id: number; token_contract?: string } - try { - const result = await executeSettlementWithBalanceCheck( - { settlement_id: settlementId, token_contract }, - env, - ) - // #212 — balance changed; evict the cache so the next GET /balances is fresh - invalidateBalanceCache(`execute-settlement id=${settlementId}`) - res.json(result) - } catch (err: unknown) { - const status = (err as { status?: number })?.status ?? 500 - const message = err instanceof Error ? err.message : String(err) - res.status(status).json({ error: message }) - } -}) + const result = await executeSettlementWithBalanceCheck( + { settlement_id: settlementId, token_contract }, + env, + ) + + // #891 — mark the settlement as Executed now that the ledger has confirmed it + const database = await connectMongo() + await getSettlementsCollection(database).updateOne( + { id: settlementId }, + { $set: { status: "Executed", executed_tx_hash: result.tx_hash, updated_at: new Date() } }, + ) + + // #212 — balance changed; evict the cache so the next GET /balances is fresh + invalidateBalanceCache(`execute-settlement id=${settlementId}`) + res.json(result) +})) export interface SimulateSettlementBody { settlement_id: number