Spaces:
Paused
Paused
open coupons priced, UPI asides
Browse files- app/channels.py +10 -1
- app/offers.py +31 -0
- app/offers_data/curated_offers.json +203 -0
app/channels.py
CHANGED
|
@@ -18,7 +18,7 @@ from typing import Dict, List, Optional
|
|
| 18 |
|
| 19 |
from availability import channel_availability
|
| 20 |
from card_catalogue import CATALOGUE_BY_ID
|
| 21 |
-
from offers import active_offers_for, best_open_offer, best_conditional_offer
|
| 22 |
from scoring_engine import TxnContext, score_transaction, _r2
|
| 23 |
|
| 24 |
# Brands that are themselves a booking platform: when the user's query resolved
|
|
@@ -428,6 +428,9 @@ def compare_channels(
|
|
| 428 |
open_inr, open_text, open_code, open_prebook = best_open_offer(ctx.offers, price, category, ch["channel"])
|
| 429 |
# Claimable only on a first booking. Reported, never priced.
|
| 430 |
cond_inr, cond_text, cond_code, _cond_prebook = best_conditional_offer(ctx.offers, price, category, ch["channel"])
|
|
|
|
|
|
|
|
|
|
| 431 |
# A bank offer and a public promo code are NOT assumed to stack. Most
|
| 432 |
# Indian OTAs make them mutually exclusive, and we have no evidence
|
| 433 |
# either way for any given pair - so take the better of the two rather
|
|
@@ -469,6 +472,12 @@ def compare_channels(
|
|
| 469 |
"emi_offer_inr": top.get("emi_offer_inr", 0.0),
|
| 470 |
"emi_offer_text": top.get("emi_offer_text"),
|
| 471 |
"emi_offer_code": top.get("emi_offer_code"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
"open_discount_inr": _r2(open_inr),
|
| 473 |
"open_discount_text": open_text,
|
| 474 |
# Named so the user can actually type it. An offer we price but cannot
|
|
|
|
| 18 |
|
| 19 |
from availability import channel_availability
|
| 20 |
from card_catalogue import CATALOGUE_BY_ID
|
| 21 |
+
from offers import active_offers_for, best_open_offer, best_conditional_offer, best_upi_offer
|
| 22 |
from scoring_engine import TxnContext, score_transaction, _r2
|
| 23 |
|
| 24 |
# Brands that are themselves a booking platform: when the user's query resolved
|
|
|
|
| 428 |
open_inr, open_text, open_code, open_prebook = best_open_offer(ctx.offers, price, category, ch["channel"])
|
| 429 |
# Claimable only on a first booking. Reported, never priced.
|
| 430 |
cond_inr, cond_text, cond_code, _cond_prebook = best_conditional_offer(ctx.offers, price, category, ch["channel"])
|
| 431 |
+
# Claimable only by paying UPI instead of the card. Reported, never
|
| 432 |
+
# priced - like EMI, the user hears the trade and decides.
|
| 433 |
+
upi_inr, upi_text, upi_code = best_upi_offer(ctx.offers, price, category, ch["channel"])
|
| 434 |
# A bank offer and a public promo code are NOT assumed to stack. Most
|
| 435 |
# Indian OTAs make them mutually exclusive, and we have no evidence
|
| 436 |
# either way for any given pair - so take the better of the two rather
|
|
|
|
| 472 |
"emi_offer_inr": top.get("emi_offer_inr", 0.0),
|
| 473 |
"emi_offer_text": top.get("emi_offer_text"),
|
| 474 |
"emi_offer_code": top.get("emi_offer_code"),
|
| 475 |
+
# UPI-only coupon at this channel (EMTUPI, UPIPAY): claimable by
|
| 476 |
+
# paying UPI instead of the card, forfeiting its rewards. A
|
| 477 |
+
# labelled aside, never in cash_off / net_cost / savings.
|
| 478 |
+
"upi_offer_inr": _r2(upi_inr),
|
| 479 |
+
"upi_offer_text": upi_text,
|
| 480 |
+
"upi_offer_code": upi_code,
|
| 481 |
"open_discount_inr": _r2(open_inr),
|
| 482 |
"open_discount_text": open_text,
|
| 483 |
# Named so the user can actually type it. An offer we price but cannot
|
app/offers.py
CHANGED
|
@@ -587,6 +587,37 @@ def best_conditional_offer(offs: List[Dict], amount: float, category: Optional[s
|
|
| 587 |
return _best_unlinked(offs, amount, category, channel, conditional=True)
|
| 588 |
|
| 589 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
def _best_unlinked(offs, amount, category, channel, conditional: bool):
|
| 591 |
"""conditional=False -> discounts anyone can actually take today.
|
| 592 |
conditional=True -> real, quoted, but gated on something we cannot check."""
|
|
|
|
| 587 |
return _best_unlinked(offs, amount, category, channel, conditional=True)
|
| 588 |
|
| 589 |
|
| 590 |
+
def best_upi_offer(offs: List[Dict], amount: float, category: Optional[str] = None,
|
| 591 |
+
channel: Optional[str] = None):
|
| 592 |
+
"""Best open discount that only a UPI payment can claim (EMTUPI, UPIPAY).
|
| 593 |
+
|
| 594 |
+
A UPI coupon is real, checkable money - the user just has to pay by UPI
|
| 595 |
+
instead of the card we are recommending, forfeiting the card's rewards. So
|
| 596 |
+
like EMI it is a labelled aside, never inside the card answer or the price:
|
| 597 |
+
the user hears the trade in so many words and decides. Only unlinked rows;
|
| 598 |
+
a card-linked offer confined to a payments app stays dropped outright
|
| 599 |
+
(scoring_engine._offer_applies), because no card can claim it as a card.
|
| 600 |
+
Returns (rupees, text|None, code|None)."""
|
| 601 |
+
best, note, code = 0.0, None, None
|
| 602 |
+
for off in (offs or []):
|
| 603 |
+
if not off.get("requires_upi_app"):
|
| 604 |
+
continue
|
| 605 |
+
if is_card_linked(off) or off.get("requires_debit") or off.get("requires_emi"):
|
| 606 |
+
continue
|
| 607 |
+
if off.get("requires_new_user"):
|
| 608 |
+
continue
|
| 609 |
+
if off.get("requires_promo_code") and not (off.get("promo_code") or "").strip():
|
| 610 |
+
continue
|
| 611 |
+
och = off.get("channel", "both")
|
| 612 |
+
if channel and och != "both" and och != channel:
|
| 613 |
+
continue
|
| 614 |
+
val = offer_value(off, amount, category)
|
| 615 |
+
if val > best:
|
| 616 |
+
best, note = val, _display(off.get("text") or off.get("evidence"))
|
| 617 |
+
code = (off.get("promo_code") or "").strip() or None
|
| 618 |
+
return best, note, code
|
| 619 |
+
|
| 620 |
+
|
| 621 |
def _best_unlinked(offs, amount, category, channel, conditional: bool):
|
| 622 |
"""conditional=False -> discounts anyone can actually take today.
|
| 623 |
conditional=True -> real, quoted, but gated on something we cannot check."""
|
app/offers_data/curated_offers.json
CHANGED
|
@@ -7463,5 +7463,208 @@
|
|
| 7463 |
"source": "https://www.ixigo.com/offers",
|
| 7464 |
"last_verified": "2026-07-29",
|
| 7465 |
"evidence": "Up to ₹2,000 Off on Domestic Flights with HDFC Bank Credit Card + Interest Free EMI"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7466 |
}
|
| 7467 |
]
|
|
|
|
| 7463 |
"source": "https://www.ixigo.com/offers",
|
| 7464 |
"last_verified": "2026-07-29",
|
| 7465 |
"evidence": "Up to ₹2,000 Off on Domestic Flights with HDFC Bank Credit Card + Interest Free EMI"
|
| 7466 |
+
},
|
| 7467 |
+
{
|
| 7468 |
+
"id": "of_easemytrip__pct_a8a240",
|
| 7469 |
+
"merchant_key": "easemytrip",
|
| 7470 |
+
"merchant_name": "EaseMyTrip",
|
| 7471 |
+
"applies_to_issuer": "",
|
| 7472 |
+
"card_id": null,
|
| 7473 |
+
"valid_from": "2026-08-02",
|
| 7474 |
+
"valid_to": "2026-08-31",
|
| 7475 |
+
"last_verified": "2026-08-02",
|
| 7476 |
+
"min_spend": 0,
|
| 7477 |
+
"max_discount": 5000,
|
| 7478 |
+
"network": "any",
|
| 7479 |
+
"geo": "any",
|
| 7480 |
+
"categories": [
|
| 7481 |
+
"travel_flights"
|
| 7482 |
+
],
|
| 7483 |
+
"channel": "online",
|
| 7484 |
+
"requires_debit": false,
|
| 7485 |
+
"requires_new_user": false,
|
| 7486 |
+
"requires_promo_code": true,
|
| 7487 |
+
"requires_upi_app": false,
|
| 7488 |
+
"requires_prebooking": false,
|
| 7489 |
+
"excluded_card_ids": [],
|
| 7490 |
+
"type": "pct",
|
| 7491 |
+
"value": 5.0,
|
| 7492 |
+
"promo_code": "EMT2026",
|
| 7493 |
+
"source": "https://www.easemytrip.com/offers/coupon.html",
|
| 7494 |
+
"evidence": "5% instant discount on flight bookings Rs. 5,000 EMT2026"
|
| 7495 |
+
},
|
| 7496 |
+
{
|
| 7497 |
+
"id": "of_easemytrip__pct_85872c",
|
| 7498 |
+
"merchant_key": "easemytrip",
|
| 7499 |
+
"merchant_name": "EaseMyTrip",
|
| 7500 |
+
"applies_to_issuer": "",
|
| 7501 |
+
"card_id": null,
|
| 7502 |
+
"valid_from": "2026-08-02",
|
| 7503 |
+
"valid_to": "2026-08-31",
|
| 7504 |
+
"last_verified": "2026-08-02",
|
| 7505 |
+
"min_spend": 0,
|
| 7506 |
+
"max_discount": 750,
|
| 7507 |
+
"network": "any",
|
| 7508 |
+
"geo": "any",
|
| 7509 |
+
"categories": [
|
| 7510 |
+
"travel_flights"
|
| 7511 |
+
],
|
| 7512 |
+
"channel": "online",
|
| 7513 |
+
"requires_debit": false,
|
| 7514 |
+
"requires_new_user": false,
|
| 7515 |
+
"requires_promo_code": true,
|
| 7516 |
+
"requires_upi_app": true,
|
| 7517 |
+
"requires_prebooking": false,
|
| 7518 |
+
"excluded_card_ids": [],
|
| 7519 |
+
"type": "pct",
|
| 7520 |
+
"value": 5.0,
|
| 7521 |
+
"promo_code": "UPIPAY",
|
| 7522 |
+
"source": "https://www.easemytrip.com/offers/upi-payment.html",
|
| 7523 |
+
"evidence": "5% up to Rs. 750 This offer is valid for payment done via UPI method UPIPAY"
|
| 7524 |
+
},
|
| 7525 |
+
{
|
| 7526 |
+
"id": "of_easemytrip__pct_1e10da",
|
| 7527 |
+
"merchant_key": "easemytrip",
|
| 7528 |
+
"merchant_name": "EaseMyTrip",
|
| 7529 |
+
"applies_to_issuer": "",
|
| 7530 |
+
"card_id": null,
|
| 7531 |
+
"valid_from": "2026-08-02",
|
| 7532 |
+
"valid_to": "2026-08-31",
|
| 7533 |
+
"last_verified": "2026-08-02",
|
| 7534 |
+
"min_spend": 0,
|
| 7535 |
+
"max_discount": 1500,
|
| 7536 |
+
"network": "any",
|
| 7537 |
+
"geo": "any",
|
| 7538 |
+
"categories": [
|
| 7539 |
+
"travel_hotels"
|
| 7540 |
+
],
|
| 7541 |
+
"channel": "online",
|
| 7542 |
+
"requires_debit": false,
|
| 7543 |
+
"requires_new_user": false,
|
| 7544 |
+
"requires_promo_code": true,
|
| 7545 |
+
"requires_upi_app": true,
|
| 7546 |
+
"requires_prebooking": false,
|
| 7547 |
+
"excluded_card_ids": [],
|
| 7548 |
+
"type": "pct",
|
| 7549 |
+
"value": 15.0,
|
| 7550 |
+
"promo_code": "UPIPAY",
|
| 7551 |
+
"source": "https://www.easemytrip.com/offers/upi-payment.html",
|
| 7552 |
+
"evidence": "15% up to Rs. 1,500 This offer is valid for payment done via UPI method UPIPAY"
|
| 7553 |
+
},
|
| 7554 |
+
{
|
| 7555 |
+
"id": "of_easemytrip__pct_bb7bf3",
|
| 7556 |
+
"merchant_key": "easemytrip",
|
| 7557 |
+
"merchant_name": "EaseMyTrip",
|
| 7558 |
+
"applies_to_issuer": "",
|
| 7559 |
+
"card_id": null,
|
| 7560 |
+
"valid_from": "2026-08-02",
|
| 7561 |
+
"valid_to": "2026-08-31",
|
| 7562 |
+
"last_verified": "2026-08-02",
|
| 7563 |
+
"min_spend": 0,
|
| 7564 |
+
"max_discount": 150,
|
| 7565 |
+
"network": "any",
|
| 7566 |
+
"geo": "any",
|
| 7567 |
+
"categories": [
|
| 7568 |
+
"bus"
|
| 7569 |
+
],
|
| 7570 |
+
"channel": "online",
|
| 7571 |
+
"requires_debit": false,
|
| 7572 |
+
"requires_new_user": false,
|
| 7573 |
+
"requires_promo_code": true,
|
| 7574 |
+
"requires_upi_app": true,
|
| 7575 |
+
"requires_prebooking": false,
|
| 7576 |
+
"excluded_card_ids": [],
|
| 7577 |
+
"type": "pct",
|
| 7578 |
+
"value": 7.0,
|
| 7579 |
+
"promo_code": "UPIPAY",
|
| 7580 |
+
"source": "https://www.easemytrip.com/offers/upi-payment.html",
|
| 7581 |
+
"evidence": "7% up to Rs. 150 This offer is valid for payment done via UPI method UPIPAY"
|
| 7582 |
+
},
|
| 7583 |
+
{
|
| 7584 |
+
"id": "of_easemytrip__flat_3039f3",
|
| 7585 |
+
"merchant_key": "easemytrip",
|
| 7586 |
+
"merchant_name": "EaseMyTrip",
|
| 7587 |
+
"applies_to_issuer": "",
|
| 7588 |
+
"card_id": null,
|
| 7589 |
+
"valid_from": "2026-08-02",
|
| 7590 |
+
"valid_to": "2026-08-31",
|
| 7591 |
+
"last_verified": "2026-08-02",
|
| 7592 |
+
"min_spend": 0,
|
| 7593 |
+
"max_discount": null,
|
| 7594 |
+
"network": "any",
|
| 7595 |
+
"geo": "any",
|
| 7596 |
+
"categories": [
|
| 7597 |
+
"travel_flights"
|
| 7598 |
+
],
|
| 7599 |
+
"channel": "online",
|
| 7600 |
+
"requires_debit": false,
|
| 7601 |
+
"requires_new_user": false,
|
| 7602 |
+
"requires_promo_code": true,
|
| 7603 |
+
"requires_upi_app": true,
|
| 7604 |
+
"requires_prebooking": false,
|
| 7605 |
+
"excluded_card_ids": [],
|
| 7606 |
+
"type": "flat",
|
| 7607 |
+
"value": 400.0,
|
| 7608 |
+
"promo_code": "EMTUPI",
|
| 7609 |
+
"source": "https://www.easemytrip.com/",
|
| 7610 |
+
"evidence": "Instant Discount of Rs.400 has been applied successfully. Please note- EMTUPI Coupon is valid only on payments made via UPI"
|
| 7611 |
+
},
|
| 7612 |
+
{
|
| 7613 |
+
"id": "of_easemytrip__flat_7dde75",
|
| 7614 |
+
"merchant_key": "easemytrip",
|
| 7615 |
+
"merchant_name": "EaseMyTrip",
|
| 7616 |
+
"applies_to_issuer": "",
|
| 7617 |
+
"card_id": null,
|
| 7618 |
+
"valid_from": "2026-08-02",
|
| 7619 |
+
"valid_to": "2026-08-31",
|
| 7620 |
+
"last_verified": "2026-08-02",
|
| 7621 |
+
"min_spend": 0,
|
| 7622 |
+
"max_discount": null,
|
| 7623 |
+
"network": "any",
|
| 7624 |
+
"geo": "domestic",
|
| 7625 |
+
"categories": [
|
| 7626 |
+
"travel_flights"
|
| 7627 |
+
],
|
| 7628 |
+
"channel": "online",
|
| 7629 |
+
"requires_debit": false,
|
| 7630 |
+
"requires_new_user": false,
|
| 7631 |
+
"requires_promo_code": true,
|
| 7632 |
+
"requires_upi_app": false,
|
| 7633 |
+
"requires_prebooking": false,
|
| 7634 |
+
"excluded_card_ids": [],
|
| 7635 |
+
"type": "flat",
|
| 7636 |
+
"value": 400.0,
|
| 7637 |
+
"promo_code": "EMTNCF",
|
| 7638 |
+
"source": "https://www.easemytrip.com/",
|
| 7639 |
+
"evidence": "EMTNCF applied Congratulations! Zero Convenience Coupon is applied. You saved Rs.400 per passenger on domestic flight"
|
| 7640 |
+
},
|
| 7641 |
+
{
|
| 7642 |
+
"id": "of_easemytrip__flat_3dca2f",
|
| 7643 |
+
"merchant_key": "easemytrip",
|
| 7644 |
+
"merchant_name": "EaseMyTrip",
|
| 7645 |
+
"applies_to_issuer": "",
|
| 7646 |
+
"card_id": null,
|
| 7647 |
+
"valid_from": "2026-08-02",
|
| 7648 |
+
"valid_to": "2026-08-31",
|
| 7649 |
+
"last_verified": "2026-08-02",
|
| 7650 |
+
"min_spend": 0,
|
| 7651 |
+
"max_discount": null,
|
| 7652 |
+
"network": "any",
|
| 7653 |
+
"geo": "international",
|
| 7654 |
+
"categories": [
|
| 7655 |
+
"travel_flights"
|
| 7656 |
+
],
|
| 7657 |
+
"channel": "online",
|
| 7658 |
+
"requires_debit": false,
|
| 7659 |
+
"requires_new_user": false,
|
| 7660 |
+
"requires_promo_code": true,
|
| 7661 |
+
"requires_upi_app": false,
|
| 7662 |
+
"requires_prebooking": false,
|
| 7663 |
+
"excluded_card_ids": [],
|
| 7664 |
+
"type": "flat",
|
| 7665 |
+
"value": 1500.0,
|
| 7666 |
+
"promo_code": "EMTNCF",
|
| 7667 |
+
"source": "https://www.easemytrip.com/",
|
| 7668 |
+
"evidence": "Zero Convenience Coupon is applied. You saved Rs.1500 per passenger on international flight Convenience fees not applicable EMTNCF"
|
| 7669 |
}
|
| 7670 |
]
|