refactor
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
export interface RequestAvatarGenerationResultDTO {
|
||||
requestId: string;
|
||||
status: 'validating' | 'generating' | 'completed' | 'failed';
|
||||
avatarUrls?: string[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export interface IRequestAvatarGenerationPresenter {
|
||||
reset(): void;
|
||||
present(dto: RequestAvatarGenerationResultDTO): void;
|
||||
get viewModel(): any;
|
||||
getViewModel(): any;
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { AsyncUseCase, Logger } from '@gridpilot/shared/application';
|
||||
import type { UseCase, Logger } from '@gridpilot/shared/application';
|
||||
import type { IAvatarGenerationRepository } from '../../domain/repositories/IAvatarGenerationRepository';
|
||||
import type { FaceValidationPort } from '../ports/FaceValidationPort';
|
||||
import type { AvatarGenerationPort } from '../ports/AvatarGenerationPort';
|
||||
import type { IRequestAvatarGenerationPresenter, RequestAvatarGenerationResultDTO } from '../presenters/IRequestAvatarGenerationPresenter';
|
||||
import { AvatarGenerationRequest } from '../../domain/entities/AvatarGenerationRequest';
|
||||
import type { RacingSuitColor, AvatarStyle } from '../../domain/types/AvatarGenerationRequest';
|
||||
|
||||
@@ -12,15 +13,8 @@ export interface RequestAvatarGenerationCommand {
|
||||
style?: AvatarStyle;
|
||||
}
|
||||
|
||||
export interface RequestAvatarGenerationResult {
|
||||
requestId: string;
|
||||
status: 'validating' | 'generating' | 'completed' | 'failed';
|
||||
avatarUrls?: string[];
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export class RequestAvatarGenerationUseCase
|
||||
implements AsyncUseCase<RequestAvatarGenerationCommand, RequestAvatarGenerationResult> {
|
||||
implements UseCase<RequestAvatarGenerationCommand, RequestAvatarGenerationResultDTO, any, IRequestAvatarGenerationPresenter> {
|
||||
constructor(
|
||||
private readonly avatarRepository: IAvatarGenerationRepository,
|
||||
private readonly faceValidation: FaceValidationPort,
|
||||
@@ -28,7 +22,8 @@ export class RequestAvatarGenerationUseCase
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(command: RequestAvatarGenerationCommand): Promise<RequestAvatarGenerationResult> {
|
||||
async execute(command: RequestAvatarGenerationCommand, presenter: IRequestAvatarGenerationPresenter): Promise<void> {
|
||||
presenter.reset();
|
||||
this.logger.debug(
|
||||
`Executing RequestAvatarGenerationUseCase for userId: ${command.userId}`,
|
||||
command,
|
||||
@@ -64,11 +59,12 @@ export class RequestAvatarGenerationUseCase
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`Face validation failed for request ${requestId}: ${errorMessage}`);
|
||||
return {
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: validationResult.errorMessage || 'Please upload a clear photo of your face',
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validationResult.hasFace) {
|
||||
@@ -76,11 +72,12 @@ export class RequestAvatarGenerationUseCase
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`No face detected for request ${requestId}: ${errorMessage}`);
|
||||
return {
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: 'No face detected. Please upload a photo that clearly shows your face.',
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (validationResult.faceCount > 1) {
|
||||
@@ -88,11 +85,12 @@ export class RequestAvatarGenerationUseCase
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`Multiple faces detected for request ${requestId}: ${errorMessage}`);
|
||||
return {
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: 'Multiple faces detected. Please upload a photo with only your face.',
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.logger.info(`Face validation successful for request ${requestId}.`);
|
||||
|
||||
@@ -119,11 +117,12 @@ export class RequestAvatarGenerationUseCase
|
||||
request.fail(errorMessage);
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.error(`Avatar generation failed for request ${requestId}: ${errorMessage}`);
|
||||
return {
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'failed',
|
||||
errorMessage: generationResult.errorMessage || 'Failed to generate avatars. Please try again.',
|
||||
};
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Complete with generated avatars
|
||||
@@ -132,18 +131,18 @@ export class RequestAvatarGenerationUseCase
|
||||
await this.avatarRepository.save(request);
|
||||
this.logger.info(`Avatar generation completed successfully for request ${requestId}.`);
|
||||
|
||||
return {
|
||||
presenter.present({
|
||||
requestId,
|
||||
status: 'completed',
|
||||
avatarUrls,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`An unexpected error occurred during avatar generation for userId: ${command.userId}`,
|
||||
error,
|
||||
error as Error,
|
||||
);
|
||||
// Re-throw or return a generic error, depending on desired error handling strategy
|
||||
throw error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
2
core/payments/application/index.ts
Normal file
2
core/payments/application/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './presenters';
|
||||
export * from './use-cases';
|
||||
16
core/payments/application/presenters/IAwardPrizePresenter.ts
Normal file
16
core/payments/application/presenters/IAwardPrizePresenter.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Presenter Interface: IAwardPrizePresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { PrizeDto } from './IGetPrizesPresenter';
|
||||
|
||||
export interface AwardPrizeResultDTO {
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export interface AwardPrizeViewModel {
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export interface IAwardPrizePresenter extends Presenter<AwardPrizeResultDTO, AwardPrizeViewModel> {}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Presenter Interface: ICreatePaymentPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { PaymentDto } from './IGetPaymentsPresenter';
|
||||
|
||||
export interface CreatePaymentResultDTO {
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export interface CreatePaymentViewModel {
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export interface ICreatePaymentPresenter extends Presenter<CreatePaymentResultDTO, CreatePaymentViewModel> {}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Presenter Interface: ICreatePrizePresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { PrizeDto } from './IGetPrizesPresenter';
|
||||
|
||||
export interface CreatePrizeResultDTO {
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export interface CreatePrizeViewModel {
|
||||
prize: PrizeDto;
|
||||
}
|
||||
|
||||
export interface ICreatePrizePresenter extends Presenter<CreatePrizeResultDTO, CreatePrizeViewModel> {}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Presenter Interface: IDeletePrizePresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface DeletePrizeResultDTO {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface DeletePrizeViewModel {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface IDeletePrizePresenter extends Presenter<DeletePrizeResultDTO, DeletePrizeViewModel> {}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Presenter Interface: IGetMembershipFeesPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { MembershipFeeType } from '../../domain/entities/MembershipFee';
|
||||
import type { MemberPaymentStatus } from '../../domain/entities/MemberPayment';
|
||||
|
||||
export interface MembershipFeeDto {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
type: MembershipFeeType;
|
||||
amount: number;
|
||||
enabled: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
export interface MemberPaymentDto {
|
||||
id: string;
|
||||
feeId: string;
|
||||
driverId: string;
|
||||
amount: number;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
status: MemberPaymentStatus;
|
||||
dueDate: Date;
|
||||
paidAt?: Date;
|
||||
}
|
||||
|
||||
export interface GetMembershipFeesResultDTO {
|
||||
fee: MembershipFeeDto | null;
|
||||
payments: MemberPaymentDto[];
|
||||
}
|
||||
|
||||
export interface GetMembershipFeesViewModel {
|
||||
fee: MembershipFeeDto | null;
|
||||
payments: MemberPaymentDto[];
|
||||
}
|
||||
|
||||
export interface IGetMembershipFeesPresenter extends Presenter<GetMembershipFeesResultDTO, GetMembershipFeesViewModel> {}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Presenter Interface: IGetPaymentsPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { PaymentType, PayerType, PaymentStatus } from '../../domain/entities/Payment';
|
||||
|
||||
export interface PaymentDto {
|
||||
id: string;
|
||||
type: PaymentType;
|
||||
amount: number;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
payerId: string;
|
||||
payerType: PayerType;
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
status: PaymentStatus;
|
||||
createdAt: Date;
|
||||
completedAt?: Date;
|
||||
}
|
||||
|
||||
export interface GetPaymentsResultDTO {
|
||||
payments: PaymentDto[];
|
||||
}
|
||||
|
||||
export interface GetPaymentsViewModel {
|
||||
payments: PaymentDto[];
|
||||
}
|
||||
|
||||
export interface IGetPaymentsPresenter extends Presenter<GetPaymentsResultDTO, GetPaymentsViewModel> {}
|
||||
31
core/payments/application/presenters/IGetPrizesPresenter.ts
Normal file
31
core/payments/application/presenters/IGetPrizesPresenter.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Presenter Interface: IGetPrizesPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { PrizeType } from '../../domain/entities/Prize';
|
||||
|
||||
export interface PrizeDto {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
position: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
type: PrizeType;
|
||||
description?: string;
|
||||
awarded: boolean;
|
||||
awardedTo?: string;
|
||||
awardedAt?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface GetPrizesResultDTO {
|
||||
prizes: PrizeDto[];
|
||||
}
|
||||
|
||||
export interface GetPrizesViewModel {
|
||||
prizes: PrizeDto[];
|
||||
}
|
||||
|
||||
export interface IGetPrizesPresenter extends Presenter<GetPrizesResultDTO, GetPrizesViewModel> {}
|
||||
40
core/payments/application/presenters/IGetWalletPresenter.ts
Normal file
40
core/payments/application/presenters/IGetWalletPresenter.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Presenter Interface: IGetWalletPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { TransactionType, ReferenceType } from '../../domain/entities/Wallet';
|
||||
|
||||
export interface WalletDto {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
balance: number;
|
||||
totalRevenue: number;
|
||||
totalPlatformFees: number;
|
||||
totalWithdrawn: number;
|
||||
currency: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface TransactionDto {
|
||||
id: string;
|
||||
walletId: string;
|
||||
type: TransactionType;
|
||||
amount: number;
|
||||
description: string;
|
||||
referenceId?: string;
|
||||
referenceType?: ReferenceType;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface GetWalletResultDTO {
|
||||
wallet: WalletDto;
|
||||
transactions: TransactionDto[];
|
||||
}
|
||||
|
||||
export interface GetWalletViewModel {
|
||||
wallet: WalletDto;
|
||||
transactions: TransactionDto[];
|
||||
}
|
||||
|
||||
export interface IGetWalletPresenter extends Presenter<GetWalletResultDTO, GetWalletViewModel> {}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Presenter Interface: IProcessWalletTransactionPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { WalletDto, TransactionDto } from './IGetWalletPresenter';
|
||||
|
||||
export interface ProcessWalletTransactionResultDTO {
|
||||
wallet: WalletDto;
|
||||
transaction: TransactionDto;
|
||||
}
|
||||
|
||||
export interface ProcessWalletTransactionViewModel {
|
||||
wallet: WalletDto;
|
||||
transaction: TransactionDto;
|
||||
}
|
||||
|
||||
export interface IProcessWalletTransactionPresenter extends Presenter<ProcessWalletTransactionResultDTO, ProcessWalletTransactionViewModel> {}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Presenter Interface: IUpdateMemberPaymentPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { MemberPaymentDto } from './IGetMembershipFeesPresenter';
|
||||
|
||||
export interface UpdateMemberPaymentResultDTO {
|
||||
payment: MemberPaymentDto;
|
||||
}
|
||||
|
||||
export interface UpdateMemberPaymentViewModel {
|
||||
payment: MemberPaymentDto;
|
||||
}
|
||||
|
||||
export interface IUpdateMemberPaymentPresenter extends Presenter<UpdateMemberPaymentResultDTO, UpdateMemberPaymentViewModel> {}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Presenter Interface: IUpdatePaymentStatusPresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { PaymentDto } from './IGetPaymentsPresenter';
|
||||
|
||||
export interface UpdatePaymentStatusResultDTO {
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export interface UpdatePaymentStatusViewModel {
|
||||
payment: PaymentDto;
|
||||
}
|
||||
|
||||
export interface IUpdatePaymentStatusPresenter extends Presenter<UpdatePaymentStatusResultDTO, UpdatePaymentStatusViewModel> {}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Presenter Interface: IUpsertMembershipFeePresenter
|
||||
*/
|
||||
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { MembershipFeeDto } from './IGetMembershipFeesPresenter';
|
||||
|
||||
export interface UpsertMembershipFeeResultDTO {
|
||||
fee: MembershipFeeDto;
|
||||
}
|
||||
|
||||
export interface UpsertMembershipFeeViewModel {
|
||||
fee: MembershipFeeDto;
|
||||
}
|
||||
|
||||
export interface IUpsertMembershipFeePresenter extends Presenter<UpsertMembershipFeeResultDTO, UpsertMembershipFeeViewModel> {}
|
||||
12
core/payments/application/presenters/index.ts
Normal file
12
core/payments/application/presenters/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './IGetPaymentsPresenter';
|
||||
export * from './ICreatePaymentPresenter';
|
||||
export * from './IUpdatePaymentStatusPresenter';
|
||||
export * from './IGetMembershipFeesPresenter';
|
||||
export * from './IUpsertMembershipFeePresenter';
|
||||
export * from './IUpdateMemberPaymentPresenter';
|
||||
export * from './IGetPrizesPresenter';
|
||||
export * from './ICreatePrizePresenter';
|
||||
export * from './IAwardPrizePresenter';
|
||||
export * from './IDeletePrizePresenter';
|
||||
export * from './IGetWalletPresenter';
|
||||
export * from './IProcessWalletTransactionPresenter';
|
||||
67
core/payments/application/use-cases/AwardPrizeUseCase.ts
Normal file
67
core/payments/application/use-cases/AwardPrizeUseCase.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Application Use Case: AwardPrizeUseCase
|
||||
*
|
||||
* Awards a prize to a driver.
|
||||
*/
|
||||
|
||||
import type { IPrizeRepository } from '../../domain/repositories/IPrizeRepository';
|
||||
import type {
|
||||
IAwardPrizePresenter,
|
||||
AwardPrizeResultDTO,
|
||||
AwardPrizeViewModel,
|
||||
} from '../presenters/IAwardPrizePresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface AwardPrizeInput {
|
||||
prizeId: string;
|
||||
driverId: string;
|
||||
}
|
||||
|
||||
export class AwardPrizeUseCase
|
||||
implements UseCase<AwardPrizeInput, AwardPrizeResultDTO, AwardPrizeViewModel, IAwardPrizePresenter>
|
||||
{
|
||||
constructor(private readonly prizeRepository: IPrizeRepository) {}
|
||||
|
||||
async execute(
|
||||
input: AwardPrizeInput,
|
||||
presenter: IAwardPrizePresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { prizeId, driverId } = input;
|
||||
|
||||
const prize = await this.prizeRepository.findById(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Prize has already been awarded');
|
||||
}
|
||||
|
||||
prize.awarded = true;
|
||||
prize.awardedTo = driverId;
|
||||
prize.awardedAt = new Date();
|
||||
|
||||
const updatedPrize = await this.prizeRepository.update(prize);
|
||||
|
||||
const dto: AwardPrizeResultDTO = {
|
||||
prize: {
|
||||
id: updatedPrize.id,
|
||||
leagueId: updatedPrize.leagueId,
|
||||
seasonId: updatedPrize.seasonId,
|
||||
position: updatedPrize.position,
|
||||
name: updatedPrize.name,
|
||||
amount: updatedPrize.amount,
|
||||
type: updatedPrize.type,
|
||||
description: updatedPrize.description,
|
||||
awarded: updatedPrize.awarded,
|
||||
awardedTo: updatedPrize.awardedTo,
|
||||
awardedAt: updatedPrize.awardedAt,
|
||||
createdAt: updatedPrize.createdAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
79
core/payments/application/use-cases/CreatePaymentUseCase.ts
Normal file
79
core/payments/application/use-cases/CreatePaymentUseCase.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Application Use Case: CreatePaymentUseCase
|
||||
*
|
||||
* Creates a new payment.
|
||||
*/
|
||||
|
||||
import type { IPaymentRepository } from '../../domain/repositories/IPaymentRepository';
|
||||
import type { PaymentType, PayerType, PaymentStatus, Payment } from '../../domain/entities/Payment';
|
||||
import type {
|
||||
ICreatePaymentPresenter,
|
||||
CreatePaymentResultDTO,
|
||||
CreatePaymentViewModel,
|
||||
} from '../presenters/ICreatePaymentPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
const PLATFORM_FEE_RATE = 0.10;
|
||||
|
||||
export interface CreatePaymentInput {
|
||||
type: PaymentType;
|
||||
amount: number;
|
||||
payerId: string;
|
||||
payerType: PayerType;
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class CreatePaymentUseCase
|
||||
implements UseCase<CreatePaymentInput, CreatePaymentResultDTO, CreatePaymentViewModel, ICreatePaymentPresenter>
|
||||
{
|
||||
constructor(private readonly paymentRepository: IPaymentRepository) {}
|
||||
|
||||
async execute(
|
||||
input: CreatePaymentInput,
|
||||
presenter: ICreatePaymentPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { type, amount, payerId, payerType, leagueId, seasonId } = input;
|
||||
|
||||
const platformFee = amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = amount - platformFee;
|
||||
|
||||
const id = `payment-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const payment: Payment = {
|
||||
id,
|
||||
type,
|
||||
amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
payerId,
|
||||
payerType,
|
||||
leagueId,
|
||||
seasonId,
|
||||
status: 'pending' as PaymentStatus,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const createdPayment = await this.paymentRepository.create(payment);
|
||||
|
||||
const dto: CreatePaymentResultDTO = {
|
||||
payment: {
|
||||
id: createdPayment.id,
|
||||
type: createdPayment.type,
|
||||
amount: createdPayment.amount,
|
||||
platformFee: createdPayment.platformFee,
|
||||
netAmount: createdPayment.netAmount,
|
||||
payerId: createdPayment.payerId,
|
||||
payerType: createdPayment.payerType,
|
||||
leagueId: createdPayment.leagueId,
|
||||
seasonId: createdPayment.seasonId,
|
||||
status: createdPayment.status,
|
||||
createdAt: createdPayment.createdAt,
|
||||
completedAt: createdPayment.completedAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
79
core/payments/application/use-cases/CreatePrizeUseCase.ts
Normal file
79
core/payments/application/use-cases/CreatePrizeUseCase.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Application Use Case: CreatePrizeUseCase
|
||||
*
|
||||
* Creates a new prize.
|
||||
*/
|
||||
|
||||
import type { IPrizeRepository } from '../../domain/repositories/IPrizeRepository';
|
||||
import type { PrizeType, Prize } from '../../domain/entities/Prize';
|
||||
import type {
|
||||
ICreatePrizePresenter,
|
||||
CreatePrizeResultDTO,
|
||||
CreatePrizeViewModel,
|
||||
} from '../presenters/ICreatePrizePresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface CreatePrizeInput {
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
position: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
type: PrizeType;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class CreatePrizeUseCase
|
||||
implements UseCase<CreatePrizeInput, CreatePrizeResultDTO, CreatePrizeViewModel, ICreatePrizePresenter>
|
||||
{
|
||||
constructor(private readonly prizeRepository: IPrizeRepository) {}
|
||||
|
||||
async execute(
|
||||
input: CreatePrizeInput,
|
||||
presenter: ICreatePrizePresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { leagueId, seasonId, position, name, amount, type, description } = input;
|
||||
|
||||
const existingPrize = await this.prizeRepository.findByPosition(leagueId, seasonId, position);
|
||||
if (existingPrize) {
|
||||
throw new Error(`Prize for position ${position} already exists`);
|
||||
}
|
||||
|
||||
const id = `prize-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const prize: Prize = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId,
|
||||
position,
|
||||
name,
|
||||
amount,
|
||||
type,
|
||||
description,
|
||||
awarded: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const createdPrize = await this.prizeRepository.create(prize);
|
||||
|
||||
const dto: CreatePrizeResultDTO = {
|
||||
prize: {
|
||||
id: createdPrize.id,
|
||||
leagueId: createdPrize.leagueId,
|
||||
seasonId: createdPrize.seasonId,
|
||||
position: createdPrize.position,
|
||||
name: createdPrize.name,
|
||||
amount: createdPrize.amount,
|
||||
type: createdPrize.type,
|
||||
description: createdPrize.description,
|
||||
awarded: createdPrize.awarded,
|
||||
awardedTo: createdPrize.awardedTo,
|
||||
awardedAt: createdPrize.awardedAt,
|
||||
createdAt: createdPrize.createdAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
49
core/payments/application/use-cases/DeletePrizeUseCase.ts
Normal file
49
core/payments/application/use-cases/DeletePrizeUseCase.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Application Use Case: DeletePrizeUseCase
|
||||
*
|
||||
* Deletes a prize.
|
||||
*/
|
||||
|
||||
import type { IPrizeRepository } from '../../domain/repositories/IPrizeRepository';
|
||||
import type {
|
||||
IDeletePrizePresenter,
|
||||
DeletePrizeResultDTO,
|
||||
DeletePrizeViewModel,
|
||||
} from '../presenters/IDeletePrizePresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface DeletePrizeInput {
|
||||
prizeId: string;
|
||||
}
|
||||
|
||||
export class DeletePrizeUseCase
|
||||
implements UseCase<DeletePrizeInput, DeletePrizeResultDTO, DeletePrizeViewModel, IDeletePrizePresenter>
|
||||
{
|
||||
constructor(private readonly prizeRepository: IPrizeRepository) {}
|
||||
|
||||
async execute(
|
||||
input: DeletePrizeInput,
|
||||
presenter: IDeletePrizePresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { prizeId } = input;
|
||||
|
||||
const prize = await this.prizeRepository.findById(prizeId);
|
||||
if (!prize) {
|
||||
throw new Error('Prize not found');
|
||||
}
|
||||
|
||||
if (prize.awarded) {
|
||||
throw new Error('Cannot delete an awarded prize');
|
||||
}
|
||||
|
||||
await this.prizeRepository.delete(prizeId);
|
||||
|
||||
const dto: DeletePrizeResultDTO = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Application Use Case: GetMembershipFeesUseCase
|
||||
*
|
||||
* Retrieves membership fees and member payments.
|
||||
*/
|
||||
|
||||
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '../../domain/repositories/IMembershipFeeRepository';
|
||||
import type {
|
||||
IGetMembershipFeesPresenter,
|
||||
GetMembershipFeesResultDTO,
|
||||
GetMembershipFeesViewModel,
|
||||
} from '../presenters/IGetMembershipFeesPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetMembershipFeesInput {
|
||||
leagueId: string;
|
||||
driverId?: string;
|
||||
}
|
||||
|
||||
export class GetMembershipFeesUseCase
|
||||
implements UseCase<GetMembershipFeesInput, GetMembershipFeesResultDTO, GetMembershipFeesViewModel, IGetMembershipFeesPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipFeeRepository: IMembershipFeeRepository,
|
||||
private readonly memberPaymentRepository: IMemberPaymentRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetMembershipFeesInput,
|
||||
presenter: IGetMembershipFeesPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { leagueId, driverId } = input;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('leagueId is required');
|
||||
}
|
||||
|
||||
const fee = await this.membershipFeeRepository.findByLeagueId(leagueId);
|
||||
|
||||
let payments: any[] = [];
|
||||
if (driverId && fee) {
|
||||
const memberPayments = await this.memberPaymentRepository.findByLeagueIdAndDriverId(leagueId, driverId, this.membershipFeeRepository);
|
||||
payments = memberPayments.map(p => ({
|
||||
id: p.id,
|
||||
feeId: p.feeId,
|
||||
driverId: p.driverId,
|
||||
amount: p.amount,
|
||||
platformFee: p.platformFee,
|
||||
netAmount: p.netAmount,
|
||||
status: p.status,
|
||||
dueDate: p.dueDate,
|
||||
paidAt: p.paidAt,
|
||||
}));
|
||||
}
|
||||
|
||||
const dto: GetMembershipFeesResultDTO = {
|
||||
fee: fee ? {
|
||||
id: fee.id,
|
||||
leagueId: fee.leagueId,
|
||||
seasonId: fee.seasonId,
|
||||
type: fee.type,
|
||||
amount: fee.amount,
|
||||
enabled: fee.enabled,
|
||||
createdAt: fee.createdAt,
|
||||
updatedAt: fee.updatedAt,
|
||||
} : null,
|
||||
payments,
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
58
core/payments/application/use-cases/GetPaymentsUseCase.ts
Normal file
58
core/payments/application/use-cases/GetPaymentsUseCase.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Application Use Case: GetPaymentsUseCase
|
||||
*
|
||||
* Retrieves payments based on filters.
|
||||
*/
|
||||
|
||||
import type { IPaymentRepository } from '../../domain/repositories/IPaymentRepository';
|
||||
import type { PaymentType } from '../../domain/entities/Payment';
|
||||
import type {
|
||||
IGetPaymentsPresenter,
|
||||
GetPaymentsResultDTO,
|
||||
GetPaymentsViewModel,
|
||||
} from '../presenters/IGetPaymentsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetPaymentsInput {
|
||||
leagueId?: string;
|
||||
payerId?: string;
|
||||
type?: PaymentType;
|
||||
}
|
||||
|
||||
export class GetPaymentsUseCase
|
||||
implements UseCase<GetPaymentsInput, GetPaymentsResultDTO, GetPaymentsViewModel, IGetPaymentsPresenter>
|
||||
{
|
||||
constructor(private readonly paymentRepository: IPaymentRepository) {}
|
||||
|
||||
async execute(
|
||||
input: GetPaymentsInput,
|
||||
presenter: IGetPaymentsPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const payments = await this.paymentRepository.findByFilters({
|
||||
leagueId: input.leagueId,
|
||||
payerId: input.payerId,
|
||||
type: input.type,
|
||||
});
|
||||
|
||||
const dto: GetPaymentsResultDTO = {
|
||||
payments: payments.map(payment => ({
|
||||
id: payment.id,
|
||||
type: payment.type,
|
||||
amount: payment.amount,
|
||||
platformFee: payment.platformFee,
|
||||
netAmount: payment.netAmount,
|
||||
payerId: payment.payerId,
|
||||
payerType: payment.payerType,
|
||||
leagueId: payment.leagueId,
|
||||
seasonId: payment.seasonId,
|
||||
status: payment.status,
|
||||
createdAt: payment.createdAt,
|
||||
completedAt: payment.completedAt,
|
||||
})),
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
61
core/payments/application/use-cases/GetPrizesUseCase.ts
Normal file
61
core/payments/application/use-cases/GetPrizesUseCase.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Application Use Case: GetPrizesUseCase
|
||||
*
|
||||
* Retrieves prizes for a league or season.
|
||||
*/
|
||||
|
||||
import type { IPrizeRepository } from '../../domain/repositories/IPrizeRepository';
|
||||
import type {
|
||||
IGetPrizesPresenter,
|
||||
GetPrizesResultDTO,
|
||||
GetPrizesViewModel,
|
||||
} from '../presenters/IGetPrizesPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetPrizesInput {
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
}
|
||||
|
||||
export class GetPrizesUseCase
|
||||
implements UseCase<GetPrizesInput, GetPrizesResultDTO, GetPrizesViewModel, IGetPrizesPresenter>
|
||||
{
|
||||
constructor(private readonly prizeRepository: IPrizeRepository) {}
|
||||
|
||||
async execute(
|
||||
input: GetPrizesInput,
|
||||
presenter: IGetPrizesPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { leagueId, seasonId } = input;
|
||||
|
||||
let prizes;
|
||||
if (seasonId) {
|
||||
prizes = await this.prizeRepository.findByLeagueIdAndSeasonId(leagueId, seasonId);
|
||||
} else {
|
||||
prizes = await this.prizeRepository.findByLeagueId(leagueId);
|
||||
}
|
||||
|
||||
prizes.sort((a, b) => a.position - b.position);
|
||||
|
||||
const dto: GetPrizesResultDTO = {
|
||||
prizes: prizes.map(prize => ({
|
||||
id: prize.id,
|
||||
leagueId: prize.leagueId,
|
||||
seasonId: prize.seasonId,
|
||||
position: prize.position,
|
||||
name: prize.name,
|
||||
amount: prize.amount,
|
||||
type: prize.type,
|
||||
description: prize.description,
|
||||
awarded: prize.awarded,
|
||||
awardedTo: prize.awardedTo,
|
||||
awardedAt: prize.awardedAt,
|
||||
createdAt: prize.createdAt,
|
||||
})),
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
85
core/payments/application/use-cases/GetWalletUseCase.ts
Normal file
85
core/payments/application/use-cases/GetWalletUseCase.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Application Use Case: GetWalletUseCase
|
||||
*
|
||||
* Retrieves wallet information and transactions.
|
||||
*/
|
||||
|
||||
import type { IWalletRepository, ITransactionRepository } from '../../domain/repositories/IWalletRepository';
|
||||
import type { Wallet } from '../../domain/entities/Wallet';
|
||||
import type {
|
||||
IGetWalletPresenter,
|
||||
GetWalletResultDTO,
|
||||
GetWalletViewModel,
|
||||
} from '../presenters/IGetWalletPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetWalletInput {
|
||||
leagueId: string;
|
||||
}
|
||||
|
||||
export class GetWalletUseCase
|
||||
implements UseCase<GetWalletInput, GetWalletResultDTO, GetWalletViewModel, IGetWalletPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly walletRepository: IWalletRepository,
|
||||
private readonly transactionRepository: ITransactionRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: GetWalletInput,
|
||||
presenter: IGetWalletPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { leagueId } = input;
|
||||
|
||||
if (!leagueId) {
|
||||
throw new Error('LeagueId is required');
|
||||
}
|
||||
|
||||
let wallet = await this.walletRepository.findByLeagueId(leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newWallet: Wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
currency: 'USD',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
wallet = await this.walletRepository.create(newWallet);
|
||||
}
|
||||
|
||||
const transactions = await this.transactionRepository.findByWalletId(wallet.id);
|
||||
transactions.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
|
||||
|
||||
const dto: GetWalletResultDTO = {
|
||||
wallet: {
|
||||
id: wallet.id,
|
||||
leagueId: wallet.leagueId,
|
||||
balance: wallet.balance,
|
||||
totalRevenue: wallet.totalRevenue,
|
||||
totalPlatformFees: wallet.totalPlatformFees,
|
||||
totalWithdrawn: wallet.totalWithdrawn,
|
||||
currency: wallet.currency,
|
||||
createdAt: wallet.createdAt,
|
||||
},
|
||||
transactions: transactions.map(t => ({
|
||||
id: t.id,
|
||||
walletId: t.walletId,
|
||||
type: t.type,
|
||||
amount: t.amount,
|
||||
description: t.description,
|
||||
referenceId: t.referenceId,
|
||||
referenceType: t.referenceType,
|
||||
createdAt: t.createdAt,
|
||||
})),
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Application Use Case: ProcessWalletTransactionUseCase
|
||||
*
|
||||
* Processes a wallet transaction (deposit or withdrawal).
|
||||
*/
|
||||
|
||||
import type { IWalletRepository, ITransactionRepository } from '../../domain/repositories/IWalletRepository';
|
||||
import type { Wallet, Transaction, TransactionType, ReferenceType } from '../../domain/entities/Wallet';
|
||||
import type {
|
||||
IProcessWalletTransactionPresenter,
|
||||
ProcessWalletTransactionResultDTO,
|
||||
ProcessWalletTransactionViewModel,
|
||||
} from '../presenters/IProcessWalletTransactionPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface ProcessWalletTransactionInput {
|
||||
leagueId: string;
|
||||
type: TransactionType;
|
||||
amount: number;
|
||||
description: string;
|
||||
referenceId?: string;
|
||||
referenceType?: ReferenceType;
|
||||
}
|
||||
|
||||
export class ProcessWalletTransactionUseCase
|
||||
implements UseCase<ProcessWalletTransactionInput, ProcessWalletTransactionResultDTO, ProcessWalletTransactionViewModel, IProcessWalletTransactionPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly walletRepository: IWalletRepository,
|
||||
private readonly transactionRepository: ITransactionRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: ProcessWalletTransactionInput,
|
||||
presenter: IProcessWalletTransactionPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { leagueId, type, amount, description, referenceId, referenceType } = input;
|
||||
|
||||
if (!leagueId || !type || amount === undefined || !description) {
|
||||
throw new Error('Missing required fields: leagueId, type, amount, description');
|
||||
}
|
||||
|
||||
if (type !== ('deposit' as TransactionType) && type !== ('withdrawal' as TransactionType)) {
|
||||
throw new Error('Type must be "deposit" or "withdrawal"');
|
||||
}
|
||||
|
||||
let wallet = await this.walletRepository.findByLeagueId(leagueId);
|
||||
|
||||
if (!wallet) {
|
||||
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newWallet: Wallet = {
|
||||
id,
|
||||
leagueId,
|
||||
balance: 0,
|
||||
totalRevenue: 0,
|
||||
totalPlatformFees: 0,
|
||||
totalWithdrawn: 0,
|
||||
currency: 'USD',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
wallet = await this.walletRepository.create(newWallet);
|
||||
}
|
||||
|
||||
if (type === ('withdrawal' as TransactionType)) {
|
||||
if (amount > wallet.balance) {
|
||||
throw new Error('Insufficient balance');
|
||||
}
|
||||
}
|
||||
|
||||
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const transaction: Transaction = {
|
||||
id: transactionId,
|
||||
walletId: wallet.id,
|
||||
type,
|
||||
amount,
|
||||
description,
|
||||
referenceId,
|
||||
referenceType,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const createdTransaction = await this.transactionRepository.create(transaction);
|
||||
|
||||
if (type === ('deposit' as TransactionType)) {
|
||||
wallet.balance += amount;
|
||||
wallet.totalRevenue += amount;
|
||||
} else {
|
||||
wallet.balance -= amount;
|
||||
wallet.totalWithdrawn += amount;
|
||||
}
|
||||
|
||||
const updatedWallet = await this.walletRepository.update(wallet);
|
||||
|
||||
const dto: ProcessWalletTransactionResultDTO = {
|
||||
wallet: {
|
||||
id: updatedWallet.id,
|
||||
leagueId: updatedWallet.leagueId,
|
||||
balance: updatedWallet.balance,
|
||||
totalRevenue: updatedWallet.totalRevenue,
|
||||
totalPlatformFees: updatedWallet.totalPlatformFees,
|
||||
totalWithdrawn: updatedWallet.totalWithdrawn,
|
||||
currency: updatedWallet.currency,
|
||||
createdAt: updatedWallet.createdAt,
|
||||
},
|
||||
transaction: {
|
||||
id: createdTransaction.id,
|
||||
walletId: createdTransaction.walletId,
|
||||
type: createdTransaction.type,
|
||||
amount: createdTransaction.amount,
|
||||
description: createdTransaction.description,
|
||||
referenceId: createdTransaction.referenceId,
|
||||
referenceType: createdTransaction.referenceType,
|
||||
createdAt: createdTransaction.createdAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Application Use Case: UpdateMemberPaymentUseCase
|
||||
*
|
||||
* Updates a member payment record.
|
||||
*/
|
||||
|
||||
import type { IMembershipFeeRepository, IMemberPaymentRepository } from '../../domain/repositories/IMembershipFeeRepository';
|
||||
import type { MemberPaymentStatus, MemberPayment } from '../../domain/entities/MemberPayment';
|
||||
import type {
|
||||
IUpdateMemberPaymentPresenter,
|
||||
UpdateMemberPaymentResultDTO,
|
||||
UpdateMemberPaymentViewModel,
|
||||
} from '../presenters/IUpdateMemberPaymentPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
const PLATFORM_FEE_RATE = 0.10;
|
||||
|
||||
export interface UpdateMemberPaymentInput {
|
||||
feeId: string;
|
||||
driverId: string;
|
||||
status?: MemberPaymentStatus;
|
||||
paidAt?: Date | string;
|
||||
}
|
||||
|
||||
export class UpdateMemberPaymentUseCase
|
||||
implements UseCase<UpdateMemberPaymentInput, UpdateMemberPaymentResultDTO, UpdateMemberPaymentViewModel, IUpdateMemberPaymentPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly membershipFeeRepository: IMembershipFeeRepository,
|
||||
private readonly memberPaymentRepository: IMemberPaymentRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: UpdateMemberPaymentInput,
|
||||
presenter: IUpdateMemberPaymentPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { feeId, driverId, status, paidAt } = input;
|
||||
|
||||
const fee = await this.membershipFeeRepository.findById(feeId);
|
||||
if (!fee) {
|
||||
throw new Error('Membership fee configuration not found');
|
||||
}
|
||||
|
||||
let payment = await this.memberPaymentRepository.findByFeeIdAndDriverId(feeId, driverId);
|
||||
|
||||
if (!payment) {
|
||||
const platformFee = fee.amount * PLATFORM_FEE_RATE;
|
||||
const netAmount = fee.amount - platformFee;
|
||||
|
||||
const paymentId = `mp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newPayment: MemberPayment = {
|
||||
id: paymentId,
|
||||
feeId,
|
||||
driverId,
|
||||
amount: fee.amount,
|
||||
platformFee,
|
||||
netAmount,
|
||||
status: 'pending' as MemberPaymentStatus,
|
||||
dueDate: new Date(),
|
||||
};
|
||||
payment = await this.memberPaymentRepository.create(newPayment);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
payment.status = status;
|
||||
}
|
||||
if (paidAt || status === ('paid' as MemberPaymentStatus)) {
|
||||
payment.paidAt = paidAt ? new Date(paidAt as string) : new Date();
|
||||
}
|
||||
|
||||
const updatedPayment = await this.memberPaymentRepository.update(payment);
|
||||
|
||||
const dto: UpdateMemberPaymentResultDTO = {
|
||||
payment: {
|
||||
id: updatedPayment.id,
|
||||
feeId: updatedPayment.feeId,
|
||||
driverId: updatedPayment.driverId,
|
||||
amount: updatedPayment.amount,
|
||||
platformFee: updatedPayment.platformFee,
|
||||
netAmount: updatedPayment.netAmount,
|
||||
status: updatedPayment.status,
|
||||
dueDate: updatedPayment.dueDate,
|
||||
paidAt: updatedPayment.paidAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Application Use Case: UpdatePaymentStatusUseCase
|
||||
*
|
||||
* Updates the status of a payment.
|
||||
*/
|
||||
|
||||
import type { IPaymentRepository } from '../../domain/repositories/IPaymentRepository';
|
||||
import type { PaymentStatus } from '../../domain/entities/Payment';
|
||||
import type {
|
||||
IUpdatePaymentStatusPresenter,
|
||||
UpdatePaymentStatusResultDTO,
|
||||
UpdatePaymentStatusViewModel,
|
||||
} from '../presenters/IUpdatePaymentStatusPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface UpdatePaymentStatusInput {
|
||||
paymentId: string;
|
||||
status: PaymentStatus;
|
||||
}
|
||||
|
||||
export class UpdatePaymentStatusUseCase
|
||||
implements UseCase<UpdatePaymentStatusInput, UpdatePaymentStatusResultDTO, UpdatePaymentStatusViewModel, IUpdatePaymentStatusPresenter>
|
||||
{
|
||||
constructor(private readonly paymentRepository: IPaymentRepository) {}
|
||||
|
||||
async execute(
|
||||
input: UpdatePaymentStatusInput,
|
||||
presenter: IUpdatePaymentStatusPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { paymentId, status } = input;
|
||||
|
||||
const payment = await this.paymentRepository.findById(paymentId);
|
||||
if (!payment) {
|
||||
throw new Error('Payment not found');
|
||||
}
|
||||
|
||||
payment.status = status;
|
||||
if (status === ('completed' as PaymentStatus)) {
|
||||
payment.completedAt = new Date();
|
||||
}
|
||||
|
||||
const updatedPayment = await this.paymentRepository.update(payment);
|
||||
|
||||
const dto: UpdatePaymentStatusResultDTO = {
|
||||
payment: {
|
||||
id: updatedPayment.id,
|
||||
type: updatedPayment.type,
|
||||
amount: updatedPayment.amount,
|
||||
platformFee: updatedPayment.platformFee,
|
||||
netAmount: updatedPayment.netAmount,
|
||||
payerId: updatedPayment.payerId,
|
||||
payerType: updatedPayment.payerType,
|
||||
leagueId: updatedPayment.leagueId,
|
||||
seasonId: updatedPayment.seasonId,
|
||||
status: updatedPayment.status,
|
||||
createdAt: updatedPayment.createdAt,
|
||||
completedAt: updatedPayment.completedAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Application Use Case: UpsertMembershipFeeUseCase
|
||||
*
|
||||
* Creates or updates membership fee configuration.
|
||||
*/
|
||||
|
||||
import type { IMembershipFeeRepository } from '../../domain/repositories/IMembershipFeeRepository';
|
||||
import type { MembershipFeeType, MembershipFee } from '../../domain/entities/MembershipFee';
|
||||
import type {
|
||||
IUpsertMembershipFeePresenter,
|
||||
UpsertMembershipFeeResultDTO,
|
||||
UpsertMembershipFeeViewModel,
|
||||
} from '../presenters/IUpsertMembershipFeePresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface UpsertMembershipFeeInput {
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
type: MembershipFeeType;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export class UpsertMembershipFeeUseCase
|
||||
implements UseCase<UpsertMembershipFeeInput, UpsertMembershipFeeResultDTO, UpsertMembershipFeeViewModel, IUpsertMembershipFeePresenter>
|
||||
{
|
||||
constructor(private readonly membershipFeeRepository: IMembershipFeeRepository) {}
|
||||
|
||||
async execute(
|
||||
input: UpsertMembershipFeeInput,
|
||||
presenter: IUpsertMembershipFeePresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const { leagueId, seasonId, type, amount } = input;
|
||||
|
||||
let existingFee = await this.membershipFeeRepository.findByLeagueId(leagueId);
|
||||
|
||||
let fee: MembershipFee;
|
||||
if (existingFee) {
|
||||
existingFee.type = type;
|
||||
existingFee.amount = amount;
|
||||
existingFee.seasonId = seasonId;
|
||||
existingFee.enabled = amount > 0;
|
||||
existingFee.updatedAt = new Date();
|
||||
fee = await this.membershipFeeRepository.update(existingFee);
|
||||
} else {
|
||||
const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
const newFee: MembershipFee = {
|
||||
id,
|
||||
leagueId,
|
||||
seasonId,
|
||||
type,
|
||||
amount,
|
||||
enabled: amount > 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
fee = await this.membershipFeeRepository.create(newFee);
|
||||
}
|
||||
|
||||
const dto: UpsertMembershipFeeResultDTO = {
|
||||
fee: {
|
||||
id: fee.id,
|
||||
leagueId: fee.leagueId,
|
||||
seasonId: fee.seasonId,
|
||||
type: fee.type,
|
||||
amount: fee.amount,
|
||||
enabled: fee.enabled,
|
||||
createdAt: fee.createdAt,
|
||||
updatedAt: fee.updatedAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
12
core/payments/application/use-cases/index.ts
Normal file
12
core/payments/application/use-cases/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './GetPaymentsUseCase';
|
||||
export * from './CreatePaymentUseCase';
|
||||
export * from './UpdatePaymentStatusUseCase';
|
||||
export * from './GetMembershipFeesUseCase';
|
||||
export * from './UpsertMembershipFeeUseCase';
|
||||
export * from './UpdateMemberPaymentUseCase';
|
||||
export * from './GetPrizesUseCase';
|
||||
export * from './CreatePrizeUseCase';
|
||||
export * from './AwardPrizeUseCase';
|
||||
export * from './DeletePrizeUseCase';
|
||||
export * from './GetWalletUseCase';
|
||||
export * from './ProcessWalletTransactionUseCase';
|
||||
21
core/payments/domain/entities/MemberPayment.ts
Normal file
21
core/payments/domain/entities/MemberPayment.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Domain Entity: MemberPayment
|
||||
*/
|
||||
|
||||
export enum MemberPaymentStatus {
|
||||
PENDING = 'pending',
|
||||
PAID = 'paid',
|
||||
OVERDUE = 'overdue',
|
||||
}
|
||||
|
||||
export interface MemberPayment {
|
||||
id: string;
|
||||
feeId: string;
|
||||
driverId: string;
|
||||
amount: number;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
status: MemberPaymentStatus;
|
||||
dueDate: Date;
|
||||
paidAt?: Date;
|
||||
}
|
||||
20
core/payments/domain/entities/MembershipFee.ts
Normal file
20
core/payments/domain/entities/MembershipFee.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Domain Entity: MembershipFee
|
||||
*/
|
||||
|
||||
export enum MembershipFeeType {
|
||||
SEASON = 'season',
|
||||
MONTHLY = 'monthly',
|
||||
PER_RACE = 'per_race',
|
||||
}
|
||||
|
||||
export interface MembershipFee {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
type: MembershipFeeType;
|
||||
amount: number;
|
||||
enabled: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
35
core/payments/domain/entities/Payment.ts
Normal file
35
core/payments/domain/entities/Payment.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Domain Entity: Payment
|
||||
*/
|
||||
|
||||
export enum PaymentType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
}
|
||||
|
||||
export enum PayerType {
|
||||
SPONSOR = 'sponsor',
|
||||
DRIVER = 'driver',
|
||||
}
|
||||
|
||||
export enum PaymentStatus {
|
||||
PENDING = 'pending',
|
||||
COMPLETED = 'completed',
|
||||
FAILED = 'failed',
|
||||
REFUNDED = 'refunded',
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
id: string;
|
||||
type: PaymentType;
|
||||
amount: number;
|
||||
platformFee: number;
|
||||
netAmount: number;
|
||||
payerId: string;
|
||||
payerType: PayerType;
|
||||
leagueId: string;
|
||||
seasonId?: string;
|
||||
status: PaymentStatus;
|
||||
createdAt: Date;
|
||||
completedAt?: Date;
|
||||
}
|
||||
24
core/payments/domain/entities/Prize.ts
Normal file
24
core/payments/domain/entities/Prize.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Domain Entity: Prize
|
||||
*/
|
||||
|
||||
export enum PrizeType {
|
||||
CASH = 'cash',
|
||||
MERCHANDISE = 'merchandise',
|
||||
OTHER = 'other',
|
||||
}
|
||||
|
||||
export interface Prize {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
seasonId: string;
|
||||
position: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
type: PrizeType;
|
||||
description?: string;
|
||||
awarded: boolean;
|
||||
awardedTo?: string;
|
||||
awardedAt?: Date;
|
||||
createdAt: Date;
|
||||
}
|
||||
37
core/payments/domain/entities/Wallet.ts
Normal file
37
core/payments/domain/entities/Wallet.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Domain Entity: Wallet
|
||||
*/
|
||||
|
||||
export interface Wallet {
|
||||
id: string;
|
||||
leagueId: string;
|
||||
balance: number;
|
||||
totalRevenue: number;
|
||||
totalPlatformFees: number;
|
||||
totalWithdrawn: number;
|
||||
currency: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export enum TransactionType {
|
||||
DEPOSIT = 'deposit',
|
||||
WITHDRAWAL = 'withdrawal',
|
||||
PLATFORM_FEE = 'platform_fee',
|
||||
}
|
||||
|
||||
export enum ReferenceType {
|
||||
SPONSORSHIP = 'sponsorship',
|
||||
MEMBERSHIP_FEE = 'membership_fee',
|
||||
PRIZE = 'prize',
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
id: string;
|
||||
walletId: string;
|
||||
type: TransactionType;
|
||||
amount: number;
|
||||
description: string;
|
||||
referenceId?: string;
|
||||
referenceType?: ReferenceType;
|
||||
createdAt: Date;
|
||||
}
|
||||
5
core/payments/domain/entities/index.ts
Normal file
5
core/payments/domain/entities/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './Payment';
|
||||
export * from './MembershipFee';
|
||||
export * from './MemberPayment';
|
||||
export * from './Prize';
|
||||
export * from './Wallet';
|
||||
2
core/payments/domain/index.ts
Normal file
2
core/payments/domain/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './entities';
|
||||
export * from './repositories';
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Repository Interface: IMembershipFeeRepository
|
||||
*/
|
||||
|
||||
import type { MembershipFee } from '../entities/MembershipFee';
|
||||
import type { MemberPayment } from '../entities/MemberPayment';
|
||||
|
||||
export interface IMembershipFeeRepository {
|
||||
findById(id: string): Promise<MembershipFee | null>;
|
||||
findByLeagueId(leagueId: string): Promise<MembershipFee | null>;
|
||||
create(fee: MembershipFee): Promise<MembershipFee>;
|
||||
update(fee: MembershipFee): Promise<MembershipFee>;
|
||||
}
|
||||
|
||||
export interface IMemberPaymentRepository {
|
||||
findById(id: string): Promise<MemberPayment | null>;
|
||||
findByFeeIdAndDriverId(feeId: string, driverId: string): Promise<MemberPayment | null>;
|
||||
findByLeagueIdAndDriverId(leagueId: string, driverId: string, membershipFeeRepo: IMembershipFeeRepository): Promise<MemberPayment[]>;
|
||||
create(payment: MemberPayment): Promise<MemberPayment>;
|
||||
update(payment: MemberPayment): Promise<MemberPayment>;
|
||||
}
|
||||
15
core/payments/domain/repositories/IPaymentRepository.ts
Normal file
15
core/payments/domain/repositories/IPaymentRepository.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Repository Interface: IPaymentRepository
|
||||
*/
|
||||
|
||||
import type { Payment, PaymentType } from '../entities/Payment';
|
||||
|
||||
export interface IPaymentRepository {
|
||||
findById(id: string): Promise<Payment | null>;
|
||||
findByLeagueId(leagueId: string): Promise<Payment[]>;
|
||||
findByPayerId(payerId: string): Promise<Payment[]>;
|
||||
findByType(type: PaymentType): Promise<Payment[]>;
|
||||
findByFilters(filters: { leagueId?: string; payerId?: string; type?: PaymentType }): Promise<Payment[]>;
|
||||
create(payment: Payment): Promise<Payment>;
|
||||
update(payment: Payment): Promise<Payment>;
|
||||
}
|
||||
15
core/payments/domain/repositories/IPrizeRepository.ts
Normal file
15
core/payments/domain/repositories/IPrizeRepository.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Repository Interface: IPrizeRepository
|
||||
*/
|
||||
|
||||
import type { Prize } from '../entities/Prize';
|
||||
|
||||
export interface IPrizeRepository {
|
||||
findById(id: string): Promise<Prize | null>;
|
||||
findByLeagueId(leagueId: string): Promise<Prize[]>;
|
||||
findByLeagueIdAndSeasonId(leagueId: string, seasonId: string): Promise<Prize[]>;
|
||||
findByPosition(leagueId: string, seasonId: string, position: number): Promise<Prize | null>;
|
||||
create(prize: Prize): Promise<Prize>;
|
||||
update(prize: Prize): Promise<Prize>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
18
core/payments/domain/repositories/IWalletRepository.ts
Normal file
18
core/payments/domain/repositories/IWalletRepository.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Repository Interface: IWalletRepository
|
||||
*/
|
||||
|
||||
import type { Wallet, Transaction } from '../entities/Wallet';
|
||||
|
||||
export interface IWalletRepository {
|
||||
findById(id: string): Promise<Wallet | null>;
|
||||
findByLeagueId(leagueId: string): Promise<Wallet | null>;
|
||||
create(wallet: Wallet): Promise<Wallet>;
|
||||
update(wallet: Wallet): Promise<Wallet>;
|
||||
}
|
||||
|
||||
export interface ITransactionRepository {
|
||||
findById(id: string): Promise<Transaction | null>;
|
||||
findByWalletId(walletId: string): Promise<Transaction[]>;
|
||||
create(transaction: Transaction): Promise<Transaction>;
|
||||
}
|
||||
4
core/payments/domain/repositories/index.ts
Normal file
4
core/payments/domain/repositories/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './IPaymentRepository';
|
||||
export * from './IMembershipFeeRepository';
|
||||
export * from './IPrizeRepository';
|
||||
export * from './IWalletRepository';
|
||||
2
core/payments/index.ts
Normal file
2
core/payments/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './domain';
|
||||
export * from './application';
|
||||
@@ -18,6 +18,9 @@ export * from './use-cases/GetLeagueStandingsUseCase';
|
||||
export * from './use-cases/GetLeagueDriverSeasonStatsUseCase';
|
||||
export * from './use-cases/GetAllLeaguesWithCapacityUseCase';
|
||||
export * from './use-cases/GetAllLeaguesWithCapacityAndScoringUseCase';
|
||||
export * from './use-cases/GetAllRacesUseCase';
|
||||
export * from './use-cases/GetTotalRacesUseCase';
|
||||
export * from './use-cases/ImportRaceResultsApiUseCase';
|
||||
export * from './use-cases/ListLeagueScoringPresetsUseCase';
|
||||
export * from './use-cases/GetLeagueScoringConfigUseCase';
|
||||
export * from './use-cases/RecalculateChampionshipStandingsUseCase';
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface SponsorDto {
|
||||
id: string;
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
websiteUrl: string | undefined;
|
||||
logoUrl: string | undefined;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface CreateSponsorViewModel {
|
||||
sponsor: SponsorDto;
|
||||
}
|
||||
|
||||
export interface CreateSponsorResultDTO {
|
||||
sponsor: SponsorDto;
|
||||
}
|
||||
|
||||
export interface ICreateSponsorPresenter extends Presenter<CreateSponsorResultDTO, CreateSponsorViewModel> {}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
import type { GetEntitySponsorshipPricingResultDTO } from '../use-cases/GetEntitySponsorshipPricingUseCase';
|
||||
|
||||
export interface IEntitySponsorshipPricingPresenter {
|
||||
present(data: GetEntitySponsorshipPricingResultDTO | null): void;
|
||||
}
|
||||
export interface IEntitySponsorshipPricingPresenter extends Presenter<GetEntitySponsorshipPricingResultDTO | null, GetEntitySponsorshipPricingResultDTO | null> {}
|
||||
20
core/racing/application/presenters/IGetAllRacesPresenter.ts
Normal file
20
core/racing/application/presenters/IGetAllRacesPresenter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface RaceViewModel {
|
||||
id: string;
|
||||
name: string;
|
||||
date: string;
|
||||
leagueName?: string;
|
||||
}
|
||||
|
||||
export interface AllRacesPageViewModel {
|
||||
races: RaceViewModel[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface GetAllRacesResultDTO {
|
||||
races: RaceViewModel[];
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
export interface IGetAllRacesPresenter extends Presenter<GetAllRacesResultDTO, AllRacesPageViewModel> {}
|
||||
20
core/racing/application/presenters/IGetSponsorsPresenter.ts
Normal file
20
core/racing/application/presenters/IGetSponsorsPresenter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface SponsorDto {
|
||||
id: string;
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
websiteUrl: string | undefined;
|
||||
logoUrl: string | undefined;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface GetSponsorsViewModel {
|
||||
sponsors: SponsorDto[];
|
||||
}
|
||||
|
||||
export interface GetSponsorsResultDTO {
|
||||
sponsors: SponsorDto[];
|
||||
}
|
||||
|
||||
export interface IGetSponsorsPresenter extends Presenter<GetSponsorsResultDTO, GetSponsorsViewModel> {}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface SponsorshipPricingItemDto {
|
||||
id: string;
|
||||
level: string;
|
||||
price: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export interface GetSponsorshipPricingResultDTO {
|
||||
pricing: SponsorshipPricingItemDto[];
|
||||
}
|
||||
|
||||
export interface GetSponsorshipPricingViewModel {
|
||||
pricing: SponsorshipPricingItemDto[];
|
||||
}
|
||||
|
||||
export interface IGetSponsorshipPricingPresenter extends Presenter<GetSponsorshipPricingResultDTO, GetSponsorshipPricingViewModel> {}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface GetTotalRacesViewModel {
|
||||
totalRaces: number;
|
||||
}
|
||||
|
||||
export interface GetTotalRacesResultDTO {
|
||||
totalRaces: number;
|
||||
}
|
||||
|
||||
export interface IGetTotalRacesPresenter extends Presenter<GetTotalRacesResultDTO, GetTotalRacesViewModel> {}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Presenter } from '@gridpilot/shared/presentation/Presenter';
|
||||
|
||||
export interface ImportRaceResultsSummaryViewModel {
|
||||
success: boolean;
|
||||
raceId: string;
|
||||
driversProcessed: number;
|
||||
resultsRecorded: number;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface ImportRaceResultsApiResultDTO {
|
||||
success: boolean;
|
||||
raceId: string;
|
||||
driversProcessed: number;
|
||||
resultsRecorded: number;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface IImportRaceResultsApiPresenter extends Presenter<ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel> {}
|
||||
61
core/racing/application/use-cases/CreateSponsorUseCase.ts
Normal file
61
core/racing/application/use-cases/CreateSponsorUseCase.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Application Use Case: CreateSponsorUseCase
|
||||
*
|
||||
* Creates a new sponsor.
|
||||
*/
|
||||
|
||||
import { Sponsor, type SponsorProps } from '../../domain/entities/Sponsor';
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type {
|
||||
ICreateSponsorPresenter,
|
||||
CreateSponsorResultDTO,
|
||||
CreateSponsorViewModel,
|
||||
} from '../presenters/ICreateSponsorPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface CreateSponsorInput {
|
||||
name: string;
|
||||
contactEmail: string;
|
||||
websiteUrl?: string;
|
||||
logoUrl?: string;
|
||||
}
|
||||
|
||||
export class CreateSponsorUseCase
|
||||
implements UseCase<CreateSponsorInput, CreateSponsorResultDTO, CreateSponsorViewModel, ICreateSponsorPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
input: CreateSponsorInput,
|
||||
presenter: ICreateSponsorPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const id = `sponsor-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
const sponsor = Sponsor.create({
|
||||
id,
|
||||
name: input.name,
|
||||
contactEmail: input.contactEmail,
|
||||
...(input.websiteUrl !== undefined ? { websiteUrl: input.websiteUrl } : {}),
|
||||
...(input.logoUrl !== undefined ? { logoUrl: input.logoUrl } : {}),
|
||||
} as any);
|
||||
|
||||
await this.sponsorRepository.create(sponsor);
|
||||
|
||||
const dto: CreateSponsorResultDTO = {
|
||||
sponsor: {
|
||||
id: sponsor.id,
|
||||
name: sponsor.name,
|
||||
contactEmail: sponsor.contactEmail,
|
||||
websiteUrl: sponsor.websiteUrl,
|
||||
logoUrl: sponsor.logoUrl,
|
||||
createdAt: sponsor.createdAt,
|
||||
},
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
34
core/racing/application/use-cases/GetAllRacesUseCase.ts
Normal file
34
core/racing/application/use-cases/GetAllRacesUseCase.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { ILeagueRepository } from '../../domain/repositories/ILeagueRepository';
|
||||
import type { IGetAllRacesPresenter, GetAllRacesResultDTO, AllRacesPageViewModel } from '../presenters/IGetAllRacesPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetAllRacesUseCaseParams {}
|
||||
|
||||
export class GetAllRacesUseCase implements UseCase<GetAllRacesUseCaseParams, GetAllRacesResultDTO, AllRacesPageViewModel, IGetAllRacesPresenter> {
|
||||
constructor(
|
||||
private readonly raceRepository: IRaceRepository,
|
||||
private readonly leagueRepository: ILeagueRepository,
|
||||
) {}
|
||||
|
||||
async execute(params: GetAllRacesUseCaseParams, presenter: IGetAllRacesPresenter): Promise<void> {
|
||||
const races = await this.raceRepository.findAll();
|
||||
const leagues = await this.leagueRepository.findAll();
|
||||
const leagueMap = new Map(leagues.map(league => [league.id, league.name]));
|
||||
|
||||
const raceViewModels = races.map(race => ({
|
||||
id: race.id,
|
||||
name: `Race ${race.id}`, // Placeholder, adjust based on domain
|
||||
date: race.scheduledAt.toISOString(),
|
||||
leagueName: leagueMap.get(race.leagueId) || 'Unknown League',
|
||||
}));
|
||||
|
||||
const dto: GetAllRacesResultDTO = {
|
||||
races: raceViewModels,
|
||||
totalCount: races.length,
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -11,8 +11,7 @@ import type { ISeasonSponsorshipRepository } from '../../domain/repositories/ISe
|
||||
import type { SponsorableEntityType } from '../../domain/entities/SponsorshipRequest';
|
||||
import type { SponsorshipTier } from '../../domain/entities/SeasonSponsorship';
|
||||
import type { IEntitySponsorshipPricingPresenter } from '../presenters/IEntitySponsorshipPricingPresenter';
|
||||
import type { AsyncUseCase } from '@gridpilot/shared/application';
|
||||
import type { Logger } from '../../../shared/src/logging/Logger';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetEntitySponsorshipPricingDTO {
|
||||
entityType: SponsorableEntityType;
|
||||
@@ -41,35 +40,28 @@ export interface GetEntitySponsorshipPricingResultDTO {
|
||||
}
|
||||
|
||||
export class GetEntitySponsorshipPricingUseCase
|
||||
implements AsyncUseCase<GetEntitySponsorshipPricingDTO, void> {
|
||||
implements UseCase<GetEntitySponsorshipPricingDTO, GetEntitySponsorshipPricingResultDTO | null, GetEntitySponsorshipPricingResultDTO | null, IEntitySponsorshipPricingPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly sponsorshipPricingRepo: ISponsorshipPricingRepository,
|
||||
private readonly sponsorshipRequestRepo: ISponsorshipRequestRepository,
|
||||
private readonly seasonSponsorshipRepo: ISeasonSponsorshipRepository,
|
||||
private readonly presenter: IEntitySponsorshipPricingPresenter,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
async execute(dto: GetEntitySponsorshipPricingDTO): Promise<void> {
|
||||
this.logger.debug(
|
||||
`Executing GetEntitySponsorshipPricingUseCase for entityType: ${dto.entityType}, entityId: ${dto.entityId}`,
|
||||
{ dto },
|
||||
);
|
||||
async execute(
|
||||
dto: GetEntitySponsorshipPricingDTO,
|
||||
presenter: IEntitySponsorshipPricingPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
try {
|
||||
const pricing = await this.sponsorshipPricingRepo.findByEntity(dto.entityType, dto.entityId);
|
||||
|
||||
if (!pricing) {
|
||||
this.logger.warn(
|
||||
`No pricing found for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Presenting null.`,
|
||||
{ dto },
|
||||
);
|
||||
this.presenter.present(null);
|
||||
presenter.present(null);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`Found pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`, { pricing });
|
||||
|
||||
// Count pending requests by tier
|
||||
const pendingRequests = await this.sponsorshipRequestRepo.findPendingByEntity(
|
||||
dto.entityType,
|
||||
@@ -78,10 +70,6 @@ export class GetEntitySponsorshipPricingUseCase
|
||||
const pendingMainCount = pendingRequests.filter(r => r.tier === 'main').length;
|
||||
const pendingSecondaryCount = pendingRequests.filter(r => r.tier === 'secondary').length;
|
||||
|
||||
this.logger.debug(
|
||||
`Pending requests counts: main=${pendingMainCount}, secondary=${pendingSecondaryCount}`,
|
||||
);
|
||||
|
||||
// Count filled slots (for seasons, check SeasonSponsorship table)
|
||||
let filledMainSlots = 0;
|
||||
let filledSecondarySlots = 0;
|
||||
@@ -91,9 +79,6 @@ export class GetEntitySponsorshipPricingUseCase
|
||||
const activeSponsorships = sponsorships.filter(s => s.isActive());
|
||||
filledMainSlots = activeSponsorships.filter(s => s.tier === 'main').length;
|
||||
filledSecondarySlots = activeSponsorships.filter(s => s.tier === 'secondary').length;
|
||||
this.logger.debug(
|
||||
`Filled slots for season: main=${filledMainSlots}, secondary=${filledSecondarySlots}`,
|
||||
);
|
||||
}
|
||||
|
||||
const result: GetEntitySponsorshipPricingResultDTO = {
|
||||
@@ -118,7 +103,6 @@ export class GetEntitySponsorshipPricingUseCase
|
||||
filledSlots: filledMainSlots,
|
||||
pendingRequests: pendingMainCount,
|
||||
};
|
||||
this.logger.debug(`Main slot pricing information processed`, { mainSlot: result.mainSlot });
|
||||
}
|
||||
|
||||
if (pricing.secondarySlots) {
|
||||
@@ -135,26 +119,10 @@ export class GetEntitySponsorshipPricingUseCase
|
||||
filledSlots: filledSecondarySlots,
|
||||
pendingRequests: pendingSecondaryCount,
|
||||
};
|
||||
this.logger.debug(`Secondary slot pricing information processed`, {
|
||||
secondarySlot: result.secondarySlot,
|
||||
});
|
||||
}
|
||||
|
||||
this.logger.info(
|
||||
`Successfully retrieved and processed entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}`,
|
||||
{ result },
|
||||
);
|
||||
this.presenter.present(result);
|
||||
presenter.present(result);
|
||||
} catch (error: unknown) {
|
||||
let errorMessage = 'An unknown error occurred';
|
||||
if (error instanceof Error) {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
this.logger.error(
|
||||
`Failed to get entity sponsorship pricing for entityType: ${dto.entityType}, entityId: ${dto.entityId}. Error: ${errorMessage}`,
|
||||
{ error, dto },
|
||||
);
|
||||
// Re-throw the error or present an error state if the presenter supports it
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
43
core/racing/application/use-cases/GetSponsorsUseCase.ts
Normal file
43
core/racing/application/use-cases/GetSponsorsUseCase.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Application Use Case: GetSponsorsUseCase
|
||||
*
|
||||
* Retrieves all sponsors.
|
||||
*/
|
||||
|
||||
import type { ISponsorRepository } from '../../domain/repositories/ISponsorRepository';
|
||||
import type {
|
||||
IGetSponsorsPresenter,
|
||||
GetSponsorsResultDTO,
|
||||
GetSponsorsViewModel,
|
||||
} from '../presenters/IGetSponsorsPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export class GetSponsorsUseCase
|
||||
implements UseCase<void, GetSponsorsResultDTO, GetSponsorsViewModel, IGetSponsorsPresenter>
|
||||
{
|
||||
constructor(
|
||||
private readonly sponsorRepository: ISponsorRepository,
|
||||
) {}
|
||||
|
||||
async execute(
|
||||
_input: void,
|
||||
presenter: IGetSponsorsPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const sponsors = await this.sponsorRepository.findAll();
|
||||
|
||||
const dto: GetSponsorsResultDTO = {
|
||||
sponsors: sponsors.map(sponsor => ({
|
||||
id: sponsor.id,
|
||||
name: sponsor.name,
|
||||
contactEmail: sponsor.contactEmail,
|
||||
websiteUrl: sponsor.websiteUrl,
|
||||
logoUrl: sponsor.logoUrl,
|
||||
createdAt: sponsor.createdAt,
|
||||
})),
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Application Use Case: GetSponsorshipPricingUseCase
|
||||
*
|
||||
* Retrieves general sponsorship pricing tiers.
|
||||
*/
|
||||
|
||||
import type {
|
||||
IGetSponsorshipPricingPresenter,
|
||||
GetSponsorshipPricingResultDTO,
|
||||
GetSponsorshipPricingViewModel,
|
||||
} from '../presenters/IGetSponsorshipPricingPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export class GetSponsorshipPricingUseCase
|
||||
implements UseCase<void, GetSponsorshipPricingResultDTO, GetSponsorshipPricingViewModel, IGetSponsorshipPricingPresenter>
|
||||
{
|
||||
constructor() {}
|
||||
|
||||
async execute(
|
||||
_input: void,
|
||||
presenter: IGetSponsorshipPricingPresenter,
|
||||
): Promise<void> {
|
||||
presenter.reset();
|
||||
|
||||
const dto: GetSponsorshipPricingResultDTO = {
|
||||
pricing: [
|
||||
{ id: 'tier-bronze', level: 'Bronze', price: 100, currency: 'USD' },
|
||||
{ id: 'tier-silver', level: 'Silver', price: 250, currency: 'USD' },
|
||||
{ id: 'tier-gold', level: 'Gold', price: 500, currency: 'USD' },
|
||||
],
|
||||
};
|
||||
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
20
core/racing/application/use-cases/GetTotalRacesUseCase.ts
Normal file
20
core/racing/application/use-cases/GetTotalRacesUseCase.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { IRaceRepository } from '../../domain/repositories/IRaceRepository';
|
||||
import type { IGetTotalRacesPresenter, GetTotalRacesResultDTO, GetTotalRacesViewModel } from '../presenters/IGetTotalRacesPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface GetTotalRacesUseCaseParams {}
|
||||
|
||||
export interface GetTotalRacesResultDTO {
|
||||
totalRaces: number;
|
||||
}
|
||||
|
||||
export class GetTotalRacesUseCase implements UseCase<GetTotalRacesUseCaseParams, GetTotalRacesResultDTO, GetTotalRacesViewModel, IGetTotalRacesPresenter> {
|
||||
constructor(private readonly raceRepository: IRaceRepository) {}
|
||||
|
||||
async execute(params: GetTotalRacesUseCaseParams, presenter: IGetTotalRacesPresenter): Promise<void> {
|
||||
const races = await this.raceRepository.findAll();
|
||||
const dto: GetTotalRacesResultDTO = { totalRaces: races.length };
|
||||
presenter.reset();
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { IImportRaceResultsApiPresenter, ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel } from '../presenters/IImportRaceResultsApiPresenter';
|
||||
import type { UseCase } from '@gridpilot/shared/application/UseCase';
|
||||
|
||||
export interface ImportRaceResultsApiParams {
|
||||
raceId: string;
|
||||
resultsFileContent: string;
|
||||
}
|
||||
|
||||
export class ImportRaceResultsApiUseCase implements UseCase<ImportRaceResultsApiParams, ImportRaceResultsApiResultDTO, ImportRaceResultsSummaryViewModel, IImportRaceResultsApiPresenter> {
|
||||
constructor() {} // No repositories for mock
|
||||
|
||||
async execute(params: ImportRaceResultsApiParams, presenter: IImportRaceResultsApiPresenter): Promise<void> {
|
||||
// Mock implementation
|
||||
const dto: ImportRaceResultsApiResultDTO = {
|
||||
success: true,
|
||||
raceId: params.raceId,
|
||||
driversProcessed: 10,
|
||||
resultsRecorded: 10,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
presenter.reset();
|
||||
presenter.present(dto);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user