File

apps/recallassess/recallassess-api/src/api/shared/stripe/stripe.controller.ts

Prefix

api/stripe

Index

Methods

Methods

Async createCheckoutSession
createCheckoutSession(body: CreateCheckoutSessionDto)
Decorators :
@Public()
@Post('checkout/create')

Create checkout session for participant payment Public endpoint - participants need to create payment sessions

Parameters :
Name Type Optional
body CreateCheckoutSessionDto No
Async createPaymentIntent
createPaymentIntent(body: CreatePaymentIntentDto)
Decorators :
@Public()
@Post('payment-intent/create')

Create payment intent (for embedded card form) Public endpoint - for in-dialog card payment

Parameters :
Name Type Optional
body CreatePaymentIntentDto No
getConfig
getConfig()
Decorators :
@Public()
@Get('config')

Get Stripe publishable key (for frontend) Public endpoint - no authentication required

Async getInvoiceStatus
getInvoiceStatus(invoiceId: string)
Decorators :
@Public()
@Get('invoice/:invoiceId/status')

Get invoice payment status Public endpoint - participants need to check payment status

Parameters :
Name Type Optional
invoiceId string No
Returns : unknown
Async handleWebhook
handleWebhook(signature: string, rawBody: Buffer, req: Request)
Decorators :
@Public()
@Post('webhook')
@HttpCode(HttpStatus.OK)

Webhook endpoint for Stripe events This is called by Stripe when events occur (payment success, etc.) Public endpoint - Stripe calls this directly

Parameters :
Name Type Optional
signature string No
rawBody Buffer No
req Request No
Returns : unknown
Private nodeEnvForDiagnostics
nodeEnvForDiagnostics()

For JSON payloads: omit key when unset (matches previous process.env.NODE_ENV).

Returns : string | undefined
Async testRefundWebhook
testRefundWebhook(refundId: string)
Decorators :
@Public()
@Post('webhook/test-refund/:refundId')
@HttpCode(HttpStatus.OK)

Manual test endpoint to sync a refund by id Useful when refunds were created in Stripe Dashboard before webhook handling existed. TODO: Remove or secure this endpoint in production

Parameters :
Name Type Optional
refundId string No
Returns : unknown
Async testWebhook
testWebhook(sessionId: string)
Decorators :
@Public()
@Post('webhook/test/:sessionId')
@HttpCode(HttpStatus.OK)

Manual test endpoint to process a checkout session by ID This is for debugging when webhooks are not working TODO: Remove or secure this endpoint in production

Parameters :
Name Type Optional
sessionId string No
Returns : unknown
Async testWebhookConnectivity
testWebhookConnectivity(signature: string, rawBody: Buffer, req: Request)
Decorators :
@Public()
@Post('webhook/test-connectivity')
@HttpCode(HttpStatus.OK)

Test webhook connectivity and signature verification TODO: Remove this endpoint in production

Parameters :
Name Type Optional
signature string No
rawBody Buffer No
req Request No
Returns : unknown
import { optionalEnv } from "@bish-nest/core";
import { Public } from "@bish-nest/core/auth/decorator/public.decorator";
import {
  BadRequestException,
  Body,
  Controller,
  Get,
  Headers,
  HttpCode,
  HttpStatus,
  Logger,
  Param,
  Post,
  RawBody,
  RawBodyRequest,
  Req,
} from "@nestjs/common";
import { Request } from "express";
import {
  CheckoutSessionResponseDto,
  CreateCheckoutSessionDto,
  CreatePaymentIntentDto,
  PaymentIntentResponseDto,
  StripePublishableKeyDto,
} from "./dto/payment.dto";
import { StripeService } from "./services/stripe.service";
import { StripePaymentService } from "./services/stripe-payment.service";
import { StripeWebhookService } from "./services/stripe-webhook.service";

@Controller("api/stripe")
export class StripeController {
  private readonly logger = new Logger(StripeController.name);

  /** For JSON payloads: omit key when unset (matches previous `process.env.NODE_ENV`). */
  private nodeEnvForDiagnostics(): string | undefined {
    const v = optionalEnv("NODE_ENV", "");
    return v === "" ? undefined : v;
  }

  constructor(
    private readonly stripeService: StripeService,
    private readonly paymentService: StripePaymentService,
    private readonly webhookService: StripeWebhookService,
  ) {}

  /**
   * Get Stripe publishable key (for frontend)
   * Public endpoint - no authentication required
   */
  @Public()
  @Get("config")
  getConfig(): StripePublishableKeyDto {
    return {
      publishable_key: this.stripeService.getPublishableKey(),
    };
  }

  /**
   * Create checkout session for participant payment
   * Public endpoint - participants need to create payment sessions
   */
  @Public()
  @Post("checkout/create")
  async createCheckoutSession(@Body() body: CreateCheckoutSessionDto): Promise<CheckoutSessionResponseDto> {
    return await this.paymentService.createCheckoutSession(body);
  }

  /**
   * Create payment intent (for embedded card form)
   * Public endpoint - for in-dialog card payment
   */
  @Public()
  @Post("payment-intent/create")
  async createPaymentIntent(@Body() body: CreatePaymentIntentDto): Promise<PaymentIntentResponseDto> {
    return await this.paymentService.createPaymentIntent(body);
  }

  /**
   * Get invoice payment status
   * Public endpoint - participants need to check payment status
   */
  @Public()
  @Get("invoice/:invoiceId/status")
  async getInvoiceStatus(@Param("invoiceId") invoiceId: string) {
    return await this.paymentService.getInvoiceStatus(parseInt(invoiceId, 10));
  }

