Skip to content
Merged
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
7 changes: 7 additions & 0 deletions apps/api/src/domains/bookingPmsManualPriceCorrection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type BookingScope = {
currency: string;
lifecycleStatus: string;
timezone: string | null;
hasRefund: boolean;
};
type CurrentTip = {
id: string;
Expand Down Expand Up @@ -65,6 +66,8 @@ export async function correctBookingPmsManualPrices(
const booking = await transaction.query<BookingScope>(
`SELECT source_booking_id AS "sourceBookingReference",trim(currency) AS currency,
lifecycle_status AS "lifecycleStatus",
EXISTS (SELECT 1 FROM booking.nightly_revenue_evidence evidence
WHERE evidence.guest_booking_id=booking.id AND economic_event='refund') AS "hasRefund",
(SELECT timezone FROM hotel_catalog.property_locations
WHERE property_id=booking.property_id) AS timezone
FROM booking.guest_bookings booking WHERE id=$1::uuid AND property_id=$2::uuid
Expand All @@ -78,6 +81,10 @@ export async function correctBookingPmsManualPrices(
"Manual booking prices cannot be corrected",
scope?.lifecycleStatus ?? "missing",
);
if (scope.hasRefund)
throw new ManualPriceCorrectionEvidenceError(
"Manual booking prices cannot be corrected after a refund",
);
const zone = scope.timezone && getTimezone(scope.timezone);
if (!zone || zone.name !== scope.timezone || zone.aliasOf !== null)
throw new ManualPriceCorrectionEvidenceError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,68 @@ describe.skipIf(!TEST_DATABASE_URL)("target manual-booking PostgreSQL transactio
expect(stored.rows[0]).toEqual({ evidence: 2, keys: 0, audits: 0 });
});

it.each([
["partial", ["25.00"]],
["full", ["100.00", "100.00"]],
])("rejects price correction after a %s refund", async (kind, amounts) => {
const created = await repository.createManualBooking(
command(`price-after-${kind}-refund`, "paid", "cash", "2026-08-20", false),
);
const evidence = await admin.query<{ payment: string; id: string }>(
`SELECT payment.id::text AS payment,evidence.id::text
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
FROM finance.payments payment JOIN booking.nightly_revenue_evidence evidence
ON evidence.guest_booking_id=payment.guest_booking_id
WHERE payment.guest_booking_id=$1::uuid AND payment.payment_kind='manual'
ORDER BY evidence.stay_date,evidence.line_position`,
[created.guestBookingId],
);
const refund = {
propertyId,
guestBookingId: created.guestBookingId,
commandId: `refund-before-price-${kind}`,
idempotencyKey: `refund-before-price-${kind}`,
paymentEvidenceId: evidence.rows[0]!.payment,
accountingDate: "2026-08-21",
allocations: amounts.map((amountDecimal, index) => ({
evidenceId: evidence.rows[index]!.id,
amount: { amountDecimal, currency: "EUR" },
})),
audit: {
actor: { kind: "user" as const, userId: actorId, organizationId },
requestId: `refund-before-price-${kind}`,
reason: "Refund manual booking",
requestedAt: acceptedAt.toISOString(),
},
};
await expect(operations.refundManualBooking!(refund)).resolves.toMatchObject({ ok: true });
const refundTarget = await admin.query<{ id: string }>(
`SELECT id::text FROM booking.nightly_revenue_evidence
WHERE guest_booking_id=$1::uuid AND economic_event='refund' ORDER BY id LIMIT 1`,
[created.guestBookingId],
);
await expect(
operations.correctManualBookingPrices!(
priceCorrection(created.guestBookingId, `after-${kind}-refund`, {
kind: "exact",
nights: [
{
targetEvidenceId: refundTarget.rows[0]!.id,
replacementAmount: { amountDecimal: "100.00", currency: "EUR" },
},
],
}),
),
).resolves.toMatchObject({ ok: false, code: "invalid_body" });
const stored = await admin.query<{ corrections: number; keys: number }>(
`SELECT count(*) FILTER (WHERE economic_event='correction')::int corrections,
(SELECT count(*)::int FROM platform.idempotency_keys WHERE property_id=$2::uuid
AND operation='manual_price_correction_command') keys
FROM booking.nightly_revenue_evidence WHERE guest_booking_id=$1::uuid`,
[created.guestBookingId, propertyId],
);
expect(stored.rows[0]).toEqual({ corrections: 0, keys: 0 });
});

it("atomically cancels a manual booking with explicit retained-charge evidence", async () => {
const created = await repository.createManualBooking(
command("cancel", "unpaid", "cash", "2026-08-20", false),
Expand Down
Loading