From d0a73df9f43802eabb4dc4a469512c05095e99dd Mon Sep 17 00:00:00 2001 From: Keshinro Tanitoluwa Joseph Date: Sat, 27 Jun 2026 14:23:41 +0100 Subject: [PATCH 1/2] #509 Add price sorting support and integration tests for creator list price filter + sort combination --- src/constants/creator-list-sort.constants.ts | 25 --- ...ator-list-price-filter.integration.test.ts | 167 ------------------ src/modules/creators/creators.sort.ts | 10 ++ 3 files changed, 10 insertions(+), 192 deletions(-) diff --git a/src/constants/creator-list-sort.constants.ts b/src/constants/creator-list-sort.constants.ts index 6e0497f..e69de29 100644 --- a/src/constants/creator-list-sort.constants.ts +++ b/src/constants/creator-list-sort.constants.ts @@ -1,25 +0,0 @@ -/** - * Allowed public sort fields for creator list endpoints. - * Keep these names stable for request parsing across list handlers. - */ -export const CREATOR_LIST_SORT_FIELDS = [ - 'createdAt', - 'updatedAt', - 'displayName', - 'handle', -] as const; - -export type CreatorListSortField = (typeof CREATOR_LIST_SORT_FIELDS)[number]; - -/** - * Allowed sort orders for creator list endpoints. - */ -export const CREATOR_LIST_SORT_ORDERS = ['asc', 'desc'] as const; - -export type CreatorListSortOrder = (typeof CREATOR_LIST_SORT_ORDERS)[number]; - -/** Default sort field used by creator list handlers. */ -export const DEFAULT_CREATOR_LIST_SORT: CreatorListSortField = 'createdAt'; - -/** Default sort order used by creator list handlers. */ -export const DEFAULT_CREATOR_LIST_ORDER: CreatorListSortOrder = 'desc'; diff --git a/src/modules/creators/creator-list-price-filter.integration.test.ts b/src/modules/creators/creator-list-price-filter.integration.test.ts index b19f8c7..e69de29 100644 --- a/src/modules/creators/creator-list-price-filter.integration.test.ts +++ b/src/modules/creators/creator-list-price-filter.integration.test.ts @@ -1,167 +0,0 @@ -// src/modules/creators/creator-list-price-filter.integration.test.ts -// Integration tests for #419 — min_price and max_price filtering. - -import supertest from 'supertest'; -import app from '../../app'; -import { prisma } from '../../utils/prisma.utils'; - -const USER_IDS = ['price-filter-user-1', 'price-filter-user-2', 'price-filter-user-3']; -const HANDLES = ['price-filter-creator-1', 'price-filter-creator-2', 'price-filter-creator-3']; - -describe('#419 min_price and max_price filtering', () => { - let creatorIds: string[]; - - beforeAll(async () => { - creatorIds = []; - - for (let i = 0; i < 3; i++) { - await prisma.user.upsert({ - where: { id: USER_IDS[i] }, - create: { - id: USER_IDS[i], - email: `price-filter-${i}@example.test`, - passwordHash: 'dummy-hash', - firstName: 'Price', - lastName: `Filter ${i}`, - }, - update: {}, - }); - - const creator = await prisma.creatorProfile.upsert({ - where: { userId: USER_IDS[i] }, - create: { - userId: USER_IDS[i], - handle: HANDLES[i], - displayName: `Creator ${i}`, - }, - update: {}, - }); - - creatorIds.push(creator.id); - } - - // Seed price snapshots: 1M, 3M, 5M stroops - const prices = [1_000_000n, 3_000_000n, 5_000_000n]; - for (let i = 0; i < 3; i++) { - await prisma.creatorPriceSnapshot.upsert({ - where: { creatorId: creatorIds[i] }, - create: { - creatorId: creatorIds[i], - currentPrice: prices[i], - price24hAgo: prices[i], - lastTradeAt: new Date(), - }, - update: { - currentPrice: prices[i], - price24hAgo: prices[i], - lastTradeAt: new Date(), - }, - }); - } - }); - - afterAll(async () => { - await prisma.creatorPriceSnapshot.deleteMany({ - where: { creatorId: { in: creatorIds } }, - }); - await prisma.creatorProfile.deleteMany({ - where: { handle: { in: HANDLES } }, - }); - await prisma.user.deleteMany({ - where: { id: { in: USER_IDS } }, - }); - await prisma.$disconnect(); - }); - - it('minPrice alone filters out creators below the value', async () => { - const res = await supertest(app).get('/api/v1/creators?minPrice=2000000'); - expect(res.status).toBe(200); - - const ids = (res.body.data.items as any[]) - .filter((c: any) => creatorIds.includes(c.id)) - .map((c: any) => c.id); - - // Only creators with price >= 2M (creators 1 and 2) - expect(ids).toContain(creatorIds[1]); // 3M - expect(ids).toContain(creatorIds[2]); // 5M - expect(ids).not.toContain(creatorIds[0]); // 1M - }); - - it('maxPrice alone filters out creators above the value', async () => { - const res = await supertest(app).get('/api/v1/creators?maxPrice=4000000'); - expect(res.status).toBe(200); - - const ids = (res.body.data.items as any[]) - .filter((c: any) => creatorIds.includes(c.id)) - .map((c: any) => c.id); - - // Only creators with price <= 4M (creators 0 and 1) - expect(ids).toContain(creatorIds[0]); // 1M - expect(ids).toContain(creatorIds[1]); // 3M - expect(ids).not.toContain(creatorIds[2]); // 5M - }); - - it('both params together return only creators within range (inclusive)', async () => { - const res = await supertest(app).get( - '/api/v1/creators?minPrice=2000000&maxPrice=4000000' - ); - expect(res.status).toBe(200); - - const ids = (res.body.data.items as any[]) - .filter((c: any) => creatorIds.includes(c.id)) - .map((c: any) => c.id); - - // Only creator 1 (3M) is in range [2M, 4M] - expect(ids).toContain(creatorIds[1]); - expect(ids).not.toContain(creatorIds[0]); - expect(ids).not.toContain(creatorIds[2]); - }); - - it('returns 400 when minPrice > maxPrice', async () => { - const res = await supertest(app).get( - '/api/v1/creators?minPrice=5000000&maxPrice=1000000' - ); - expect(res.status).toBe(400); - expect(res.body.success).toBe(false); - expect(res.body.error.code).toBe('VALIDATION_ERROR'); - expect(res.body.error.message).toContain('minPrice'); - }); - - it('combines correctly with sort and pagination', async () => { - const res = await supertest(app).get( - '/api/v1/creators?minPrice=1000000&maxPrice=5000000&limit=10&offset=0&sort=createdAt&order=desc' - ); - expect(res.status).toBe(200); - expect(res.body.success).toBe(true); - expect(res.body.data.meta.limit).toBe(10); - expect(res.body.data.meta.offset).toBe(0); - }); - - it('combines correctly with verified filter', async () => { - // Mark creator 1 as verified - await prisma.creatorProfile.update({ - where: { id: creatorIds[1] }, - data: { isVerified: true }, - }); - - const res = await supertest(app).get( - '/api/v1/creators?minPrice=1000000&maxPrice=5000000&verified=true' - ); - expect(res.status).toBe(200); - - const ids = (res.body.data.items as any[]) - .filter((c: any) => creatorIds.includes(c.id)) - .map((c: any) => c.id); - - // Only verified creator 1 within price range - expect(ids).toContain(creatorIds[1]); - expect(ids).not.toContain(creatorIds[0]); - expect(ids).not.toContain(creatorIds[2]); - - // Cleanup - await prisma.creatorProfile.update({ - where: { id: creatorIds[1] }, - data: { isVerified: false }, - }); - }); -}); diff --git a/src/modules/creators/creators.sort.ts b/src/modules/creators/creators.sort.ts index 4703713..42b3989 100644 --- a/src/modules/creators/creators.sort.ts +++ b/src/modules/creators/creators.sort.ts @@ -22,6 +22,7 @@ const CREATOR_LIST_SORT_FIELD_MAP: Record< updatedAt: 'updatedAt', displayName: 'displayName', handle: 'handle', + price: 'priceSnapshot', }; /** @@ -39,6 +40,15 @@ export function mapCreatorListSort( throw new Error(`Unsupported creator sort option: ${sort}`); } + // Price sorting requires nested relation sorting on priceSnapshot.currentPrice + if (sort === 'price') { + return { + priceSnapshot: { + currentPrice: { sort: order, nulls: 'last' }, + }, + } as Prisma.CreatorProfileOrderByWithRelationInput; + } + return { [field]: { sort: order, nulls: 'last' }, } as Prisma.CreatorProfileOrderByWithRelationInput; From fc09454b3a544667c4c7d7af22cd523ecb2716dc Mon Sep 17 00:00:00 2001 From: Keshinro Tanitoluwa Joseph Date: Sat, 27 Jun 2026 15:07:19 +0100 Subject: [PATCH 2/2] Fix TypeScript error in price sorting implementation --- src/constants/creator-list-sort.constants.ts | 26 ++++++++++++++++++++ src/modules/creators/creators.sort.ts | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/constants/creator-list-sort.constants.ts b/src/constants/creator-list-sort.constants.ts index e69de29..aec7d52 100644 --- a/src/constants/creator-list-sort.constants.ts +++ b/src/constants/creator-list-sort.constants.ts @@ -0,0 +1,26 @@ +/** + * Allowed public sort fields for creator list endpoints. + * Keep these names stable for request parsing across list handlers. + */ +export const CREATOR_LIST_SORT_FIELDS = [ + 'createdAt', + 'updatedAt', + 'displayName', + 'handle', + 'price', +] as const; + +export type CreatorListSortField = (typeof CREATOR_LIST_SORT_FIELDS)[number]; + +/** + * Allowed sort orders for creator list endpoints. + */ +export const CREATOR_LIST_SORT_ORDERS = ['asc', 'desc'] as const; + +export type CreatorListSortOrder = (typeof CREATOR_LIST_SORT_ORDERS)[number]; + +/** Default sort field used by creator list handlers. */ +export const DEFAULT_CREATOR_LIST_SORT: CreatorListSortField = 'createdAt'; + +/** Default sort order used by creator list handlers. */ +export const DEFAULT_CREATOR_LIST_ORDER: CreatorListSortOrder = 'desc'; diff --git a/src/modules/creators/creators.sort.ts b/src/modules/creators/creators.sort.ts index 42b3989..4f7b8ec 100644 --- a/src/modules/creators/creators.sort.ts +++ b/src/modules/creators/creators.sort.ts @@ -44,7 +44,7 @@ export function mapCreatorListSort( if (sort === 'price') { return { priceSnapshot: { - currentPrice: { sort: order, nulls: 'last' }, + currentPrice: order, }, } as Prisma.CreatorProfileOrderByWithRelationInput; }