refactor
This commit is contained in:
@@ -17,8 +17,7 @@ import { ISponsorshipPricingRepository } from '@core/racing/domain/repositories/
|
||||
import { ISponsorshipRequestRepository } from '@core/racing/domain/repositories/ISponsorshipRequestRepository';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
// Import use cases / application services
|
||||
import { SponsorBillingService } from '@core/payments/application/services/SponsorBillingService';
|
||||
import { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
|
||||
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
|
||||
import { CreateSponsorUseCase } from '@core/racing/application/use-cases/CreateSponsorUseCase';
|
||||
import { GetEntitySponsorshipPricingUseCase } from '@core/racing/application/use-cases/GetEntitySponsorshipPricingUseCase';
|
||||
@@ -63,7 +62,7 @@ export const GET_SPONSOR_USE_CASE_TOKEN = 'GetSponsorUseCase';
|
||||
export const GET_PENDING_SPONSORSHIP_REQUESTS_USE_CASE_TOKEN = 'GetPendingSponsorshipRequestsUseCase';
|
||||
export const ACCEPT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'AcceptSponsorshipRequestUseCase';
|
||||
export const REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN = 'RejectSponsorshipRequestUseCase';
|
||||
export const SPONSOR_BILLING_SERVICE_TOKEN = 'SponsorBillingService';
|
||||
export const GET_SPONSOR_BILLING_USE_CASE_TOKEN = 'GetSponsorBillingUseCase';
|
||||
|
||||
export const SponsorProviders: Provider[] = [
|
||||
SponsorService,
|
||||
@@ -154,9 +153,9 @@ export const SponsorProviders: Provider[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
provide: SPONSOR_BILLING_SERVICE_TOKEN,
|
||||
provide: GET_SPONSOR_BILLING_USE_CASE_TOKEN,
|
||||
useFactory: (paymentRepo: IPaymentRepository, seasonSponsorshipRepo: ISeasonSponsorshipRepository) =>
|
||||
new SponsorBillingService(paymentRepo, seasonSponsorshipRepo),
|
||||
new GetSponsorBillingUseCase(paymentRepo, seasonSponsorshipRepo),
|
||||
inject: ['IPaymentRepository', SEASON_SPONSORSHIP_REPOSITORY_TOKEN],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
} from '@core/racing/application/use-cases/GetPendingSponsorshipRequestsUseCase';
|
||||
import { AcceptSponsorshipRequestUseCase } from '@core/racing/application/use-cases/AcceptSponsorshipRequestUseCase';
|
||||
import { RejectSponsorshipRequestUseCase } from '@core/racing/application/use-cases/RejectSponsorshipRequestUseCase';
|
||||
import { GetSponsorBillingUseCase } from '@core/payments/application/use-cases/GetSponsorBillingUseCase';
|
||||
import { GET_SPONSOR_BILLING_USE_CASE_TOKEN } from './SponsorProviders';
|
||||
import type { SponsorableEntityType } from '@core/racing/domain/entities/SponsorshipRequest';
|
||||
import type { Logger } from '@core/shared/application';
|
||||
|
||||
@@ -82,6 +84,8 @@ export class SponsorService {
|
||||
private readonly acceptSponsorshipRequestUseCase: AcceptSponsorshipRequestUseCase,
|
||||
@Inject(REJECT_SPONSORSHIP_REQUEST_USE_CASE_TOKEN)
|
||||
private readonly rejectSponsorshipRequestUseCase: RejectSponsorshipRequestUseCase,
|
||||
@Inject(GET_SPONSOR_BILLING_USE_CASE_TOKEN)
|
||||
private readonly getSponsorBillingUseCase: GetSponsorBillingUseCase,
|
||||
@Inject(LOGGER_TOKEN)
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
@@ -102,20 +106,15 @@ export class SponsorService {
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getSponsors(): Promise<GetSponsorsPresenter> {
|
||||
async getSponsors(): Promise<GetSponsorsOutputDTO> {
|
||||
this.logger.debug('[SponsorService] Fetching sponsors.');
|
||||
|
||||
const presenter = new GetSponsorsPresenter();
|
||||
const result = await this.getSponsorsUseCase.execute();
|
||||
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to fetch sponsors.', result.error);
|
||||
presenter.present({ sponsors: [] });
|
||||
return presenter;
|
||||
}
|
||||
presenter.present(result);
|
||||
|
||||
presenter.present(result.value);
|
||||
return presenter;
|
||||
return presenter.responseModel;
|
||||
}
|
||||
|
||||
async createSponsor(input: CreateSponsorInputDTO): Promise<CreateSponsorPresenter> {
|
||||
@@ -264,92 +263,18 @@ export class SponsorService {
|
||||
return presenter;
|
||||
}
|
||||
|
||||
async getSponsorBilling(sponsorId: string): Promise<SponsorBillingPresenter> {
|
||||
async getSponsorBilling(sponsorId: string): Promise<GetSponsorBillingPresenter> {
|
||||
this.logger.debug('[SponsorService] Fetching sponsor billing.', { sponsorId });
|
||||
|
||||
const presenter = new SponsorBillingPresenter();
|
||||
const result = await this.getSponsorBillingUseCase.execute({ sponsorId });
|
||||
|
||||
// Mock data - in real implementation, this would come from repositories
|
||||
const paymentMethods: PaymentMethodDTO[] = [
|
||||
{
|
||||
id: 'pm-1',
|
||||
type: 'card',
|
||||
last4: '4242',
|
||||
brand: 'Visa',
|
||||
isDefault: true,
|
||||
expiryMonth: 12,
|
||||
expiryYear: 2027,
|
||||
},
|
||||
{
|
||||
id: 'pm-2',
|
||||
type: 'card',
|
||||
last4: '5555',
|
||||
brand: 'Mastercard',
|
||||
isDefault: false,
|
||||
expiryMonth: 6,
|
||||
expiryYear: 2026,
|
||||
},
|
||||
{
|
||||
id: 'pm-3',
|
||||
type: 'sepa',
|
||||
last4: '8901',
|
||||
bankName: 'Deutsche Bank',
|
||||
isDefault: false,
|
||||
},
|
||||
];
|
||||
if (result.isErr()) {
|
||||
this.logger.error('[SponsorService] Failed to fetch sponsor billing.', result.error);
|
||||
throw new Error(result.error.details?.message || 'Failed to fetch sponsor billing');
|
||||
}
|
||||
|
||||
const invoices: InvoiceDTO[] = [
|
||||
{
|
||||
id: 'inv-1',
|
||||
invoiceNumber: 'GP-2025-001234',
|
||||
date: '2025-11-01',
|
||||
dueDate: '2025-11-15',
|
||||
amount: 1090.91,
|
||||
vatAmount: 207.27,
|
||||
totalAmount: 1298.18,
|
||||
status: 'paid',
|
||||
description: 'GT3 Pro Championship - Primary Sponsor (Q4 2025)',
|
||||
sponsorshipType: 'league',
|
||||
pdfUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 'inv-2',
|
||||
invoiceNumber: 'GP-2025-001235',
|
||||
date: '2025-10-01',
|
||||
dueDate: '2025-10-15',
|
||||
amount: 363.64,
|
||||
vatAmount: 69.09,
|
||||
totalAmount: 432.73,
|
||||
status: 'paid',
|
||||
description: 'Team Velocity - Gear Sponsor (Q4 2025)',
|
||||
sponsorshipType: 'team',
|
||||
pdfUrl: '#',
|
||||
},
|
||||
{
|
||||
id: 'inv-3',
|
||||
invoiceNumber: 'GP-2025-001236',
|
||||
date: '2025-12-01',
|
||||
dueDate: '2025-12-15',
|
||||
amount: 318.18,
|
||||
vatAmount: 60.45,
|
||||
totalAmount: 378.63,
|
||||
status: 'pending',
|
||||
description: 'Alex Thompson - Driver Sponsorship (Dec 2025)',
|
||||
sponsorshipType: 'driver',
|
||||
pdfUrl: '#',
|
||||
},
|
||||
];
|
||||
|
||||
const stats: BillingStatsDTO = {
|
||||
totalSpent: 12450,
|
||||
pendingAmount: 919.54,
|
||||
nextPaymentDate: '2025-12-15',
|
||||
nextPaymentAmount: 378.63,
|
||||
activeSponsorships: 6,
|
||||
averageMonthlySpend: 2075,
|
||||
};
|
||||
|
||||
presenter.present({ paymentMethods, invoices, stats });
|
||||
const presenter = new GetSponsorBillingPresenter();
|
||||
presenter.present(result.value);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type {
|
||||
GetSponsorsResult,
|
||||
GetSponsorsErrorCode,
|
||||
} from '@core/racing/application/use-cases/GetSponsorsUseCase';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import { GetSponsorsPresenter } from './GetSponsorsPresenter';
|
||||
|
||||
describe('GetSponsorsPresenter', () => {
|
||||
@@ -9,54 +15,92 @@ describe('GetSponsorsPresenter', () => {
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should reset the result to null', () => {
|
||||
const mockResult = { sponsors: [] };
|
||||
presenter.present(mockResult);
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
it('should reset the model to null and cause responseModel to throw', () => {
|
||||
const result = Result.ok<GetSponsorsResult, never>({ sponsors: [] });
|
||||
presenter.present(result);
|
||||
expect(presenter.responseModel).toEqual({ sponsors: [] });
|
||||
|
||||
presenter.reset();
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
expect(() => presenter.responseModel).toThrow('Presenter not presented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('present', () => {
|
||||
it('should store the result', () => {
|
||||
const mockResult = {
|
||||
it('should map Result.ok sponsors to DTO responseModel', () => {
|
||||
const result = Result.ok<GetSponsorsResult, never>({
|
||||
sponsors: [
|
||||
{ id: 'sponsor-1', name: 'Sponsor One', contactEmail: 's1@example.com' },
|
||||
{ id: 'sponsor-2', name: 'Sponsor Two', contactEmail: 's2@example.com' },
|
||||
{
|
||||
id: 'sponsor-1',
|
||||
name: 'Sponsor One',
|
||||
contactEmail: 's1@example.com',
|
||||
logoUrl: 'logo1.png',
|
||||
websiteUrl: 'https://one.example.com',
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
},
|
||||
{
|
||||
id: 'sponsor-2',
|
||||
name: 'Sponsor Two',
|
||||
contactEmail: 's2@example.com',
|
||||
logoUrl: undefined,
|
||||
websiteUrl: undefined,
|
||||
createdAt: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
presenter.present(mockResult);
|
||||
presenter.present(result);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
expect(presenter.responseModel).toEqual({
|
||||
sponsors: [
|
||||
{
|
||||
id: 'sponsor-1',
|
||||
name: 'Sponsor One',
|
||||
contactEmail: 's1@example.com',
|
||||
logoUrl: 'logo1.png',
|
||||
websiteUrl: 'https://one.example.com',
|
||||
createdAt: new Date('2024-01-01T00:00:00Z'),
|
||||
},
|
||||
{
|
||||
id: 'sponsor-2',
|
||||
name: 'Sponsor Two',
|
||||
contactEmail: 's2@example.com',
|
||||
logoUrl: undefined,
|
||||
websiteUrl: undefined,
|
||||
createdAt: undefined,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getViewModel', () => {
|
||||
describe('getResponseModel', () => {
|
||||
it('should return null when not presented', () => {
|
||||
expect(presenter.getViewModel()).toBeNull();
|
||||
expect(presenter.getResponseModel()).toBeNull();
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsors: [] };
|
||||
presenter.present(mockResult);
|
||||
it('should return the model when presented', () => {
|
||||
const result = Result.ok<GetSponsorsResult, never>({ sponsors: [] });
|
||||
presenter.present(result);
|
||||
|
||||
expect(presenter.getViewModel()).toEqual(mockResult);
|
||||
expect(presenter.getResponseModel()).toEqual({ sponsors: [] });
|
||||
});
|
||||
});
|
||||
|
||||
describe('viewModel', () => {
|
||||
describe('responseModel', () => {
|
||||
it('should throw error when not presented', () => {
|
||||
expect(() => presenter.viewModel).toThrow('Presenter not presented');
|
||||
expect(() => presenter.responseModel).toThrow('Presenter not presented');
|
||||
});
|
||||
|
||||
it('should return the result when presented', () => {
|
||||
const mockResult = { sponsors: [] };
|
||||
presenter.present(mockResult);
|
||||
it('should fallback to empty sponsors list on error', () => {
|
||||
const error = {
|
||||
code: 'REPOSITORY_ERROR' as GetSponsorsErrorCode,
|
||||
details: { message: 'DB error' },
|
||||
} satisfies ApplicationErrorCode<GetSponsorsErrorCode, { message: string }>;
|
||||
const result = Result.err<GetSponsorsResult, typeof error>(error);
|
||||
|
||||
expect(presenter.viewModel).toEqual(mockResult);
|
||||
presenter.present(result);
|
||||
|
||||
expect(presenter.responseModel).toEqual({ sponsors: [] });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,51 @@
|
||||
import type { GetSponsorsOutputPort } from '@core/racing/application/ports/output/GetSponsorsOutputPort';
|
||||
import type { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type {
|
||||
GetSponsorsResult,
|
||||
GetSponsorsErrorCode,
|
||||
} from '@core/racing/application/use-cases/GetSponsorsUseCase';
|
||||
import { GetSponsorsOutputDTO } from '../dtos/GetSponsorsOutputDTO';
|
||||
import type { SponsorDTO } from '../dtos/SponsorDTO';
|
||||
|
||||
export class GetSponsorsPresenter {
|
||||
private result: GetSponsorsOutputDTO | null = null;
|
||||
private model: GetSponsorsOutputDTO | null = null;
|
||||
|
||||
reset() {
|
||||
this.result = null;
|
||||
this.model = null;
|
||||
}
|
||||
|
||||
present(outputPort: GetSponsorsOutputPort) {
|
||||
this.result = {
|
||||
sponsors: outputPort.sponsors,
|
||||
present(
|
||||
result: Result<
|
||||
GetSponsorsResult,
|
||||
ApplicationErrorCode<GetSponsorsErrorCode, { message: string }>
|
||||
>,
|
||||
): void {
|
||||
if (result.isErr()) {
|
||||
// For sponsor listing, fall back to empty list on error
|
||||
this.model = { sponsors: [] };
|
||||
return;
|
||||
}
|
||||
|
||||
const output = result.unwrap();
|
||||
|
||||
this.model = {
|
||||
sponsors: output.sponsors.map<SponsorDTO>((sponsor) => ({
|
||||
id: sponsor.id,
|
||||
name: sponsor.name,
|
||||
contactEmail: sponsor.contactEmail,
|
||||
logoUrl: sponsor.logoUrl,
|
||||
websiteUrl: sponsor.websiteUrl,
|
||||
createdAt: sponsor.createdAt,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
getViewModel(): GetSponsorsOutputDTO | null {
|
||||
return this.result;
|
||||
getResponseModel(): GetSponsorsOutputDTO | null {
|
||||
return this.model;
|
||||
}
|
||||
|
||||
get viewModel(): GetSponsorsOutputDTO {
|
||||
if (!this.result) throw new Error('Presenter not presented');
|
||||
return this.result;
|
||||
get responseModel(): GetSponsorsOutputDTO {
|
||||
if (!this.model) throw new Error('Presenter not presented');
|
||||
return this.model;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user