102 lines
4.1 KiB
TypeScript
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}`);
|
|
}
|
|
}
|