All files / src/platform/billing/providers stripe.provider.ts

0% Statements 0/27
0% Branches 0/13
0% Functions 0/4
0% Lines 0/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128                                                                                                                                                                                                                                                               
import { PaymentProvider } from '../payment-provider.interface';
import Stripe from 'stripe';
import { Logger } from '@nestjs/common';
 
export class StripeProvider implements PaymentProvider {
  name = 'Stripe';
  private stripe: Stripe;
  private readonly logger = new Logger(StripeProvider.name);
  private webhookSecret: string;
 
  constructor(apiKey: string, webhookSecret: string = '') {
    this.stripe = new Stripe(apiKey);
    this.webhookSecret = webhookSecret;
  }
 
  async createCheckoutLink(params: {
    tenantId: string;
    amount: number;
    currency: string;
    invoiceId: string;
    callbackUrl: string;
  }): Promise<{ checkoutUrl: string; providerIntentId: string }> {
    try {
      const session = await this.stripe.checkout.sessions.create({
        payment_method_types: ['card'],
        line_items: [
          {
            price_data: {
              currency: params.currency.toLowerCase(),
              product_data: {
                name: `Invoice ${params.invoiceId}`,
              },
              unit_amount: params.amount,
            },
            quantity: 1,
          },
        ],
        mode: 'payment',
        success_url: `${params.callbackUrl}?session_id={CHECKOUT_SESSION_ID}&status=success`,
        cancel_url: `${params.callbackUrl}?status=cancel`,
        client_reference_id: params.tenantId,
        metadata: {
          invoiceId: params.invoiceId,
          tenantId: params.tenantId,
        },
      });
 
      return {
        checkoutUrl: session.url as string,
        providerIntentId: session.id,
      };
    } catch (error: any) {
      this.logger.error(`Stripe create checkout failed: ${error.message}`);
      throw error;
    }
  }
 
  async verifyWebhook(
    payload: any,
    signature: string,
  ): Promise<{
    success: boolean;
    transactionId: string;
    amountPaid: number;
    status: 'SUCCESS' | 'FAILED';
  }> {
    let event: Stripe.Event;
 
    // Ideally, payload should be the raw body buffer.
    // If we have the webhook secret, we use constructEvent.
    if (this.webhookSecret && typeof payload === 'string') {
      try {
        event = this.stripe.webhooks.constructEvent(
          payload,
          signature,
          this.webhookSecret,
        );
      } catch (err: any) {
        this.logger.error(
          `Webhook signature verification failed: ${err.message}`,
        );
        return {
          success: false,
          transactionId: '',
          amountPaid: 0,
          status: 'FAILED',
        };
      }
    } else {
      // Fallback for when raw body is not passed or no secret provided (testing environments)
      event = typeof payload === 'string' ? JSON.parse(payload) : payload;
    }
 
    if (event.type === 'checkout.session.completed') {
      const session = event.data.object;
      return {
        success: true,
        transactionId: (session.payment_intent as string) || session.id,
        amountPaid: session.amount_total || 0,
        status: 'SUCCESS',
      };
    }
 
    return {
      success: true,
      transactionId: '',
      amountPaid: 0,
      status: 'FAILED',
    };
  }
 
  async refund(
    transactionId: string,
    amount: number,
  ): Promise<{ success: boolean; refundId: string }> {
    try {
      const refund = await this.stripe.refunds.create({
        payment_intent: transactionId,
        amount,
      });
      return { success: true, refundId: refund.id };
    } catch (error: any) {
      this.logger.error(`Stripe refund failed: ${error.message}`);
      return { success: false, refundId: '' };
    }
  }
}