This commit is contained in:
2025-12-16 10:50:15 +01:00
parent 775d41e055
commit 8ed6ba1fd1
144 changed files with 5763 additions and 1985 deletions

View File

@@ -1,346 +1,190 @@
import { Injectable } from '@nestjs/common';
import { CreatePaymentInput, CreatePaymentOutput, UpdatePaymentStatusInput, UpdatePaymentStatusOutput, PaymentDto, GetPaymentsQuery, GetPaymentsOutput, PaymentStatus, MembershipFeeDto, MemberPaymentDto, GetMembershipFeesQuery, GetMembershipFeesOutput, UpsertMembershipFeeInput, UpsertMembershipFeeOutput, UpdateMemberPaymentInput, UpdateMemberPaymentOutput, MembershipFeeType, MemberPaymentStatus, PrizeDto, GetPrizesQuery, GetPrizesOutput, CreatePrizeInput, CreatePrizeOutput, AwardPrizeInput, AwardPrizeOutput, DeletePrizeInput, DeletePrizeOutput, PrizeType, WalletDto, TransactionDto, GetWalletQuery, GetWalletOutput, ProcessWalletTransactionInput, ProcessWalletTransactionOutput, TransactionType, ReferenceType } from './dto/PaymentsDto';
import { LeagueSettingsDto, LeagueConfigFormModelStructureDto } from '../league/dto/LeagueDto'; // For the mock data definitions
import { Injectable, Inject } from '@nestjs/common';
import type { Logger } from '@gridpilot/shared/application/Logger';
const payments: Map<string, PaymentDto> = new Map();
const membershipFees: Map<string, MembershipFeeDto> = new Map();
const memberPayments: Map<string, MemberPaymentDto> = new Map();
const prizes: Map<string, PrizeDto> = new Map();
const wallets: Map<string, WalletDto> = new Map();
const transactions: Map<string, TransactionDto> = new Map();
// Use cases
import type { GetPaymentsUseCase } from '@gridpilot/payments/application/use-cases/GetPaymentsUseCase';
import type { CreatePaymentUseCase } from '@gridpilot/payments/application/use-cases/CreatePaymentUseCase';
import type { UpdatePaymentStatusUseCase } from '@gridpilot/payments/application/use-cases/UpdatePaymentStatusUseCase';
import type { GetMembershipFeesUseCase } from '@gridpilot/payments/application/use-cases/GetMembershipFeesUseCase';
import type { UpsertMembershipFeeUseCase } from '@gridpilot/payments/application/use-cases/UpsertMembershipFeeUseCase';
import type { UpdateMemberPaymentUseCase } from '@gridpilot/payments/application/use-cases/UpdateMemberPaymentUseCase';
import type { GetPrizesUseCase } from '@gridpilot/payments/application/use-cases/GetPrizesUseCase';
import type { CreatePrizeUseCase } from '@gridpilot/payments/application/use-cases/CreatePrizeUseCase';
import type { AwardPrizeUseCase } from '@gridpilot/payments/application/use-cases/AwardPrizeUseCase';
import type { DeletePrizeUseCase } from '@gridpilot/payments/application/use-cases/DeletePrizeUseCase';
import type { GetWalletUseCase } from '@gridpilot/payments/application/use-cases/GetWalletUseCase';
import type { ProcessWalletTransactionUseCase } from '@gridpilot/payments/application/use-cases/ProcessWalletTransactionUseCase';
const PLATFORM_FEE_RATE = 0.10;
// Presenters
import { GetPaymentsPresenter } from './presenters/GetPaymentsPresenter';
import { CreatePaymentPresenter } from './presenters/CreatePaymentPresenter';
import { UpdatePaymentStatusPresenter } from './presenters/UpdatePaymentStatusPresenter';
import { GetMembershipFeesPresenter } from './presenters/GetMembershipFeesPresenter';
import { UpsertMembershipFeePresenter } from './presenters/UpsertMembershipFeePresenter';
import { UpdateMemberPaymentPresenter } from './presenters/UpdateMemberPaymentPresenter';
import { GetPrizesPresenter } from './presenters/GetPrizesPresenter';
import { CreatePrizePresenter } from './presenters/CreatePrizePresenter';
import { AwardPrizePresenter } from './presenters/AwardPrizePresenter';
import { DeletePrizePresenter } from './presenters/DeletePrizePresenter';
import { GetWalletPresenter } from './presenters/GetWalletPresenter';
import { ProcessWalletTransactionPresenter } from './presenters/ProcessWalletTransactionPresenter';
// DTOs
import type {
CreatePaymentInput,
CreatePaymentOutput,
UpdatePaymentStatusInput,
UpdatePaymentStatusOutput,
GetPaymentsQuery,
GetPaymentsOutput,
GetMembershipFeesQuery,
GetMembershipFeesOutput,
UpsertMembershipFeeInput,
UpsertMembershipFeeOutput,
UpdateMemberPaymentInput,
UpdateMemberPaymentOutput,
GetPrizesQuery,
GetPrizesOutput,
CreatePrizeInput,
CreatePrizeOutput,
AwardPrizeInput,
AwardPrizeOutput,
DeletePrizeInput,
DeletePrizeOutput,
GetWalletQuery,
GetWalletOutput,
ProcessWalletTransactionInput,
ProcessWalletTransactionOutput,
} from './dto/PaymentsDto';
// Injection tokens
import {
GET_PAYMENTS_USE_CASE_TOKEN,
CREATE_PAYMENT_USE_CASE_TOKEN,
UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN,
GET_MEMBERSHIP_FEES_USE_CASE_TOKEN,
UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN,
UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN,
GET_PRIZES_USE_CASE_TOKEN,
CREATE_PRIZE_USE_CASE_TOKEN,
AWARD_PRIZE_USE_CASE_TOKEN,
DELETE_PRIZE_USE_CASE_TOKEN,
GET_WALLET_USE_CASE_TOKEN,
PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN,
LOGGER_TOKEN,
} from './PaymentsProviders';
@Injectable()
export class PaymentsService {
constructor(
@Inject(GET_PAYMENTS_USE_CASE_TOKEN) private readonly getPaymentsUseCase: GetPaymentsUseCase,
@Inject(CREATE_PAYMENT_USE_CASE_TOKEN) private readonly createPaymentUseCase: CreatePaymentUseCase,
@Inject(UPDATE_PAYMENT_STATUS_USE_CASE_TOKEN) private readonly updatePaymentStatusUseCase: UpdatePaymentStatusUseCase,
@Inject(GET_MEMBERSHIP_FEES_USE_CASE_TOKEN) private readonly getMembershipFeesUseCase: GetMembershipFeesUseCase,
@Inject(UPSERT_MEMBERSHIP_FEE_USE_CASE_TOKEN) private readonly upsertMembershipFeeUseCase: UpsertMembershipFeeUseCase,
@Inject(UPDATE_MEMBER_PAYMENT_USE_CASE_TOKEN) private readonly updateMemberPaymentUseCase: UpdateMemberPaymentUseCase,
@Inject(GET_PRIZES_USE_CASE_TOKEN) private readonly getPrizesUseCase: GetPrizesUseCase,
@Inject(CREATE_PRIZE_USE_CASE_TOKEN) private readonly createPrizeUseCase: CreatePrizeUseCase,
@Inject(AWARD_PRIZE_USE_CASE_TOKEN) private readonly awardPrizeUseCase: AwardPrizeUseCase,
@Inject(DELETE_PRIZE_USE_CASE_TOKEN) private readonly deletePrizeUseCase: DeletePrizeUseCase,
@Inject(GET_WALLET_USE_CASE_TOKEN) private readonly getWalletUseCase: GetWalletUseCase,
@Inject(PROCESS_WALLET_TRANSACTION_USE_CASE_TOKEN) private readonly processWalletTransactionUseCase: ProcessWalletTransactionUseCase,
@Inject(LOGGER_TOKEN) private readonly logger: Logger,
) {}
async getPayments(query: GetPaymentsQuery): Promise<GetPaymentsOutput> {
let results = Array.from(payments.values());
this.logger.debug('[PaymentsService] Getting payments', { query });
if (query.leagueId) {
results = results.filter(p => p.leagueId === query.leagueId);
}
if (query.payerId) {
results = results.filter(p => p.payerId === query.payerId);
}
if (query.type) {
results = results.filter(p => p.type === query.type);
}
return { payments: results };
const presenter = new GetPaymentsPresenter();
await this.getPaymentsUseCase.execute(query, presenter);
return presenter.viewModel;
}
async createPayment(input: CreatePaymentInput): Promise<CreatePaymentOutput> {
const { type, amount, payerId, payerType, leagueId, seasonId } = input;
this.logger.debug('[PaymentsService] Creating payment', { 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: PaymentDto = {
id,
type,
amount,
platformFee,
netAmount,
payerId,
payerType,
leagueId,
seasonId: seasonId || undefined,
status: PaymentStatus.PENDING,
createdAt: new Date(),
};
payments.set(id, payment);
return { payment };
const presenter = new CreatePaymentPresenter();
await this.createPaymentUseCase.execute(input, presenter);
return presenter.viewModel;
}
async updatePaymentStatus(input: UpdatePaymentStatusInput): Promise<UpdatePaymentStatusOutput> {
const { paymentId, status } = input;
this.logger.debug('[PaymentsService] Updating payment status', { input });
const payment = payments.get(paymentId);
if (!payment) {
throw new Error('Payment not found');
}
payment.status = status;
if (status === PaymentStatus.COMPLETED) {
payment.completedAt = new Date();
}
payments.set(paymentId, payment);
return { payment };
const presenter = new UpdatePaymentStatusPresenter();
await this.updatePaymentStatusUseCase.execute(input, presenter);
return presenter.viewModel;
}
async getMembershipFees(query: GetMembershipFeesQuery): Promise<GetMembershipFeesOutput> {
const { leagueId, driverId } = query;
this.logger.debug('[PaymentsService] Getting membership fees', { query });
if (!leagueId) {
throw new Error('leagueId is required');
}
const fee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId) || null;
let payments: MemberPaymentDto[] = [];
if (driverId) {
payments = Array.from(memberPayments.values()).filter(
p => membershipFees.get(p.feeId)?.leagueId === leagueId && p.driverId === driverId
);
}
return { fee, payments };
const presenter = new GetMembershipFeesPresenter();
await this.getMembershipFeesUseCase.execute(query, presenter);
return presenter.viewModel;
}
async upsertMembershipFee(input: UpsertMembershipFeeInput): Promise<UpsertMembershipFeeOutput> {
const { leagueId, seasonId, type, amount } = input;
this.logger.debug('[PaymentsService] Upserting membership fee', { input });
// Check for existing fee config
let existingFee = Array.from(membershipFees.values()).find(f => f.leagueId === leagueId);
if (existingFee) {
// Update existing fee
existingFee.type = type;
existingFee.amount = amount;
existingFee.seasonId = seasonId || existingFee.seasonId;
existingFee.enabled = amount > 0;
existingFee.updatedAt = new Date();
membershipFees.set(existingFee.id, existingFee);
return { fee: existingFee };
}
const id = `fee-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const fee: MembershipFeeDto = {
id,
leagueId,
seasonId: seasonId || undefined,
type,
amount,
enabled: amount > 0,
createdAt: new Date(),
updatedAt: new Date(),
};
membershipFees.set(id, fee);
return { fee };
const presenter = new UpsertMembershipFeePresenter();
await this.upsertMembershipFeeUseCase.execute(input, presenter);
return presenter.viewModel;
}
async updateMemberPayment(input: UpdateMemberPaymentInput): Promise<UpdateMemberPaymentOutput> {
const { feeId, driverId, status, paidAt } = input;
this.logger.debug('[PaymentsService] Updating member payment', { input });
const fee = membershipFees.get(feeId);
if (!fee) {
throw new Error('Membership fee configuration not found');
}
// Find or create payment record
let payment = Array.from(memberPayments.values()).find(
p => p.feeId === feeId && p.driverId === 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)}`;
payment = {
id: paymentId,
feeId,
driverId,
amount: fee.amount,
platformFee,
netAmount,
status: MemberPaymentStatus.PENDING,
dueDate: new Date(),
};
memberPayments.set(paymentId, payment);
}
if (status) {
payment.status = status;
}
if (paidAt || status === MemberPaymentStatus.PAID) {
payment.paidAt = paidAt ? new Date(paidAt) : new Date();
}
memberPayments.set(payment.id, payment);
return { payment };
const presenter = new UpdateMemberPaymentPresenter();
await this.updateMemberPaymentUseCase.execute(input, presenter);
return presenter.viewModel;
}
async getPrizes(query: GetPrizesQuery): Promise<GetPrizesOutput> {
const { leagueId, seasonId } = query;
this.logger.debug('[PaymentsService] Getting prizes', { query });
let results = Array.from(prizes.values()).filter(p => p.leagueId === leagueId);
if (seasonId) {
results = results.filter(p => p.seasonId === seasonId);
}
results.sort((a, b) => a.position - b.position);
return { prizes: results };
const presenter = new GetPrizesPresenter();
await this.getPrizesUseCase.execute({ leagueId: query.leagueId!, seasonId: query.seasonId }, presenter);
return presenter.viewModel;
}
async createPrize(input: CreatePrizeInput): Promise<CreatePrizeOutput> {
const { leagueId, seasonId, position, name, amount, type, description } = input;
this.logger.debug('[PaymentsService] Creating prize', { input });
// Check for duplicate position
const existingPrize = Array.from(prizes.values()).find(
p => p.leagueId === leagueId && p.seasonId === seasonId && p.position === 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: PrizeDto = {
id,
leagueId,
seasonId,
position,
name,
amount,
type,
description: description || undefined,
awarded: false,
createdAt: new Date(),
};
prizes.set(id, prize);
return { prize };
const presenter = new CreatePrizePresenter();
await this.createPrizeUseCase.execute(input, presenter);
return presenter.viewModel;
}
async awardPrize(input: AwardPrizeInput): Promise<AwardPrizeOutput> {
const { prizeId, driverId } = input;
this.logger.debug('[PaymentsService] Awarding prize', { input });
const prize = prizes.get(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();
prizes.set(prizeId, prize);
return { prize };
const presenter = new AwardPrizePresenter();
await this.awardPrizeUseCase.execute(input, presenter);
return presenter.viewModel;
}
async deletePrize(input: DeletePrizeInput): Promise<DeletePrizeOutput> {
const { prizeId } = input;
this.logger.debug('[PaymentsService] Deleting prize', { input });
const prize = prizes.get(prizeId);
if (!prize) {
throw new Error('Prize not found');
}
if (prize.awarded) {
throw new Error('Cannot delete an awarded prize');
}
prizes.delete(prizeId);
return { success: true };
const presenter = new DeletePrizePresenter();
await this.deletePrizeUseCase.execute(input, presenter);
return presenter.viewModel;
}
async getWallet(query: GetWalletQuery): Promise<GetWalletOutput> {
const { leagueId } = query;
this.logger.debug('[PaymentsService] Getting wallet', { query });
if (!leagueId) {
throw new Error('LeagueId is required');
}
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
if (!wallet) {
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
wallet = {
id,
leagueId,
balance: 0,
totalRevenue: 0,
totalPlatformFees: 0,
totalWithdrawn: 0,
createdAt: new Date(),
currency: 'USD', // Assuming default currency (mock)
};
wallets.set(id, wallet);
}
const walletTransactions = Array.from(transactions.values())
.filter(t => t.walletId === wallet!.id)
.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
return { wallet, transactions: walletTransactions };
const presenter = new GetWalletPresenter();
await this.getWalletUseCase.execute({ leagueId: query.leagueId! }, presenter);
return presenter.viewModel;
}
async processWalletTransaction(input: ProcessWalletTransactionInput): Promise<ProcessWalletTransactionOutput> {
const { leagueId, type, amount, description, referenceId, referenceType } = input;
this.logger.debug('[PaymentsService] Processing wallet transaction', { input });
if (!leagueId || !type || amount === undefined || !description) {
throw new Error('Missing required fields: leagueId, type, amount, description');
}
if (type !== TransactionType.DEPOSIT && type !== TransactionType.WITHDRAWAL) {
throw new Error('Type must be "deposit" or "withdrawal"');
}
let wallet = Array.from(wallets.values()).find(w => w.leagueId === leagueId);
if (!wallet) {
const id = `wallet-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
wallet = {
id,
leagueId,
balance: 0,
totalRevenue: 0,
totalPlatformFees: 0,
totalWithdrawn: 0,
createdAt: new Date(),
currency: 'USD', // Assuming default currency (mock)
};
wallets.set(id, wallet);
}
if (type === TransactionType.WITHDRAWAL) {
if (amount > wallet.balance) {
throw new Error('Insufficient balance');
}
}
const transactionId = `txn-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
const transaction: TransactionDto = {
id: transactionId,
walletId: wallet.id,
type,
amount,
description,
referenceId: referenceId || undefined,
referenceType: referenceType || undefined,
createdAt: new Date(),
};
transactions.set(transactionId, transaction);
if (type === TransactionType.DEPOSIT) {
wallet.balance += amount;
wallet.totalRevenue += amount;
} else {
wallet.balance -= amount;
wallet.totalWithdrawn += amount;
}
wallets.set(wallet.id, wallet);
return { wallet, transaction };
const presenter = new ProcessWalletTransactionPresenter();
await this.processWalletTransactionUseCase.execute(input, presenter);
return presenter.viewModel;
}
}