chore: add logic for remove completed payment

This commit is contained in:
mahyargdz
2025-05-15 14:19:52 +03:30
parent 2ac29d1a91
commit 1bc8b23452
5 changed files with 66 additions and 24 deletions
+1 -2
View File
@@ -10,9 +10,8 @@ export function bullMqConfig(): SharedBullAsyncConfiguration {
},
prefix: "danak-console",
defaultJobOptions: {
removeOnComplete: false,
removeOnComplete: true,
removeOnFail: false,
attempts: 5,
},
}),
};
@@ -38,13 +38,6 @@ export class CreateAnnouncementDto {
@ApiProperty({ description: "Is this announcement important?", example: false })
isImportant: boolean;
// @IsOptional()
// @IsNotEmpty({ message: AnnouncementMessage.SERVICE_MUST_BE_ID })
// @ArrayMinSize(1)
// @IsUUID("4", { message: AnnouncementMessage.SERVICE_MUST_BE_UUID, each: true })
// @ApiProperty({ type: [String], description: "Service IDs", example: ["d290f1ee-6c54-4b01-90e6-d701748f0851"] })
// serviceIds: string[];
@IsOptional()
@IsNotEmpty({ message: AnnouncementMessage.SERVICE_MUST_BE_ID })
@IsUUID("4", { message: AnnouncementMessage.SERVICE_MUST_BE_UUID })
+7 -1
View File
@@ -24,7 +24,13 @@ import { ReferralsModule } from "../referrals/referrals.module";
@Module({
imports: [
TypeOrmModule.forFeature([Payment, PaymentGateway, BankAccount, DepositRequest]),
BullModule.registerQueue({ name: PAYMENT.QUEUE_NAME }),
BullModule.registerQueue({
name: PAYMENT.QUEUE_NAME,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: false,
},
}),
WalletsModule,
NotificationModule,
ReferralsModule,
@@ -176,10 +176,19 @@ export class PaymentsService {
if (verifyData.code === 100) {
return this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr);
} else if (verifyData.code === 101) {
return { status: PaymentStatus.PENDING, payment };
const freshPayment = await qryRnr.manager.findOne(Payment, { where: { id: payment.id } });
//
if (freshPayment && freshPayment.status === PaymentStatus.COMPLETED) {
this.logger.log(`Payment ${payment.id} already marked as COMPLETED in DB (code 101)`);
return { status: payment.status, payment };
} else {
this.logger.log(`Payment ${payment.id} marked as COMPLETED after gateway verification (code 101)`);
return await this.processSuccessfulVerification(payment, verifyData.ref_id, qryRnr);
}
//
} else {
await this.handleFailedPayment(payment.id, qryRnr);
return { status: PaymentStatus.FAILED, payment };
return { status: payment.status, payment };
}
}
//===============================================
@@ -250,6 +259,7 @@ export class PaymentsService {
{ status: PaymentStatus.COMPLETED, transactionId: transactionId.toString() },
);
await this.removePaymentReminderJobs(payment.id);
await this.handleReferralReward(payment, queryRunner);
return this.walletsService.createPaymentTransaction(userId, amount, payment, queryRunner);
}
@@ -614,4 +624,22 @@ export class PaymentsService {
},
);
}
//===============================================
/**
* Remove all scheduled/delayed payment jobs for a paymentId from the queue
*/
private async removePaymentReminderJobs(paymentId: string) {
const jobs = await this.paymentQueue.getJobs(["delayed", "waiting"]);
for (const job of jobs) {
if (
(job.name === PAYMENT.FIRST_REMINDER || job.name === PAYMENT.SECOND_REMINDER || job.name === PAYMENT.CANCELLATION) &&
job.data &&
job.data.paymentId === paymentId
) {
await job.remove();
this.logger.log(`Removed scheduled job ${job.name} for payment ${paymentId}`);
}
}
}
}
+28 -12
View File
@@ -52,13 +52,13 @@ export class PaymentProcessor extends WorkerProcessor {
private async processJobWithTransaction(job: Job<PaymentJobData>, queryRunner: QueryRunner) {
switch (job.name) {
case PAYMENT.START:
return this.handlePaymentStart(job as Job<{ payment: Payment }>, queryRunner);
return await this.handlePaymentStart(job as Job<{ payment: Payment }>, queryRunner);
case PAYMENT.FIRST_REMINDER:
return this.handleFirstReminder(job as Job<{ paymentId: string }>, queryRunner);
return await this.handleFirstReminder(job as Job<{ paymentId: string }>, queryRunner);
case PAYMENT.SECOND_REMINDER:
return this.handleSecondReminder(job as Job<{ paymentId: string }>, queryRunner);
return await this.handleSecondReminder(job as Job<{ paymentId: string }>, queryRunner);
case PAYMENT.CANCELLATION:
return this.handlePaymentCancellation(job as Job<{ paymentId: string }>, queryRunner);
return await this.handlePaymentCancellation(job as Job<{ paymentId: string }>, queryRunner);
default:
this.logger.error(`Unknown job name: ${job.name}`);
return;
@@ -77,7 +77,9 @@ export class PaymentProcessor extends WorkerProcessor {
private async handleFirstReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
if (!this.isPaymentPending(payment, queryRunner)) {
const isPending = await this.isPaymentPending(payment, queryRunner);
if (!isPending) {
this.logger.log(`Payment ${payment.id} is no longer pending, skipping reminder`);
return payment;
}
@@ -92,7 +94,9 @@ export class PaymentProcessor extends WorkerProcessor {
private async handleSecondReminder(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
if (!this.isPaymentPending(payment, queryRunner)) {
const isPending = await this.isPaymentPending(payment, queryRunner);
if (!isPending) {
this.logger.log(`Payment ${payment.id} is no longer pending, skipping reminder`);
return payment;
}
@@ -107,7 +111,9 @@ export class PaymentProcessor extends WorkerProcessor {
private async handlePaymentCancellation(job: Job<{ paymentId: string }>, queryRunner: QueryRunner) {
const payment = await this.fetchPayment(job.data.paymentId, queryRunner);
if (!this.isPaymentPending(payment, queryRunner)) {
const isPending = await this.isPaymentPending(payment, queryRunner);
if (!isPending) {
this.logger.log(`Payment ${payment.id} is no longer pending, skipping cancellation`);
return payment;
}
@@ -169,7 +175,7 @@ export class PaymentProcessor extends WorkerProcessor {
private async fetchPayment(paymentId: string, queryRunner: QueryRunner): Promise<Payment> {
const payment = await queryRunner.manager.findOne(Payment, {
where: { id: paymentId },
relations: { user: true },
relations: { user: true, paymentGateway: true },
});
if (!payment) throw new Error(`Payment not found: ${paymentId}`);
@@ -179,6 +185,7 @@ export class PaymentProcessor extends WorkerProcessor {
//===============================================
private async isPaymentPending(payment: Payment, queryRunner: QueryRunner): Promise<boolean> {
// 1. Trust DB first
if (payment.status !== PaymentStatus.PENDING) {
this.logger.log(`Payment ${payment.id} is not in pending status`);
return false;
@@ -191,15 +198,24 @@ export class PaymentProcessor extends WorkerProcessor {
const verifyData = await gateway.verifyPayment({ reference: payment.reference, amount: payment.amount });
const result = await this.paymentsService.processVerificationResult(verifyData, payment, queryRunner);
if (result.status === PaymentStatus.FAILED || result.status === PaymentStatus.PENDING) {
this.logger.log(`Payment ${payment.id} verification returned code ${verifyData.code} (failed or pending)`);
// If still pending or failed, return true (will trigger reminder/cancellation as needed)
if (result.status === PaymentStatus.PENDING || result.status === PaymentStatus.FAILED || result.status === PaymentStatus.CANCELLED) {
this.logger.log(`Payment ${payment.id} is still pending or failed or cancelled (gateway code: ${verifyData.code})`);
return true;
}
this.logger.log(`Payment ${payment.id} verification returned code ${verifyData.code} (completed)`);
if (result.status === PaymentStatus.COMPLETED) {
this.logger.log(`Payment ${payment.id} is completed (gateway code: ${verifyData.code})`);
return false;
}
// For any other code, treat as not pending
this.logger.log(`Payment ${payment.id} gateway returned code ${verifyData.code}, treating as not pending`);
return false;
} catch (error) {
this.logger.error(`Error verifying payment ${payment.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
this.logger.error(`Error verifying payment ${payment.id}: ${error instanceof Error ? error.message : error}`);
// On error, assume still pending to avoid false negatives
return true;
}
}