website refactor

This commit is contained in:
2026-01-17 22:55:03 +01:00
parent 64d9e7fd16
commit 69d4cce7f1
64 changed files with 1146 additions and 1014 deletions

View File

@@ -1,28 +1,20 @@
import { describe, it, expect, vi, Mocked } from 'vitest';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SponsorService } from './SponsorService';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { SponsorViewModel } from '@/lib/view-models/SponsorViewModel';
import { SponsorDashboardViewModel } from '@/lib/view-models/SponsorDashboardViewModel';
import { SponsorSponsorshipsViewModel } from '@/lib/view-models/SponsorSponsorshipsViewModel';
// Mock the API client
vi.mock('@/lib/api/sponsors/SponsorsApiClient');
describe('SponsorService', () => {
let mockApiClient: Mocked<SponsorsApiClient>;
let service: SponsorService;
let mockApiClientInstance: any;
beforeEach(() => {
mockApiClient = {
getAll: vi.fn(),
getDashboard: vi.fn(),
getSponsorships: vi.fn(),
create: vi.fn(),
getPricing: vi.fn(),
getSponsor: vi.fn(),
getPendingSponsorshipRequests: vi.fn(),
acceptSponsorshipRequest: vi.fn(),
rejectSponsorshipRequest: vi.fn(),
} as Mocked<SponsorsApiClient>;
service = new SponsorService(mockApiClient);
vi.clearAllMocks();
service = new SponsorService();
// @ts-ignore - accessing private property for testing
mockApiClientInstance = service.apiClient;
});
describe('getAllSponsors', () => {
@@ -38,147 +30,90 @@ describe('SponsorService', () => {
],
};
mockApiClient.getAll.mockResolvedValue(mockDto);
mockApiClientInstance.getAll.mockResolvedValue(mockDto);
const result = await service.getAllSponsors();
expect(mockApiClient.getAll).toHaveBeenCalled();
expect(mockApiClientInstance.getAll).toHaveBeenCalled();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(SponsorViewModel);
expect(result[0].id).toBe('sponsor-1');
expect(result[0].name).toBe('Test Sponsor');
expect(result[0].hasWebsite).toBe(true);
});
it('should throw error when apiClient.getAll fails', async () => {
const error = new Error('API call failed');
mockApiClient.getAll.mockRejectedValue(error);
await expect(service.getAllSponsors()).rejects.toThrow('API call failed');
});
});
describe('getSponsorDashboard', () => {
it('should call apiClient.getDashboard and return SponsorDashboardViewModel when data exists', async () => {
it('should call apiClient.getDashboard and return Result.ok when data exists', async () => {
const mockDto = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
};
mockApiClient.getDashboard.mockResolvedValue(mockDto);
mockApiClientInstance.getDashboard.mockResolvedValue(mockDto as any);
const result = await service.getSponsorDashboard('sponsor-1');
expect(mockApiClient.getDashboard).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeInstanceOf(SponsorDashboardViewModel);
expect(result?.sponsorId).toBe('sponsor-1');
expect(result?.sponsorName).toBe('Test Sponsor');
expect(mockApiClientInstance.getDashboard).toHaveBeenCalledWith('sponsor-1');
expect(result.isOk()).toBe(true);
expect(result.unwrap().sponsorId).toBe('sponsor-1');
expect(result.unwrap().sponsorName).toBe('Test Sponsor');
});
it('should return null when apiClient.getDashboard returns null', async () => {
mockApiClient.getDashboard.mockResolvedValue(null);
it('should return Result.err with type "notFound" when apiClient.getDashboard returns null', async () => {
mockApiClientInstance.getDashboard.mockResolvedValue(null);
const result = await service.getSponsorDashboard('sponsor-1');
expect(mockApiClient.getDashboard).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeNull();
expect(mockApiClientInstance.getDashboard).toHaveBeenCalledWith('sponsor-1');
expect(result.isErr()).toBe(true);
expect(result.getError().type).toBe('notFound');
});
it('should throw error when apiClient.getDashboard fails', async () => {
it('should return Result.err with type "serverError" when apiClient.getDashboard fails', async () => {
const error = new Error('API call failed');
mockApiClient.getDashboard.mockRejectedValue(error);
mockApiClientInstance.getDashboard.mockRejectedValue(error);
await expect(service.getSponsorDashboard('sponsor-1')).rejects.toThrow('API call failed');
const result = await service.getSponsorDashboard('sponsor-1');
expect(result.isErr()).toBe(true);
expect(result.getError().type).toBe('serverError');
});
});
describe('getSponsorSponsorships', () => {
it('should call apiClient.getSponsorships and return SponsorSponsorshipsViewModel when data exists', async () => {
it('should call apiClient.getSponsorships and return Result.ok when data exists', async () => {
const mockDto = {
sponsorId: 'sponsor-1',
sponsorName: 'Test Sponsor',
};
mockApiClient.getSponsorships.mockResolvedValue(mockDto);
mockApiClientInstance.getSponsorships.mockResolvedValue(mockDto as any);
const result = await service.getSponsorSponsorships('sponsor-1');
expect(mockApiClient.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeInstanceOf(SponsorSponsorshipsViewModel);
expect(result?.sponsorId).toBe('sponsor-1');
expect(result?.sponsorName).toBe('Test Sponsor');
expect(mockApiClientInstance.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result.isOk()).toBe(true);
expect(result.unwrap().sponsorId).toBe('sponsor-1');
expect(result.unwrap().sponsorName).toBe('Test Sponsor');
});
it('should return null when apiClient.getSponsorships returns null', async () => {
mockApiClient.getSponsorships.mockResolvedValue(null);
it('should return Result.err with type "notFound" when apiClient.getSponsorships returns null', async () => {
mockApiClientInstance.getSponsorships.mockResolvedValue(null);
const result = await service.getSponsorSponsorships('sponsor-1');
expect(mockApiClient.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result).toBeNull();
expect(mockApiClientInstance.getSponsorships).toHaveBeenCalledWith('sponsor-1');
expect(result.isErr()).toBe(true);
expect(result.getError().type).toBe('notFound');
});
it('should throw error when apiClient.getSponsorships fails', async () => {
it('should return Result.err with type "serverError" when apiClient.getSponsorships fails', async () => {
const error = new Error('API call failed');
mockApiClient.getSponsorships.mockRejectedValue(error);
mockApiClientInstance.getSponsorships.mockRejectedValue(error);
await expect(service.getSponsorSponsorships('sponsor-1')).rejects.toThrow('API call failed');
});
});
describe('createSponsor', () => {
it('should call apiClient.create and return the result', async () => {
const input = {
name: 'New Sponsor',
};
const mockOutput = {
id: 'sponsor-123',
name: 'New Sponsor',
};
mockApiClient.create.mockResolvedValue(mockOutput);
const result = await service.createSponsor(input);
expect(mockApiClient.create).toHaveBeenCalledWith(input);
expect(result).toEqual(mockOutput);
});
it('should throw error when apiClient.create fails', async () => {
const input = {
name: 'New Sponsor',
};
const error = new Error('API call failed');
mockApiClient.create.mockRejectedValue(error);
await expect(service.createSponsor(input)).rejects.toThrow('API call failed');
});
});
describe('getSponsorshipPricing', () => {
it('should call apiClient.getPricing and return the result', async () => {
const mockPricing = {
pricing: [
{ entityType: 'league', price: 100 },
{ entityType: 'driver', price: 50 },
],
};
mockApiClient.getPricing.mockResolvedValue(mockPricing);
const result = await service.getSponsorshipPricing();
expect(mockApiClient.getPricing).toHaveBeenCalled();
expect(result).toEqual(mockPricing);
});
it('should throw error when apiClient.getPricing fails', async () => {
const error = new Error('API call failed');
mockApiClient.getPricing.mockRejectedValue(error);
await expect(service.getSponsorshipPricing()).rejects.toThrow('API call failed');
const result = await service.getSponsorSponsorships('sponsor-1');
expect(result.isErr()).toBe(true);
expect(result.getError().type).toBe('serverError');
});
});
});

View File

@@ -1,120 +1,55 @@
import { Result } from '@/lib/contracts/Result';
import { DomainError, Service } from '@/lib/contracts/services/Service';
import { injectable } from 'inversify';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { SponsorViewModel } from '@/lib/view-models/SponsorViewModel';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { getWebsiteServerEnv } from '@/lib/config/env';
import { Service, type DomainError } from '@/lib/contracts/services/Service';
import { Result } from '@/lib/contracts/Result';
import type { SponsorDashboardDTO } from '@/lib/types/generated/SponsorDashboardDTO';
import type { SponsorSponsorshipsDTO } from '@/lib/types/generated/SponsorSponsorshipsDTO';
import type { GetSponsorOutputDTO } from '@/lib/types/generated/GetSponsorOutputDTO';
import type { GetPendingSponsorshipRequestsOutputDTO } from '@/lib/types/generated/GetPendingSponsorshipRequestsOutputDTO';
import type { SponsorBillingDTO } from '@/lib/types/tbd/SponsorBillingDTO';
import type { AvailableLeaguesDTO } from '@/lib/types/tbd/AvailableLeaguesDTO';
import type { LeagueDetailForSponsorDTO } from '@/lib/types/tbd/LeagueDetailForSponsorDTO';
import type { SponsorSettingsDTO } from '@/lib/types/tbd/SponsorSettingsDTO';
/**
* Sponsor Service - DTO Only
*
* Returns raw API DTOs. No ViewModels or UX logic.
* All client-side presentation logic must be handled by hooks/components.
*/
@injectable()
export class SponsorService implements Service {
private apiClient: SponsorsApiClient;
private readonly apiClient: SponsorsApiClient;
constructor() {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const { NODE_ENV } = getWebsiteServerEnv();
const errorReporter = new EnhancedErrorReporter(logger, {
showUserNotifications: true,
logToConsole: true,
reportToExternal: NODE_ENV === 'production',
});
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new SponsorsApiClient(baseUrl, errorReporter, logger);
}
async getSponsorById(sponsorId: string): Promise<Result<GetSponsorOutputDTO, DomainError>> {
try {
const result = await this.apiClient.getSponsor(sponsorId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Sponsor not found' });
}
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to get sponsor' });
}
async getAllSponsors(): Promise<SponsorViewModel[]> {
const data = await this.apiClient.getAll();
return data.sponsors.map(s => new SponsorViewModel(s));
}
async getSponsorDashboard(sponsorId: string): Promise<Result<SponsorDashboardDTO, DomainError>> {
try {
const result = await this.apiClient.getDashboard(sponsorId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Dashboard not found' });
}
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'notImplemented', message: (error as Error).message || 'getSponsorDashboard' });
const data = await this.apiClient.getDashboard(sponsorId);
if (!data) return Result.err({ type: 'notFound', message: 'Sponsor dashboard not found' });
return Result.ok(data);
} catch (error) {
return Result.err({ type: 'serverError', message: error instanceof Error ? error.message : 'Unknown error' });
}
}
async getSponsorSponsorships(sponsorId: string): Promise<Result<SponsorSponsorshipsDTO, DomainError>> {
try {
const result = await this.apiClient.getSponsorships(sponsorId);
if (!result) {
return Result.err({ type: 'notFound', message: 'Sponsorships not found' });
}
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'notImplemented', message: (error as Error).message || 'getSponsorSponsorships' });
const data = await this.apiClient.getSponsorships(sponsorId);
if (!data) return Result.err({ type: 'notFound', message: 'Sponsor sponsorships not found' });
return Result.ok(data);
} catch (error) {
return Result.err({ type: 'serverError', message: error instanceof Error ? error.message : 'Unknown error' });
}
}
async getBilling(_: string): Promise<Result<SponsorBillingDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getBilling' });
async createSponsor(input: any): Promise<any> {
return this.apiClient.create(input);
}
async getAvailableLeagues(): Promise<Result<AvailableLeaguesDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getAvailableLeagues' });
}
async getLeagueDetail(): Promise<Result<LeagueDetailForSponsorDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getLeagueDetail' });
}
async getSettings(): Promise<Result<SponsorSettingsDTO, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'getSettings' });
}
async updateSettings(): Promise<Result<void, DomainError>> {
return Result.err({ type: 'notImplemented', message: 'updateSettings' });
}
async acceptSponsorshipRequest(requestId: string, sponsorId: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.acceptSponsorshipRequest(requestId, { respondedBy: sponsorId });
return Result.ok(undefined);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to accept sponsorship request' });
}
}
async rejectSponsorshipRequest(requestId: string, sponsorId: string, reason?: string): Promise<Result<void, DomainError>> {
try {
await this.apiClient.rejectSponsorshipRequest(requestId, { respondedBy: sponsorId, reason });
return Result.ok(undefined);
} catch (error: unknown) {
return Result.err({ type: 'unknown', message: (error as Error).message || 'Failed to reject sponsorship request' });
}
}
async getPendingSponsorshipRequests(input: { entityType: string; entityId: string }): Promise<Result<GetPendingSponsorshipRequestsOutputDTO, DomainError>> {
try {
const result = await this.apiClient.getPendingSponsorshipRequests(input);
return Result.ok(result);
} catch (error: unknown) {
return Result.err({ type: 'notImplemented', message: (error as Error).message || 'getPendingSponsorshipRequests' });
}
async getSponsorshipPricing(): Promise<any> {
return this.apiClient.getPricing();
}
}

View File

@@ -0,0 +1,43 @@
import { injectable, unmanaged } from 'inversify';
import { SponsorsApiClient } from '@/lib/api/sponsors/SponsorsApiClient';
import { SponsorshipPricingViewModel } from '@/lib/view-models/SponsorshipPricingViewModel';
import { SponsorSponsorshipsViewModel } from '@/lib/view-models/SponsorSponsorshipsViewModel';
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
import { ConsoleLogger } from '@/lib/infrastructure/logging/ConsoleLogger';
import { EnhancedErrorReporter } from '@/lib/infrastructure/EnhancedErrorReporter';
import { Service } from '@/lib/contracts/services/Service';
@injectable()
export class SponsorshipService implements Service {
private readonly apiClient: SponsorsApiClient;
constructor(@unmanaged() apiClient?: SponsorsApiClient) {
if (apiClient) {
this.apiClient = apiClient;
} else {
const baseUrl = getWebsiteApiBaseUrl();
const logger = new ConsoleLogger();
const errorReporter = new EnhancedErrorReporter(logger);
this.apiClient = new SponsorsApiClient(baseUrl, errorReporter, logger);
}
}
async getSponsorshipPricing(leagueId?: string): Promise<SponsorshipPricingViewModel> {
const data = await this.apiClient.getPricing();
// Map the array-based pricing to the expected view model format
const mainSlot = data.pricing.find(p => p.entityType === 'league');
const secondarySlot = data.pricing.find(p => p.entityType === 'driver');
return new SponsorshipPricingViewModel({
mainSlotPrice: mainSlot?.price || 0,
secondarySlotPrice: secondarySlot?.price || 0,
currency: 'USD' // Default currency as it's missing from API
});
}
async getSponsorSponsorships(sponsorId: string): Promise<SponsorSponsorshipsViewModel | null> {
const data = await this.apiClient.getSponsorships(sponsorId);
if (!data) return null;
return new SponsorSponsorshipsViewModel(data);
}
}