refactor api modules

This commit is contained in:
2025-12-22 19:17:33 +01:00
parent c90b2166c1
commit 1333f5e907
100 changed files with 2226 additions and 1936 deletions

View File

@@ -1,15 +1,27 @@
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
import { Result } from '@core/shared/application/Result';
import type { Logger } from '@core/shared/application';
import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
import type { GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import type { GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import type { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
import type { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
import type { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
import type { GetPendingSponsorshipRequestsUseCase } from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
import type { GetSponsorDashboardInput, GetSponsorDashboardUseCase } from '@core/racing/application/use-cases/GetSponsorDashboardUseCase';
import type { GetSponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetSponsorshipPricingUseCase';
import type { GetSponsorSponsorshipsInput, GetSponsorSponsorshipsUseCase } from '@core/racing/application/use-cases/GetSponsorSponsorshipsUseCase';
import type { GetSponsorsUseCase } from '@core/racing/application/use-cases/GetSponsorsUseCase';
import type { GetSponsorUseCase } from '@core/racing/application/use-cases/GetSponsorUseCase';
import type { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
import type { Logger } from '@core/shared/application';
import { Result } from '@core/shared/application/Result';
import { beforeEach, describe, expect, it, Mock, vi } from 'vitest';
import type { CreateSponsorInputDTO } from './dtos/CreateSponsorInputDTO';
import { AcceptSponsorshipRequestPresenter } from './presenters/AcceptSponsorshipRequestPresenter';
import { CreateSponsorPresenter } from './presenters/CreateSponsorPresenter';
import { GetEntitySponsorshipPricingPresenter } from './presenters/GetEntitySponsorshipPricingPresenter';
import { GetPendingSponsorshipRequestsPresenter } from './presenters/GetPendingSponsorshipRequestsPresenter';
import { GetSponsorDashboardPresenter } from './presenters/GetSponsorDashboardPresenter';
import { GetSponsorPresenter } from './presenters/GetSponsorPresenter';
import { GetSponsorSponsorshipsPresenter } from './presenters/GetSponsorSponsorshipsPresenter';
import { GetSponsorsPresenter } from './presenters/GetSponsorsPresenter';
import { RejectSponsorshipRequestPresenter } from './presenters/RejectSponsorshipRequestPresenter';
import { SponsorBillingPresenter } from './presenters/SponsorBillingPresenter';
import { SponsorService } from './SponsorService';
describe('SponsorService', () => {
@@ -23,8 +35,21 @@ describe('SponsorService', () => {
let getPendingSponsorshipRequestsUseCase: { execute: Mock };
let acceptSponsorshipRequestUseCase: { execute: Mock };
let rejectSponsorshipRequestUseCase: { execute: Mock };
let getSponsorBillingUseCase: { execute: Mock };
let logger: Logger;
// Presenters
let getEntitySponsorshipPricingPresenter: GetEntitySponsorshipPricingPresenter;
let getSponsorsPresenter: GetSponsorsPresenter;
let createSponsorPresenter: CreateSponsorPresenter;
let getSponsorDashboardPresenter: GetSponsorDashboardPresenter;
let getSponsorSponsorshipsPresenter: GetSponsorSponsorshipsPresenter;
let getSponsorPresenter: GetSponsorPresenter;
let getPendingSponsorshipRequestsPresenter: GetPendingSponsorshipRequestsPresenter;
let acceptSponsorshipRequestPresenter: AcceptSponsorshipRequestPresenter;
let rejectSponsorshipRequestPresenter: RejectSponsorshipRequestPresenter;
let sponsorBillingPresenter: SponsorBillingPresenter;
beforeEach(() => {
getSponsorshipPricingUseCase = { execute: vi.fn() };
getSponsorsUseCase = { execute: vi.fn() };
@@ -35,6 +60,7 @@ describe('SponsorService', () => {
getPendingSponsorshipRequestsUseCase = { execute: vi.fn() };
acceptSponsorshipRequestUseCase = { execute: vi.fn() };
rejectSponsorshipRequestUseCase = { execute: vi.fn() };
getSponsorBillingUseCase = { execute: vi.fn() };
logger = {
debug: vi.fn(),
info: vi.fn(),
@@ -42,6 +68,18 @@ describe('SponsorService', () => {
error: vi.fn(),
} as unknown as Logger;
// Initialize presenters
getEntitySponsorshipPricingPresenter = new GetEntitySponsorshipPricingPresenter();
getSponsorsPresenter = new GetSponsorsPresenter();
createSponsorPresenter = new CreateSponsorPresenter();
getSponsorDashboardPresenter = new GetSponsorDashboardPresenter();
getSponsorSponsorshipsPresenter = new GetSponsorSponsorshipsPresenter();
getSponsorPresenter = new GetSponsorPresenter();
getPendingSponsorshipRequestsPresenter = new GetPendingSponsorshipRequestsPresenter();
acceptSponsorshipRequestPresenter = new AcceptSponsorshipRequestPresenter();
rejectSponsorshipRequestPresenter = new RejectSponsorshipRequestPresenter();
sponsorBillingPresenter = new SponsorBillingPresenter();
service = new SponsorService(
getSponsorshipPricingUseCase as unknown as GetSponsorshipPricingUseCase,
getSponsorsUseCase as unknown as GetSponsorsUseCase,
@@ -52,28 +90,39 @@ describe('SponsorService', () => {
getPendingSponsorshipRequestsUseCase as unknown as GetPendingSponsorshipRequestsUseCase,
acceptSponsorshipRequestUseCase as unknown as AcceptSponsorshipRequestUseCase,
rejectSponsorshipRequestUseCase as unknown as RejectSponsorshipRequestUseCase,
getSponsorBillingUseCase as unknown as GetSponsorBillingUseCase,
logger,
getEntitySponsorshipPricingPresenter,
getSponsorsPresenter,
createSponsorPresenter,
getSponsorDashboardPresenter,
getSponsorSponsorshipsPresenter,
getSponsorPresenter,
getPendingSponsorshipRequestsPresenter,
acceptSponsorshipRequestPresenter,
rejectSponsorshipRequestPresenter,
sponsorBillingPresenter,
);
});
describe('getEntitySponsorshipPricing', () => {
it('returns presenter with pricing data on success', async () => {
it('returns pricing data on success', async () => {
const outputPort = {
entityType: 'season',
entityId: 'season-1',
pricing: [
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
tiers: [
{ name: 'Gold', price: 500, benefits: ['Main slot'] },
],
};
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getEntitySponsorshipPricing();
const result = await service.getEntitySponsorshipPricing();
expect(presenter.viewModel).toEqual({
expect(result).toEqual({
entityType: 'season',
entityId: 'season-1',
pricing: [
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
{ id: 'Gold', level: 'Gold', price: 500, currency: 'USD' },
],
});
});
@@ -81,9 +130,9 @@ describe('SponsorService', () => {
it('returns empty pricing on error', async () => {
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const presenter = await service.getEntitySponsorshipPricing();
const result = await service.getEntitySponsorshipPricing();
expect(presenter.viewModel).toEqual({
expect(result).toEqual({
entityType: 'season',
entityId: '',
pricing: [],
@@ -92,82 +141,93 @@ describe('SponsorService', () => {
});
describe('getSponsors', () => {
it('returns sponsors in presenter on success', async () => {
const outputPort = { sponsors: [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }] };
it('returns sponsors on success', async () => {
const sponsors = [{ id: 's1', name: 'S1', contactEmail: 's1@test', createdAt: new Date() }];
const outputPort = { sponsors };
getSponsorsUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getSponsors();
const result = await service.getSponsors();
expect(presenter.viewModel).toEqual({ sponsors: outputPort.sponsors });
expect(result).toEqual({ sponsors });
});
it('returns empty list on error', async () => {
getSponsorsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const presenter = await service.getSponsors();
const result = await service.getSponsors();
expect(presenter.viewModel).toEqual({ sponsors: [] });
expect(result).toEqual({ sponsors: [] });
});
});
describe('createSponsor', () => {
it('returns created sponsor in presenter on success', async () => {
const input = { name: 'Test', contactEmail: 'test@example.com' };
const outputPort = {
sponsor: {
id: 's1',
name: 'Test',
contactEmail: 'test@example.com',
createdAt: new Date(),
},
it('returns created sponsor on success', async () => {
const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
const sponsor = {
id: 's1',
name: 'Test',
contactEmail: 'test@example.com',
createdAt: new Date(),
};
const outputPort = { sponsor };
createSponsorUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.createSponsor(input as any);
const result = await service.createSponsor(input);
expect(presenter.viewModel).toEqual({ sponsor: outputPort.sponsor });
expect(result).toEqual({ sponsor });
});
it('throws on error', async () => {
const input = { name: 'Test', contactEmail: 'test@example.com' };
const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
createSponsorUseCase.execute.mockResolvedValue(
Result.err({ code: 'VALIDATION_ERROR', details: { message: 'Invalid' } }),
);
await expect(service.createSponsor(input as any)).rejects.toThrow('Invalid');
await expect(service.createSponsor(input)).rejects.toThrow('Invalid');
});
});
describe('getSponsorDashboard', () => {
it('returns dashboard in presenter on success', async () => {
const params = { sponsorId: 's1' };
it('returns dashboard on success', async () => {
const params: GetSponsorDashboardInput = { sponsorId: 's1' };
const outputPort = {
sponsorId: 's1',
sponsorName: 'S1',
metrics: {} as any,
metrics: {
impressions: 0,
impressionsChange: 0,
uniqueViewers: 0,
viewersChange: 0,
races: 0,
drivers: 0,
exposure: 0,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {} as any,
investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
};
getSponsorDashboardUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getSponsorDashboard(params as any);
const result = await service.getSponsorDashboard(params);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns null in presenter on error', async () => {
const params = { sponsorId: 's1' };
it('throws on error', async () => {
const params: GetSponsorDashboardInput = { sponsorId: 's1' };
getSponsorDashboardUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const presenter = await service.getSponsorDashboard(params as any);
expect(presenter.viewModel).toBeNull();
await expect(service.getSponsorDashboard(params)).rejects.toThrow('Sponsor dashboard not found');
});
});
describe('getSponsorSponsorships', () => {
it('returns sponsorships in presenter on success', async () => {
const params = { sponsorId: 's1' };
it('returns sponsorships on success', async () => {
const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
const outputPort = {
sponsorId: 's1',
sponsorName: 'S1',
@@ -182,46 +242,43 @@ describe('SponsorService', () => {
};
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getSponsorSponsorships(params as any);
const result = await service.getSponsorSponsorships(params);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns null in presenter on error', async () => {
const params = { sponsorId: 's1' };
it('throws on error', async () => {
const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(
Result.err({ code: 'REPOSITORY_ERROR' }),
);
const presenter = await service.getSponsorSponsorships(params as any);
expect(presenter.viewModel).toBeNull();
await expect(service.getSponsorSponsorships(params)).rejects.toThrow('Sponsor sponsorships not found');
});
});
describe('getSponsor', () => {
it('returns sponsor in presenter when found', async () => {
it('returns sponsor when found', async () => {
const sponsorId = 's1';
const output = { sponsor: { id: sponsorId, name: 'S1' } };
const sponsor = { id: sponsorId, name: 'S1' };
const output = { sponsor };
getSponsorUseCase.execute.mockResolvedValue(Result.ok(output));
const presenter = await service.getSponsor(sponsorId);
const result = await service.getSponsor(sponsorId);
expect(presenter.viewModel).toEqual({ sponsor: output.sponsor });
expect(result).toEqual({ sponsor });
});
it('returns null in presenter when not found', async () => {
it('throws when not found', async () => {
const sponsorId = 's1';
getSponsorUseCase.execute.mockResolvedValue(Result.ok(null));
const presenter = await service.getSponsor(sponsorId);
expect(presenter.viewModel).toBeNull();
await expect(service.getSponsor(sponsorId)).rejects.toThrow('Sponsor not found');
});
});
describe('getPendingSponsorshipRequests', () => {
it('returns requests in presenter on success', async () => {
it('returns requests on success', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
const outputPort = {
entityType: 'season',
@@ -231,9 +288,9 @@ describe('SponsorService', () => {
};
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.getPendingSponsorshipRequests(params);
const result = await service.getPendingSponsorshipRequests(params);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns empty result on error', async () => {
@@ -242,9 +299,9 @@ describe('SponsorService', () => {
Result.err({ code: 'REPOSITORY_ERROR' }),
);
const presenter = await service.getPendingSponsorshipRequests(params);
const result = await service.getPendingSponsorshipRequests(params);
expect(presenter.viewModel).toEqual({
expect(result).toEqual({
entityType: 'season',
entityId: 'season-1',
requests: [],
@@ -253,8 +310,8 @@ describe('SponsorService', () => {
});
});
describe('acceptSponsorshipRequest', () => {
it('returns accept result in presenter on success', async () => {
describe('SponsorshipRequest', () => {
it('returns accept result on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
const outputPort = {
@@ -267,100 +324,114 @@ describe('SponsorService', () => {
};
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(outputPort));
const presenter = await service.acceptSponsorshipRequest(requestId, respondedBy);
const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(presenter.viewModel).toEqual(outputPort);
expect(result).toEqual(outputPort);
});
it('returns null in presenter on error', async () => {
it('throws on error', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
);
const presenter = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(presenter.viewModel).toBeNull();
await expect(service.acceptSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Accept sponsorship request failed');
});
});
describe('rejectSponsorshipRequest', () => {
it('returns reject result in presenter on success', async () => {
it('returns reject result on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
const reason = 'Not interested';
const output = {
requestId,
status: 'rejected' as const,
rejectedAt: new Date(),
reason,
respondedAt: new Date(),
rejectionReason: reason,
};
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(output));
const presenter = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
expect(presenter.viewModel).toEqual(output);
expect(result).toEqual(output);
});
it('returns null in presenter on error', async () => {
it('throws on error', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
);
const presenter = await service.rejectSponsorshipRequest(requestId, respondedBy);
expect(presenter.viewModel).toBeNull();
await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).rejects.toThrow('Reject sponsorship request failed');
});
});
describe('getSponsorBilling', () => {
it('returns mock billing data in presenter', async () => {
const presenter = await service.getSponsorBilling('s1');
it('returns billing data', async () => {
// Mock the use case to set up the presenter
getSponsorBillingUseCase.execute.mockImplementation(async () => {
sponsorBillingPresenter.present({
paymentMethods: [],
invoices: [],
stats: {
totalSpent: 0,
pendingAmount: 0,
nextPaymentDate: '',
nextPaymentAmount: 0,
activeSponsorships: 0,
averageMonthlySpend: 0,
},
});
return Result.ok(undefined);
});
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.paymentMethods).toBeInstanceOf(Array);
expect(presenter.viewModel?.invoices).toBeInstanceOf(Array);
expect(presenter.viewModel?.stats).toBeDefined();
const result = await service.getSponsorBilling('s1');
expect(result).not.toBeNull();
expect(result.paymentMethods).toBeInstanceOf(Array);
expect(result.invoices).toBeInstanceOf(Array);
expect(result.stats).toBeDefined();
});
});
describe('getAvailableLeagues', () => {
it('returns mock leagues in presenter', async () => {
const presenter = await service.getAvailableLeagues();
it('returns mock leagues', async () => {
const result = await service.getAvailableLeagues();
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.length).toBeGreaterThan(0);
expect(result).not.toBeNull();
expect(result.viewModel).not.toBeNull();
expect(result.viewModel?.length).toBeGreaterThan(0);
});
});
describe('getLeagueDetail', () => {
it('returns league detail in presenter', async () => {
const presenter = await service.getLeagueDetail('league-1');
it('returns league detail', async () => {
const result = await service.getLeagueDetail('league-1');
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.league.id).toBe('league-1');
expect(result).not.toBeNull();
expect(result.viewModel?.league.id).toBe('league-1');
});
});
describe('getSponsorSettings', () => {
it('returns settings in presenter', async () => {
const presenter = await service.getSponsorSettings('s1');
it('returns settings', async () => {
const result = await service.getSponsorSettings('s1');
expect(presenter.viewModel).not.toBeNull();
expect(presenter.viewModel?.profile).toBeDefined();
expect(presenter.viewModel?.notifications).toBeDefined();
expect(presenter.viewModel?.privacy).toBeDefined();
expect(result).not.toBeNull();
expect(result.viewModel?.profile).toBeDefined();
expect(result.viewModel?.notifications).toBeDefined();
expect(result.viewModel?.privacy).toBeDefined();
});
});
describe('updateSponsorSettings', () => {
it('returns success result in presenter', async () => {
const presenter = await service.updateSponsorSettings('s1', {});
it('returns success result', async () => {
const result = await service.updateSponsorSettings('s1', {});
expect(presenter.viewModel).toEqual({ success: true });
expect(result.viewModel).toEqual({ success: true });
});
});
});
});