From 8e49b62036073b3013fc43b10c9ece93b0f731d4 Mon Sep 17 00:00:00 2001 From: uche102 Date: Tue, 20 Oct 2026 04:17:25 +0100 Subject: [PATCH 1/7] fix(ci): resolve admin route auth test failures Limit community admin auth to its own routes so settlement requests reach the intended JWT gate. Complete router fixtures for market feature tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/routes/community-programs.routes.ts | 10 ++++++---- tests/unit/market-features.test.ts | 9 +++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/routes/community-programs.routes.ts b/src/routes/community-programs.routes.ts index d6c6118..bef4e6b 100644 --- a/src/routes/community-programs.routes.ts +++ b/src/routes/community-programs.routes.ts @@ -52,16 +52,18 @@ export function createCommunityProgramsRouter(dataSource: DataSource, authServic }); const admin = Router(); - admin.use(authenticate, requireAdmin((process.env.ADMIN_WALLETS ?? "").split(",").map((wallet) => wallet.trim()).filter(Boolean))); - admin.get("/reports", async (req: Request, res: Response, next) => { + const authorizeAdmin = requireAdmin( + (process.env.ADMIN_WALLETS ?? "").split(",").map((wallet) => wallet.trim()).filter(Boolean) + ); + admin.get("/reports", authenticate, authorizeAdmin, async (req: Request, res: Response, next) => { try { const page = Math.max(1, Number(req.query.page) || 1); const pageSize = Math.min(100, Math.max(1, Number(req.query.pageSize) || 25)); const [items, total] = await service.listFraudReports(page, pageSize); res.json({ success: true, data: items, pagination: { page, pageSize, total } }); } catch (error) { forward(next, error); } }); - admin.patch("/reports/:id", async (req: Request, res: Response, next) => { + admin.patch("/reports/:id", authenticate, authorizeAdmin, async (req: Request<{ id: string }>, res: Response, next) => { try { res.json({ success: true, data: await service.reviewFraudReport(req.params.id, String(req.body?.status) as FraudReportStatus, req.body?.reviewNote) }); } catch (error) { forward(next, error); } }); - admin.post("/waitlist/:wallet/approve", async (req: Request, res: Response, next) => { + admin.post("/waitlist/:wallet/approve", authenticate, authorizeAdmin, async (req: Request<{ wallet: string }>, res: Response, next) => { try { res.json({ success: true, data: await service.approveWaitlist(req.params.wallet) }); } catch (error) { forward(next, error); } }); diff --git a/tests/unit/market-features.test.ts b/tests/unit/market-features.test.ts index 4b508ea..49916b9 100644 --- a/tests/unit/market-features.test.ts +++ b/tests/unit/market-features.test.ts @@ -41,7 +41,7 @@ describe("Market Features (#660, #661, #663, #664)", () => { describe("Issue #660: Investor Scoring API", () => { const app = express(); - const mockAuthService = {} as AuthService; + const mockAuthService = { getCurrentUser: jest.fn() } as unknown as AuthService; const mockAcknowledgementService = {} as InvestorAcknowledgementService; app.use( @@ -78,7 +78,12 @@ describe("Market Features (#660, #661, #663, #664)", () => { } as unknown as InvoiceService; const mockConfig = { - ipfs: { maxFileSizeMB: 10, allowedMimeTypes: ["application/pdf"] }, + ipfs: { + maxFileSizeMB: 10, + allowedMimeTypes: ["application/pdf"], + uploadRateLimit: { windowMs: 60_000, maxUploads: 10 }, + }, + kyc: { skipVerification: true }, cache: { enabled: false }, } as any; From 2f30fd158bb4335f34e71a2e15254a668bc77826 Mon Sep 17 00:00:00 2001 From: uche102 Date: Tue, 20 Oct 2026 04:46:31 +0100 Subject: [PATCH 2/7] fix(ci): resolve shared CI failures Fix route param and cache API typing, remove lint errors, and clean up test fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/app.ts | 1 + src/controllers/benchmark.controller.ts | 6 +- src/controllers/insurance.controller.ts | 7 +- .../invoice-extension.controller.ts | 7 +- .../issuer-blacklist.controller.ts | 2 +- src/controllers/referral-tier.controller.ts | 4 +- .../secondary-auction.controller.ts | 12 ++- src/controllers/yield-boost.controller.ts | 6 +- src/routes/community-programs.routes.ts | 6 +- src/routes/onchain-event-views.routes.ts | 21 +++-- src/services/benchmark.service.ts | 16 ++-- src/services/contract-registry.service.ts | 61 ++++++++----- src/services/extension-proposal.service.ts | 78 +++++++++++------ src/services/invoice-insurance.service.ts | 87 ++++++++++++------- src/services/issuer-blacklist.service.ts | 47 ++++++---- src/services/referral-tier.service.ts | 10 ++- src/services/secondary-auction.service.ts | 32 ++++--- src/services/yield-boost.service.ts | 10 ++- tests/benchmark.service.test.ts | 10 +-- tests/referral-tier.service.test.ts | 10 +-- tests/secondary-auction.service.test.ts | 16 ++-- tests/yield-boost.service.test.ts | 6 +- 22 files changed, 288 insertions(+), 167 deletions(-) diff --git a/src/app.ts b/src/app.ts index c7d6cd8..fab06aa 100644 --- a/src/app.ts +++ b/src/app.ts @@ -50,6 +50,7 @@ import { createRoyaltiesRouter } from "./routes/royalties.routes"; import { createSubscriptionsRouter } from "./routes/subscriptions.routes"; import { createOnboardingRouter } from "./routes/onboarding.routes"; import { createCommunityProgramsRouter } from "./routes/community-programs.routes"; +import { createIssuerReputationRouter } from "./routes/issuer-reputation.routes"; import { createOnchainEventViewsRouter } from "./routes/onchain-event-views.routes"; import { createFxRouter } from "./routes/fx.routes"; import { diff --git a/src/controllers/benchmark.controller.ts b/src/controllers/benchmark.controller.ts index 8bdc195..dee8bb9 100644 --- a/src/controllers/benchmark.controller.ts +++ b/src/controllers/benchmark.controller.ts @@ -7,7 +7,7 @@ export class BenchmarkController { /** GET /api/v1/benchmarks/:sector */ getSectorBenchmark = async (req: Request, res: Response) => { try { - const sector = req.params.sector; + const sector = String(req.params.sector); const benchmark = await this.benchmarkService.getSectorBenchmark(sector); return res.status(200).json({ success: true, data: benchmark }); } catch (err: unknown) { @@ -18,7 +18,9 @@ export class BenchmarkController { /** GET /api/v1/invoices/:invoiceId/benchmark */ getInvoiceBenchmark = async (req: Request, res: Response) => { try { - const invoiceId = req.params.invoiceId ?? (req.params as Record).invoice_id; + const invoiceId = String( + req.params.invoiceId ?? (req.params as Record).invoice_id + ); const comparison = await this.benchmarkService.getInvoiceBenchmark(invoiceId); return res.status(200).json({ success: true, data: comparison }); } catch (err: unknown) { diff --git a/src/controllers/insurance.controller.ts b/src/controllers/insurance.controller.ts index 1243c33..894289f 100644 --- a/src/controllers/insurance.controller.ts +++ b/src/controllers/insurance.controller.ts @@ -9,7 +9,8 @@ export class InsuranceController { getInvoiceInsurance = async (req: Request, res: Response) => { try { - const { invoiceId, wallet } = req.params; + const invoiceId = String(req.params.invoiceId); + const wallet = String(req.params.wallet); const result = await this.insuranceService.getInvoiceInsurance(invoiceId, wallet); if (!result) { return res.status(404).json({ error: { code: "NOT_FOUND", message: "Insurance policy not found" } }); @@ -22,7 +23,7 @@ export class InsuranceController { getWalletPolicies = async (req: Request, res: Response) => { try { - const { wallet } = req.params; + const wallet = String(req.params.wallet); const result = await this.insuranceService.getWalletActivePolicies(wallet); return res.status(200).json({ success: true, data: result }); } catch (err: unknown) { @@ -32,7 +33,7 @@ export class InsuranceController { getWalletPayoutHistory = async (req: Request, res: Response) => { try { - const { wallet } = req.params; + const wallet = String(req.params.wallet); const result = await this.insuranceService.getWalletPayoutHistory(wallet); return res.status(200).json({ success: true, data: result }); } catch (err: unknown) { diff --git a/src/controllers/invoice-extension.controller.ts b/src/controllers/invoice-extension.controller.ts index cae1358..935aa9f 100644 --- a/src/controllers/invoice-extension.controller.ts +++ b/src/controllers/invoice-extension.controller.ts @@ -9,7 +9,7 @@ export class InvoiceExtensionController { getActiveProposal = async (req: Request, res: Response) => { try { - const { invoiceId } = req.params; + const invoiceId = String(req.params.invoiceId); const result = await this.extensionService.getActiveProposal(invoiceId); return res.status(200).json({ success: true, data: result }); } catch (err: unknown) { @@ -19,7 +19,8 @@ export class InvoiceExtensionController { getWalletVote = async (req: Request, res: Response) => { try { - const { invoiceId, wallet } = req.params; + const invoiceId = String(req.params.invoiceId); + const wallet = String(req.params.wallet); const result = await this.extensionService.getWalletVote(invoiceId, wallet); return res.status(200).json({ success: true, data: result }); } catch (err: unknown) { @@ -29,7 +30,7 @@ export class InvoiceExtensionController { getProposalHistory = async (req: Request, res: Response) => { try { - const { invoiceId } = req.params; + const invoiceId = String(req.params.invoiceId); const result = await this.extensionService.getProposalHistory(invoiceId); return res.status(200).json({ success: true, data: result }); } catch (err: unknown) { diff --git a/src/controllers/issuer-blacklist.controller.ts b/src/controllers/issuer-blacklist.controller.ts index 6b13e6b..008869a 100644 --- a/src/controllers/issuer-blacklist.controller.ts +++ b/src/controllers/issuer-blacklist.controller.ts @@ -9,7 +9,7 @@ export class IssuerBlacklistController { getBlacklistStatus = async (req: Request, res: Response) => { try { - const { wallet } = req.params; + const wallet = String(req.params.wallet); const result = await this.blacklistService.getBlacklistStatus(wallet); return res.status(200).json({ success: true, data: result }); } catch (err: unknown) { diff --git a/src/controllers/referral-tier.controller.ts b/src/controllers/referral-tier.controller.ts index 6576c9a..16869db 100644 --- a/src/controllers/referral-tier.controller.ts +++ b/src/controllers/referral-tier.controller.ts @@ -7,7 +7,7 @@ export class ReferralTierController { /** GET /api/v1/referrals/:wallet/tier */ getTier = async (req: Request, res: Response) => { try { - const wallet = req.params.wallet; + const wallet = String(req.params.wallet); const tier = await this.referralTierService.getTier(wallet); return res.status(200).json({ success: true, data: tier }); } catch (err: unknown) { @@ -18,7 +18,7 @@ export class ReferralTierController { /** GET /api/v1/referrals/:wallet/tier/history */ getTierHistory = async (req: Request, res: Response) => { try { - const wallet = req.params.wallet; + const wallet = String(req.params.wallet); const page = Number(req.query.page ?? 1); const limit = Number(req.query.limit ?? 20); const history = await this.referralTierService.getTierHistory(wallet, page, limit); diff --git a/src/controllers/secondary-auction.controller.ts b/src/controllers/secondary-auction.controller.ts index db28c1a..3ee6ef8 100644 --- a/src/controllers/secondary-auction.controller.ts +++ b/src/controllers/secondary-auction.controller.ts @@ -7,7 +7,9 @@ export class SecondaryAuctionController { /** GET /api/v1/auctions/:auctionId */ getAuction = async (req: Request, res: Response) => { try { - const auctionId = req.params.auctionId ?? (req.params as Record).auction_id; + const auctionId = String( + req.params.auctionId ?? (req.params as Record).auction_id + ); const auction = await this.secondaryAuctionService.getAuction(auctionId); return res.status(200).json({ success: true, data: auction }); } catch (err: unknown) { @@ -18,7 +20,9 @@ export class SecondaryAuctionController { /** GET /api/v1/auctions/:auctionId/bids */ getBids = async (req: Request, res: Response) => { try { - const auctionId = req.params.auctionId ?? (req.params as Record).auction_id; + const auctionId = String( + req.params.auctionId ?? (req.params as Record).auction_id + ); const page = Number(req.query.page ?? 1); const limit = Number(req.query.limit ?? 20); const bids = await this.secondaryAuctionService.getBids(auctionId, page, limit); @@ -31,7 +35,9 @@ export class SecondaryAuctionController { /** GET /api/v1/invoices/:invoiceId/auctions */ getInvoiceAuctions = async (req: Request, res: Response) => { try { - const invoiceId = req.params.invoiceId ?? (req.params as Record).invoice_id; + const invoiceId = String( + req.params.invoiceId ?? (req.params as Record).invoice_id + ); const auctions = await this.secondaryAuctionService.getAuctionsForInvoice(invoiceId); return res.status(200).json({ success: true, data: auctions }); } catch (err: unknown) { diff --git a/src/controllers/yield-boost.controller.ts b/src/controllers/yield-boost.controller.ts index 68423aa..a074a51 100644 --- a/src/controllers/yield-boost.controller.ts +++ b/src/controllers/yield-boost.controller.ts @@ -7,8 +7,10 @@ export class YieldBoostController { /** GET /api/v1/invoices/:invoiceId/boost/:wallet */ getBoost = async (req: Request, res: Response) => { try { - const invoiceId = req.params.invoiceId ?? (req.params as Record).invoice_id; - const wallet = req.params.wallet; + const invoiceId = String( + req.params.invoiceId ?? (req.params as Record).invoice_id + ); + const wallet = String(req.params.wallet); const boost = await this.yieldBoostService.getBoost(invoiceId, wallet); return res.status(200).json({ success: true, data: boost }); } catch (err: unknown) { diff --git a/src/routes/community-programs.routes.ts b/src/routes/community-programs.routes.ts index bef4e6b..93e72fd 100644 --- a/src/routes/community-programs.routes.ts +++ b/src/routes/community-programs.routes.ts @@ -36,7 +36,9 @@ export function createCommunityProgramsRouter(dataSource: DataSource, authServic }); router.get("/waitlist/:wallet", async (req: Request, res: Response, next) => { try { - const entry = await dataSource.getRepository(WaitlistEntry).findOne({ where: { wallet: req.params.wallet } }); + const entry = await dataSource.getRepository(WaitlistEntry).findOne({ + where: { wallet: String(req.params.wallet) }, + }); if (!entry) { res.status(404).json({ success: false, error: "Waitlist entry not found" }); return; } res.json({ success: true, data: await service.waitlistView(entry as never) }); } catch (error) { forward(next, error); } @@ -64,7 +66,7 @@ export function createCommunityProgramsRouter(dataSource: DataSource, authServic catch (error) { forward(next, error); } }); admin.post("/waitlist/:wallet/approve", authenticate, authorizeAdmin, async (req: Request<{ wallet: string }>, res: Response, next) => { - try { res.json({ success: true, data: await service.approveWaitlist(req.params.wallet) }); } + try { res.json({ success: true, data: await service.approveWaitlist(String(req.params.wallet)) }); } catch (error) { forward(next, error); } }); router.use("/admin", admin); diff --git a/src/routes/onchain-event-views.routes.ts b/src/routes/onchain-event-views.routes.ts index e7f5808..4a4230e 100644 --- a/src/routes/onchain-event-views.routes.ts +++ b/src/routes/onchain-event-views.routes.ts @@ -42,7 +42,7 @@ export function createOnchainEventViewsRouter(dataSource: DataSource): Router { const data = await byPayloadField( ["syndicate_created", "syndicate_joined", "syndicate_member_exited", "syndicate_executed"], "invoice_id", - req.params.invoiceId, + String(req.params.invoiceId), parseLimit(req), ); res.json({ success: true, data }); @@ -52,7 +52,12 @@ export function createOnchainEventViewsRouter(dataSource: DataSource): Router { // #673 — risk alert events for one invoice. router.get("/invoices/:invoiceId/risk-status", async (req: Request, res: Response, next) => { try { - const data = await byPayloadField(["invoice_at_risk"], "invoice_id", req.params.invoiceId, parseLimit(req)); + const data = await byPayloadField( + ["invoice_at_risk"], + "invoice_id", + String(req.params.invoiceId), + parseLimit(req) + ); res.json({ success: true, data }); } catch (error) { forward(next, error); } }); @@ -60,11 +65,17 @@ export function createOnchainEventViewsRouter(dataSource: DataSource): Router { // #674 — loyalty tier history for one wallet. router.get("/investors/:wallet/loyalty", async (req: Request, res: Response, next) => { try { - if (!isValidStellarPublicKey(req.params.wallet)) { + const wallet = String(req.params.wallet); + if (!isValidStellarPublicKey(wallet)) { res.status(400).json({ success: false, error: "Invalid wallet address" }); return; } - const data = await byPayloadField(["loyalty_tier_upgraded"], "wallet", req.params.wallet, parseLimit(req)); + const data = await byPayloadField( + ["loyalty_tier_upgraded"], + "wallet", + wallet, + parseLimit(req) + ); res.json({ success: true, data, current: data.at(-1) ?? null }); } catch (error) { forward(next, error); } }); @@ -75,7 +86,7 @@ export function createOnchainEventViewsRouter(dataSource: DataSource): Router { const data = await byPayloadField( ["pool_created", "pool_investment_executed"], "pool_id", - req.params.poolId, + String(req.params.poolId), parseLimit(req), ); res.json({ success: true, data }); diff --git a/src/services/benchmark.service.ts b/src/services/benchmark.service.ts index bc70bad..a7d4db4 100644 --- a/src/services/benchmark.service.ts +++ b/src/services/benchmark.service.ts @@ -5,6 +5,7 @@ import { ServiceError } from "../utils/service-error"; import { TtlCache } from "../lib/ttl-cache"; const CACHE_TTL_MS = 5 * 60 * 1000; +const CACHE_TTL_SECONDS = CACHE_TTL_MS / 1000; export interface BenchmarkUpdatedEvent { sector: string; @@ -43,8 +44,11 @@ export interface InvoiceBenchmarkComparison { * through a 5-minute TTL cache that is invalidated per sector on write. */ export class BenchmarkService { - private readonly cache = new TtlCache(CACHE_TTL_MS); - private readonly comparisonCache = new TtlCache(CACHE_TTL_MS); + private readonly cache = new TtlCache({ ttlSeconds: CACHE_TTL_SECONDS, namespace: "benchmark" }); + private readonly comparisonCache = new TtlCache({ + ttlSeconds: CACHE_TTL_SECONDS, + namespace: "benchmark-comparison", + }); constructor(private readonly dataSource: DataSource) {} @@ -76,7 +80,7 @@ export class BenchmarkService { async getSectorBenchmark(sector: string): Promise { const normalized = sector.trim().toLowerCase(); - const cached = this.cache.get(normalized); + const cached = await this.cache.get(normalized); if (cached) return cached; const row = await this.dataSource.getRepository(SectorBenchmark).findOneBy({ @@ -91,13 +95,13 @@ export class BenchmarkService { } const view = toView(row); - this.cache.set(normalized, view); + await this.cache.set(normalized, view); return view; } /** Invoice metrics side-by-side with its sector benchmark (#676). */ async getInvoiceBenchmark(invoiceId: string): Promise { - const cached = this.comparisonCache.get(invoiceId); + const cached = await this.comparisonCache.get(invoiceId); if (cached) return cached; const invoice = await this.dataSource.getRepository(Invoice).findOneBy({ id: invoiceId }); @@ -126,7 +130,7 @@ export class BenchmarkService { benchmark, }; - this.comparisonCache.set(invoiceId, comparison); + await this.comparisonCache.set(invoiceId, comparison); return comparison; } diff --git a/src/services/contract-registry.service.ts b/src/services/contract-registry.service.ts index 7df03fe..6b87feb 100644 --- a/src/services/contract-registry.service.ts +++ b/src/services/contract-registry.service.ts @@ -20,6 +20,21 @@ export interface ContractDetail extends ContractEntry { versionHistory: ContractVersionHistory[]; } +interface ContractRegistryRow { + name: string; + version: string; + address: string; + last_upgraded: number; + registered_at: number; +} + +interface ContractVersionHistoryRow { + version: string; + address: string; + upgraded_at: number; + upgraded_by: string; +} + /** * #682 — Platform upgrade registry REST API for contract version tracking. * Indexes ContractRegistered and ContractUpgraded events. @@ -33,50 +48,50 @@ export class ContractRegistryService { async getAllContracts(): Promise { const cacheKey = "all"; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT name, version, address, last_upgraded, registered_at FROM contract_registry ORDER BY name`, - ); + )) as ContractRegistryRow[]; - const contracts = rows.map((r: any) => this.mapEntry(r)); - this.cache.set(cacheKey, contracts); + const contracts = rows.map((row) => this.mapEntry(row)); + await this.cache.set(cacheKey, contracts); return contracts; } async getContractDetail(name: string): Promise { const cacheKey = `detail:${name}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT name, version, address, last_upgraded, registered_at FROM contract_registry WHERE name = $1`, [name], - ); + )) as ContractRegistryRow[]; if (!rows.length) return null; - const historyRows = await this.dataSource.query( + const historyRows = (await this.dataSource.query( `SELECT version, address, upgraded_at, upgraded_by FROM contract_version_history WHERE contract_name = $1 ORDER BY upgraded_at DESC`, [name], - ); + )) as ContractVersionHistoryRow[]; const detail: ContractDetail = { ...this.mapEntry(rows[0]), - versionHistory: historyRows.map((r: any) => ({ - version: r.version, - address: r.address, - upgradedAt: r.upgraded_at, - upgradedBy: r.upgraded_by, + versionHistory: historyRows.map((row) => ({ + version: row.version, + address: row.address, + upgradedAt: row.upgraded_at, + upgradedBy: row.upgraded_by, })), }; - this.cache.set(cacheKey, detail); + await this.cache.set(cacheKey, detail); return detail; } @@ -88,13 +103,13 @@ export class ContractRegistryService { return { compatible, currentVersion: detail.version }; } - private mapEntry(r: any): ContractEntry { + private mapEntry(row: ContractRegistryRow): ContractEntry { return { - name: r.name, - version: r.version, - address: r.address, - lastUpgraded: r.last_upgraded, - registeredAt: r.registered_at, + name: row.name, + version: row.version, + address: row.address, + lastUpgraded: row.last_upgraded, + registeredAt: row.registered_at, }; } @@ -111,6 +126,6 @@ export class ContractRegistryService { /** Invalidate cache when a registry event occurs. */ invalidate(): void { - this.cache.clearNamespace("contract-registry"); + this.cache.clear(); } } diff --git a/src/services/extension-proposal.service.ts b/src/services/extension-proposal.service.ts index b32272b..9ad95cb 100644 --- a/src/services/extension-proposal.service.ts +++ b/src/services/extension-proposal.service.ts @@ -21,6 +21,26 @@ export interface ExtensionVote { votedAt: number; } +interface ExtensionProposalRow { + id: string; + invoice_id: string; + proposed_by: string; + new_deadline: number; + status: string; + votes_for: number; + votes_against: number; + quorum_bps: number; + eta: number; + created_at: number; +} + +interface ExtensionVoteRow { + wallet: string; + vote: "for" | "against"; + weight: string; + voted_at: number; +} + /** * #681 — Invoice extension proposal indexer and vote tracking REST API. * Indexes ExtensionProposed, ExtensionVoteCast, and ExtensionExecuted events. @@ -34,35 +54,35 @@ export class ExtensionProposalService { async getActiveProposal(invoiceId: string): Promise { const cacheKey = `active:${invoiceId}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT id, invoice_id, proposed_by, new_deadline, status, votes_for, votes_against, quorum_bps, eta, created_at FROM extension_proposals WHERE invoice_id = $1 AND status = 'active' ORDER BY created_at DESC LIMIT 1`, [invoiceId], - ); + )) as ExtensionProposalRow[]; if (!rows.length) return null; const proposal = this.mapProposal(rows[0]); - this.cache.set(cacheKey, proposal); + await this.cache.set(cacheKey, proposal); return proposal; } async getWalletVote(invoiceId: string, wallet: string): Promise { const cacheKey = `vote:${invoiceId}:${wallet}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT wallet, vote, weight, voted_at FROM extension_votes WHERE invoice_id = $1 AND wallet = $2`, [invoiceId, wallet], - ); + )) as ExtensionVoteRow[]; if (!rows.length) return null; @@ -73,48 +93,50 @@ export class ExtensionProposalService { votedAt: rows[0].voted_at, }; - this.cache.set(cacheKey, vote); + await this.cache.set(cacheKey, vote); return vote; } async getProposalHistory(invoiceId: string): Promise { const cacheKey = `history:${invoiceId}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT id, invoice_id, proposed_by, new_deadline, status, votes_for, votes_against, quorum_bps, eta, created_at FROM extension_proposals WHERE invoice_id = $1 AND status IN ('completed', 'rejected') ORDER BY created_at DESC`, [invoiceId], - ); + )) as ExtensionProposalRow[]; - const proposals = rows.map((r: any) => this.mapProposal(r)); - this.cache.set(cacheKey, proposals); + const proposals = rows.map((row) => this.mapProposal(row)); + await this.cache.set(cacheKey, proposals); return proposals; } - private mapProposal(r: any): ExtensionProposal { + private mapProposal(row: ExtensionProposalRow): ExtensionProposal { return { - id: r.id, - invoiceId: r.invoice_id, - proposedBy: r.proposed_by, - newDeadline: r.new_deadline, - status: r.status, - votesFor: r.votes_for, - votesAgainst: r.votes_against, - quorumBps: r.quorum_bps, - eta: r.eta, - createdAt: r.created_at, + id: row.id, + invoiceId: row.invoice_id, + proposedBy: row.proposed_by, + newDeadline: row.new_deadline, + status: row.status, + votesFor: row.votes_for, + votesAgainst: row.votes_against, + quorumBps: row.quorum_bps, + eta: row.eta, + createdAt: row.created_at, }; } /** Invalidate cache for an invoice when an extension event occurs. */ - invalidate(invoiceId: string): void { - this.cache.delete(`active:${invoiceId}`); - this.cache.delete(`history:${invoiceId}`); - this.cache.clearNamespace("extension-proposal"); + async invalidate(invoiceId: string): Promise { + await Promise.all([ + this.cache.delete(`active:${invoiceId}`), + this.cache.delete(`history:${invoiceId}`), + ]); + this.cache.invalidatePrefix(`vote:${invoiceId}:`); } } diff --git a/src/services/invoice-insurance.service.ts b/src/services/invoice-insurance.service.ts index 22989bc..35a66a5 100644 --- a/src/services/invoice-insurance.service.ts +++ b/src/services/invoice-insurance.service.ts @@ -26,6 +26,27 @@ export interface InvoiceInsuranceResponse { expiry: number; } +interface InvoiceInsuranceRow { + is_active: boolean; + coverage_bps: number; + premium_paid: string; + expiry: number; +} + +interface InsurancePolicyRow extends InvoiceInsuranceRow { + invoice_id: string; + wallet: string; + purchased_at: number; +} + +interface InsurancePayoutRow { + invoice_id: string; + wallet: string; + amount: string; + trigger_date: number; + tx_hash: string; +} + /** * #680 — Invoice insurance indexer and policy status REST API. * Indexes InsurancePurchased and InsurancePayoutTriggered events. @@ -39,14 +60,14 @@ export class InvoiceInsuranceService { async getInvoiceInsurance(invoiceId: string, wallet: string): Promise { const cacheKey = `invoice:${invoiceId}:${wallet}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const row = await this.dataSource.query( + const row = (await this.dataSource.query( `SELECT is_active, coverage_bps, premium_paid, expiry FROM invoice_insurance WHERE invoice_id = $1 AND wallet = $2`, [invoiceId, wallet], - ); + )) as InvoiceInsuranceRow[]; if (!row.length) return null; @@ -57,62 +78,64 @@ export class InvoiceInsuranceService { expiry: row[0].expiry, }; - this.cache.set(cacheKey, result); + await this.cache.set(cacheKey, result); return result; } async getWalletActivePolicies(wallet: string): Promise { const cacheKey = `wallet:${wallet}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT invoice_id, wallet, is_active, coverage_bps, premium_paid, expiry, purchased_at FROM invoice_insurance WHERE wallet = $1 AND is_active = true`, [wallet], - ); - - const policies = rows.map((r: any) => ({ - invoiceId: r.invoice_id, - wallet: r.wallet, - isActive: r.is_active, - coverageBps: r.coverage_bps, - premiumPaid: r.premium_paid, - expiry: r.expiry, - purchasedAt: r.purchased_at, + )) as InsurancePolicyRow[]; + + const policies = rows.map((row) => ({ + invoiceId: row.invoice_id, + wallet: row.wallet, + isActive: row.is_active, + coverageBps: row.coverage_bps, + premiumPaid: row.premium_paid, + expiry: row.expiry, + purchasedAt: row.purchased_at, })); - this.cache.set(cacheKey, policies); + await this.cache.set(cacheKey, policies); return policies; } async getWalletPayoutHistory(wallet: string): Promise { const cacheKey = `payouts:${wallet}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT invoice_id, wallet, amount, trigger_date, tx_hash FROM insurance_payouts WHERE wallet = $1 ORDER BY trigger_date DESC`, [wallet], - ); - - const payouts = rows.map((r: any) => ({ - invoiceId: r.invoice_id, - wallet: r.wallet, - amount: r.amount, - triggerDate: r.trigger_date, - txHash: r.tx_hash, + )) as InsurancePayoutRow[]; + + const payouts = rows.map((row) => ({ + invoiceId: row.invoice_id, + wallet: row.wallet, + amount: row.amount, + triggerDate: row.trigger_date, + txHash: row.tx_hash, })); - this.cache.set(cacheKey, payouts); + await this.cache.set(cacheKey, payouts); return payouts; } /** Invalidate cache for a wallet/invoice pair when an insurance event occurs. */ - invalidate(invoiceId: string, wallet: string): void { - this.cache.delete(`invoice:${invoiceId}:${wallet}`); - this.cache.delete(`wallet:${wallet}`); - this.cache.delete(`payouts:${wallet}`); + async invalidate(invoiceId: string, wallet: string): Promise { + await Promise.all([ + this.cache.delete(`invoice:${invoiceId}:${wallet}`), + this.cache.delete(`wallet:${wallet}`), + this.cache.delete(`payouts:${wallet}`), + ]); } } diff --git a/src/services/issuer-blacklist.service.ts b/src/services/issuer-blacklist.service.ts index 440ad8c..cc0a06a 100644 --- a/src/services/issuer-blacklist.service.ts +++ b/src/services/issuer-blacklist.service.ts @@ -20,6 +20,22 @@ export interface BlacklistPage { pageSize: number; } +interface BlacklistStatusRow { + is_blacklisted: boolean; + reason: string | null; + blacklisted_at: number | null; +} + +interface BlacklistedIssuerRow { + wallet: string; + reason: string; + blacklisted_at: number; +} + +interface BlacklistCountRow { + total: number | string; +} + /** * #685 — Issuer blacklist indexer and status REST API. * Indexes IssuerBlacklisted and IssuerRestored events. @@ -33,14 +49,14 @@ export class IssuerBlacklistService { async getBlacklistStatus(wallet: string): Promise { const cacheKey = `status:${wallet}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; - const rows = await this.dataSource.query( + const rows = (await this.dataSource.query( `SELECT is_blacklisted, reason, blacklisted_at FROM issuer_blacklist WHERE wallet = $1`, [wallet], - ); + )) as BlacklistStatusRow[]; let status: BlacklistStatus; if (rows.length && rows[0].is_blacklisted) { @@ -53,13 +69,13 @@ export class IssuerBlacklistService { status = { isBlacklisted: false, reason: null, blacklistedAt: null }; } - this.cache.set(cacheKey, status); + await this.cache.set(cacheKey, status); return status; } async getBlacklistedIssuers(page: number = 1, pageSize: number = 20): Promise { const cacheKey = `list:${page}:${pageSize}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; const offset = (page - 1) * pageSize; @@ -73,27 +89,26 @@ export class IssuerBlacklistService { this.dataSource.query( `SELECT COUNT(*) as total FROM issuer_blacklist WHERE is_blacklisted = true`, ), - ]); + ]) as [BlacklistedIssuerRow[], BlacklistCountRow[]]; const result: BlacklistPage = { - issuers: rows.map((r: any) => ({ - wallet: r.wallet, - reason: r.reason, - blacklistedAt: r.blacklisted_at, + issuers: rows.map((row) => ({ + wallet: row.wallet, + reason: row.reason, + blacklistedAt: row.blacklisted_at, })), - total: parseInt(countResult[0].total, 10), + total: parseInt(String(countResult[0].total), 10), page, pageSize, }; - this.cache.set(cacheKey, result); + await this.cache.set(cacheKey, result); return result; } /** Invalidate cache for a wallet when a blacklist event occurs. */ - invalidate(wallet: string): void { - this.cache.delete(`status:${wallet}`); - // Clear list cache entries - this.cache.clearNamespace("issuer-blacklist"); + async invalidate(wallet: string): Promise { + await this.cache.delete(`status:${wallet}`); + this.cache.invalidatePrefix("list:"); } } diff --git a/src/services/referral-tier.service.ts b/src/services/referral-tier.service.ts index a564a2b..25d51df 100644 --- a/src/services/referral-tier.service.ts +++ b/src/services/referral-tier.service.ts @@ -6,6 +6,7 @@ import { ServiceError } from "../utils/service-error"; import { TtlCache } from "../lib/ttl-cache"; const CACHE_TTL_MS = 60_000; +const CACHE_TTL_SECONDS = CACHE_TTL_MS / 1000; const HISTORY_PAGE_SIZE = 20; /** Volume thresholds (in referred volume units) that gate each tier. */ @@ -66,7 +67,10 @@ export interface ReferralTierHistoryPage { * history get Standard defaults at read time. */ export class ReferralTierService { - private readonly cache = new TtlCache(CACHE_TTL_MS); + private readonly cache = new TtlCache({ + ttlSeconds: CACHE_TTL_SECONDS, + namespace: "referral-tier", + }); constructor(private readonly dataSource: DataSource) {} @@ -103,7 +107,7 @@ export class ReferralTierService { async getTier(wallet: string): Promise { const normalized = wallet.trim(); - const cached = this.cache.get(normalized); + const cached = await this.cache.get(normalized); if (cached) return cached; const row = await this.dataSource.getRepository(ReferralTier).findOneBy({ @@ -111,7 +115,7 @@ export class ReferralTierService { }); const view = row ? toView(row) : defaultView(normalized); - this.cache.set(normalized, view); + await this.cache.set(normalized, view); return view; } diff --git a/src/services/secondary-auction.service.ts b/src/services/secondary-auction.service.ts index fccac39..33110b8 100644 --- a/src/services/secondary-auction.service.ts +++ b/src/services/secondary-auction.service.ts @@ -70,22 +70,30 @@ export interface BidPage { */ export class SecondaryAuctionService { /** Active auctions: short TTL so countdown / highest bid stay fresh. */ - private readonly activeCache = new TtlCache(ACTIVE_CACHE_TTL_MS); + private readonly activeCache = new TtlCache({ + ttlSeconds: ACTIVE_CACHE_TTL_MS / 1000, + namespace: "secondary-auction-active", + }); /** Settled auctions are immutable, so they can be cached much longer. */ - private readonly settledCache = new TtlCache(SETTLED_CACHE_TTL_MS); + private readonly settledCache = new TtlCache({ + ttlSeconds: SETTLED_CACHE_TTL_MS / 1000, + namespace: "secondary-auction-settled", + }); constructor( private readonly dataSource: DataSource, private readonly now: () => Date = () => new Date(), ) {} - private cacheFor(view: Pick): TtlCache { + private cacheFor(view: Pick): TtlCache { return view.status === AuctionStatus.ACTIVE ? this.activeCache : this.settledCache; } - private invalidate(auctionId: string): void { - this.activeCache.delete(auctionId); - this.settledCache.delete(auctionId); + private async invalidate(auctionId: string): Promise { + await Promise.all([ + this.activeCache.delete(auctionId), + this.settledCache.delete(auctionId), + ]); } /** Indexer: create/refresh an auction from AuctionCreated. */ @@ -105,7 +113,7 @@ export class SecondaryAuctionService { if (event.eventId) row.createdEventId = event.eventId; const saved = await repo.save(row); - this.invalidate(event.auctionId); + await this.invalidate(event.auctionId); return saved; } @@ -136,7 +144,7 @@ export class SecondaryAuctionService { } await auctionRepo.save(auction); - this.invalidate(event.auctionId); + await this.invalidate(event.auctionId); return savedBid; } @@ -154,14 +162,14 @@ export class SecondaryAuctionService { row.settlementAmount = event.settlementAmount ?? row.highestBid; const saved = await repo.save(row); - this.invalidate(event.auctionId); + await this.invalidate(event.auctionId); return saved; } async getAuction(auctionId: string): Promise { - const activeHit = this.activeCache.get(auctionId); + const activeHit = await this.activeCache.get(auctionId); if (activeHit) return activeHit; - const settledHit = this.settledCache.get(auctionId); + const settledHit = await this.settledCache.get(auctionId); if (settledHit) return settledHit; const row = await this.dataSource.getRepository(SecondaryAuction).findOneBy({ auctionId }); @@ -170,7 +178,7 @@ export class SecondaryAuctionService { } const view = toView(row, this.now()); - this.cacheFor(view).set(auctionId, view); + await this.cacheFor(view).set(auctionId, view); return view; } diff --git a/src/services/yield-boost.service.ts b/src/services/yield-boost.service.ts index cd071ab..534a6a9 100644 --- a/src/services/yield-boost.service.ts +++ b/src/services/yield-boost.service.ts @@ -6,6 +6,7 @@ import { ServiceError } from "../utils/service-error"; import { TtlCache } from "../lib/ttl-cache"; const CACHE_TTL_MS = 60_000; +const CACHE_TTL_SECONDS = CACHE_TTL_MS / 1000; /** Boost in basis points per eligible loyalty tier (#678). Bronze is ineligible. */ const TIER_BOOST_BPS: Partial> = { @@ -39,7 +40,10 @@ export interface YieldBoostView { * loyalty tier at request time (Bronze and unregistered are ineligible). */ export class YieldBoostService { - private readonly cache = new TtlCache(CACHE_TTL_MS); + private readonly cache = new TtlCache({ + ttlSeconds: CACHE_TTL_SECONDS, + namespace: "yield-boost", + }); constructor(private readonly dataSource: DataSource) {} @@ -81,7 +85,7 @@ export class YieldBoostService { async getBoost(invoiceId: string, wallet: string): Promise { const normalized = wallet.trim(); const cacheKey = `${invoiceId}:${normalized}`; - const cached = this.cache.get(cacheKey); + const cached = await this.cache.get(cacheKey); if (cached) return cached; const invoice = await this.dataSource.getRepository(Invoice).findOneBy({ id: invoiceId }); @@ -112,7 +116,7 @@ export class YieldBoostService { boostedYield, }; - this.cache.set(cacheKey, view); + await this.cache.set(cacheKey, view); return view; } diff --git a/tests/benchmark.service.test.ts b/tests/benchmark.service.test.ts index 292b250..6e6ebaa 100644 --- a/tests/benchmark.service.test.ts +++ b/tests/benchmark.service.test.ts @@ -40,12 +40,12 @@ function createFakeDataSource(benchmarks: SectorBenchmark[] = [], invoices: Invo save: async (row: SectorBenchmark) => { const idx = saved.findIndex((b) => b.sector === row.sector); const full = { - id: row.id ?? `bench-${saved.length + 1}`, - sampleCount: 0, - lastEventId: null, - createdAt: new Date(), - updatedAt: new Date(), ...row, + id: row.id ?? `bench-${saved.length + 1}`, + sampleCount: row.sampleCount ?? 0, + lastEventId: row.lastEventId ?? null, + createdAt: row.createdAt ?? new Date(), + updatedAt: row.updatedAt ?? new Date(), } as SectorBenchmark; if (idx >= 0) saved[idx] = full; else saved.push(full); diff --git a/tests/referral-tier.service.test.ts b/tests/referral-tier.service.test.ts index 1edcad2..6cc9369 100644 --- a/tests/referral-tier.service.test.ts +++ b/tests/referral-tier.service.test.ts @@ -21,10 +21,10 @@ function createFakeDataSource( save: async (row: ReferralTier) => { const idx = tierRows.findIndex((t) => t.wallet === row.wallet); const full = { - id: row.id ?? `tier-${tierRows.length + 1}`, - lastEventId: null, - createdAt: new Date(), ...row, + id: row.id ?? `tier-${tierRows.length + 1}`, + lastEventId: row.lastEventId ?? null, + createdAt: row.createdAt ?? new Date(), } as ReferralTier; if (idx >= 0) tierRows[idx] = full; else tierRows.push(full); @@ -37,9 +37,9 @@ function createFakeDataSource( create: (data: Partial) => ({ ...data }) as ReferralTierHistory, save: async (row: ReferralTierHistory) => { const full = { - id: `hist-${historyRows.length + 1}`, - createdAt: new Date(), ...row, + id: row.id ?? `hist-${historyRows.length + 1}`, + createdAt: row.createdAt ?? new Date(), } as ReferralTierHistory; historyRows.push(full); return full; diff --git a/tests/secondary-auction.service.test.ts b/tests/secondary-auction.service.test.ts index 7f6047f..ab1475e 100644 --- a/tests/secondary-auction.service.test.ts +++ b/tests/secondary-auction.service.test.ts @@ -23,13 +23,13 @@ function createFakeDataSource( save: async (row: SecondaryAuction) => { const idx = auctionRows.findIndex((a) => a.auctionId === row.auctionId); const full = { - id: row.id ?? `auc-${auctionRows.length + 1}`, - bidCount: 0, - highestBid: "0.0000", - highestBidder: null, - createdAt: new Date(), - updatedAt: new Date(), ...row, + id: row.id ?? `auc-${auctionRows.length + 1}`, + bidCount: row.bidCount ?? 0, + highestBid: row.highestBid ?? "0.0000", + highestBidder: row.highestBidder ?? null, + createdAt: row.createdAt ?? new Date(), + updatedAt: row.updatedAt ?? new Date(), } as SecondaryAuction; if (idx >= 0) auctionRows[idx] = full; else auctionRows.push(full); @@ -42,9 +42,9 @@ function createFakeDataSource( create: (data: Partial) => ({ ...data }) as SecondaryBid, save: async (row: SecondaryBid) => { const full = { - id: `bid-${bidRows.length + 1}`, - createdAt: new Date(), ...row, + id: row.id ?? `bid-${bidRows.length + 1}`, + createdAt: row.createdAt ?? new Date(), } as SecondaryBid; bidRows.push(full); return full; diff --git a/tests/yield-boost.service.test.ts b/tests/yield-boost.service.test.ts index eb4d405..a6757f6 100644 --- a/tests/yield-boost.service.test.ts +++ b/tests/yield-boost.service.test.ts @@ -41,10 +41,10 @@ function createFakeDataSource(boosts: YieldBoost[] = [], invoices: Invoice[] = [ (b) => b.invoiceId === row.invoiceId && b.wallet === row.wallet, ); const full = { - id: row.id ?? `boost-${rows.length + 1}`, - lastEventId: null, - createdAt: new Date(), ...row, + id: row.id ?? `boost-${rows.length + 1}`, + lastEventId: row.lastEventId ?? null, + createdAt: row.createdAt ?? new Date(), } as YieldBoost; if (idx >= 0) rows[idx] = full; else rows.push(full); From 3f7fcf9c9ae25379d937201ce2d3dc4733df56e5 Mon Sep 17 00:00:00 2001 From: uche102 Date: Tue, 20 Oct 2026 13:15:48 +0100 Subject: [PATCH 3/7] fix(ci): finish CI compatibility fixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/routes/admin/admin.routes.ts | 3 +-- src/routes/admin/waitlist.routes.ts | 13 ++----------- src/routes/community-programs.routes.ts | 2 +- src/services/secondary-market.service.ts | 2 +- tests/secondary-market-notifications.test.ts | 9 ++++++++- tests/unit/ip-whitelist.middleware.test.ts | 5 +++-- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/routes/admin/admin.routes.ts b/src/routes/admin/admin.routes.ts index 7d88092..503be54 100644 --- a/src/routes/admin/admin.routes.ts +++ b/src/routes/admin/admin.routes.ts @@ -35,7 +35,6 @@ import type { AuthenticatedRequest } from "@/types/auth"; import { listInvoices } from "./list-invoices"; import { reviewInvoice } from "./review-invoice"; import type { WaitlistService } from "@/services/waitlist.service"; -import { createWaitlistService } from "@/services/waitlist.service"; export interface AdminRouterDependencies { dataSource: DataSource; @@ -299,7 +298,7 @@ export function createAdminRouter({ if (_waitlistService) { router.use( "/waitlist", - createWaitlistRouter(dataSource, _waitlistService, authService, _notificationService) + createWaitlistRouter(_waitlistService) ); } diff --git a/src/routes/admin/waitlist.routes.ts b/src/routes/admin/waitlist.routes.ts index dfd34b3..c65d9ae 100644 --- a/src/routes/admin/waitlist.routes.ts +++ b/src/routes/admin/waitlist.routes.ts @@ -1,19 +1,10 @@ import { Router, Request, Response, NextFunction } from "express"; -import { DataSource } from "typeorm"; import { logger } from "../../observability/logger"; import type { WaitlistService } from "../../services/waitlist.service"; import { AppError } from "../../utils/http-error"; -import type { AuthService } from "../../services/auth.service"; - -interface AuthenticatedRequest extends Request { - user?: { wallet: string }; -} export function createWaitlistRouter( - dataSource: DataSource, waitlistService: WaitlistService, - authService: AuthService, - notificationService?: unknown ) { const router = Router(); @@ -39,7 +30,7 @@ export function createWaitlistRouter( router.post( "/:wallet/approve", - async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + async (req: Request<{ wallet: string }>, res: Response, next: NextFunction) => { try { const { wallet } = req.params; @@ -67,7 +58,7 @@ export function createWaitlistRouter( router.post( "/bulk-approve", - async (req: AuthenticatedRequest, res: Response, next: NextFunction) => { + async (req: Request, res: Response, next: NextFunction) => { try { const { count } = req.body; diff --git a/src/routes/community-programs.routes.ts b/src/routes/community-programs.routes.ts index 93e72fd..8b8bb83 100644 --- a/src/routes/community-programs.routes.ts +++ b/src/routes/community-programs.routes.ts @@ -49,7 +49,7 @@ export function createCommunityProgramsRouter(dataSource: DataSource, authServic }); router.post("/invoices/:id/reports", authenticate, async (req: AuthenticatedRequest, res: Response, next) => { - try { res.status(201).json({ success: true, data: await service.createFraudReport({ invoiceId: req.params.id, wallet: String(req.user?.stellarAddress ?? ""), reason: String(req.body?.reason ?? ""), detail: String(req.body?.detail ?? "") }) }); } + try { res.status(201).json({ success: true, data: await service.createFraudReport({ invoiceId: String(req.params.id), wallet: String(req.user?.stellarAddress ?? ""), reason: String(req.body?.reason ?? ""), detail: String(req.body?.detail ?? "") }) }); } catch (error) { forward(next, error); } }); diff --git a/src/services/secondary-market.service.ts b/src/services/secondary-market.service.ts index ef2b9e3..5f886dd 100644 --- a/src/services/secondary-market.service.ts +++ b/src/services/secondary-market.service.ts @@ -312,7 +312,7 @@ export class SecondaryMarketService { let soldTotalPrice: string | undefined; const result = await this.dataSource.transaction(async (manager: EntityManager) => { - const { listingId, buyerWallet, quantity } = input; + const { listingId, buyerWallet, buyerId, quantity } = input; // Get listing const listing = await manager.findOne(SecondaryListing, { diff --git a/tests/secondary-market-notifications.test.ts b/tests/secondary-market-notifications.test.ts index 095677d..28d689b 100644 --- a/tests/secondary-market-notifications.test.ts +++ b/tests/secondary-market-notifications.test.ts @@ -43,7 +43,14 @@ function buildListing(overrides: Partial> = {}) { function buildDataSource(listing: ReturnType, failCommit = false) { const manager = { - findOne: jest.fn().mockResolvedValue(listing), + findOne: jest + .fn() + .mockResolvedValueOnce(listing) + .mockResolvedValueOnce({ status: InvoiceStatus.FUNDED }), + find: jest.fn().mockResolvedValue([ + { investmentAmount: "100.0000", expectedReturn: "110.0000" }, + ]), + create: jest.fn((_entity: unknown, values: object) => values), save: jest.fn().mockImplementation(async (entity: unknown) => entity), }; return { diff --git a/tests/unit/ip-whitelist.middleware.test.ts b/tests/unit/ip-whitelist.middleware.test.ts index 7578531..2c00794 100644 --- a/tests/unit/ip-whitelist.middleware.test.ts +++ b/tests/unit/ip-whitelist.middleware.test.ts @@ -73,7 +73,7 @@ describe("ipWhitelistMiddleware", () => { expect(res.status).toHaveBeenCalledWith(403); }); - it("extracts client IP from X-Forwarded-For header", () => { + it("does not trust X-Forwarded-For when Express has not resolved a client IP", () => { const middleware = ipWhitelistMiddleware(allowedCidrs); const req = makeReq({ headers: { "x-forwarded-for": "127.0.0.1, 70.41.3.18, 150.172.238.178" }, @@ -83,7 +83,8 @@ describe("ipWhitelistMiddleware", () => { middleware(req as never, res as never, next as never); - expect(next).toHaveBeenCalledTimes(1); + expect(next).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(403); }); it("returns 403 when the first IP in X-Forwarded-For is not whitelisted", () => { From dc21bbb45b45418cd0a7b00504f761119027b925 Mon Sep 17 00:00:00 2001 From: uche102 Date: Tue, 20 Oct 2026 13:23:47 +0100 Subject: [PATCH 4/7] fix(ci): resolve current dev type errors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/routes/indexer-views.routes.ts | 10 +++++----- src/services/stellar/event-indexer.service.ts | 2 ++ src/types/soroban.types.ts | 1 + .../services/stellar/event-indexer.service.test.ts | 1 + 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/routes/indexer-views.routes.ts b/src/routes/indexer-views.routes.ts index c6fba2d..02b783c 100644 --- a/src/routes/indexer-views.routes.ts +++ b/src/routes/indexer-views.routes.ts @@ -26,7 +26,7 @@ export function createIndexerViewsRouter(dataSource: DataSource): Router { "/invoices/:invoice_id/rating", async (req: Request, res: Response, next: NextFunction) => { try { - const summary = await ratingService.getInvoiceRating(req.params.invoice_id); + const summary = await ratingService.getInvoiceRating(String(req.params.invoice_id)); res.json(summary); } catch (err) { next(err); @@ -38,7 +38,7 @@ export function createIndexerViewsRouter(dataSource: DataSource): Router { "/issuers/:wallet/rating", async (req: Request, res: Response, next: NextFunction) => { try { - const summary = await ratingService.getIssuerRating(req.params.wallet); + const summary = await ratingService.getIssuerRating(String(req.params.wallet)); res.json(summary); } catch (err) { next(err); @@ -52,7 +52,7 @@ export function createIndexerViewsRouter(dataSource: DataSource): Router { "/pools/:pool_id/rebalances/pending", async (req: Request, res: Response, next: NextFunction) => { try { - const pending = await rebalanceService.getPending(req.params.pool_id); + const pending = await rebalanceService.getPending(String(req.params.pool_id)); if (!pending) { res.status(404).json({ error: "No pending rebalance for this pool" }); return; @@ -70,7 +70,7 @@ export function createIndexerViewsRouter(dataSource: DataSource): Router { try { const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10) || 1); const limit = Math.min(100, Math.max(1, parseInt(String(req.query.limit ?? "20"), 10) || 20)); - const history = await rebalanceService.getHistory(req.params.pool_id, page, limit); + const history = await rebalanceService.getHistory(String(req.params.pool_id), page, limit); res.json(history); } catch (err) { next(err); @@ -86,7 +86,7 @@ export function createIndexerViewsRouter(dataSource: DataSource): Router { try { const page = Math.max(1, parseInt(String(req.query.page ?? "1"), 10) || 1); const limit = Math.min(100, Math.max(1, parseInt(String(req.query.limit ?? "20"), 10) || 20)); - const result = await bulkService.getCancelledOrders(req.params.wallet, page, limit); + const result = await bulkService.getCancelledOrders(String(req.params.wallet), page, limit); res.json(result); } catch (err) { next(err); diff --git a/src/services/stellar/event-indexer.service.ts b/src/services/stellar/event-indexer.service.ts index fac8f7c..5ffd2c0 100644 --- a/src/services/stellar/event-indexer.service.ts +++ b/src/services/stellar/event-indexer.service.ts @@ -151,12 +151,14 @@ export class EventIndexerService { : typeof rawRecord.pagingToken === "string" ? rawRecord.pagingToken : rawEvent.id; + const occurredAt = new Date(rawEvent.ledgerClosedAt); return { id: rawEvent.id, contractId: contractIdStr, ledger: Number(rawEvent.ledger), ledgerClosedAt: rawEvent.ledgerClosedAt, + occurredAt: Number.isNaN(occurredAt.getTime()) ? undefined : occurredAt, txHash, topic: primaryTopic, topics: decodedTopics, diff --git a/src/types/soroban.types.ts b/src/types/soroban.types.ts index d0cfbe3..1838407 100644 --- a/src/types/soroban.types.ts +++ b/src/types/soroban.types.ts @@ -55,6 +55,7 @@ export interface DecodedSorobanEvent { contractId: string; ledger: number; ledgerClosedAt: string; + occurredAt?: Date; txHash: string; topic: string; topics: unknown[]; diff --git a/tests/unit/services/stellar/event-indexer.service.test.ts b/tests/unit/services/stellar/event-indexer.service.test.ts index e52a2a1..2aefe79 100644 --- a/tests/unit/services/stellar/event-indexer.service.test.ts +++ b/tests/unit/services/stellar/event-indexer.service.test.ts @@ -99,6 +99,7 @@ describe("EventIndexerService (Issue #135)", () => { expect(decoded.id).toBe("evt-001"); expect(decoded.contractId).toBe(ESCROW_CONTRACT_ID); expect(decoded.ledger).toBe(1050); + expect(decoded.occurredAt).toEqual(new Date(rawEvent.ledgerClosedAt)); expect(decoded.topic).toBe("fund_escrow"); expect(decoded.topics).toEqual(["fund_escrow", "INV-2026-001"]); expect(BigInt(decoded.data as any)).toBe(100_000n); From de4849b1be5c9f414f83a706323293c14099d1b8 Mon Sep 17 00:00:00 2001 From: uche102 Date: Sat, 24 Oct 2026 10:28:42 +0100 Subject: [PATCH 5/7] fix: repair CI regressions across merged branches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/middleware/ip-whitelist.middleware.ts | 7 +------ src/routes/admin/admin.routes.ts | 2 +- src/services/secondary-market.service.ts | 2 +- src/types/soroban.types.ts | 1 - tests/secondary-market.service.test.ts | 4 +++- 5 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/middleware/ip-whitelist.middleware.ts b/src/middleware/ip-whitelist.middleware.ts index f936cb4..9b8bf0e 100644 --- a/src/middleware/ip-whitelist.middleware.ts +++ b/src/middleware/ip-whitelist.middleware.ts @@ -3,12 +3,7 @@ import ipRangeCheck from "ip-range-check"; export function ipWhitelistMiddleware(allowedCidrs: string[]) { return (req: Request, res: Response, next: NextFunction): void => { - const forwardedFor = req.headers["x-forwarded-for"]; - const forwardedIp = - typeof forwardedFor === "string" - ? forwardedFor.split(",")[0]?.trim() - : forwardedFor?.[0]?.split(",")[0]?.trim(); - const clientIp = req.ip || forwardedIp || req.socket.remoteAddress; + const clientIp = req.ip || req.socket.remoteAddress; if (!clientIp || !ipRangeCheck(clientIp, allowedCidrs)) { res.status(403).json({ diff --git a/src/routes/admin/admin.routes.ts b/src/routes/admin/admin.routes.ts index 503be54..a16abc7 100644 --- a/src/routes/admin/admin.routes.ts +++ b/src/routes/admin/admin.routes.ts @@ -298,7 +298,7 @@ export function createAdminRouter({ if (_waitlistService) { router.use( "/waitlist", - createWaitlistRouter(_waitlistService) + createWaitlistRouter(_waitlistService, authService, _notificationService) ); } diff --git a/src/services/secondary-market.service.ts b/src/services/secondary-market.service.ts index 5f886dd..e4c10cd 100644 --- a/src/services/secondary-market.service.ts +++ b/src/services/secondary-market.service.ts @@ -1,4 +1,4 @@ -import { DataSource, EntityManager, MoreThan } from "typeorm"; +import { DataSource, EntityManager, In, MoreThan } from "typeorm"; import { Decimal } from "decimal.js"; import { SecondaryListing } from "../models/SecondaryListing.model"; import { Invoice } from "../models/Invoice.model"; diff --git a/src/types/soroban.types.ts b/src/types/soroban.types.ts index 59a2069..1838407 100644 --- a/src/types/soroban.types.ts +++ b/src/types/soroban.types.ts @@ -61,7 +61,6 @@ export interface DecodedSorobanEvent { topics: unknown[]; data: unknown; inSuccessfulContractCall: boolean; - occurredAt?: Date; } export interface SimulateTransactionResult { diff --git a/tests/secondary-market.service.test.ts b/tests/secondary-market.service.test.ts index fa88e37..f5ca2ea 100644 --- a/tests/secondary-market.service.test.ts +++ b/tests/secondary-market.service.test.ts @@ -24,7 +24,9 @@ describe("SecondaryMarketService", () => { beforeEach(() => { manager = { - findOne: jest.fn().mockResolvedValue(invoice), + findOne: jest.fn((entity: typeof Invoice) => + Promise.resolve(entity === Invoice ? invoice : null) + ), find: jest.fn((entity: typeof Investment | typeof SecondaryListing) => Promise.resolve(entity === Investment ? investments : [existingListing]) ), From 074976445ad6d0d36b4631101ef016197b15f517 Mon Sep 17 00:00:00 2001 From: uche102 Date: Sat, 24 Oct 2026 10:38:34 +0100 Subject: [PATCH 6/7] fix: drop unused typeorm import Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/services/secondary-market.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/secondary-market.service.ts b/src/services/secondary-market.service.ts index e4c10cd..5f886dd 100644 --- a/src/services/secondary-market.service.ts +++ b/src/services/secondary-market.service.ts @@ -1,4 +1,4 @@ -import { DataSource, EntityManager, In, MoreThan } from "typeorm"; +import { DataSource, EntityManager, MoreThan } from "typeorm"; import { Decimal } from "decimal.js"; import { SecondaryListing } from "../models/SecondaryListing.model"; import { Invoice } from "../models/Invoice.model"; From 101a0e8ee6c37a50e9e5ce902c1b188845d061ac Mon Sep 17 00:00:00 2001 From: uche102 Date: Sat, 24 Oct 2026 14:39:29 +0100 Subject: [PATCH 7/7] ci: serialize database integration tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 668c8c0..9de5698 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -104,7 +104,7 @@ jobs: exit 1 timeout-minutes: 2 - name: Run tests - run: npm test + run: npm test -- --runInBand timeout-minutes: 10 env: DATABASE_URL: postgresql://postgres:password@localhost:5432/stellarsettle_test