Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 3 additions & 15 deletions apps/bot/services/payment-service/src/payment-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { BookingServiceClient } from './booking-service-client.js'
import type { BookingConfirmation } from './booking-service-client.js'
import type { PaymentServiceLogger } from './logger.js'
import { ValidationError } from './errors.js'
import { markSagaBookingCompleted, markSagaBookingFailed } from './saga-transitions.js'
import { parsePaymentCompletedEvent } from './validation.js'

// Зависимости consumer-а.
Expand Down Expand Up @@ -100,14 +101,7 @@ async function handlePaymentCompleted(event: PaymentCompletedEvent, deps: Paymen
)
// Если booking создан, закрываем hold и saga.
await deps.prisma.$transaction(async (tx) => {
await tx.slotHold.updateMany({
data: { status: 'paid' },
where: { invoiceId: event.invoiceId, status: 'held' },
})
await tx.paymentSaga.updateMany({
data: { bookingId: booking.id, status: 'completed' },
where: { invoiceId: event.invoiceId },
})
await markSagaBookingCompleted(tx, event.invoiceId, booking.id)
})
await writeAudit(deps.prisma, deps.logger, {
action: 'payment.booking_created',
Expand All @@ -125,13 +119,7 @@ async function handlePaymentCompleted(event: PaymentCompletedEvent, deps: Paymen
// Ошибка booking-service переводит saga в failed для ручного recovery.
const failureReason = error instanceof Error ? error.message : 'booking create failed'
await deps.prisma.$transaction(async (tx) => {
await tx.paymentSaga.updateMany({
data: {
failureReason,
status: 'failed',
},
where: { invoiceId: event.invoiceId },
})
await markSagaBookingFailed(tx, event.invoiceId, failureReason)
})
await writeAudit(deps.prisma, deps.logger, {
action: 'payment.booking_failed',
Expand Down
19 changes: 3 additions & 16 deletions apps/bot/services/payment-service/src/payment-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { AuthenticationError, ConflictError, DownstreamServiceError, NotFoundErr
import { sendJson } from './http-response.js'
import type { PaymentServiceLogger } from './logger.js'
import type { BookingServiceClient } from './booking-service-client.js'
import { markSagaAwaitingBooking, startSagaCompensation } from './saga-transitions.js'
import { parseCreateInvoiceInput, parsePreCheckoutQuery, parseSuccessfulPaymentMessage, readIdFromPath } from './validation.js'

// Telegram Stars ограничивает сумму одного инвойса значением 9 999 999 минимальных единиц.
Expand Down Expand Up @@ -460,10 +461,7 @@ export class PaymentRouter {
// Обновляем invoice и saga в базе.
audit({ ts: new Date().toISOString(), service: 'payment', action: 'payment.completed', userId: Number(invoice.telegramUserId), invoiceId: payload })
await this.deps.prisma.$transaction(async (tx) => {
await tx.paymentSaga.updateMany({
data: { paidAmount: paid, status: 'awaiting_booking' },
where: { invoiceId: payload },
})
await markSagaAwaitingBooking(tx, payload, paid)
await tx.pendingInvoice.update({
data: { completedAt: new Date(), paidAmountMinorUnits: paid, status: 'completed' },
where: { id: payload },
Expand Down Expand Up @@ -579,18 +577,7 @@ export class PaymentRouter {

// В транзакции переводим saga и связанные записи в recovery-состояние.
await this.deps.prisma.$transaction(async (tx) => {
await tx.paymentSaga.update({
data: { status: 'compensating' },
where: { invoiceId },
})
await tx.slotHold.updateMany({
data: { status: 'cancelled' },
where: { invoiceId, status: 'held' },
})
await tx.pendingInvoice.updateMany({
data: { status: 'failed' },
where: { id: invoiceId },
})
await startSagaCompensation(tx, invoiceId)
})
await this.writeAudit({
action: 'payment.compensation_started',
Expand Down
82 changes: 82 additions & 0 deletions apps/bot/services/payment-service/src/saga-transitions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { Prisma } from '@prisma/client'

type PaymentSagaTransaction = Prisma.TransactionClient
type ReleasedHoldStatus = 'cancelled' | 'paid'

/**
* Releases a held slot as part of an invoice saga transition.
*/
export async function releaseHeldSlot(
tx: PaymentSagaTransaction,
invoiceId: string,
status: ReleasedHoldStatus,
): Promise<void> {
await tx.slotHold.updateMany({
data: { status },
where: { invoiceId, status: 'held' },
})
}

/**
* Marks the saga as fully completed after booking-service confirms a booking.
*/
export async function markSagaBookingCompleted(
tx: PaymentSagaTransaction,
invoiceId: string,
bookingId: string,
): Promise<void> {
await releaseHeldSlot(tx, invoiceId, 'paid')
await tx.paymentSaga.updateMany({
data: { bookingId, status: 'completed' },
where: { invoiceId },
})
}

/**
* Moves a paid saga into manual recovery after booking creation fails.
*/
export async function markSagaBookingFailed(
tx: PaymentSagaTransaction,
invoiceId: string,
failureReason: string,
): Promise<void> {
await tx.paymentSaga.updateMany({
data: {
failureReason,
status: 'failed',
},
where: { invoiceId },
})
}

/**
* Records a completed payment before booking-service creates the booking.
*/
export async function markSagaAwaitingBooking(
tx: PaymentSagaTransaction,
invoiceId: string,
paidAmount: number,
): Promise<void> {
await tx.paymentSaga.updateMany({
data: { paidAmount, status: 'awaiting_booking' },
where: { invoiceId },
})
}

/**
* Starts manual compensation and releases the held slot.
*/
export async function startSagaCompensation(
tx: PaymentSagaTransaction,
invoiceId: string,
): Promise<void> {
await tx.paymentSaga.update({
data: { status: 'compensating' },
where: { invoiceId },
})
await releaseHeldSlot(tx, invoiceId, 'cancelled')
await tx.pendingInvoice.updateMany({
data: { status: 'failed' },
where: { id: invoiceId },
})
}
Loading