Files
gridpilot.gg/apps/api/src/domain/sponsor/SponsorService.test.ts
2025-12-23 23:14:51 +01:00

599 lines
22 KiB
TypeScript

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 { Sponsor } from '@core/racing/domain/entities/sponsor/Sponsor';
import { Money } from '@core/racing/domain/value-objects/Money';
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', () => {
let service: SponsorService;
let getSponsorshipPricingUseCase: { execute: Mock };
let getSponsorsUseCase: { execute: Mock };
let createSponsorUseCase: { execute: Mock };
let getSponsorDashboardUseCase: { execute: Mock };
let getSponsorSponsorshipsUseCase: { execute: Mock };
let getSponsorUseCase: { execute: Mock };
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() };
createSponsorUseCase = { execute: vi.fn() };
getSponsorDashboardUseCase = { execute: vi.fn() };
getSponsorSponsorshipsUseCase = { execute: vi.fn() };
getSponsorUseCase = { execute: vi.fn() };
getPendingSponsorshipRequestsUseCase = { execute: vi.fn() };
acceptSponsorshipRequestUseCase = { execute: vi.fn() };
rejectSponsorshipRequestUseCase = { execute: vi.fn() };
getSponsorBillingUseCase = { execute: vi.fn() };
logger = {
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
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,
createSponsorUseCase as unknown as CreateSponsorUseCase,
getSponsorDashboardUseCase as unknown as GetSponsorDashboardUseCase,
getSponsorSponsorshipsUseCase as unknown as GetSponsorSponsorshipsUseCase,
getSponsorUseCase as unknown as GetSponsorUseCase,
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 pricing data on success', async () => {
const outputPort = {
entityType: 'season',
entityId: 'season-1',
tiers: [{ name: 'Gold', price: { amount: 500, currency: 'USD' }, benefits: ['Main slot'] }],
};
getSponsorshipPricingUseCase.execute.mockImplementation(async () => {
getEntitySponsorshipPricingPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getEntitySponsorshipPricing();
expect(result).toEqual({
entityType: 'season',
entityId: 'season-1',
pricing: [{ id: 'Gold', level: 'Gold', price: 500, currency: 'USD' }],
});
});
it('returns empty pricing on error', async () => {
getSponsorshipPricingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const result = await service.getEntitySponsorshipPricing();
expect(result).toEqual({
entityType: 'season',
entityId: '',
pricing: [],
});
});
});
describe('getSponsors', () => {
it('returns sponsors on success', async () => {
const sponsors = [
Sponsor.create({
id: 'sponsor-1',
name: 'S1',
contactEmail: 's1@test.com',
createdAt: new Date('2024-01-01T00:00:00Z'),
}),
];
getSponsorsUseCase.execute.mockImplementation(async () => {
getSponsorsPresenter.present(sponsors);
return Result.ok(undefined);
});
const result = await service.getSponsors();
expect(result).toEqual({
sponsors: [
{
id: 'sponsor-1',
name: 'S1',
contactEmail: 's1@test.com',
createdAt: new Date('2024-01-01T00:00:00Z'),
},
],
});
});
it('returns empty list on error', async () => {
getSponsorsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const result = await service.getSponsors();
expect(result).toEqual({ sponsors: [] });
});
});
describe('createSponsor', () => {
it('returns created sponsor on success', async () => {
const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
const sponsor = Sponsor.create({
id: 'sponsor-1',
name: 'Test',
contactEmail: 'test@example.com',
createdAt: new Date('2024-01-01T00:00:00Z'),
});
createSponsorUseCase.execute.mockImplementation(async () => {
createSponsorPresenter.present(sponsor);
return Result.ok(undefined);
});
const result = await service.createSponsor(input);
expect(result).toEqual({
sponsor: {
id: 'sponsor-1',
name: 'Test',
contactEmail: 'test@example.com',
createdAt: new Date('2024-01-01T00:00:00Z'),
},
});
});
it('throws on error', async () => {
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)).rejects.toThrow('Invalid');
});
it('throws using error.message when details.message is missing', async () => {
const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
createSponsorUseCase.execute.mockResolvedValue(Result.err({ code: 'VALIDATION_ERROR', message: 'Boom' } as any));
await expect(service.createSponsor(input)).rejects.toThrow('Boom');
});
it('throws default message when details.message and message are missing', async () => {
const input: CreateSponsorInputDTO = { name: 'Test', contactEmail: 'test@example.com' };
createSponsorUseCase.execute.mockResolvedValue(Result.err({ code: 'VALIDATION_ERROR' } as any));
await expect(service.createSponsor(input)).rejects.toThrow('Failed to create sponsor');
});
});
describe('getSponsorDashboard', () => {
it('returns dashboard on success', async () => {
const params: GetSponsorDashboardInput = { sponsorId: 's1' };
const outputPort = {
sponsorId: 's1',
sponsorName: 'S1',
metrics: {
impressions: 0,
impressionsChange: 0,
uniqueViewers: 0,
viewersChange: 0,
races: 0,
drivers: 0,
exposure: 0,
exposureChange: 0,
},
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
costPerThousandViews: 0,
},
};
getSponsorDashboardUseCase.execute.mockImplementation(async () => {
getSponsorDashboardPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getSponsorDashboard(params);
expect(result).toEqual({
sponsorId: 's1',
sponsorName: 'S1',
metrics: outputPort.metrics,
sponsoredLeagues: [],
investment: {
activeSponsorships: 0,
totalInvestment: 0,
costPerThousandViews: 0,
},
sponsorships: {
leagues: [],
teams: [],
drivers: [],
races: [],
platform: [],
},
recentActivity: [],
upcomingRenewals: [],
});
});
it('throws on error', async () => {
const params: GetSponsorDashboardInput = { sponsorId: 's1' };
getSponsorDashboardUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
await expect(service.getSponsorDashboard(params)).rejects.toThrow('Sponsor dashboard not found');
});
});
describe('getSponsorSponsorships', () => {
it('returns sponsorships on success', async () => {
const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
const outputPort = {
sponsor: Sponsor.create({
id: 's1',
name: 'S1',
contactEmail: 's1@example.com',
}),
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: Money.create(0, 'USD'),
totalPlatformFees: Money.create(0, 'USD'),
},
};
getSponsorSponsorshipsUseCase.execute.mockImplementation(async () => {
getSponsorSponsorshipsPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getSponsorSponsorships(params);
expect(result).toEqual({
sponsorId: 's1',
sponsorName: 'S1',
sponsorships: [],
summary: {
totalSponsorships: 0,
activeSponsorships: 0,
totalInvestment: 0,
totalPlatformFees: 0,
currency: 'USD',
},
});
});
it('throws on error', async () => {
const params: GetSponsorSponsorshipsInput = { sponsorId: 's1' };
getSponsorSponsorshipsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
await expect(service.getSponsorSponsorships(params)).rejects.toThrow(
'Sponsor sponsorships not found',
);
});
});
describe('getSponsor', () => {
it('returns sponsor when found', async () => {
const sponsorId = 's1';
const output = { sponsor: { id: sponsorId, name: 'S1' } };
getSponsorUseCase.execute.mockImplementation(async () => {
getSponsorPresenter.present(output);
return Result.ok(undefined);
});
const result = await service.getSponsor(sponsorId);
expect(result).toEqual(output);
});
it('throws when not found', async () => {
const sponsorId = 's1';
getSponsorUseCase.execute.mockResolvedValue(Result.err({ code: 'SPONSOR_NOT_FOUND' }));
await expect(service.getSponsor(sponsorId)).rejects.toThrow('Sponsor not found');
});
it('throws when viewModel is missing even if use case succeeds', async () => {
const sponsorId = 's1';
getSponsorUseCase.execute.mockResolvedValue(Result.ok(undefined));
await expect(service.getSponsor(sponsorId)).rejects.toThrow('Sponsor not found');
});
});
describe('getPendingSponsorshipRequests', () => {
it('returns requests on success', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
const outputPort = {
entityType: 'season',
entityId: 'season-1',
requests: [],
totalCount: 0,
};
getPendingSponsorshipRequestsUseCase.execute.mockImplementation(async () => {
getPendingSponsorshipRequestsPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.getPendingSponsorshipRequests(params);
expect(result).toEqual(outputPort);
});
it('returns empty result on error', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' }));
const result = await service.getPendingSponsorshipRequests(params);
expect(result).toEqual({
entityType: 'season',
entityId: 'season-1',
requests: [],
totalCount: 0,
});
});
it('throws when presenter viewModel is missing on success', async () => {
const params = { entityType: 'season' as const, entityId: 'season-1' };
getPendingSponsorshipRequestsUseCase.execute.mockResolvedValue(Result.ok(undefined));
await expect(service.getPendingSponsorshipRequests(params)).rejects.toThrow('Pending sponsorship requests not found');
});
});
describe('SponsorshipRequest', () => {
it('returns accept result on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
const outputPort = {
requestId,
sponsorshipId: 'sp1',
status: 'accepted' as const,
acceptedAt: new Date(),
platformFee: 10,
netAmount: 90,
};
acceptSponsorshipRequestUseCase.execute.mockImplementation(async () => {
acceptSponsorshipRequestPresenter.present(outputPort as any);
return Result.ok(undefined);
});
const result = await service.acceptSponsorshipRequest(requestId, respondedBy);
expect(result).toEqual(outputPort);
});
it('throws on error', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
);
await expect(service.acceptSponsorshipRequest(requestId, respondedBy)).rejects.toThrow(
'Accept sponsorship request failed',
);
});
it('throws when presenter viewModel is missing on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
acceptSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(undefined));
await expect(service.acceptSponsorshipRequest(requestId, respondedBy)).rejects.toThrow(
'Accept sponsorship request failed',
);
});
});
describe('rejectSponsorshipRequest', () => {
it('returns reject result on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
const reason = 'Not interested';
const output = {
requestId,
status: 'rejected' as const,
respondedAt: new Date(),
rejectionReason: reason,
};
rejectSponsorshipRequestUseCase.execute.mockImplementation(async () => {
rejectSponsorshipRequestPresenter.present(output as any);
return Result.ok(undefined);
});
const result = await service.rejectSponsorshipRequest(requestId, respondedBy, reason);
expect(result).toEqual(output);
});
it('passes no reason when reason is undefined', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
rejectSponsorshipRequestUseCase.execute.mockImplementation(async (input: any) => {
expect(input).toEqual({ requestId, respondedBy });
rejectSponsorshipRequestPresenter.present({
requestId,
status: 'rejected' as const,
respondedAt: new Date(),
rejectionReason: '',
} as any);
return Result.ok(undefined);
});
await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).resolves.toMatchObject({
requestId,
status: 'rejected',
});
});
it('throws on error', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(
Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' }),
);
await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).rejects.toThrow(
'Reject sponsorship request failed',
);
});
it('throws when presenter viewModel is missing on success', async () => {
const requestId = 'r1';
const respondedBy = 'u1';
rejectSponsorshipRequestUseCase.execute.mockResolvedValue(Result.ok(undefined));
await expect(service.rejectSponsorshipRequest(requestId, respondedBy)).rejects.toThrow(
'Reject sponsorship request failed',
);
});
});
describe('getSponsorBilling', () => {
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);
});
const result = await service.getSponsorBilling('s1');
expect(result).not.toBeNull();
expect(result.paymentMethods).toBeInstanceOf(Array);
expect(result.invoices).toBeInstanceOf(Array);
expect(result.stats).toBeDefined();
});
it('throws on error', async () => {
getSponsorBillingUseCase.execute.mockResolvedValue(Result.err({ code: 'REPOSITORY_ERROR' } as any));
await expect(service.getSponsorBilling('s1')).rejects.toThrow('Sponsor billing not found');
});
});
describe('getAvailableLeagues', () => {
it('returns mock leagues', async () => {
const result = await service.getAvailableLeagues();
expect(result).not.toBeNull();
expect(result.viewModel).not.toBeNull();
expect(result.viewModel?.length).toBeGreaterThan(0);
});
});
describe('getLeagueDetail', () => {
it('returns league detail', async () => {
const result = await service.getLeagueDetail('league-1');
expect(result).not.toBeNull();
expect(result.viewModel?.league.id).toBe('league-1');
});
});
describe('getSponsorSettings', () => {
it('returns settings', async () => {
const result = await service.getSponsorSettings('s1');
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', async () => {
const result = await service.updateSponsorSettings('s1', {});
expect(result.viewModel).toEqual({ success: true });
});
});
});