diff --git a/src/config.schema.ts b/src/config.schema.ts index 6a0f507..18abee4 100644 --- a/src/config.schema.ts +++ b/src/config.schema.ts @@ -288,6 +288,7 @@ export const envSchema = z .int() .positive() .default(1000), + }) .superRefine((data, ctx) => { if (data.MODE === 'production' && data.STELLAR_NETWORK === 'testnet') { diff --git a/src/constants/error.constants.ts b/src/constants/error.constants.ts index d285028..d027e18 100644 --- a/src/constants/error.constants.ts +++ b/src/constants/error.constants.ts @@ -4,6 +4,7 @@ */ export const ErrorCode = { VALIDATION_ERROR: 'VALIDATION_ERROR', + UNPROCESSABLE_ENTITY: 'UNPROCESSABLE_ENTITY', NOT_FOUND: 'NOT_FOUND', UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', diff --git a/src/modules/creator/creator.routes.ts b/src/modules/creator/creator.routes.ts index 70a2a93..3db4342 100644 --- a/src/modules/creator/creator.routes.ts +++ b/src/modules/creator/creator.routes.ts @@ -74,7 +74,9 @@ creatorsRouter.post( 'capBps must be between 100 and 2500' ); return; - } + } const keyId = Array.isArray(req.params.keyId) + ? req.params.keyId[0] + : req.params.keyId; const keyId = Array.isArray(req.params.keyId) ? req.params.keyId[0] diff --git a/src/modules/indexer/price-snapshot-debug-log.unit.test.ts b/src/modules/indexer/price-snapshot-debug-log.unit.test.ts index c5ee457..def9dfe 100644 --- a/src/modules/indexer/price-snapshot-debug-log.unit.test.ts +++ b/src/modules/indexer/price-snapshot-debug-log.unit.test.ts @@ -16,6 +16,9 @@ jest.mock('../../utils/prisma.utils', () => ({ create: jest.fn(), update: jest.fn(), }, + creatorPriceHistory: { + create: jest.fn(), + }, }, })); @@ -34,6 +37,9 @@ const mockPrisma = prisma as unknown as { create: jest.Mock; update: jest.Mock; }; + creatorPriceHistory: { + create: jest.Mock; + }; }; const mockLogger = logger as unknown as { @@ -89,6 +95,67 @@ describe('#636 price snapshot write debug log', () => { expect(fields.previous_price).toBeNull(); }); + it('skips snapshot and history writes when the price is unchanged', async () => { + mockPrisma.creatorPriceSnapshot.findUnique.mockResolvedValue({ + creatorId: CREATOR_ID, + currentPrice: BigInt(1_000_000), + price24hAgo: BigInt(900_000), + lastTradeAt: new Date('2026-01-01T00:00:00Z'), + }); + + await upsertPriceSnapshot({ + creatorId: CREATOR_ID, + price: BigInt(1_000_000), + tradeAt: new Date('2026-01-02T00:00:00Z'), + }); + + expect(mockPrisma.creatorPriceSnapshot.update).not.toHaveBeenCalled(); + expect(mockPrisma.creatorPriceHistory.create).not.toHaveBeenCalled(); + }); + + it('writes a new snapshot and history record when the price changes', async () => { + mockPrisma.creatorPriceSnapshot.findUnique.mockResolvedValue({ + creatorId: CREATOR_ID, + currentPrice: BigInt(1_000_000), + price24hAgo: BigInt(900_000), + lastTradeAt: new Date('2026-01-01T00:00:00Z'), + }); + mockPrisma.creatorPriceSnapshot.update.mockResolvedValue({}); + mockPrisma.creatorPriceHistory.create.mockResolvedValue({}); + + const tradeAt = new Date('2026-01-02T00:00:00Z'); + await upsertPriceSnapshot({ + creatorId: CREATOR_ID, + price: BigInt(1_100_000), + tradeAt, + }); + + expect(mockPrisma.creatorPriceSnapshot.update).toHaveBeenCalledTimes(1); + expect(mockPrisma.creatorPriceHistory.create).toHaveBeenCalledTimes(1); + expect(mockPrisma.creatorPriceHistory.create).toHaveBeenCalledWith({ + data: { + creatorId: CREATOR_ID, + price: BigInt(1_100_000), + recordedAt: tradeAt, + }, + }); + }); + + it('writes the first snapshot when no previous snapshot exists', async () => { + mockPrisma.creatorPriceSnapshot.findUnique.mockResolvedValue(null); + mockPrisma.creatorPriceSnapshot.create.mockResolvedValue({}); + mockPrisma.creatorPriceHistory.create.mockResolvedValue({}); + + await upsertPriceSnapshot({ + creatorId: CREATOR_ID, + price: BigInt(1_000_000), + tradeAt: new Date('2026-01-01T00:00:00Z'), + }); + + expect(mockPrisma.creatorPriceSnapshot.create).toHaveBeenCalledTimes(1); + expect(mockPrisma.creatorPriceHistory.create).toHaveBeenCalledTimes(1); + }); + it('emits a debug log with the previous price on a subsequent (update) write', async () => { mockPrisma.creatorPriceSnapshot.findUnique.mockResolvedValue({ creatorId: CREATOR_ID,