mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 12:30:58 +00:00
Manuel ticket generation, seat management by route added
This commit is contained in:
@@ -33,6 +33,7 @@ import {
|
||||
PaymentMethodTypeEnum,
|
||||
PaymentPlatformDto,
|
||||
BookingAmountResponseDto,
|
||||
ForceConfirmDto,
|
||||
} from "./payments.dto";
|
||||
import { PassengerStaff } from "../../common/passenger-guards";
|
||||
import { PASSENGER_PERMS } from "../../seed/passenger-permissions.registry";
|
||||
@@ -124,6 +125,19 @@ export class PaymentsController {
|
||||
return this.service.refund(dto);
|
||||
}
|
||||
|
||||
@Post(":bookingId/force-confirm")
|
||||
@PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth("IAM-auth")
|
||||
@ApiOperation({
|
||||
summary: "Force-confirm payment & generate ticket (back-office only)",
|
||||
description:
|
||||
"Marks the payment as SUCCEEDED, confirms the booking, and generates the ticket. " +
|
||||
"Use when a vendor payment completed but the webhook was never delivered. Idempotent.",
|
||||
})
|
||||
forceConfirm(@Param("bookingId") bookingId: string, @Body() dto: ForceConfirmDto) {
|
||||
return this.service.forceConfirmPayment(bookingId, dto);
|
||||
}
|
||||
|
||||
@Post("methods")
|
||||
@PassengerStaff([PASSENGER_PERMS.payments.manageMethods, PASSENGER_PERMS.admin])
|
||||
@ApiBearerAuth("IAM-auth")
|
||||
|
||||
@@ -142,3 +142,14 @@ export class BookingAmountResponseDto {
|
||||
@ApiProperty({ example: 'DJF', description: 'Currency of the returned amount' }) currency: string;
|
||||
@ApiProperty({ example: 162.5, description: 'Booking total converted to the requested currency (major units)' }) amount: number;
|
||||
}
|
||||
|
||||
export class ForceConfirmDto {
|
||||
@ApiPropertyOptional({ description: 'External payment reference / transaction ID from the vendor', example: 'TXN-123456' })
|
||||
@IsOptional() @IsString() paymentReference?: string;
|
||||
|
||||
@ApiPropertyOptional({ enum: PaymentMethodTypeEnum, description: 'Payment method used externally', example: 'TELEBIRR' })
|
||||
@IsOptional() @IsEnum(PaymentMethodTypeEnum) paymentMethod?: PaymentMethodTypeEnum;
|
||||
|
||||
@ApiPropertyOptional({ description: 'Internal notes about why this was force-confirmed', example: 'Vendor confirmed via phone' })
|
||||
@IsOptional() @IsString() notes?: string;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
InitiateResponseDto,
|
||||
IntentStatusDto,
|
||||
PaymentRegionEnum,
|
||||
ForceConfirmDto,
|
||||
} from "./payments.dto";
|
||||
import { PaymentEventDto, MarkPaidResponseDto } from "./internal-payments.dto";
|
||||
import { PaymentClientService } from "./payment-client.service";
|
||||
@@ -773,6 +774,51 @@ export class PaymentsService {
|
||||
return { processed: true, alreadyFinalized };
|
||||
}
|
||||
|
||||
async forceConfirmPayment(bookingId: string, dto: ForceConfirmDto = {}): Promise<{ alreadyFinalized: boolean }> {
|
||||
const booking = await this.prisma.booking.findUnique({ where: { id: bookingId } });
|
||||
if (!booking) throw new NotFoundException('Booking not found');
|
||||
|
||||
const resolvedMethod = dto.paymentMethod
|
||||
? (dto.paymentMethod as unknown as PaymentMethodType)
|
||||
: PaymentMethodType.TELEBIRR;
|
||||
|
||||
let intent = await this.prisma.paymentIntent.findUnique({ where: { bookingId } });
|
||||
if (!intent) {
|
||||
intent = await this.prisma.paymentIntent.create({
|
||||
data: {
|
||||
bookingId,
|
||||
amountMinor: booking.totalMinor,
|
||||
currency: booking.currency,
|
||||
method: resolvedMethod,
|
||||
status: PaymentIntentStatus.PROCESSING,
|
||||
providerRef: `FORCE-${Date.now()}`,
|
||||
providerTxnId: dto.paymentReference ?? null,
|
||||
failureMessage: dto.notes ?? null,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Update method/reference/notes regardless of current status
|
||||
const updateData: any = {};
|
||||
if (dto.paymentReference) updateData.providerTxnId = dto.paymentReference;
|
||||
if (dto.paymentMethod) updateData.method = resolvedMethod;
|
||||
if (dto.notes) updateData.failureMessage = dto.notes;
|
||||
if (intent.status === PaymentIntentStatus.CANCELLED || intent.status === PaymentIntentStatus.FAILED) {
|
||||
updateData.status = PaymentIntentStatus.PROCESSING;
|
||||
}
|
||||
if (Object.keys(updateData).length) {
|
||||
intent = await this.prisma.paymentIntent.update({
|
||||
where: { id: intent.id },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return this.finalizePaymentSuccess({
|
||||
intentId: intent.id,
|
||||
providerTxnId: dto.paymentReference ?? intent.providerTxnId ?? undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async markPaymentFailed(input: {
|
||||
intentId: string;
|
||||
failureCode?: string;
|
||||
|
||||
@@ -258,8 +258,8 @@ export class TicketsService {
|
||||
type: booking.bookingType,
|
||||
passenger: passengerName,
|
||||
seats: passengerSeats.map(ps => ({
|
||||
seat: ps.seat.seatNumber,
|
||||
coach: ps.seat.coach.number,
|
||||
seat: ps.seat?.seatNumber,
|
||||
coach: ps.seat?.coach?.number,
|
||||
leg: ps.leg || 1,
|
||||
scheduleId: ps.scheduleId || booking.scheduleId,
|
||||
})),
|
||||
|
||||
Reference in New Issue
Block a user