diff --git a/apps/edr-passenger-api/package.json b/apps/edr-passenger-api/package.json index 043211ce2..6c2aee76b 100644 --- a/apps/edr-passenger-api/package.json +++ b/apps/edr-passenger-api/package.json @@ -32,6 +32,7 @@ "@nestjs/platform-express": "^11.1.19", "@nestjs/schedule": "^6.1.3", "@nestjs/swagger": "^7.4.0", + "@prisma/client": "^6.19.3", "@sendgrid/mail": "^8.1.0", "axios": "^1.7.7", "bcrypt": "^5.1.1", @@ -44,8 +45,7 @@ "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", "swagger-ui-express": "^5.0.0", - "tsconfig-paths": "^4.2.0", - "@prisma/client": "^6.19.3" + "tsconfig-paths": "^4.2.0" }, "devDependencies": { "@edr/eslint-config": "workspace:*", @@ -54,6 +54,7 @@ "@nestjs/schematics": "^11.1.0", "@nestjs/testing": "^11.1.19", "@types/bcrypt": "^5.0.2", + "@types/express": "^5.0.6", "@types/jest": "^29.5.11", "@types/node": "^20.10.6", "@types/passport-jwt": "^4.0.1", diff --git a/apps/edr-passenger-api/src/modules/payments/payments.controller.ts b/apps/edr-passenger-api/src/modules/payments/payments.controller.ts index 3ff1fee32..49b570ab1 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.controller.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.controller.ts @@ -1,7 +1,8 @@ -import { Body, Controller, Get, Param, Post, Query, UseGuards } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiOkResponse } from '@nestjs/swagger'; +import { Body, Controller, Get, HttpStatus, Param, Post, Query, Res, UseGuards } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiQuery, ApiOkResponse, ApiProduces } from '@nestjs/swagger'; +import { Response } from 'express'; import { PaymentsService } from './payments.service'; -import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto, PaymentRegionEnum, SupportedPaymentMethodDto } from './payments.dto'; +import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto, PaymentRegionEnum, SupportedPaymentMethodDto, PaymentMethodTypeEnum, PaymentPlatformDto } from './payments.dto'; import { JwtGuard } from '../../common/jwt.guard'; import { RolesGuard } from '../../common/roles.guard'; import { Roles } from '../../common/roles.decorator'; @@ -62,4 +63,113 @@ export class PaymentsController { @ApiQuery({ name: 'region', enum: PaymentRegionEnum, required: false }) @ApiOkResponse({ type: [SupportedPaymentMethodDto] }) getMethods(@Query('region') region?: PaymentRegionEnum) { return this.service.getSupportedPaymentMethods(region); } + + @Get('checkout') + @ApiOperation({ + summary: 'Browser checkout redirect', + description: 'Initiates payment and returns an HTML page that auto-redirects the browser to the provider checkout URL. Designed to be opened directly in a browser tab.', + }) + @ApiQuery({ name: 'bookingId', required: true }) + @ApiQuery({ name: 'method', enum: PaymentMethodTypeEnum, required: true }) + @ApiQuery({ name: 'platform', enum: ['web', 'mobile'], required: false }) + @ApiProduces('text/html') + async checkout( + @Query('bookingId') bookingId: string, + @Query('method') method: PaymentMethodTypeEnum, + @Query('platform') platform: PaymentPlatformDto = 'web', + @Res() res: Response, + ) { + if (!bookingId) { + return res.status(HttpStatus.BAD_REQUEST).type('html').send(this.buildErrorHtml('Missing required query parameter: bookingId')); + } + if (!method || !Object.values(PaymentMethodTypeEnum).includes(method)) { + return res.status(HttpStatus.BAD_REQUEST).type('html').send(this.buildErrorHtml('Missing or invalid query parameter: method')); + } + + try { + const result = await this.service.initiatePayment({ bookingId, method, platform }); + const url = result.clientAction?.type === 'REDIRECT' ? result.clientAction.url : undefined; + + if (url) { + return res.status(HttpStatus.OK).type('html').send(this.buildRedirectHtml(url)); + } + + return res.status(HttpStatus.OK).type('html').send(this.buildStatusHtml(result.status, result.intentId)); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : 'An unexpected error occurred'; + return res.status(HttpStatus.OK).type('html').send(this.buildErrorHtml(message)); + } + } + + private buildRedirectHtml(url: string): string { + const escaped = url.replace(/"/g, '"'); + return ` + + + + + Redirecting to payment… + + + +
+
+

Redirecting to payment provider…

+

Click here if you are not redirected

+
+ + +`; + } + + private buildStatusHtml(status: string, intentId: string): string { + return ` + + + + Payment status + + + +
+
${status}
+ Intent: ${intentId} +
+ +`; + } + + private buildErrorHtml(message: string): string { + return ` + + + + Payment error + + + +
+
Payment could not be initiated
+

${message}

+
+ +`; + } }