  /**
   * Webhook endpoint for Stripe events
   * This is called by Stripe when events occur (payment success, etc.)
   * Public endpoint - Stripe calls this directly
   */
  @Public()
  @Post("webhook")
  @HttpCode(HttpStatus.OK)
  async handleWebhook(@Headers("stripe-signature") signature: string, @RawBody() rawBody: Buffer, @Req() req: Request) {
    const startTime = Date.now();
    this.logger.log("=== WEBHOOK REQUEST RECEIVED AT CONTROLLER ===");
    this.logger.log(`URL: ${req.url}`);
    this.logger.log(`Method: ${req.method}`);
    this.logger.log(`Has signature: ${!!signature}`);
    this.logger.log(`Raw body type: ${typeof rawBody}`);
    this.logger.log(`Raw body length: ${rawBody ? rawBody.length : 'undefined'}`);
    this.logger.log(`User-Agent: ${req.headers['user-agent']}`);
    this.logger.log(`Content-Type: ${req.headers['content-type']}`);

    if (!rawBody) {
      this.logger.error("Missing raw body in webhook request");
      throw new BadRequestException("Missing raw body");
    }

    try {
      const result = await this.webhookService.processWebhook(rawBody, signature);
      const duration = Date.now() - startTime;
      this.logger.log(`=== WEBHOOK PROCESSED SUCCESSFULLY in ${duration}ms ===`);
      return result;
    } catch (error) {
      const duration = Date.now() - startTime;
      this.logger.error(`=== WEBHOOK PROCESSING FAILED in ${duration}ms ===`);
      this.logger.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
      throw error;
    }
  }

  /**
   * Manual test endpoint to process a checkout session by ID
   * This is for debugging when webhooks are not working
   * TODO: Remove or secure this endpoint in production
   */
  @Public()
  @Post("webhook/test/:sessionId")
  @HttpCode(HttpStatus.OK)
  async testWebhook(@Param("sessionId") sessionId: string) {
    this.logger.log(`=== MANUAL WEBHOOK TEST FOR SESSION: ${sessionId} ===`);

    try {
      // Retrieve the checkout session from Stripe
      const session = await this.stripeService.retrieveCheckoutSession(sessionId);

      this.logger.log(`Retrieved session: ${session.id}, Status: ${session.payment_status}`);
      this.logger.log(`Session metadata: ${JSON.stringify(session.metadata)}`);

      // Process the checkout session
      await this.paymentService.handleCheckoutSessionCompleted(session);

      return {
        success: true,
        message: `Processed checkout session ${sessionId}`,
        session_status: session.payment_status,
      };
    } catch (error) {
      this.logger.error(`Failed to process test webhook for session ${sessionId}:`, error);
      throw new BadRequestException(`Failed to process session: ${error instanceof Error ? error.message : String(error)}`);
    }
  }

  /**
   * Manual test endpoint to sync a refund by id
   * Useful when refunds were created in Stripe Dashboard before webhook handling existed.
   * TODO: Remove or secure this endpoint in production
   */
  @Public()
  @Post("webhook/test-refund/:refundId")
  @HttpCode(HttpStatus.OK)
  async testRefundWebhook(@Param("refundId") refundId: string) {
    this.logger.log(`=== MANUAL REFUND SYNC: ${refundId} ===`);
    try {
      const refund = await this.stripeService.retrieveRefund(refundId);
      await this.paymentService.handleRefundCreated(refund);
      return { success: true, message: `Synced refund ${refundId}` };
    } catch (error) {
      this.logger.error(`Failed to sync refund ${refundId}:`, error);
      throw new BadRequestException(
        `Failed to sync refund: ${error instanceof Error ? error.message : String(error)}`,
      );
    }
  }

  /**
   * Test webhook connectivity and signature verification
   * TODO: Remove this endpoint in production
   */
  @Public()
  @Post("webhook/test-connectivity")
  @HttpCode(HttpStatus.OK)
  async testWebhookConnectivity(@Headers("stripe-signature") signature: string, @RawBody() rawBody: Buffer, @Req() req: Request) {
    this.logger.log(`=== WEBHOOK CONNECTIVITY TEST ===`);
    this.logger.log(`Environment: ${optionalEnv("NODE_ENV", "unknown")}`);
    this.logger.log(`Webhook secret configured: ${optionalEnv("STRIPE_WEBHOOK_SECRET", "") !== ""}`);
    this.logger.log(`Signature provided: ${!!signature}`);
    this.logger.log(`Body length: ${rawBody ? rawBody.length : 0}`);

    if (!signature) {
      return {
        status: "error",
        message: "Missing stripe-signature header",
        environment: this.nodeEnvForDiagnostics(),
        timestamp: new Date().toISOString()
      };
    }

    if (!rawBody) {
      return {
        status: "error",
        message: "Missing request body",
        environment: this.nodeEnvForDiagnostics(),
        timestamp: new Date().toISOString()
      };
    }

    try {
      // Try to verify the signature
      const event = this.stripeService.verifyWebhookSignature(rawBody, signature);

      return {
        status: "success",
        message: "Webhook connectivity test successful",
        environment: this.nodeEnvForDiagnostics(),
        event_type: event.type,
        event_id: event.id,
        timestamp: new Date().toISOString()
      };
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      this.logger.error(`Webhook signature verification failed: ${errorMessage}`);

      return {
        status: "error",
        message: `Signature verification failed: ${errorMessage}`,
        environment: this.nodeEnvForDiagnostics(),
        timestamp: new Date().toISOString()
      };
    }
  }
}

results matching ""

    No results matching ""