Skip to content

Instantly share code, notes, and snippets.

@Irostovsky
Created June 9, 2026 07:56
Show Gist options
  • Select an option

  • Save Irostovsky/d62c1ed47becb07fd5b8fb8db147ac53 to your computer and use it in GitHub Desktop.

Select an option

Save Irostovsky/d62c1ed47becb07fd5b8fb8db147ac53 to your computer and use it in GitHub Desktop.
GH #35580 PFRI voucher acceptance test helpers
"""Create a reservation by walking the real direct-booking-website endpoints.
Mimics the sequence the direct-booking-website-frontend makes against the local
Django dev server. Goes through the actual ReservationPriceCalculator, so any
voucher applies through the real `paid_by` logic and the stored
chiffre_affaire / frais_plateforme reflect what the calculator produces.
Steps:
1. POST /payment/pre_reservations/ — creates PaymentPreReservation + Stripe PaymentIntent
2. POST /payment/pre_reservations/<pk>/update_voucher_code/ (only if VOUCHER_CODE set)
3. PATCH /payment/pre_reservations/<pk>/ — guest details
4. POST /payment/pre_reservations/<pk>/assign_full_payment/
5. stripe.PaymentIntent.confirm(...) — simulates browser Stripe.js with `pm_card_visa`
6. POST /payment/pre_reservation/process/ — backend captures & creates the Reservation
Inputs (env vars, both optional):
VOUCHER_CODE a voucher code to apply (default: no voucher)
RESERVATION_PLATFORM reservation platform name (default: BOOK.GUESTREADY.COM)
Run with:
docker compose exec -T bnbstaff python manage.py shell < scratches/create_dbw_reservation.py
docker compose exec -T -e VOUCHER_CODE=GR-RENTAL-10 -e RESERVATION_PLATFORM=HOMELIKE bnbstaff \\
python manage.py shell < scratches/create_dbw_reservation.py
"""
import datetime
import json
import os
import time
import requests
import stripe
from accounting_manager.services.invoices.create_reservation_invoices import (
CreateReservationGuestInvoice,
)
from booking_widget.models import Voucher, VoucherRedemption
from payment_system.services.reservation_split.strategies.short_term import (
SplitReservationAcceptedPayment,
)
from staffing.models import (
Appartement,
PaymentPreReservation,
Reservation,
ReservationPlatform,
)
from stripe_bnblord.helpers import fetch_stripe_platform
# ─── tweak me ────────────────────────────────────────────────────────────────
RENTAL_CONTRACT = "0000"
# Both come from env vars (with defaults). Each call to the booking widget /
# DBW backend uses a single (platform, voucher) pair — the voucher's own
# reservation_platform must match the booking's reservation_platform, otherwise
# ValidateVoucher rejects it.
RESERVATION_PLATFORM_NAME = os.environ.get("RESERVATION_PLATFORM", "BOOK.GUESTREADY.COM")
VOUCHER_CODE: str | None = os.environ.get("VOUCHER_CODE") or None
CHECK_IN = datetime.date.today() + datetime.timedelta(days=22)
NIGHTS = 3
ADULTS = 2
CHILDREN = 0
GUEST_FIRST_NAME = "Test"
# Append the voucher code (or NOVOUCHER) so reservations are easy to scan in the
# reservation list / search.
GUEST_LAST_NAME = f"Guest [{VOUCHER_CODE or 'NOVOUCHER'}] - NEW"
GUEST_EMAIL = "test.guest@example.com"
GUEST_PHONE = "+33612345678"
# ─── fixed for local dev ─────────────────────────────────────────────────────
BASE = "http://127.0.0.1:8000"
API = f"{BASE}/direct_booking_website/api/v1"
# Stripe test payment method. `pm_card_visa` -> requires_capture immediately
# under manual-capture, which is what the DBW flow expects.
PAYMENT_METHOD_ID = "pm_card_visa"
def http(method: str, path: str, **kw) -> dict | None:
url = path if path.startswith("http") else f"{API}{path}"
response = requests.request(method, url, timeout=30, **kw)
body: dict | str
try:
body = response.json()
except Exception:
body = response.text
print(f" → {method} {path} [{response.status_code}]")
if not response.ok:
print(f" body: {body!r}")
response.raise_for_status()
return body if isinstance(body, dict) else None
def main() -> None:
rental = Appartement.objects.get(numero_contrat=RENTAL_CONTRACT)
platform = ReservationPlatform.objects.get(name=RESERVATION_PLATFORM_NAME)
stripe_platform = fetch_stripe_platform(rental)
check_out = CHECK_IN + datetime.timedelta(days=NIGHTS)
print(f"Rental {rental.pk} | platform {platform.name} | {CHECK_IN} → {check_out}")
if VOUCHER_CODE:
voucher = Voucher.objects.filter(code=VOUCHER_CODE).first()
if not voucher:
raise SystemExit(f"Voucher {VOUCHER_CODE!r} not found")
print(f"Voucher {VOUCHER_CODE} | paid_by={voucher.get_paid_by()}")
# 1) Create PaymentPreReservation
create_body: dict = {
"rental": rental.pk,
"reservation_platform": platform.pk,
"date_debut_reservation": CHECK_IN.isoformat(),
"date_fin_reservation": check_out.isoformat(),
"adults": ADULTS,
"children": CHILDREN,
"infants": 0,
"pets": 0,
"user_language": "en",
}
if VOUCHER_CODE:
create_body["voucher_code"] = VOUCHER_CODE
created = http("POST", "/payment/pre_reservations/", json=create_body)
pre_pk = created["pk"]
client_secret = created["client_secret"]
print(f"PaymentPreReservation #{pre_pk} | client_secret={client_secret[:35]}...")
print(f" pricing_data: {json.dumps(created.get('pricing_data'), indent=2, default=str)[:400]}")
# 2) (defensive) re-apply voucher via dedicated endpoint — the frontend
# sometimes calls this even when voucher was set at create time.
if VOUCHER_CODE:
http(
"POST",
f"/payment/pre_reservations/{pre_pk}/update_voucher_code/",
json={"voucher_code": VOUCHER_CODE},
)
# 3) PATCH guest details
http(
"PATCH",
f"/payment/pre_reservations/{pre_pk}/",
json={
"guest_first_name": GUEST_FIRST_NAME,
"guest_last_name": GUEST_LAST_NAME,
"guest_email": GUEST_EMAIL,
"phone_number": GUEST_PHONE,
"user_language": "en",
"check_in_time": "15:00:00",
"check_out_time": "11:00:00",
},
)
# 4) Assign full payment
http("POST", f"/payment/pre_reservations/{pre_pk}/assign_full_payment/")
# 5) Confirm the Stripe PaymentIntent — this is the part the browser would
# normally do via Stripe.js after collecting the card.
pre_obj = PaymentPreReservation.objects.get(pk=pre_pk)
payment_intent_id = pre_obj.payment_intent.stripe_id
print(f"Confirming Stripe PaymentIntent {payment_intent_id} with {PAYMENT_METHOD_ID}")
pi = stripe.PaymentIntent.confirm(
payment_intent_id,
payment_method=PAYMENT_METHOD_ID,
api_key=stripe_platform.private_key,
)
print(f" Stripe status: {pi.status}")
# Stripe needs a moment for the status to settle in its eventual-consistency layer.
time.sleep(1)
# 6) Process — backend captures and creates the real Reservation
processed = http(
"POST",
"/payment/pre_reservation/process/",
json={"client_secret": client_secret, "tracking_parameters": {}},
)
print(f"Process response: {processed}")
# 7) Read back what got created
pre_obj.refresh_from_db()
reservation: Reservation = pre_obj.reservation
if reservation is None:
raise SystemExit("No reservation linked to pre_reservation after process — check logs")
print()
print(f"Reservation #{reservation.pk}")
print(f" /staffing/reservation_detail/{reservation.pk}/")
print(f" chiffre_affaire (rental income, possibly net of voucher): {reservation.chiffre_affaire}")
print(f" frais_plateforme (platform fee): {reservation.frais_plateforme}")
print(f" frais_menage (cleaning): {reservation.frais_menage}")
print(f" payment_fee: {reservation.payment_fee}")
redemptions = VoucherRedemption.objects.filter(reservation=reservation)
for r in redemptions:
print(f" voucher: {r.voucher.code} | amount: {r.amount} | paid_by: {r.voucher.get_paid_by()}")
if not redemptions:
print(" voucher: (none)")
# 8) Generate the guest invoice via the real service (same code path the
# scheduled CO+5 job uses; exercises _get_voucher_line_items, so the
# PR-#35521 filter behavior is visible here).
print()
print("Generating guest invoice...")
try:
invoice = CreateReservationGuestInvoice.execute(reservation=reservation)
except Exception as exc:
print(f" guest invoice raised {type(exc).__name__}: {exc}")
return
if invoice is None:
print(" guest invoice returned None (no taxable line items or filtered out)")
return
print(
f" Guest invoice #{invoice.pk} | serie: {invoice.serie} | "
f"net: {invoice.total_net} | vat: {invoice.total_vat} | gross: {invoice.total_gross}"
)
if invoice.pdf:
print(f" pdf: /accounting_manager/invoices/{invoice.pk}/download/")
# 9) Run the split — same service the scheduled job calls. Produces the
# ReservationSplit + per-destination VirtualTransactions, so the voucher
# cost distribution across LANDLORD / PM / PMS is visible.
print()
print("Running split...")
reservation.refresh_from_db()
try:
split = SplitReservationAcceptedPayment.execute(reservation=reservation)
except Exception as exc:
print(f" split raised {type(exc).__name__}: {exc}")
return
print(f" ReservationSplit #{split.pk} | status: {split.status}")
for vt in reservation.virtual_transactions.all().order_by("id"):
print(f" VT {vt.id} | {vt.destination_type:14} | amount: {vt.amount} {vt.currency}")
main()
# ----------
# rental = Appartement.objects.get(pk='0000')
# for reservation in rental.reservation_set.filter(date_debut_reservation='2026-06-29'):
# reservation.cancel()
"""Set up the GR-PFRI-10 voucher on rental 0000 for the GH #35580 acceptance test.
Mode: PLATFORM_FEE_AND_RENTAL_INCOME (V ≤ PF) — 10% off, impact_platform=True,
hosted on BOOK.GUESTREADY.COM. Idempotent (get_or_create on code).
Run with:
docker compose exec -T bnbstaff python manage.py shell < setup_pfri_voucher.py
"""
import datetime
from decimal import Decimal
from booking_widget.models import Voucher
from staffing.models import Appartement, ReservationPlatform
RENTAL_CONTRACT = "0000"
rental = Appartement.objects.get(numero_contrat=RENTAL_CONTRACT)
platform = ReservationPlatform.objects.get(name="BOOK.GUESTREADY.COM")
voucher, created = Voucher.objects.get_or_create(
code="GR-PFRI-10",
defaults=dict(
kind="PERCENTAGE",
percentage=Decimal("10.00"),
currency="EUR",
reservation_platform=platform,
impact_platform=True, # → PLATFORM_FEE_AND_RENTAL_INCOME
exclude_fees=True,
travel_type="STAY",
validity_start=datetime.date.today() - datetime.timedelta(days=1),
validity_end=datetime.date.today() + datetime.timedelta(days=365),
enabled=True,
redemption_limit=10000,
description="Local test - PFRI mode (V <= platform fee)",
campaign_name="Local test - PFRI mode (V <= platform fee)",
),
)
voucher.rentals.add(rental)
voucher.antennes.add(rental.antenne)
print(
f" {'created' if created else 'updated'} {voucher.code} | "
f"paid_by={voucher.get_paid_by()} | "
f"rental {rental.pk} attached"
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment