Support regional Pro pricing#220
Conversation
|
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
🔍 API Schema Diff---REPORT--- Auto-generated by API Schema Diff workflow |
There was a problem hiding this comment.
Code Review
This pull request introduces regional pricing support for billing plans, allowing different prices and currencies (such as INR for India and USD for global users) to be applied based on the user's billing region. It updates the Razorpay checkout flow to handle region-specific subscription plan IDs and prices, and exposes regional prices in the public plans API. The feedback suggests improving the robustness of normalize_billing_region to handle whitespace-only strings as default inputs and expanding the unit tests to cover these edge cases.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| def normalize_billing_region(region: str | None) -> str: | ||
| if not region: | ||
| return BILLING_REGION_IN | ||
| return BILLING_REGION_ALIASES.get(region.strip().upper(), BILLING_REGION_GLOBAL) |
There was a problem hiding this comment.
The normalize_billing_region function currently defaults to BILLING_REGION_IN only when region is falsy (e.g., None or ""). However, if a whitespace-only string (e.g., " ") is passed, region.strip().upper() becomes "", which is not in BILLING_REGION_ALIASES, causing it to fall back to BILLING_REGION_GLOBAL.
To ensure consistent default behavior for all empty or whitespace-only inputs, we should check if the stripped string is empty before performing the lookup.
| def normalize_billing_region(region: str | None) -> str: | |
| if not region: | |
| return BILLING_REGION_IN | |
| return BILLING_REGION_ALIASES.get(region.strip().upper(), BILLING_REGION_GLOBAL) | |
| def normalize_billing_region(region: str | None) -> str: | |
| if not region or not region.strip(): | |
| return BILLING_REGION_IN | |
| return BILLING_REGION_ALIASES.get(region.strip().upper(), BILLING_REGION_GLOBAL) |
| def test_billing_region_defaults_to_india_and_unknowns_are_global() -> None: | ||
| assert billing.normalize_billing_region(None) == billing.BILLING_REGION_IN | ||
| assert billing.normalize_billing_region("india") == billing.BILLING_REGION_IN | ||
| assert billing.normalize_billing_region("outside-india") == billing.BILLING_REGION_GLOBAL |
There was a problem hiding this comment.
Let's expand the unit tests for normalize_billing_region to cover whitespace-only strings and unknown regions (e.g., "UK" or "FR") to ensure they correctly fall back to the global billing region.
| def test_billing_region_defaults_to_india_and_unknowns_are_global() -> None: | |
| assert billing.normalize_billing_region(None) == billing.BILLING_REGION_IN | |
| assert billing.normalize_billing_region("india") == billing.BILLING_REGION_IN | |
| assert billing.normalize_billing_region("outside-india") == billing.BILLING_REGION_GLOBAL | |
| def test_billing_region_defaults_to_india_and_unknowns_are_global() -> None: | |
| assert billing.normalize_billing_region(None) == billing.BILLING_REGION_IN | |
| assert billing.normalize_billing_region("") == billing.BILLING_REGION_IN | |
| assert billing.normalize_billing_region(" ") == billing.BILLING_REGION_IN | |
| assert billing.normalize_billing_region("india") == billing.BILLING_REGION_IN | |
| assert billing.normalize_billing_region("outside-india") == billing.BILLING_REGION_GLOBAL | |
| assert billing.normalize_billing_region("UK") == billing.BILLING_REGION_GLOBAL |
|
| Filename | Overview |
|---|---|
| src/utils/billing.py | Adds regional pricing constants, normalize_billing_region, plan_price, and plan_price_options helpers. Defaults None region to GLOBAL. Minor: nominal_paise_per_credit remains INR-hardcoded despite new USD regional pricing. |
| src/api/routes/billing.py | Adds _checkout_package and _pro_plan_id_for_region helpers; threads billing_region through checkout creation and stored notes. Fallback for price_minor_unit uses or which would silently skip an intentional zero value. |
| src/billing/types.py | Adds PlanPricePublic model, regional_prices to PlanPublic, billing_region to CheckoutRequest and VerifyPaymentRequest. |
| src/billing/service.py | Passes regional_prices from plan_price_options into PlanPublic; minimal, safe change. |
| src/config/settings.py | Adds razorpay_global_pro_plan_id optional env var with clear description; clean change. |
| tests/unit/test_billing_config.py | New unit tests covering regional price lookup, normalize_billing_region defaults, and plan_price_options serialization. |
Sequence Diagram
sequenceDiagram
participant Client
participant BillingRoute as POST /razorpay/order
participant BillingConfig as billing_config
participant Settings
participant Razorpay
Client->>BillingRoute: "{package_id, billing_region?}"
BillingRoute->>BillingConfig: normalize_billing_region(billing_region)
BillingConfig-->>BillingRoute: "IN | GLOBAL"
BillingRoute->>BillingConfig: plan_price(package_id, region)
BillingConfig-->>BillingRoute: "{price_minor_unit, currency}"
BillingRoute->>Settings: razorpay_pro_plan_id / razorpay_global_pro_plan_id
Settings-->>BillingRoute: plan_id or None
alt Pro plan + plan_id set
BillingRoute->>Razorpay: create_subscription(plan_id)
Razorpay-->>BillingRoute: subscription
BillingRoute->>BillingRoute: store_checkout(subscription_id, billing_region)
BillingRoute-->>Client: CheckoutResponse(subscription_id, price_minor_unit, currency)
else Pro plan GLOBAL + no plan_id, or any other plan
BillingRoute->>Razorpay: "create_order(amount=price_minor_unit, currency)"
Razorpay-->>BillingRoute: order
BillingRoute->>BillingRoute: store_checkout(order_id, billing_region)
BillingRoute-->>Client: CheckoutResponse(order_id, amount, currency)
end
Reviews (2): Last reviewed commit: "Address regional billing review feedback" | Re-trigger Greptile
| razorpay_signature: str | ||
| razorpay_order_id: Optional[str] = None | ||
| razorpay_subscription_id: Optional[str] = None | ||
| billing_region: Optional[str] = None |
There was a problem hiding this comment.
billing_region accepted but silently ignored in verify handler
billing_region is added to VerifyPaymentRequest but verify_razorpay_payment never reads request.billing_region — it neither cross-checks it against the stored checkout's billing_region nor uses it for any grant logic. Callers who pass it will see no effect, and the API surface implies a behavior that isn't implemented.
✅ Staging Deployment Report
🟢 Staging is live and healthy! Test your changes at the staging URL above. Ready to ship? Comment |
🔍 API Schema Diff---REPORT--- Auto-generated by API Schema Diff workflow |
Summary
billing_regionin Razorpay checkout and select Rs 99 INR or $3 USD accordinglyRAZORPAY_GLOBAL_PRO_PLAN_IDfor global Pro subscriptions, falling back to a USD order when not configuredregional_pricesin public plan metadata and add unit coverage for billing config helpersVerification
python -c "from src.utils import billing; assert billing.plan_price('pro','IN') == {'price_paise': 9900, 'currency': 'INR'}; assert billing.plan_price('pro','GLOBAL') == {'price_paise': 300, 'currency': 'USD'}; assert billing.normalize_billing_region('india') == 'IN'; assert billing.normalize_billing_region('outside') == 'GLOBAL'; assert billing.plan_price_options('pro')['GLOBAL']['currency'] == 'USD'; print('billing region checks passed')"python -m py_compile src\utils\billing.py src\billing\types.py src\billing\service.py src\api\routes\billing.py src\config\settings.py tests\unit\test_billing_config.pyNote:
python -m pytest tests\unit\test_billing_config.pycould not run in this local shell becausepytestis not installed.