import { Injectable, Logger } from '@nestjs/common'; import { PaymentRepository } from '../../infrastructure/repositories/payment.repository'; import { InvoiceRepository } from '../../infrastructure/repositories/invoice.repository'; import { SubscriptionRepository } from '../../infrastructure/repositories/subscription.repository'; import { PaymentStatus } from '../../domain/entities/payment.entity'; import { InvoiceStatus } from '../../domain/entities/invoice.entity'; import { SubscriptionLifecycleService } from '../../domain/services/subscription-lifecycle.service'; import { PaymentProviderType, WebhookResult } from '../../domain/ports/payment-provider.port'; import { PaymentProviderRegistry } from '../../infrastructure/payment-providers/payment-provider.registry'; @Injectable() export class HandlePaymentWebhookUseCase { private readonly logger = new Logger(HandlePaymentWebhookUseCase.name); constructor( private readonly providerRegistry: PaymentProviderRegistry, private readonly paymentRepo: PaymentRepository, private readonly invoiceRepo: InvoiceRepository, private readonly subscriptionRepo: SubscriptionRepository, private readonly lifecycle: SubscriptionLifecycleService, ) {} async execute(provider: PaymentProviderType, rawBody: Buffer, headers: Record): Promise { const paymentProvider = this.providerRegistry.get(provider); if (!paymentProvider) { this.logger.warn(`Webhook received for unconfigured provider: ${provider}`); return; } let result: WebhookResult; try { result = await paymentProvider.handleWebhook(rawBody, headers); } catch (err) { this.logger.error(`Webhook signature verification failed for ${provider}: ${err.message}`); throw err; } if (!result || result.type === 'unknown') return; if (result.type === 'payment_succeeded') { await this.handlePaymentSucceeded(result.providerPaymentId, result.metadata); } else if (result.type === 'payment_failed') { await this.handlePaymentFailed(result.providerPaymentId); } else if (result.type === 'refunded') { await this.handleRefund(result.providerPaymentId); } } private async handlePaymentSucceeded(providerPaymentId: string, metadata?: Record) { const payment = await this.paymentRepo.findByProviderPaymentId(providerPaymentId); if (!payment) { this.logger.warn(`Payment not found for provider ID: ${providerPaymentId}`); return; } payment.status = PaymentStatus.SUCCEEDED; payment.paidAt = new Date(); await this.paymentRepo.savePayment(payment); // Mark invoice as paid const invoice = await this.invoiceRepo.findById(payment.invoiceId ?? ''); if (invoice) { invoice.status = InvoiceStatus.PAID; invoice.paidAt = new Date(); await this.invoiceRepo.save(invoice); } // Activate/renew subscription const subscription = await this.subscriptionRepo.findByTenantId(payment.tenantId); if (subscription) { const activated = this.lifecycle.activate(subscription); await this.subscriptionRepo.save(activated); this.logger.log(`Subscription activated for tenant ${payment.tenantId}`); } } private async handlePaymentFailed(providerPaymentId: string) { const payment = await this.paymentRepo.findByProviderPaymentId(providerPaymentId); if (!payment) return; payment.status = PaymentStatus.FAILED; await this.paymentRepo.savePayment(payment); // Mark subscription as past_due const subscription = await this.subscriptionRepo.findByTenantId(payment.tenantId); if (subscription) { const pastDue = this.lifecycle.markPastDue(subscription); await this.subscriptionRepo.save(pastDue); this.logger.warn(`Subscription marked past_due for tenant ${payment.tenantId}`); } } private async handleRefund(providerPaymentId: string) { const payment = await this.paymentRepo.findByProviderPaymentId(providerPaymentId); if (!payment) return; payment.status = PaymentStatus.REFUNDED; await this.paymentRepo.savePayment(payment); this.logger.log(`Refund processed for payment ${providerPaymentId}`); } }