diff --git a/src/configs/bullmq.config.ts b/src/configs/bullmq.config.ts index cbbdba9..b29159e 100755 --- a/src/configs/bullmq.config.ts +++ b/src/configs/bullmq.config.ts @@ -10,9 +10,8 @@ export function bullMqConfig(): SharedBullAsyncConfiguration { }, prefix: "danak-console", defaultJobOptions: { - removeOnComplete: false, + removeOnComplete: true, removeOnFail: false, - attempts: 5, }, }), }; diff --git a/src/modules/announcements/DTO/create-announcement.dto.ts b/src/modules/announcements/DTO/create-announcement.dto.ts index 4342f19..45827b9 100755 --- a/src/modules/announcements/DTO/create-announcement.dto.ts +++ b/src/modules/announcements/DTO/create-announcement.dto.ts @@ -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 }) diff --git a/src/modules/payments/payments.module.ts b/src/modules/payments/payments.module.ts index 5844df6..26bd049 100755 --- a/src/modules/payments/payments.module.ts +++ b/src/modules/payments/payments.module.ts @@ -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, diff --git a/src/modules/payments/providers/payments.service.ts b/src/modules/payments/providers/payments.service.ts index 3c2636c..490f40b 100755 --- a/src/modules/payments/providers/payments.service.ts +++ b/src/modules/payments/providers/payments.service.ts @@ -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}`); + } + } + } } diff --git a/src/modules/payments/queue/payment.processor.ts b/src/modules/payments/queue/payment.processor.ts index 4df03be..9568c08 100755 --- a/src/modules/payments/queue/payment.processor.ts +++ b/src/modules/payments/queue/payment.processor.ts @@ -52,13 +52,13 @@ export class PaymentProcessor extends WorkerProcessor { private async processJobWithTransaction(job: Job, 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 { 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 { + // 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; } }