website refactor
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
43
apps/website/lib/services/sponsors/SponsorshipService.ts
Normal file
43
apps/website/lib/services/sponsors/SponsorshipService.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user