From 3ea4f79ded0eb5294f8feb10f3201bad3d1c15a5 Mon Sep 17 00:00:00 2001 From: innocentharuna <129168996+innocentharuna@users.noreply.github.com> Date: Mon, 26 Oct 2026 12:52:49 +0100 Subject: [PATCH] fix(revenue): persist issuer_id and reported_by in submitReport submitReport built the insert payload without issuer_id and reported_by, both NOT NULL in migration 016, so every real submission failed with a not-null violation. Populate both columns; the handler now threads the authenticated user through as reportedBy. Also add the missing isPositiveDecimalAmount import in revenueService.ts, which was failing the existing revenue tests on master. Closes #1257 Co-Authored-By: Claude Opus 5.5 --- src/handlers/revenueHandler.test.ts | 1 + src/handlers/revenueHandler.ts | 2 ++ src/services/revenueService.test.ts | 52 +++++++++++++++++++++++++++++ src/services/revenueService.ts | 5 +++ 4 files changed, 60 insertions(+) diff --git a/src/handlers/revenueHandler.test.ts b/src/handlers/revenueHandler.test.ts index 6a46d5cf..030395fc 100644 --- a/src/handlers/revenueHandler.test.ts +++ b/src/handlers/revenueHandler.test.ts @@ -122,6 +122,7 @@ describe('RevenueHandler', () => { expect(mockRevenueService.submitReport).toHaveBeenCalledWith({ offeringId: 'offering-1', issuerId: 'issuer-1', + reportedBy: 'issuer-1', amount: '500.50', periodStart: new Date('2024-02-01'), periodEnd: new Date('2024-02-28'), diff --git a/src/handlers/revenueHandler.ts b/src/handlers/revenueHandler.ts index f8ccc9c2..9d125bfc 100644 --- a/src/handlers/revenueHandler.ts +++ b/src/handlers/revenueHandler.ts @@ -76,6 +76,7 @@ export class RevenueHandler { const report = await this.revenueService.submitReport({ offeringId, issuerId, + reportedBy: issuerId, amount, periodStart: new Date(periodStart), periodEnd: new Date(periodEnd), @@ -139,6 +140,7 @@ export class RevenueHandler { const report = await this.revenueService.submitReport({ offeringId, issuerId, + reportedBy: issuerId, amount, periodStart: new Date(periodStart), periodEnd: new Date(periodEnd), diff --git a/src/services/revenueService.test.ts b/src/services/revenueService.test.ts index 77e02f01..5511444e 100644 --- a/src/services/revenueService.test.ts +++ b/src/services/revenueService.test.ts @@ -118,6 +118,58 @@ describe('RevenueService', () => { ); }); + // ── NOT NULL columns (migration 016) ──────────────────────────────────── + + it('passes issuer_id and reported_by to the repository', async () => { + mockOfferingRepo.findById.mockResolvedValue(mockOffering); + mockRevenueReportRepo.findOverlappingReport.mockResolvedValue(null); + mockLedgerLockRepo.isPeriodLocked.mockResolvedValue(false); + mockRevenueReportRepo.create.mockResolvedValue(mockReport); + + await service.submitReport(validInput); + + expect(mockRevenueReportRepo.create).toHaveBeenCalledWith( + expect.objectContaining({ + issuer_id: 'issuer-abc', + reported_by: 'issuer-abc', + }), + mockTransactionClient + ); + }); + + it('uses reportedBy for reported_by when provided', async () => { + mockOfferingRepo.findById.mockResolvedValue(mockOffering); + mockRevenueReportRepo.findOverlappingReport.mockResolvedValue(null); + mockLedgerLockRepo.isPeriodLocked.mockResolvedValue(false); + mockRevenueReportRepo.create.mockResolvedValue(mockReport); + + await service.submitReport({ ...validInput, reportedBy: 'user-xyz' }); + + expect(mockRevenueReportRepo.create).toHaveBeenCalledWith( + expect.objectContaining({ + issuer_id: 'issuer-abc', + reported_by: 'user-xyz', + }), + mockTransactionClient + ); + }); + + it('satisfies a repository that enforces the NOT NULL columns', async () => { + mockOfferingRepo.findById.mockResolvedValue(mockOffering); + mockRevenueReportRepo.findOverlappingReport.mockResolvedValue(null); + mockLedgerLockRepo.isPeriodLocked.mockResolvedValue(false); + mockRevenueReportRepo.create.mockImplementation(async (data: Record) => { + for (const column of ['issuer_id', 'reported_by']) { + if (data[column] == null) { + throw new Error(`null value in column "${column}" violates not-null constraint`); + } + } + return mockReport; + }); + + await expect(service.submitReport(validInput)).resolves.toBe(mockReport); + }); + // ── Offering not found ────────────────────────────────────────────────── it('throws NOT_FOUND when offering does not exist', async () => { diff --git a/src/services/revenueService.ts b/src/services/revenueService.ts index 4975f318..0fac0063 100644 --- a/src/services/revenueService.ts +++ b/src/services/revenueService.ts @@ -6,6 +6,7 @@ import { } from '../db/repositories/revenueReportRepository'; import { LedgerPeriodLockRepository } from '../db/repositories/ledgerPeriodLockRepository'; import { AppError, Errors } from '../lib/errors'; +import { isPositiveDecimalAmount } from '../lib/amountValidation'; import { Logger } from '../lib/logger'; import { LogLevel } from '../lib/logger'; import { Pool } from 'pg'; @@ -15,6 +16,8 @@ import { TransactionError, withTransaction } from '../db/transaction'; export interface SubmitRevenueReportInput { offeringId: string; issuerId: string; + /** Authenticated user submitting the report; defaults to `issuerId`. */ + reportedBy?: string; amount: string; periodStart: Date; periodEnd: Date; @@ -128,6 +131,8 @@ export class RevenueService { // 4. Build the insert payload before opening a transaction. const reportData: CreateRevenueReportInput = { offering_id: input.offeringId, + issuer_id: input.issuerId, + reported_by: input.reportedBy ?? input.issuerId, amount: input.amount, period_start: input.periodStart, period_end: input.periodEnd,