apps/recallassess/recallassess-api/src/api/shared/stripe/stripe.controller.ts
api/stripe
Methods |
|
| Async createCheckoutSession | ||||||
createCheckoutSession(body: CreateCheckoutSessionDto)
|
||||||
Decorators :
@Public()
|
||||||
|
Create checkout session for participant payment Public endpoint - participants need to create payment sessions
Parameters :
Returns :
Promise<CheckoutSessionResponseDto>
|
| Async createPaymentIntent | ||||||
createPaymentIntent(body: CreatePaymentIntentDto)
|
||||||
Decorators :
@Public()
|
||||||
|
Create payment intent (for embedded card form) Public endpoint - for in-dialog card payment
Parameters :
Returns :
Promise<PaymentIntentResponseDto>
|
| getConfig |
getConfig()
|
Decorators :
@Public()
|
|
Get Stripe publishable key (for frontend) Public endpoint - no authentication required
Returns :
StripePublishableKeyDto
|
| Async getInvoiceStatus | ||||||
getInvoiceStatus(invoiceId: string)
|
||||||
Decorators :
@Public()
|
||||||
|
Get invoice payment status Public endpoint - participants need to check payment status
Parameters :
Returns :
unknown
|
| Async handleWebhook | ||||||||||||
handleWebhook(signature: string, rawBody: Buffer, req: Request)
|
||||||||||||
Decorators :
@Public()
|
||||||||||||
|
Webhook endpoint for Stripe events This is called by Stripe when events occur (payment success, etc.) Public endpoint - Stripe calls this directly
Parameters :
Returns :
unknown
|
| Private nodeEnvForDiagnostics |
nodeEnvForDiagnostics()
|
|
For JSON payloads: omit key when unset (matches previous
Returns :
string | undefined
|
| Async testRefundWebhook | ||||||
testRefundWebhook(refundId: string)
|
||||||
Decorators :
@Public()
|
||||||
|
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 :
Returns :
unknown
|
| Async testWebhook | ||||||
testWebhook(sessionId: string)
|
||||||
Decorators :
@Public()
|
||||||
|
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 :
Returns :
unknown
|
| Async testWebhookConnectivity | ||||||||||||
testWebhookConnectivity(signature: string, rawBody: Buffer, req: Request)
|
||||||||||||
Decorators :
@Public()
|
||||||||||||
|
Test webhook connectivity and signature verification TODO: Remove this endpoint in production
Parameters :
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()
};
}
}
}