237 lines
8.4 KiB
TypeScript
237 lines
8.4 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { NotificationService } from '../../../../core/notifications/application/ports/NotificationService';
|
|
import type { WalletRepository } from '../../../../core/payments/domain/repositories/WalletRepository';
|
|
import type { Logger } from '../../../../core/shared/domain/Logger';
|
|
import { LeagueWallet } from '../../../../core/racing/domain/entities/league-wallet/LeagueWallet';
|
|
import { League } from '../../../../core/racing/domain/entities/League';
|
|
import { Season } from '../../../../core/racing/domain/entities/season/Season';
|
|
import { SponsorshipRequest } from '../../../../core/racing/domain/entities/SponsorshipRequest';
|
|
import { Money } from '../../../../core/racing/domain/value-objects/Money';
|
|
import { Sponsor } from '../../../../core/racing/domain/entities/sponsor/Sponsor';
|
|
import { SponsorshipPricing } from '../../../../core/racing/domain/value-objects/SponsorshipPricing';
|
|
|
|
import { ApplyForSponsorshipUseCase } from '../../../../core/racing/application/use-cases/ApplyForSponsorshipUseCase';
|
|
import { GetPendingSponsorshipRequestsUseCase } 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 { LeaguesTestContext } from '../LeaguesTestContext';
|
|
import { SponsorTestContext } from '../../sponsor/SponsorTestContext';
|
|
import { InMemorySponsorshipRequestRepository } from '../../../../adapters/racing/persistence/inmemory/InMemorySponsorshipRequestRepository';
|
|
import { InMemoryLeagueWalletRepository } from '../../../../adapters/racing/persistence/inmemory/InMemoryLeagueWalletRepository';
|
|
import { InMemoryWalletRepository } from '../../../../adapters/payments/persistence/inmemory/InMemoryWalletRepository';
|
|
|
|
const createNoopNotificationService = (): NotificationService =>
|
|
({
|
|
sendNotification: vi.fn(async () => undefined),
|
|
}) as unknown as NotificationService;
|
|
|
|
const createNoopLogger = (): Logger =>
|
|
({
|
|
info: () => {},
|
|
debug: () => {},
|
|
warn: () => {},
|
|
error: () => {},
|
|
}) as unknown as Logger;
|
|
|
|
describe('League Sponsorships - Sponsorship Applications', () => {
|
|
let leagues: LeaguesTestContext;
|
|
let sponsors: SponsorTestContext;
|
|
|
|
let sponsorshipRequestRepo: InMemorySponsorshipRequestRepository;
|
|
let sponsorWalletRepo: WalletRepository;
|
|
let leagueWalletRepo: InMemoryLeagueWalletRepository;
|
|
|
|
beforeEach(() => {
|
|
leagues = new LeaguesTestContext();
|
|
leagues.clear();
|
|
|
|
sponsors = new SponsorTestContext();
|
|
sponsors.clear();
|
|
|
|
sponsorshipRequestRepo = new InMemorySponsorshipRequestRepository(createNoopLogger());
|
|
sponsorWalletRepo = new InMemoryWalletRepository(createNoopLogger());
|
|
leagueWalletRepo = new InMemoryLeagueWalletRepository(createNoopLogger());
|
|
});
|
|
|
|
const seedLeagueAndSeason = async (params: { leagueId: string; seasonId: string }) => {
|
|
const league = League.create({
|
|
id: params.leagueId,
|
|
name: 'League 1',
|
|
description: 'League used for sponsorship integration tests',
|
|
ownerId: 'owner-1',
|
|
});
|
|
await leagues.racingLeagueRepository.create(league);
|
|
|
|
const season = Season.create({
|
|
id: params.seasonId,
|
|
leagueId: params.leagueId,
|
|
gameId: 'iracing',
|
|
name: 'Season 1',
|
|
status: 'active',
|
|
startDate: new Date('2025-01-01T00:00:00.000Z'),
|
|
endDate: new Date('2025-02-01T00:00:00.000Z'),
|
|
});
|
|
await leagues.seasonRepository.create(season);
|
|
|
|
return { league, season };
|
|
};
|
|
|
|
it('allows a sponsor to apply for a season sponsorship and lists it as pending', async () => {
|
|
const leagueId = 'league-1';
|
|
const seasonId = 'season-1';
|
|
const sponsorId = 'sponsor-1';
|
|
|
|
await seedLeagueAndSeason({ leagueId, seasonId });
|
|
|
|
const sponsor = Sponsor.create({
|
|
id: sponsorId,
|
|
name: 'Acme',
|
|
contactEmail: 'acme@example.com',
|
|
});
|
|
await sponsors.sponsorRepository.create(sponsor);
|
|
|
|
const pricing = SponsorshipPricing.create({
|
|
acceptingApplications: true,
|
|
})
|
|
.updateMainSlot({
|
|
available: true,
|
|
maxSlots: 1,
|
|
price: Money.create(1000, 'USD'),
|
|
benefits: ['logo'],
|
|
})
|
|
.updateSecondarySlot({
|
|
available: true,
|
|
maxSlots: 2,
|
|
price: Money.create(500, 'USD'),
|
|
benefits: ['mention'],
|
|
});
|
|
|
|
await sponsors.sponsorshipPricingRepository.save('season', seasonId, pricing);
|
|
|
|
const applyUseCase = new ApplyForSponsorshipUseCase(
|
|
sponsorshipRequestRepo,
|
|
sponsors.sponsorshipPricingRepository,
|
|
sponsors.sponsorRepository,
|
|
sponsors.logger,
|
|
);
|
|
|
|
const apply = await applyUseCase.execute({
|
|
sponsorId,
|
|
entityType: 'season',
|
|
entityId: seasonId,
|
|
tier: 'main',
|
|
offeredAmount: 1000,
|
|
currency: 'USD',
|
|
message: 'We would like to sponsor',
|
|
});
|
|
|
|
expect(apply.isOk()).toBe(true);
|
|
|
|
const getPending = new GetPendingSponsorshipRequestsUseCase(sponsorshipRequestRepo, sponsors.sponsorRepository);
|
|
const pending = await getPending.execute({ entityType: 'season', entityId: seasonId });
|
|
|
|
expect(pending.isOk()).toBe(true);
|
|
const value = pending.unwrap();
|
|
expect(value.totalCount).toBe(1);
|
|
expect(value.requests[0]!.request.status).toBe('pending');
|
|
expect(value.requests[0]!.sponsor?.id.toString()).toBe(sponsorId);
|
|
});
|
|
|
|
it('accepts a pending season sponsorship request, creates a sponsorship, and updates wallets', async () => {
|
|
const leagueId = 'league-1';
|
|
const seasonId = 'season-1';
|
|
const sponsorId = 'sponsor-1';
|
|
|
|
await seedLeagueAndSeason({ leagueId, seasonId });
|
|
|
|
const sponsor = Sponsor.create({
|
|
id: sponsorId,
|
|
name: 'Acme',
|
|
contactEmail: 'acme@example.com',
|
|
});
|
|
await sponsors.sponsorRepository.create(sponsor);
|
|
|
|
const request = SponsorshipRequest.create({
|
|
id: 'req-1',
|
|
sponsorId,
|
|
entityType: 'season',
|
|
entityId: seasonId,
|
|
tier: 'main',
|
|
offeredAmount: Money.create(1000, 'USD'),
|
|
message: 'Please accept',
|
|
});
|
|
await sponsorshipRequestRepo.create(request);
|
|
|
|
await sponsorWalletRepo.create({
|
|
id: sponsorId,
|
|
leagueId: 'n/a',
|
|
balance: 1500,
|
|
totalRevenue: 0,
|
|
totalPlatformFees: 0,
|
|
totalWithdrawn: 0,
|
|
currency: 'USD',
|
|
createdAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
});
|
|
|
|
const leagueWallet = LeagueWallet.create({
|
|
id: leagueId,
|
|
leagueId,
|
|
balance: Money.create(0, 'USD'),
|
|
});
|
|
await leagueWalletRepo.create(leagueWallet);
|
|
|
|
const notificationService = createNoopNotificationService();
|
|
|
|
const acceptUseCase = new AcceptSponsorshipRequestUseCase(
|
|
sponsorshipRequestRepo,
|
|
leagues.seasonSponsorshipRepository,
|
|
leagues.seasonRepository,
|
|
notificationService,
|
|
async () => ({ success: true, transactionId: 'tx-1' }),
|
|
sponsorWalletRepo,
|
|
leagueWalletRepo,
|
|
createNoopLogger(),
|
|
);
|
|
|
|
const result = await acceptUseCase.execute({ requestId: 'req-1', respondedBy: 'owner-1' });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
|
|
const updatedSponsorWallet = await sponsorWalletRepo.findById(sponsorId);
|
|
expect(updatedSponsorWallet?.balance).toBe(500);
|
|
|
|
const updatedLeagueWallet = await leagueWalletRepo.findById(leagueId);
|
|
expect(updatedLeagueWallet?.balance.amount).toBe(900);
|
|
|
|
expect((notificationService.sendNotification as unknown as ReturnType<typeof vi.fn>)).toHaveBeenCalledTimes(1);
|
|
|
|
const sponsorships = await leagues.seasonSponsorshipRepository.findBySeasonId(seasonId);
|
|
expect(sponsorships).toHaveLength(1);
|
|
expect(sponsorships[0]!.status).toBe('active');
|
|
});
|
|
|
|
it('rejects a pending sponsorship request', async () => {
|
|
const sponsorId = 'sponsor-1';
|
|
|
|
const request = SponsorshipRequest.create({
|
|
id: 'req-1',
|
|
sponsorId,
|
|
entityType: 'season',
|
|
entityId: 'season-1',
|
|
tier: 'main',
|
|
offeredAmount: Money.create(1000, 'USD'),
|
|
});
|
|
await sponsorshipRequestRepo.create(request);
|
|
|
|
const rejectUseCase = new RejectSponsorshipRequestUseCase(sponsorshipRequestRepo, createNoopLogger());
|
|
|
|
const result = await rejectUseCase.execute({ requestId: 'req-1', respondedBy: 'owner-1', reason: 'Not a fit' });
|
|
|
|
expect(result.isOk()).toBe(true);
|
|
|
|
const updated = await sponsorshipRequestRepo.findById('req-1');
|
|
expect(updated?.status).toBe('rejected');
|
|
});
|
|
});
|