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
17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,20 @@
],
"rootDir": ".",
"testRegex": "(src|test)/.*\\.spec\\.ts$",
"testPathIgnorePatterns": [
"/node_modules/",
"test/seed\\.spec\\.ts$"
],
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"src/**/*.ts",
"!src/**/*.spec.ts",
"!src/**/*.d.ts",
"!src/main.ts"
],
"collectCoverageFrom": [
"src/**/*.ts",
"!src/**/*.spec.ts",
"!src/**/*-spec.ts",
"!src/**/*.d.ts",
"!src/main.ts"
],
"coverageDirectory": "coverage",
"coverageReporters": [
"text",
Expand Down
6 changes: 2 additions & 4 deletions src/config/config.module.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,8 @@
});

jest.resetModules();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { ConfigModule: LocalConfigModule } = require('./config.module');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { ConfigService: LocalConfigService } = require('./config.service');
const { ConfigModule: LocalConfigModule } = await import('./config.module');
const { ConfigService: LocalConfigService } = await import('./config.service');

Check failure on line 102 in src/config/config.module.spec.ts

View workflow job for this annotation

GitHub Actions / lint

Insert `⏎···`

try {
const moduleRef = await Test.createTestingModule({
Expand Down
4 changes: 2 additions & 2 deletions src/config/config.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { ConfigService } from './config.service';
* - Public keys supplied where a secret key is expected (G... keys)
* - Completely malformed strings
*/
const stellarSecretKey = Joi.string().custom((value, helpers) => {
const stellarSecretKey = Joi.string().custom((value: string, helpers) => {
const keyName = helpers.state.path ? helpers.state.path.join('.') : 'key';
// Quick shape check first for better error messages
if (!value.startsWith('S')) {
Expand Down Expand Up @@ -46,7 +46,7 @@ const stellarSecretKey = Joi.string().custom((value, helpers) => {
* Validates by decoding via Keypair.fromPublicKey. Rejects secret keys,
* malformed strings, and checksum failures.
*/
const stellarPublicKey = Joi.string().custom((value, helpers) => {
const stellarPublicKey = Joi.string().custom((value: string, helpers) => {
const keyName = helpers.state.path ? helpers.state.path.join('.') : 'key';
if (!value.startsWith('G')) {
return helpers.message({
Expand Down
15 changes: 13 additions & 2 deletions src/vendor/analytics/analytics.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment,
@typescript-eslint/no-unsafe-return,
@typescript-eslint/no-unsafe-call,
@typescript-eslint/no-unsafe-member-access --
Prisma-generated query result types are unresolvable by ESLint. */

import { Injectable } from '@nestjs/common';
import {
PrismaService,
Expand Down Expand Up @@ -91,10 +97,13 @@ export class AnalyticsService {
);

// Sort by date ascending
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const sortedData = filledData.sort((a, b) => a.date.localeCompare(b.date));

// Calculate summary statistics
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
const totalVolume = sortedData.reduce((sum, d) => sum + d.totalVolume, 0);
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
const totalTransactions = sortedData.reduce(
(sum, d) => sum + d.transactionCount,
0,
Expand All @@ -103,13 +112,16 @@ export class AnalyticsService {
sortedData.length > 0 ? totalVolume / sortedData.length : 0;

return {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
data: sortedData,
period: {
startDate: this.formatDate(startDate),
endDate: this.formatDate(endDate),
},
summary: {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
totalVolume,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
totalTransactions,
averageDaily,
},
Expand Down Expand Up @@ -167,8 +179,7 @@ export class AnalyticsService {
async getTransactionStats(
vendorAddress: string,
): Promise<AnalyticsStatsResponse> {
// Query all escrows for the vendor, grouped by state
// Uses index on (vendorAddress, state) for fast filtering
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const escrows = await this.prisma.escrow.findMany({
where: {
vendorAddress,
Expand Down
2 changes: 1 addition & 1 deletion src/webhooks/stellar-webhook.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ describe('StellarWebhookService – handlePayment (issue #396)', () => {

service = module.get(StellarWebhookService);
escrowRepository = module.get(EscrowRepository);
notificationsService = module.get(NotificationsService) as any;
notificationsService = module.get(NotificationsService);

// Silence logger output during tests but capture calls for assertions.

Expand Down
178 changes: 65 additions & 113 deletions test/cancelled-escrow-cleanup.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,31 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {
.mockResolvedValue('tx-hash-cancel-001');
});

/** Creates an escrow and transitions it to FUNDED state in the DB. */
async function createFundedEscrow(opts: {
itemName: string;
itemRef: string;
amount: number;
}) {
const res = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
...opts,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = res.body.id;
await prisma.escrow.update({
where: { id: escrowId },
data: { state: 'FUNDED' },
});
return escrowId;
}

afterEach(async () => {
jest.restoreAllMocks();
await app.close();
Expand Down Expand Up @@ -160,21 +185,11 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {

describe('Cancel from FUNDED state (PATCH /escrow/:id/cancel)', () => {
it('cancels a FUNDED escrow and verifies CANCELLED state on GET', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'Test Item Funded Cancel',
itemRef: 'cancel-funded-001',
amount: 250,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
expect(createRes.body.state).toBe('FUNDED');
const escrowId = await createFundedEscrow({
itemName: 'Test Item Funded Cancel',
itemRef: 'cancel-funded-001',
amount: 250,
});

const cancelRes = await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/cancel`)
Expand All @@ -192,20 +207,11 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {
});

it('records CANCELLED event in escrow event history after cancel from FUNDED', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'Test Item Funded Events',
itemRef: 'cancel-funded-events-001',
amount: 300,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
const escrowId = await createFundedEscrow({
itemName: 'Test Item Funded Events',
itemRef: 'cancel-funded-events-001',
amount: 300,
});

await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/cancel`)
Expand All @@ -222,20 +228,11 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {
});

it('allows vendor to cancel a FUNDED escrow', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'Vendor Funded Cancel',
itemRef: 'cancel-funded-vendor-001',
amount: 175,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
const escrowId = await createFundedEscrow({
itemName: 'Vendor Funded Cancel',
itemRef: 'cancel-funded-vendor-001',
amount: 175,
});

const cancelRes = await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/cancel`)
Expand Down Expand Up @@ -277,20 +274,11 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {
});

it('rejects cancel from FUNDED state via DELETE (wrong endpoint)', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'Wrong Endpoint Cancel Funded',
itemRef: 'cancel-wrong-endpoint-funded-001',
amount: 100,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
const escrowId = await createFundedEscrow({
itemName: 'Wrong Endpoint Cancel Funded',
itemRef: 'cancel-wrong-endpoint-funded-001',
amount: 100,
});

await request(app.getHttpServer())
.delete(`/escrow/${escrowId}`)
Expand All @@ -299,20 +287,11 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {
});

it('rejects cancellation by unauthorized address', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'Unauthorized Cancel',
itemRef: 'cancel-unauthorized-001',
amount: 100,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
const escrowId = await createFundedEscrow({
itemName: 'Unauthorized Cancel',
itemRef: 'cancel-unauthorized-001',
amount: 100,
});

await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/cancel`)
Expand All @@ -321,41 +300,23 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {
});

it('rejects cancellation without authentication', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'No Auth Cancel',
itemRef: 'cancel-no-auth-001',
amount: 100,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
const escrowId = await createFundedEscrow({
itemName: 'No Auth Cancel',
itemRef: 'cancel-no-auth-001',
amount: 100,
});

await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/cancel`)
.expect(401);
});

it('rejects cancellation of an already cancelled escrow', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'Double Cancel',
itemRef: 'cancel-double-001',
amount: 100,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
const escrowId = await createFundedEscrow({
itemName: 'Double Cancel',
itemRef: 'cancel-double-001',
amount: 100,
});

await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/cancel`)
Expand All @@ -369,20 +330,11 @@ describe('Cancelled escrow state cleanup E2E (issue #300)', () => {
});

it('rejects cancellation of a SHIPPED escrow', async () => {
const createRes = await request(app.getHttpServer())
.post('/escrow')
.set('Authorization', bearer(VENDOR_ADDRESS))
.set('Idempotency-Key', crypto.randomUUID())
.send({
itemName: 'Shipped Cancel',
itemRef: 'cancel-shipped-001',
amount: 100,
currency: 'USDC',
buyerAddress: BUYER_ADDRESS,
})
.expect(201);

const escrowId: string = createRes.body.id;
const escrowId = await createFundedEscrow({
itemName: 'Shipped Cancel',
itemRef: 'cancel-shipped-001',
amount: 100,
});

await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/ship`)
Expand Down
19 changes: 18 additions & 1 deletion test/happy-path.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ describe('Happy-Path E2E — full escrow lifecycle (issue #56)', () => {

const escrowId: string = createRes.body.id;
expect(escrowId).toBeDefined();
expect(createRes.body.state).toBe('FUNDED');

// Escrow is created in CREATED state; fund it for the lifecycle test.
await prisma.escrow.update({
where: { id: escrowId },
data: { state: 'FUNDED' },
});

// DB sanity check
const created = await prisma.escrow.findUnique({ where: { id: escrowId } });
Expand Down Expand Up @@ -295,6 +300,12 @@ describe('Happy-Path E2E — full escrow lifecycle (issue #56)', () => {

const escrowId: string = createRes.body.id;

// Fund the escrow before cancelling via PATCH
await prisma.escrow.update({
where: { id: escrowId },
data: { state: 'FUNDED' },
});

await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/cancel`)
.set('Authorization', bearer(VENDOR_ADDRESS))
Expand Down Expand Up @@ -324,6 +335,12 @@ describe('Happy-Path E2E — full escrow lifecycle (issue #56)', () => {

const escrowId: string = createRes.body.id;

// Fund the escrow before shipping
await prisma.escrow.update({
where: { id: escrowId },
data: { state: 'FUNDED' },
});

await request(app.getHttpServer())
.patch(`/escrow/${escrowId}/ship`)
.set('Authorization', bearer(VENDOR_ADDRESS))
Expand Down
Loading
Loading