refactor
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user