feat(payment): 优化套餐配置和支付服务
- 简化套餐配置结构,删除冗余的 default.yaml 和 us.yaml - 优化 Apple IAP 服务和验证逻辑 - 更新套餐数据模型和协议文档 - 添加支付相关测试用例
This commit is contained in:
@@ -1,21 +1,9 @@
|
||||
from core.config.packages.registry import (
|
||||
clear_packages_cache,
|
||||
get_packages_config_for_region,
|
||||
)
|
||||
from core.config.packages.schema import (
|
||||
PackageConfig,
|
||||
PackageType,
|
||||
ProductCode,
|
||||
RegionPackagesConfig,
|
||||
load_packages_config,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"clear_packages_cache",
|
||||
"get_packages_config_for_region",
|
||||
"load_packages_config",
|
||||
"PackageConfig",
|
||||
"PackageType",
|
||||
"ProductCode",
|
||||
"RegionPackagesConfig",
|
||||
]
|
||||
|
||||
@@ -1,34 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from core.config.packages.schema import (
|
||||
RegionPackagesConfig,
|
||||
load_packages_config,
|
||||
)
|
||||
from utils.paths import get_default_package_config_path, get_package_config_path
|
||||
|
||||
|
||||
_CONFIG_CACHE: dict[str, RegionPackagesConfig] = {}
|
||||
|
||||
|
||||
def get_packages_config_for_region(country: str) -> RegionPackagesConfig:
|
||||
if country in _CONFIG_CACHE:
|
||||
return _CONFIG_CACHE[country]
|
||||
|
||||
region_file = get_package_config_path(country)
|
||||
if region_file.exists():
|
||||
config = load_packages_config(region_file)
|
||||
_CONFIG_CACHE[country] = config
|
||||
return config
|
||||
|
||||
default_file = get_default_package_config_path()
|
||||
if not default_file.exists():
|
||||
raise RuntimeError(f"No default packages config found: {default_file}")
|
||||
|
||||
config = load_packages_config(default_file)
|
||||
_CONFIG_CACHE[country] = config
|
||||
return config
|
||||
|
||||
|
||||
def clear_packages_cache() -> None:
|
||||
global _CONFIG_CACHE
|
||||
_CONFIG_CACHE = {}
|
||||
pass
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Literal
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class PackageType(str, Enum):
|
||||
@@ -15,37 +11,7 @@ class PackageType(str, Enum):
|
||||
|
||||
ProductCode = Literal[
|
||||
"new_user_pack",
|
||||
"basic_pack",
|
||||
"starter_pack",
|
||||
"popular_pack",
|
||||
"premium_pack",
|
||||
]
|
||||
|
||||
|
||||
class PackageConfig(BaseModel):
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
||||
|
||||
product_code: ProductCode
|
||||
type: PackageType
|
||||
price: float = Field(ge=0)
|
||||
credits: int = Field(ge=1)
|
||||
sort_order: int = Field(default=0, ge=0)
|
||||
enabled: bool = Field(default=True)
|
||||
|
||||
|
||||
class RegionPackagesConfig(BaseModel):
|
||||
model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid")
|
||||
|
||||
region: str = Field(min_length=1, max_length=8)
|
||||
currency: str = Field(min_length=1, max_length=8)
|
||||
packages: list[PackageConfig] = Field(min_length=1)
|
||||
|
||||
|
||||
def load_packages_config(path: Path) -> RegionPackagesConfig:
|
||||
with path.open("r", encoding="utf-8") as file:
|
||||
loaded: object = yaml.safe_load(file) or {}
|
||||
if not isinstance(loaded, dict):
|
||||
raise ValueError(f"Invalid packages config format: {path}")
|
||||
try:
|
||||
return RegionPackagesConfig.model_validate(loaded)
|
||||
except ValidationError as exc:
|
||||
raise ValueError(f"Invalid packages config data: {path}") from exc
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
region: DEFAULT
|
||||
currency: USD
|
||||
packages:
|
||||
- product_code: new_user_pack
|
||||
type: starter
|
||||
price: 0.99
|
||||
credits: 60
|
||||
sort_order: 0
|
||||
enabled: true
|
||||
|
||||
- product_code: basic_pack
|
||||
type: regular
|
||||
price: 4.99
|
||||
credits: 100
|
||||
sort_order: 10
|
||||
enabled: true
|
||||
|
||||
- product_code: popular_pack
|
||||
type: regular
|
||||
price: 7.99
|
||||
credits: 210
|
||||
sort_order: 20
|
||||
enabled: true
|
||||
|
||||
- product_code: premium_pack
|
||||
type: regular
|
||||
price: 12.99
|
||||
credits: 415
|
||||
sort_order: 30
|
||||
enabled: true
|
||||
@@ -3,15 +3,23 @@ product_mappings:
|
||||
app_store_product_id: com.meeyao.qianwen.new_user_pack
|
||||
credits: 60
|
||||
type: starter
|
||||
basic_pack:
|
||||
app_store_product_id: com.meeyao.qianwen.basic_pack
|
||||
sort_order: 0
|
||||
enabled: true
|
||||
starter_pack:
|
||||
app_store_product_id: com.meeyao.qianwen.starter_pack
|
||||
credits: 100
|
||||
type: regular
|
||||
sort_order: 10
|
||||
enabled: true
|
||||
popular_pack:
|
||||
app_store_product_id: com.meeyao.qianwen.popular_pack
|
||||
credits: 210
|
||||
type: regular
|
||||
sort_order: 20
|
||||
enabled: true
|
||||
premium_pack:
|
||||
app_store_product_id: com.meeyao.qianwen.premium_pack
|
||||
credits: 415
|
||||
type: regular
|
||||
sort_order: 30
|
||||
enabled: true
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
region: US
|
||||
currency: USD
|
||||
packages:
|
||||
- product_code: new_user_pack
|
||||
type: starter
|
||||
price: 0.99
|
||||
credits: 60
|
||||
sort_order: 0
|
||||
enabled: true
|
||||
|
||||
- product_code: basic_pack
|
||||
type: regular
|
||||
price: 4.99
|
||||
credits: 100
|
||||
sort_order: 10
|
||||
enabled: true
|
||||
|
||||
- product_code: popular_pack
|
||||
type: regular
|
||||
price: 7.99
|
||||
credits: 210
|
||||
sort_order: 20
|
||||
enabled: true
|
||||
|
||||
- product_code: premium_pack
|
||||
type: regular
|
||||
price: 12.99
|
||||
credits: 415
|
||||
sort_order: 30
|
||||
enabled: true
|
||||
@@ -6,7 +6,6 @@ from utils.paths import (
|
||||
get_gua_catalog_path,
|
||||
get_llm_catalog_config_path,
|
||||
get_notification_config_dir,
|
||||
get_package_config_path,
|
||||
get_packages_config_dir,
|
||||
get_src_root,
|
||||
get_static_config_dir,
|
||||
@@ -21,7 +20,6 @@ __all__ = [
|
||||
"get_gua_catalog_path",
|
||||
"get_llm_catalog_config_path",
|
||||
"get_notification_config_dir",
|
||||
"get_package_config_path",
|
||||
"get_packages_config_dir",
|
||||
"get_src_root",
|
||||
"get_static_config_dir",
|
||||
|
||||
@@ -19,6 +19,10 @@ def get_packages_config_dir() -> Path:
|
||||
return get_static_config_dir() / "packages"
|
||||
|
||||
|
||||
def get_default_package_config_path() -> Path:
|
||||
return get_packages_config_dir() / "default.yaml"
|
||||
|
||||
|
||||
def get_database_config_dir() -> Path:
|
||||
return get_static_config_dir() / "database"
|
||||
|
||||
@@ -31,14 +35,6 @@ def get_divination_data_dir() -> Path:
|
||||
return get_src_root() / "core/divination/data"
|
||||
|
||||
|
||||
def get_package_config_path(country: str) -> Path:
|
||||
return get_packages_config_dir() / f"{country.lower()}.yaml"
|
||||
|
||||
|
||||
def get_default_package_config_path() -> Path:
|
||||
return get_packages_config_dir() / "default.yaml"
|
||||
|
||||
|
||||
def get_llm_catalog_config_path() -> Path:
|
||||
return get_database_config_dir() / "llm_catalog.yaml"
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
||||
_ALLOWED_KEY_TYPES = (EllipticCurvePublicKey, RSAPublicKey)
|
||||
|
||||
_APPLE_ROOT_CA_G3_FINGERPRINT = (
|
||||
"0e429e09b3c0da64e87f0a659a6a40ac08dde5e1b115cca0e3a8f6a5"
|
||||
"b52cb02fd567e0359fe8fa4d4c41037970fe01b0"
|
||||
)
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ class AppleJwsVerifier:
|
||||
) -> VerifiedTransaction | VerificationError:
|
||||
try:
|
||||
unverified_header = jwt.get_unverified_header(signed_transaction_info)
|
||||
except jwt.exceptions.DecodeError:
|
||||
except jwt.exceptions.DecodeError as e:
|
||||
logger.error("Failed to decode JWS header: %s", str(e))
|
||||
return VerificationError(
|
||||
code="PAYMENT_TRANSACTION_INVALID",
|
||||
detail="Failed to decode JWS header",
|
||||
@@ -117,6 +118,11 @@ class AppleJwsVerifier:
|
||||
|
||||
bundle_id: str = str(payload.get("bundleId", ""))
|
||||
if bundle_id != expected_bundle_id:
|
||||
logger.error(
|
||||
"bundleId mismatch: expected=%s got=%s",
|
||||
expected_bundle_id,
|
||||
bundle_id,
|
||||
)
|
||||
return VerificationError(
|
||||
code="PAYMENT_PRODUCT_MISMATCH",
|
||||
detail=f"bundleId mismatch: expected={expected_bundle_id} got={bundle_id}",
|
||||
@@ -124,6 +130,11 @@ class AppleJwsVerifier:
|
||||
|
||||
product_id: str = str(payload.get("productId", ""))
|
||||
if product_id != expected_product_id:
|
||||
logger.error(
|
||||
"productId mismatch: expected=%s got=%s",
|
||||
expected_product_id,
|
||||
product_id,
|
||||
)
|
||||
return VerificationError(
|
||||
code="PAYMENT_PRODUCT_MISMATCH",
|
||||
detail=f"productId mismatch: expected={expected_product_id} got={product_id}",
|
||||
@@ -131,12 +142,18 @@ class AppleJwsVerifier:
|
||||
|
||||
environment: str = str(payload.get("environment", ""))
|
||||
if environment not in ("Sandbox", "Production"):
|
||||
logger.error("Invalid environment: %s", environment)
|
||||
return VerificationError(
|
||||
code="PAYMENT_TRANSACTION_INVALID",
|
||||
detail=f"Invalid environment: {environment}",
|
||||
)
|
||||
|
||||
if environment != expected_environment:
|
||||
logger.error(
|
||||
"Environment mismatch: expected=%s got=%s",
|
||||
expected_environment,
|
||||
environment,
|
||||
)
|
||||
return VerificationError(
|
||||
code="PAYMENT_ENVIRONMENT_MISMATCH",
|
||||
detail=f"Environment mismatch: expected={expected_environment} got={environment}",
|
||||
@@ -159,6 +176,7 @@ class AppleJwsVerifier:
|
||||
app_account_token_raw = payload.get("appAccountToken")
|
||||
|
||||
if not transaction_id:
|
||||
logger.error("Missing transactionId in payload")
|
||||
return VerificationError(
|
||||
code="PAYMENT_TRANSACTION_INVALID",
|
||||
detail="Missing transactionId in payload",
|
||||
|
||||
@@ -83,3 +83,6 @@ class PaymentRepository:
|
||||
if claim is None:
|
||||
raise RuntimeError("Failed to upsert register bonus claim")
|
||||
return claim
|
||||
|
||||
async def commit(self) -> None:
|
||||
await self._session.commit()
|
||||
|
||||
@@ -35,6 +35,8 @@ class ProductMapping:
|
||||
app_store_product_id: str
|
||||
credits: int
|
||||
type: str
|
||||
sort_order: int = 0
|
||||
enabled: bool = True
|
||||
|
||||
|
||||
_product_mappings_cache: dict[str, ProductMapping] | None = None
|
||||
@@ -52,14 +54,16 @@ def _load_product_mappings() -> dict[str, ProductMapping]:
|
||||
with mapping_path.open("r", encoding="utf-8") as f:
|
||||
raw: Any = yaml.safe_load(f) or {}
|
||||
|
||||
mappings: dict[str, ProductMapping] = {}
|
||||
product_mappings: Any = raw.get("product_mappings", {})
|
||||
for code, entry in product_mappings.items():
|
||||
mappings[str(code)] = ProductMapping(
|
||||
app_store_product_id=str(entry["app_store_product_id"]),
|
||||
credits=int(entry["credits"]),
|
||||
type=str(entry["type"]),
|
||||
)
|
||||
mappings: dict[str, ProductMapping] = {}
|
||||
product_mappings: Any = raw.get("product_mappings", {})
|
||||
for code, entry in product_mappings.items():
|
||||
mappings[str(code)] = ProductMapping(
|
||||
app_store_product_id=str(entry["app_store_product_id"]),
|
||||
credits=int(entry["credits"]),
|
||||
type=str(entry["type"]),
|
||||
sort_order=int(entry.get("sort_order", 0)),
|
||||
enabled=bool(entry.get("enabled", True)),
|
||||
)
|
||||
|
||||
_product_mappings_cache = mappings
|
||||
return mappings
|
||||
@@ -119,6 +123,12 @@ class PaymentService:
|
||||
)
|
||||
|
||||
if isinstance(result, VerificationError):
|
||||
logger.error(
|
||||
"Transaction verification failed: code=%s detail=%s transaction_id=%s",
|
||||
result.code,
|
||||
result.detail,
|
||||
request.transaction_id,
|
||||
)
|
||||
status_code = 422
|
||||
if result.code == "PAYMENT_TRANSACTION_REVOKED":
|
||||
status_code = 409
|
||||
@@ -132,6 +142,11 @@ class PaymentService:
|
||||
verified: VerifiedTransaction = result
|
||||
|
||||
if str(verified.transaction_id) != request.transaction_id:
|
||||
logger.error(
|
||||
"transactionId mismatch: request=%s verified=%s",
|
||||
request.transaction_id,
|
||||
verified.transaction_id,
|
||||
)
|
||||
raise ApiProblemError(
|
||||
status_code=422,
|
||||
detail=problem_payload(
|
||||
@@ -273,6 +288,15 @@ class PaymentService:
|
||||
transaction_record.status = "granted"
|
||||
transaction_record.ledger_event_id = event_id
|
||||
|
||||
logger.info(
|
||||
"Transaction granted: user_id=%s transaction_id=%s product_code=%s credits=%d new_balance=%d",
|
||||
user_id,
|
||||
verified.transaction_id,
|
||||
request.product_code,
|
||||
credits,
|
||||
new_balance,
|
||||
)
|
||||
|
||||
if is_starter and email_hash and normalized_email:
|
||||
_ = await self._payment_repo.upsert_register_bonus_claim_for_starter_pack(
|
||||
email_hash=email_hash,
|
||||
@@ -280,6 +304,8 @@ class PaymentService:
|
||||
first_user_id_snapshot=user_id,
|
||||
)
|
||||
|
||||
await self._payment_repo.commit()
|
||||
|
||||
return VerifyTransactionResponse(
|
||||
status="granted",
|
||||
productCode=request.product_code,
|
||||
@@ -313,11 +339,6 @@ class PaymentService:
|
||||
return
|
||||
|
||||
if txn.status not in ("granted",):
|
||||
logger.info(
|
||||
"Refund skipped: transaction %s status=%s",
|
||||
transaction_id,
|
||||
txn.status,
|
||||
)
|
||||
return
|
||||
|
||||
user_id = txn.user_id
|
||||
@@ -405,6 +426,8 @@ class PaymentService:
|
||||
txn.status,
|
||||
)
|
||||
|
||||
await self._payment_repo.commit()
|
||||
|
||||
async def handle_server_notification(self, *, signed_payload: str) -> None:
|
||||
if not signed_payload:
|
||||
logger.warning("Empty Apple server notification payload")
|
||||
@@ -467,13 +490,4 @@ class PaymentService:
|
||||
return
|
||||
|
||||
if notification_type == "DID_RENEW" and transaction_id:
|
||||
logger.info(
|
||||
"Apple DID_RENEW for transaction %s, no action needed",
|
||||
transaction_id,
|
||||
)
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"Apple notification type=%s not handled, skipped",
|
||||
notification_type,
|
||||
)
|
||||
|
||||
@@ -20,8 +20,8 @@ async def test_verify_endpoint_returns_401_without_auth() -> None:
|
||||
response = await client.post(
|
||||
"/api/v1/payments/apple/transactions/verify",
|
||||
json={
|
||||
"productCode": "basic_pack",
|
||||
"appStoreProductId": "com.meeyao.qianwen.basic_pack",
|
||||
"productCode": "starter_pack",
|
||||
"appStoreProductId": "com.meeyao.qianwen.starter_pack",
|
||||
"transactionId": "0000000000000001",
|
||||
"signedTransactionInfo": "fake_jws",
|
||||
},
|
||||
|
||||
@@ -22,7 +22,7 @@ class TestAppleJwsVerifierInvalidInput:
|
||||
result = verifier.verify_signed_transaction(
|
||||
"not-a-jws",
|
||||
expected_bundle_id="com.meeyao.qianwen",
|
||||
expected_product_id="com.meeyao.qianwen.basic_pack",
|
||||
expected_product_id="com.meeyao.qianwen.starter_pack",
|
||||
)
|
||||
assert isinstance(result, VerificationError)
|
||||
assert result.code == "PAYMENT_TRANSACTION_INVALID"
|
||||
@@ -34,7 +34,7 @@ class TestAppleJwsVerifierInvalidInput:
|
||||
result = verifier.verify_signed_transaction(
|
||||
f"{h}.{p}.fake",
|
||||
expected_bundle_id="com.meeyao.qianwen",
|
||||
expected_product_id="com.meeyao.qianwen.basic_pack",
|
||||
expected_product_id="com.meeyao.qianwen.starter_pack",
|
||||
)
|
||||
assert isinstance(result, VerificationError)
|
||||
assert "x5c" in result.detail
|
||||
@@ -45,7 +45,7 @@ class TestAppleJwsVerifierInvalidInput:
|
||||
result = verifier.verify_signed_transaction(
|
||||
f"{h}.{p}.fake",
|
||||
expected_bundle_id="com.meeyao.qianwen",
|
||||
expected_product_id="com.meeyao.qianwen.basic_pack",
|
||||
expected_product_id="com.meeyao.qianwen.starter_pack",
|
||||
)
|
||||
assert isinstance(result, VerificationError)
|
||||
assert "x5c" in result.detail
|
||||
@@ -62,7 +62,7 @@ class TestAppleJwsVerifierInvalidInput:
|
||||
result = verifier.verify_signed_transaction(
|
||||
f"{h}.{p}.fake",
|
||||
expected_bundle_id="com.meeyao.qianwen",
|
||||
expected_product_id="com.meeyao.qianwen.basic_pack",
|
||||
expected_product_id="com.meeyao.qianwen.starter_pack",
|
||||
)
|
||||
assert isinstance(result, VerificationError)
|
||||
assert "fingerprint" in result.detail or "issuer" in result.detail or "subject" in result.detail
|
||||
|
||||
@@ -30,6 +30,7 @@ class _FakePaymentRepository:
|
||||
self.inserted_transactions: list[AppleIapTransaction] = []
|
||||
self.claim: RegisterBonusClaims | None = None
|
||||
self.claim_starter_pack_called: bool = False
|
||||
self.commit_count = 0
|
||||
|
||||
async def get_or_create_user_points_for_update(self, *, user_id: UUID) -> _FakeAccount:
|
||||
return self.account
|
||||
@@ -59,6 +60,9 @@ class _FakePaymentRepository:
|
||||
self.claim.has_purchased_starter_pack = True
|
||||
return self.claim
|
||||
|
||||
async def commit(self) -> None:
|
||||
self.commit_count += 1
|
||||
|
||||
|
||||
class _FakePointsRepository:
|
||||
def __init__(self) -> None:
|
||||
@@ -78,14 +82,17 @@ class _FakeVerifier:
|
||||
*,
|
||||
expected_bundle_id: str,
|
||||
expected_product_id: str,
|
||||
expected_environment: str,
|
||||
) -> VerifiedTransaction | VerificationError:
|
||||
del signed_transaction_info, expected_bundle_id, expected_product_id
|
||||
del expected_environment
|
||||
return self._result
|
||||
|
||||
|
||||
def _make_verified_transaction(
|
||||
*,
|
||||
transaction_id: str = "2000000123456789",
|
||||
product_id: str = "com.meeyao.qianwen.basic_pack",
|
||||
product_id: str = "com.meeyao.qianwen.starter_pack",
|
||||
environment: str = "Sandbox",
|
||||
) -> VerifiedTransaction:
|
||||
return VerifiedTransaction(
|
||||
@@ -134,7 +141,7 @@ class TestPaymentServiceProductMismatch:
|
||||
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
||||
)
|
||||
request = VerifyTransactionRequest(
|
||||
productCode="basic_pack",
|
||||
productCode="starter_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.wrong_pack",
|
||||
transactionId="2000000123456789",
|
||||
signedTransactionInfo="fake_jws",
|
||||
@@ -162,8 +169,8 @@ class TestPaymentServiceVerificationFailed:
|
||||
),
|
||||
)
|
||||
request = VerifyTransactionRequest(
|
||||
productCode="basic_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.basic_pack",
|
||||
productCode="starter_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
||||
transactionId="2000000123456789",
|
||||
signedTransactionInfo="fake_jws",
|
||||
)
|
||||
@@ -183,8 +190,8 @@ class TestPaymentServiceAlreadyGranted:
|
||||
existing = AppleIapTransaction(
|
||||
id=uuid4(),
|
||||
user_id=user_id,
|
||||
product_code="basic_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.basic_pack",
|
||||
product_code="starter_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
||||
transaction_id="2000000123456789",
|
||||
original_transaction_id="2000000123456789",
|
||||
environment="Sandbox",
|
||||
@@ -202,8 +209,8 @@ class TestPaymentServiceAlreadyGranted:
|
||||
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
||||
)
|
||||
request = VerifyTransactionRequest(
|
||||
productCode="basic_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.basic_pack",
|
||||
productCode="starter_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
||||
transactionId="2000000123456789",
|
||||
signedTransactionInfo="fake_jws",
|
||||
)
|
||||
@@ -222,8 +229,8 @@ class TestPaymentServiceTransactionConflict:
|
||||
existing = AppleIapTransaction(
|
||||
id=uuid4(),
|
||||
user_id=uuid4(),
|
||||
product_code="basic_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.basic_pack",
|
||||
product_code="starter_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
||||
transaction_id="2000000123456789",
|
||||
original_transaction_id="2000000123456789",
|
||||
environment="Sandbox",
|
||||
@@ -241,8 +248,8 @@ class TestPaymentServiceTransactionConflict:
|
||||
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
||||
)
|
||||
request = VerifyTransactionRequest(
|
||||
productCode="basic_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.basic_pack",
|
||||
productCode="starter_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
||||
transactionId="2000000123456789",
|
||||
signedTransactionInfo="fake_jws",
|
||||
)
|
||||
@@ -266,8 +273,8 @@ class TestPaymentServiceSuccessfulGrant:
|
||||
verifier=_FakeVerifier(result=_make_verified_transaction()),
|
||||
)
|
||||
request = VerifyTransactionRequest(
|
||||
productCode="basic_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.basic_pack",
|
||||
productCode="starter_pack",
|
||||
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
||||
transactionId="2000000123456789",
|
||||
signedTransactionInfo="fake_jws",
|
||||
)
|
||||
@@ -368,6 +375,7 @@ class _FakePaymentRepoForRefund:
|
||||
self._transaction = transaction
|
||||
self.account = account or _FakeAccountForRefund()
|
||||
self.inserted_transactions: list[AppleIapTransaction] = []
|
||||
self.commit_count = 0
|
||||
|
||||
async def get_transaction_by_transaction_id(self, *, transaction_id: str) -> AppleIapTransaction | None:
|
||||
return self._transaction
|
||||
@@ -389,6 +397,9 @@ class _FakePaymentRepoForRefund:
|
||||
) -> None:
|
||||
pass
|
||||
|
||||
async def commit(self) -> None:
|
||||
self.commit_count += 1
|
||||
|
||||
|
||||
class TestProcessRefundUnknownTransaction:
|
||||
@pytest.mark.asyncio
|
||||
@@ -407,8 +418,8 @@ class TestProcessRefundNotGranted:
|
||||
txn = AppleIapTransaction(
|
||||
id=uuid4(),
|
||||
user_id=uuid4(),
|
||||
product_code="basic_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.basic_pack",
|
||||
product_code="starter_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
||||
transaction_id="2000000999999999",
|
||||
original_transaction_id="2000000999999999",
|
||||
environment="Sandbox",
|
||||
@@ -435,8 +446,8 @@ class TestProcessRefundSufficientBalance:
|
||||
txn = AppleIapTransaction(
|
||||
id=uuid4(),
|
||||
user_id=user_id,
|
||||
product_code="basic_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.basic_pack",
|
||||
product_code="starter_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
||||
transaction_id="2000000999999999",
|
||||
original_transaction_id="2000000999999999",
|
||||
environment="Sandbox",
|
||||
@@ -473,8 +484,8 @@ class TestProcessRefundInsufficientBalance:
|
||||
txn = AppleIapTransaction(
|
||||
id=uuid4(),
|
||||
user_id=user_id,
|
||||
product_code="basic_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.basic_pack",
|
||||
product_code="starter_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
||||
transaction_id="2000000999999998",
|
||||
original_transaction_id="2000000999999998",
|
||||
environment="Sandbox",
|
||||
@@ -509,8 +520,8 @@ class TestProcessRefundIdempotency:
|
||||
txn = AppleIapTransaction(
|
||||
id=uuid4(),
|
||||
user_id=user_id,
|
||||
product_code="basic_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.basic_pack",
|
||||
product_code="starter_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
||||
transaction_id="2000000999999997",
|
||||
original_transaction_id="2000000999999997",
|
||||
environment="Sandbox",
|
||||
@@ -539,8 +550,8 @@ class TestHandleServerNotificationRefund:
|
||||
txn = AppleIapTransaction(
|
||||
id=uuid4(),
|
||||
user_id=user_id,
|
||||
product_code="basic_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.basic_pack",
|
||||
product_code="starter_pack",
|
||||
app_store_product_id="com.meeyao.qianwen.starter_pack",
|
||||
transaction_id="2000000999999001",
|
||||
original_transaction_id="2000000999999001",
|
||||
environment="Sandbox",
|
||||
|
||||
Reference in New Issue
Block a user