219 lines
7.9 KiB
TypeScript
219 lines
7.9 KiB
TypeScript
/**
|
|
* Use Case: AcceptSponsorshipRequestUseCase
|
|
*
|
|
* Allows an entity owner to accept a sponsorship request.
|
|
* This creates an active sponsorship and notifies the sponsor.
|
|
*/
|
|
|
|
import type { NotificationService } from '@/notifications/application/ports/NotificationService';
|
|
import type { IWalletRepository } from '@core/payments/domain/repositories/IWalletRepository';
|
|
import type { Logger } from '@core/shared/application';
|
|
import { Result } from '@core/shared/application/Result';
|
|
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
|
import type { UseCaseOutputPort } from '@core/shared/application/UseCaseOutputPort';
|
|
import { SeasonSponsorship } from '../../domain/entities/season/SeasonSponsorship';
|
|
import type { ILeagueWalletRepository } from '../../domain/repositories/ILeagueWalletRepository';
|
|
import type { ISeasonRepository } from '../../domain/repositories/ISeasonRepository';
|
|
import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISeasonSponsorshipRepository';
|
|
import type { ISponsorshipRequestRepository } from '../../domain/repositories/ISponsorshipRequestRepository';
|
|
|
|
export interface AcceptSponsorshipRequestInput {
|
|
requestId: string;
|
|
respondedBy: string;
|
|
}
|
|
|
|
export interface AcceptSponsorshipResult {
|
|
requestId: string;
|
|
sponsorshipId: string;
|
|
status: 'accepted';
|
|
acceptedAt: Date;
|
|
platformFee: number;
|
|
netAmount: number;
|
|
}
|
|
|
|
export interface ProcessPaymentInput {
|
|
amount: number;
|
|
payerId: string;
|
|
description: string;
|
|
metadata?: Record<string, unknown>;
|
|
}
|
|
|
|
export interface ProcessPaymentResult {
|
|
success: boolean;
|
|
transactionId?: string;
|
|
error?: string;
|
|
}
|
|
|
|
export class AcceptSponsorshipRequestUseCase {
|
|
constructor(
|
|
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
|
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
|
private readonly seasonRepository: ISeasonRepository,
|
|
private readonly notificationService: NotificationService,
|
|
private readonly paymentProcessor: (input: ProcessPaymentInput) => Promise<ProcessPaymentResult>,
|
|
private readonly walletRepository: IWalletRepository,
|
|
private readonly leagueWalletRepository: ILeagueWalletRepository,
|
|
private readonly logger: Logger,
|
|
private readonly output: UseCaseOutputPort<AcceptSponsorshipResult>,
|
|
) {}
|
|
|
|
async execute(
|
|
input: AcceptSponsorshipRequestInput,
|
|
): Promise<
|
|
Result<
|
|
void,
|
|
ApplicationErrorCode<
|
|
| 'SPONSORSHIP_REQUEST_NOT_FOUND'
|
|
| 'SPONSORSHIP_REQUEST_NOT_PENDING'
|
|
| 'SEASON_NOT_FOUND'
|
|
| 'PAYMENT_PROCESSING_FAILED'
|
|
| 'SPONSOR_WALLET_NOT_FOUND'
|
|
| 'LEAGUE_WALLET_NOT_FOUND'
|
|
>
|
|
>
|
|
> {
|
|
this.logger.debug(`Attempting to accept sponsorship request: ${input.requestId}`, {
|
|
requestId: input.requestId,
|
|
respondedBy: input.respondedBy,
|
|
});
|
|
|
|
// Find the request
|
|
const request = await this.sponsorshipRequestRepo.findById(input.requestId);
|
|
if (!request) {
|
|
this.logger.warn(`Sponsorship request not found: ${input.requestId}`, { requestId: input.requestId });
|
|
return Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_FOUND' });
|
|
}
|
|
|
|
if (!request.isPending()) {
|
|
this.logger.warn(`Cannot accept a ${request.status} sponsorship request: ${input.requestId}`, {
|
|
requestId: input.requestId,
|
|
status: request.status,
|
|
});
|
|
return Result.err({ code: 'SPONSORSHIP_REQUEST_NOT_PENDING' });
|
|
}
|
|
|
|
this.logger.info(`Sponsorship request ${input.requestId} found and is pending. Proceeding with acceptance.`, {
|
|
requestId: input.requestId,
|
|
});
|
|
|
|
// Accept the request
|
|
const acceptedRequest = request.accept(input.respondedBy);
|
|
await this.sponsorshipRequestRepo.update(acceptedRequest);
|
|
this.logger.debug(`Sponsorship request ${input.requestId} accepted and updated in repository.`, {
|
|
requestId: input.requestId,
|
|
});
|
|
|
|
// If this is a season sponsorship, create the SeasonSponsorship record
|
|
let sponsorshipId = `spons_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
if (request.entityType === 'season') {
|
|
this.logger.debug(`Sponsorship request ${input.requestId} is for a season. Creating SeasonSponsorship record.`, {
|
|
requestId: input.requestId,
|
|
entityType: request.entityType,
|
|
});
|
|
const season = await this.seasonRepository.findById(request.entityId);
|
|
if (!season) {
|
|
this.logger.warn(
|
|
`Season not found for sponsorship request ${input.requestId} and entityId ${request.entityId}`,
|
|
{ requestId: input.requestId, entityId: request.entityId },
|
|
);
|
|
return Result.err({ code: 'SEASON_NOT_FOUND' });
|
|
}
|
|
|
|
const sponsorship = SeasonSponsorship.create({
|
|
id: sponsorshipId,
|
|
seasonId: season.id,
|
|
leagueId: season.leagueId,
|
|
sponsorId: request.sponsorId,
|
|
tier: request.tier,
|
|
pricing: request.offeredAmount,
|
|
status: 'active',
|
|
});
|
|
await this.seasonSponsorshipRepo.create(sponsorship);
|
|
this.logger.info(`Season sponsorship ${sponsorshipId} created for request ${input.requestId}.`, {
|
|
sponsorshipId,
|
|
requestId: input.requestId,
|
|
});
|
|
|
|
// Notify the sponsor
|
|
await this.notificationService.sendNotification({
|
|
recipientId: request.sponsorId,
|
|
type: 'sponsorship_request_accepted',
|
|
title: 'Sponsorship Accepted',
|
|
body: `Your sponsorship request for ${season.name} has been accepted.`,
|
|
channel: 'in_app',
|
|
urgency: 'toast',
|
|
data: {
|
|
requestId: request.id,
|
|
sponsorshipId,
|
|
},
|
|
});
|
|
|
|
// Process payment using clean input/output ports with primitive types
|
|
const paymentInput: ProcessPaymentInput = {
|
|
amount: request.offeredAmount.amount,
|
|
payerId: request.sponsorId,
|
|
description: `Sponsorship payment for ${request.entityType} ${request.entityId}`,
|
|
metadata: { requestId: request.id },
|
|
};
|
|
|
|
const paymentResult = await this.paymentProcessor(paymentInput);
|
|
if (!paymentResult.success) {
|
|
this.logger.error(
|
|
`Payment failed for sponsorship request ${request.id}: ${paymentResult.error}`,
|
|
undefined,
|
|
{ requestId: request.id },
|
|
);
|
|
return Result.err({ code: 'PAYMENT_PROCESSING_FAILED' });
|
|
}
|
|
|
|
// Update wallets
|
|
const sponsorWallet = await this.walletRepository.findById(request.sponsorId);
|
|
if (!sponsorWallet) {
|
|
this.logger.error(`Sponsor wallet not found for ${request.sponsorId}`, undefined, {
|
|
sponsorId: request.sponsorId,
|
|
});
|
|
return Result.err({ code: 'SPONSOR_WALLET_NOT_FOUND' });
|
|
}
|
|
|
|
const leagueWallet = await this.leagueWalletRepository.findById(season.leagueId);
|
|
if (!leagueWallet) {
|
|
this.logger.error(`League wallet not found for ${season.leagueId}`, undefined, {
|
|
leagueId: season.leagueId,
|
|
});
|
|
return Result.err({ code: 'LEAGUE_WALLET_NOT_FOUND' });
|
|
}
|
|
|
|
const netAmount = acceptedRequest.getNetAmount();
|
|
|
|
// Deduct from sponsor wallet
|
|
const updatedSponsorWallet = {
|
|
...sponsorWallet,
|
|
balance: sponsorWallet.balance - request.offeredAmount.amount,
|
|
};
|
|
await this.walletRepository.update(updatedSponsorWallet);
|
|
|
|
// Add to league wallet
|
|
const updatedLeagueWallet = leagueWallet.addFunds(netAmount, paymentResult.transactionId!);
|
|
await this.leagueWalletRepository.update(updatedLeagueWallet);
|
|
}
|
|
|
|
this.logger.info(`Sponsorship request ${acceptedRequest.id} successfully accepted.`, {
|
|
requestId: acceptedRequest.id,
|
|
sponsorshipId,
|
|
});
|
|
|
|
const result: AcceptSponsorshipResult = {
|
|
requestId: acceptedRequest.id,
|
|
sponsorshipId,
|
|
status: 'accepted',
|
|
acceptedAt: acceptedRequest.respondedAt!,
|
|
platformFee: acceptedRequest.getPlatformFee().amount,
|
|
netAmount: acceptedRequest.getNetAmount().amount,
|
|
};
|
|
|
|
this.output.present(result);
|
|
|
|
return Result.ok(undefined);
|
|
}
|
|
} |