fix: validate payment sub-option suffix and harden payment method handling
- Add regex pattern + max_length constraint on payment_method field - Validate sub-option suffix against known available_sub_options - Validate sub-option is enabled on the landing (not disabled via config) - Sort methods by ID length desc to prevent freekassa/freekassa_sbp ambiguity - Accept yookassa_card and cloudpayments_card/sbp in create_guest_payment - Send single sub-option to frontend (not just when >1) for correct routing
This commit is contained in:
@@ -107,7 +107,7 @@ class PurchaseRequest(BaseModel):
|
||||
period_days: int
|
||||
contact_type: str = Field(pattern=r'^(email|telegram)$')
|
||||
contact_value: str = Field(min_length=1, max_length=255)
|
||||
payment_method: str
|
||||
payment_method: str = Field(min_length=1, max_length=50, pattern=r'^[a-z0-9_]+$')
|
||||
is_gift: bool = False
|
||||
gift_recipient_type: str | None = Field(default=None, pattern=r'^(email|telegram)$')
|
||||
gift_recipient_value: str | None = Field(default=None, max_length=255)
|
||||
@@ -432,7 +432,7 @@ async def get_landing_config(
|
||||
# If landing has explicit sub_options config, respect it; otherwise all enabled
|
||||
if raw_sub_options is None or raw_sub_options.get(opt_id, True):
|
||||
resolved.append(LandingPaymentMethodSubOption(id=opt_id, name=opt['name']))
|
||||
if len(resolved) > 1:
|
||||
if resolved:
|
||||
resolved_sub_options = resolved
|
||||
|
||||
payment_methods.append(
|
||||
@@ -507,17 +507,31 @@ async def create_landing_purchase(
|
||||
|
||||
# Validate payment method is available on this landing.
|
||||
# The frontend may send a suffixed method ID (e.g. "platega_2", "yookassa_sbp")
|
||||
# to select a specific sub-option. We match against the base method_id.
|
||||
# to select a specific sub-option. We match against the base method_id and
|
||||
# validate the suffix against known & enabled sub-options.
|
||||
raw_methods = landing.payment_methods or []
|
||||
base_payment_method = body.payment_method
|
||||
method_config = next((m for m in raw_methods if m.get('method_id') == base_payment_method), None)
|
||||
method_defaults = _get_method_defaults()
|
||||
|
||||
method_config = next((m for m in raw_methods if m.get('method_id') == body.payment_method), None)
|
||||
if method_config is None:
|
||||
# Try matching by prefix: "platega_2" → base "platega"
|
||||
for m in raw_methods:
|
||||
# Sort by length descending so "freekassa_sbp" is checked before "freekassa"
|
||||
sorted_methods = sorted(raw_methods, key=lambda m: len(m.get('method_id', '')), reverse=True)
|
||||
for m in sorted_methods:
|
||||
mid = m.get('method_id', '')
|
||||
if base_payment_method.startswith(mid + '_'):
|
||||
if body.payment_method.startswith(mid + '_'):
|
||||
suffix = body.payment_method[len(mid) + 1:]
|
||||
# Validate suffix is a known sub-option
|
||||
method_def = method_defaults.get(mid)
|
||||
available = (method_def.get('available_sub_options') if method_def else None) or []
|
||||
valid_ids = {opt['id'] for opt in available}
|
||||
if suffix not in valid_ids:
|
||||
break # invalid suffix → reject
|
||||
# Validate suffix is enabled on this landing
|
||||
raw_sub_options = m.get('sub_options') # dict[str, bool] | None
|
||||
if raw_sub_options is not None and not raw_sub_options.get(suffix, True):
|
||||
break # disabled sub-option → reject
|
||||
method_config = m
|
||||
base_payment_method = mid
|
||||
break
|
||||
if method_config is None:
|
||||
raise HTTPException(
|
||||
|
||||
@@ -420,7 +420,7 @@ class PaymentService(
|
||||
)
|
||||
|
||||
# --- YooKassa (card / sbp) -------------------------------------------
|
||||
if payment_method in ('yookassa', 'yookassa_sbp'):
|
||||
if payment_method in ('yookassa', 'yookassa_card', 'yookassa_sbp'):
|
||||
if self.yookassa_service is None:
|
||||
logger.warning('YooKassa is not enabled, cannot create guest payment')
|
||||
return None
|
||||
@@ -618,7 +618,7 @@ class PaymentService(
|
||||
return None
|
||||
|
||||
# --- CloudPayments ----------------------------------------------------
|
||||
if payment_method == 'cloudpayments':
|
||||
if payment_method in ('cloudpayments', 'cloudpayments_card', 'cloudpayments_sbp'):
|
||||
if self.cloudpayments_service is None:
|
||||
logger.warning('CloudPayments is not enabled, cannot create guest payment')
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user