mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: ( payment ) add GET /payments/intents/:bookingId for status polling
This commit is contained in:
@@ -11,6 +11,7 @@ import { JwtGuard } from '../../common/jwt.guard';
|
||||
export class PaymentsController {
|
||||
constructor(private service: PaymentsService) {}
|
||||
@Post('initiate') @ApiOperation({ summary: 'Initiate payment for a booking' }) initiatePayment(@Body() dto: InitiatePaymentDto) { return this.service.initiatePayment(dto); }
|
||||
@Get('intents/:bookingId') @ApiOperation({ summary: 'Get payment intent status for a booking' }) getIntent(@Param('bookingId') bookingId: string) { return this.service.getIntentByBookingId(bookingId); }
|
||||
@Post('refund') @ApiOperation({ summary: 'Refund a confirmed booking' }) refund(@Body() dto: RefundDto) { return this.service.refund(dto); }
|
||||
@Post('methods') @ApiOperation({ summary: 'Add a payment method' }) addMethod(@Body() dto: AddPaymentMethodDto) { return this.service.addPaymentMethod(dto); }
|
||||
@Get('methods/:userId') @ApiOperation({ summary: 'Get payment methods for user' }) getMethods(@Param('userId') userId: string) { return this.service.getPaymentMethods(userId); }
|
||||
|
||||
@@ -33,3 +33,13 @@ export class InitiateResponseDto {
|
||||
@ApiPropertyOptional({ type: ClientActionDto }) clientAction?: ClientActionDto;
|
||||
@ApiPropertyOptional() merchantOrderId?: string;
|
||||
}
|
||||
|
||||
export class IntentStatusDto {
|
||||
@ApiProperty() intentId: string;
|
||||
@ApiProperty({ enum: PaymentIntentStatus }) status: PaymentIntentStatus;
|
||||
@ApiPropertyOptional({ type: ClientActionDto }) clientAction?: ClientActionDto;
|
||||
@ApiPropertyOptional() merchantOrderId?: string;
|
||||
@ApiPropertyOptional() paidAt?: string;
|
||||
@ApiPropertyOptional() failureCode?: string;
|
||||
@ApiPropertyOptional() failureMessage?: string;
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import { SeatsService } from '../seats/seats.service';
|
||||
import { TicketsService } from '../tickets/tickets.service';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Prisma, PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
|
||||
import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto, InitiateResponseDto } from './payments.dto';
|
||||
import { InitiatePaymentDto, RefundDto, AddPaymentMethodDto, InitiateResponseDto, IntentStatusDto } from './payments.dto';
|
||||
import { cbeBirrAdapter, eBirrAdapter, cardAdapter } from './payments.adapters';
|
||||
import { PaymentProvider } from './payments.types';
|
||||
import { PaymentProvider, ProviderStatus } from './payments.types';
|
||||
import { TelebirrProvider } from './providers/telebirr.provider';
|
||||
import { createMerchantOrderId } from './providers/telebirr.crypto';
|
||||
|
||||
@@ -221,6 +221,77 @@ export class PaymentsService {
|
||||
};
|
||||
}
|
||||
|
||||
async getIntentByBookingId(bookingId: string): Promise<IntentStatusDto> {
|
||||
const intent = await this.prisma.paymentIntent.findUnique({
|
||||
where: { bookingId },
|
||||
});
|
||||
if (!intent) throw new NotFoundException('PaymentIntent not found');
|
||||
|
||||
const refreshable =
|
||||
intent.status === PaymentIntentStatus.REQUIRES_ACTION ||
|
||||
intent.status === PaymentIntentStatus.PROCESSING;
|
||||
const stale = intent.updatedAt.getTime() < Date.now() - 5_000;
|
||||
const provider = this.providers.get(intent.method);
|
||||
|
||||
if (refreshable && stale && intent.merchantOrderId && provider) {
|
||||
try {
|
||||
const status = await provider.queryStatus(intent.merchantOrderId);
|
||||
await this.applyProviderStatus(intent.id, status);
|
||||
const refreshed = await this.prisma.paymentIntent.findUniqueOrThrow({
|
||||
where: { id: intent.id },
|
||||
});
|
||||
return this.formatIntentStatus(refreshed);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
this.logger.warn(
|
||||
`queryStatus failed for intent ${intent.id}: ${message}; returning cached`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.formatIntentStatus(intent);
|
||||
}
|
||||
|
||||
private async applyProviderStatus(
|
||||
intentId: string,
|
||||
status: ProviderStatus,
|
||||
): Promise<void> {
|
||||
if (status.status === PaymentIntentStatus.SUCCEEDED) {
|
||||
await this.finalizePaymentSuccess({
|
||||
intentId,
|
||||
providerTxnId: status.providerTxnId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (status.status === PaymentIntentStatus.FAILED) {
|
||||
await this.markPaymentFailed({
|
||||
intentId,
|
||||
failureCode: status.failureCode,
|
||||
failureMessage: status.failureMessage,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await this.prisma.paymentIntent.update({
|
||||
where: { id: intentId },
|
||||
data: {
|
||||
status: status.status,
|
||||
providerTxnId: status.providerTxnId ?? undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private formatIntentStatus(
|
||||
intent: Prisma.PaymentIntentGetPayload<Record<string, never>>,
|
||||
): IntentStatusDto {
|
||||
const base = this.formatIntentResponse(intent);
|
||||
return {
|
||||
...base,
|
||||
paidAt: intent.paidAt?.toISOString(),
|
||||
failureCode: intent.failureCode ?? undefined,
|
||||
failureMessage: intent.failureMessage ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async refund(dto: RefundDto) {
|
||||
const intent = await this.prisma.paymentIntent.findUnique({ where: { bookingId: dto.bookingId } });
|
||||
if (!intent || intent.status !== 'SUCCEEDED') throw new BadRequestException('No successful payment to refund');
|
||||
|
||||
Reference in New Issue
Block a user