it0/packages/services/billing-service/src/application/use-cases/handle-payment-webhook.use-...

102 lines
4.1 KiB
TypeScript

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<string, string>): Promise<void> {
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<string, any>) {
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}`);
}
}