Created
June 9, 2026 07:56
-
-
Save Irostovsky/d62c1ed47becb07fd5b8fb8db147ac53 to your computer and use it in GitHub Desktop.
GH #35580 PFRI voucher acceptance test helpers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """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