fix: update production app configuration
This commit is contained in:
+2
-12
@@ -91,12 +91,6 @@ ERYAO_LLM__PROVIDER_KEYS__DEEPSEEK=
|
|||||||
ERYAO_POINTS_POLICY__REGISTER_BONUS_POINTS=60
|
ERYAO_POINTS_POLICY__REGISTER_BONUS_POINTS=60
|
||||||
ERYAO_POINTS_POLICY__REGISTER_BONUS_HMAC_KEY=replace-with-strong-random-key
|
ERYAO_POINTS_POLICY__REGISTER_BONUS_HMAC_KEY=replace-with-strong-random-key
|
||||||
|
|
||||||
############
|
|
||||||
# 敏感词配置
|
|
||||||
############
|
|
||||||
ERYAO_SENSITIVE_WORD__USE_ALIYUN=true
|
|
||||||
ERYAO_SENSITIVE_WORD__FALLBACK_TO_LOCAL=true
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# CORS 配置
|
# CORS 配置
|
||||||
############
|
############
|
||||||
@@ -112,13 +106,9 @@ ERYAO_TEST__CODE=123456
|
|||||||
# Apple IAP 配置
|
# Apple IAP 配置
|
||||||
############
|
############
|
||||||
ERYAO_APPLE_IAP__BUNDLE_ID=com.meeyao.qianwen
|
ERYAO_APPLE_IAP__BUNDLE_ID=com.meeyao.qianwen
|
||||||
|
# Apple IAP 环境识别。auto 表示以后端验签后的 Apple transaction environment 为准。
|
||||||
|
ERYAO_APPLE_IAP__ENVIRONMENT=auto
|
||||||
# Server API 密钥(可选,用于主动查询交易状态)
|
# Server API 密钥(可选,用于主动查询交易状态)
|
||||||
ERYAO_APPLE_IAP__SERVER_API_KEY_ID=
|
ERYAO_APPLE_IAP__SERVER_API_KEY_ID=
|
||||||
ERYAO_APPLE_IAP__SERVER_API_PRIVATE_KEY=
|
ERYAO_APPLE_IAP__SERVER_API_PRIVATE_KEY=
|
||||||
ERYAO_APPLE_IAP__SERVER_API_ISSUER_ID=
|
ERYAO_APPLE_IAP__SERVER_API_ISSUER_ID=
|
||||||
# 沙盒测试账号(仅用于手动测试,不用于后端验证)
|
|
||||||
ERYAO_APPLE_IAP__SANDBOX_TESTER_EMAIL=
|
|
||||||
ERYAO_APPLE_IAP__SANDBOX_TESTER_PASSWORD=
|
|
||||||
# Server Notifications V2 URL(在 App Store Connect 中配置)
|
|
||||||
# 格式: https://<your-domain>/api/v1/payments/apple/notifications
|
|
||||||
ERYAO_APPLE_IAP__SERVER_NOTIFICATIONS_URL=
|
|
||||||
|
|||||||
+9
-6
@@ -2,9 +2,15 @@
|
|||||||
|
|
||||||
Flutter client for `觅爻签问`.
|
Flutter client for `觅爻签问`.
|
||||||
|
|
||||||
## Debug startup with backend injection
|
## Backend URL
|
||||||
|
|
||||||
This app supports injecting backend URL at startup (same pattern as social-app):
|
Default backend URL:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://api.meeyao.com
|
||||||
|
```
|
||||||
|
|
||||||
|
This app also supports injecting backend URL at startup (same pattern as social-app):
|
||||||
|
|
||||||
- Dart read path: `lib/core/config/env.dart`
|
- Dart read path: `lib/core/config/env.dart`
|
||||||
- Injection key: `BACKEND_URL`
|
- Injection key: `BACKEND_URL`
|
||||||
@@ -21,7 +27,4 @@ flutter run --dart-define=BACKEND_URL=http://192.168.1.100:5775
|
|||||||
./tool/run-dev.sh --backend-url http://192.168.1.100:5775
|
./tool/run-dev.sh --backend-url http://192.168.1.100:5775
|
||||||
```
|
```
|
||||||
|
|
||||||
If `BACKEND_URL` is not provided, fallback is:
|
If `BACKEND_URL` is not provided, the app uses the production backend URL above.
|
||||||
|
|
||||||
- Android emulator: `http://10.0.2.2:5775`
|
|
||||||
- Others: `http://localhost:5775`
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ MeeYao Divination is designed based on traditional oriental culture. Our core go
|
|||||||
|
|
||||||
**Developer:** Ann Lee
|
**Developer:** Ann Lee
|
||||||
|
|
||||||
**Contact Email:** ann@xumee.com
|
**Contact Email:** ann@xunmee.com
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ In accordance with CCPA/CPRA and U.S. local privacy laws, you enjoy the followin
|
|||||||
|
|
||||||
You can submit data requests through the only dedicated contact method:
|
You can submit data requests through the only dedicated contact method:
|
||||||
|
|
||||||
- **Contact Email**: ann@xumee.com
|
- **Contact Email**: ann@xunmee.com
|
||||||
|
|
||||||
I will respond to your legitimate request within 45 days, and properly verify your identity to ensure data security before processing.
|
I will respond to your legitimate request within 45 days, and properly verify your identity to ensure data security before processing.
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ This Privacy Policy may be updated irregularly to adapt to platform rules and le
|
|||||||
|
|
||||||
If you have any questions, suggestions or privacy-related complaints about this Privacy Policy, please contact me:
|
If you have any questions, suggestions or privacy-related complaints about this Privacy Policy, please contact me:
|
||||||
|
|
||||||
**Developer Email**: ann@xumee.com
|
**Developer Email**: ann@xunmee.com
|
||||||
|
|
||||||
If you are a California resident and dissatisfied with the processing result, you can consult the local privacy regulatory authority.
|
If you are a California resident and dissatisfied with the processing result, you can consult the local privacy regulatory authority.
|
||||||
|
|
||||||
|
|||||||
@@ -118,4 +118,4 @@ I reserve the right to revise and update these Terms of Service at any time. Mat
|
|||||||
If you have questions, feedback or legal inquiries about these Terms, please contact:
|
If you have questions, feedback or legal inquiries about these Terms, please contact:
|
||||||
|
|
||||||
- **Developer**: Individual Independent Developer
|
- **Developer**: Individual Independent Developer
|
||||||
- **Contact Email**: ann@xumee.com
|
- **Contact Email**: ann@xunmee.com
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
**开发者**:Ann Lee
|
**开发者**:Ann Lee
|
||||||
|
|
||||||
**联系邮箱**:ann@xumee.com
|
**联系邮箱**:ann@xunmee.com
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
您可以通过唯一指定联系方式提交数据请求:
|
您可以通过唯一指定联系方式提交数据请求:
|
||||||
|
|
||||||
- **联系邮箱**:ann@xumee.com
|
- **联系邮箱**:ann@xunmee.com
|
||||||
|
|
||||||
我将在 45 天内回复您的合法请求,并在处理前妥善验证您的身份以确保数据安全。
|
我将在 45 天内回复您的合法请求,并在处理前妥善验证您的身份以确保数据安全。
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
|
|
||||||
如果您对本隐私政策有任何疑问、建议或隐私相关投诉,请联系我:
|
如果您对本隐私政策有任何疑问、建议或隐私相关投诉,请联系我:
|
||||||
|
|
||||||
**开发者邮箱**:ann@xumee.com
|
**开发者邮箱**:ann@xunmee.com
|
||||||
|
|
||||||
如果您是加州居民且对处理结果不满意,可咨询当地隐私监管机构。
|
如果您是加州居民且对处理结果不满意,可咨询当地隐私监管机构。
|
||||||
|
|
||||||
|
|||||||
@@ -118,4 +118,4 @@
|
|||||||
如果您对本条款有疑问、反馈或法律咨询,请联系:
|
如果您对本条款有疑问、反馈或法律咨询,请联系:
|
||||||
|
|
||||||
- **开发者**:独立个人开发者
|
- **开发者**:独立个人开发者
|
||||||
- **联系邮箱**:ann@xumee.com
|
- **联系邮箱**:ann@xunmee.com
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
**開發者**:Ann Lee
|
**開發者**:Ann Lee
|
||||||
|
|
||||||
**聯繫郵箱**:ann@xumee.com
|
**聯繫郵箱**:ann@xunmee.com
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@
|
|||||||
|
|
||||||
您可以通過唯一指定聯繫方式提交數據請求:
|
您可以通過唯一指定聯繫方式提交數據請求:
|
||||||
|
|
||||||
- **聯繫郵箱**:ann@xumee.com
|
- **聯繫郵箱**:ann@xunmee.com
|
||||||
|
|
||||||
我將在 45 天內回覆您的合法請求,並在處理前妥善驗證您的身份以確保數據安全。
|
我將在 45 天內回覆您的合法請求,並在處理前妥善驗證您的身份以確保數據安全。
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@
|
|||||||
|
|
||||||
如果您對本隱私政策有任何疑問、建議或隱私相關投訴,請聯繫我:
|
如果您對本隱私政策有任何疑問、建議或隱私相關投訴,請聯繫我:
|
||||||
|
|
||||||
**開發者郵箱**:ann@xumee.com
|
**開發者郵箱**:ann@xunmee.com
|
||||||
|
|
||||||
如果您是加州居民且對處理結果不滿意,可諮詢當地隱私監管機構。
|
如果您是加州居民且對處理結果不滿意,可諮詢當地隱私監管機構。
|
||||||
|
|
||||||
|
|||||||
@@ -118,4 +118,4 @@
|
|||||||
如果您對本條款有疑問、反饋或法律諮詢,請聯繫:
|
如果您對本條款有疑問、反饋或法律諮詢,請聯繫:
|
||||||
|
|
||||||
- **開發者**:獨立個人開發者
|
- **開發者**:獨立個人開發者
|
||||||
- **聯繫郵箱**:ann@xumee.com
|
- **聯繫郵箱**:ann@xunmee.com
|
||||||
|
|||||||
@@ -1,19 +1,13 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
class Env {
|
class Env {
|
||||||
|
static const _productionBackendUrl = 'https://api.meeyao.com';
|
||||||
|
|
||||||
static String get backendUrl {
|
static String get backendUrl {
|
||||||
final injected = const String.fromEnvironment('BACKEND_URL');
|
final injected = const String.fromEnvironment('BACKEND_URL');
|
||||||
if (injected.isNotEmpty && injected != 'false') {
|
if (injected.isNotEmpty && injected != 'false') {
|
||||||
return injected;
|
return injected;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Platform.isAndroid) {
|
return _productionBackendUrl;
|
||||||
return 'http://10.0.2.2:5775';
|
|
||||||
}
|
|
||||||
if (Platform.isIOS) {
|
|
||||||
return 'http://192.168.1.63:5775';
|
|
||||||
}
|
|
||||||
return 'http://localhost:5775';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> init() async {}
|
static Future<void> init() async {}
|
||||||
|
|||||||
@@ -32,6 +32,22 @@ _MAX_CONTEXT_ATTACHMENTS = 3
|
|||||||
_RUNTIME_AGENT_OUTPUT_ADAPTER = TypeAdapter(RuntimeAgentOutput)
|
_RUNTIME_AGENT_OUTPUT_ADAPTER = TypeAdapter(RuntimeAgentOutput)
|
||||||
|
|
||||||
|
|
||||||
|
def create_context_messages_cache() -> Any:
|
||||||
|
from core.agentscope.caches.context_messages_cache import (
|
||||||
|
create_context_messages_cache as factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
return factory()
|
||||||
|
|
||||||
|
|
||||||
|
def create_attachment_content_cache() -> Any:
|
||||||
|
from core.agentscope.caches.attachment_content_cache import (
|
||||||
|
create_attachment_content_cache as factory,
|
||||||
|
)
|
||||||
|
|
||||||
|
return factory()
|
||||||
|
|
||||||
|
|
||||||
def _serialize_tool_agent_output(
|
def _serialize_tool_agent_output(
|
||||||
*,
|
*,
|
||||||
metadata: AgentChatMessageMetadata | None,
|
metadata: AgentChatMessageMetadata | None,
|
||||||
@@ -203,12 +219,6 @@ async def _build_recent_context_messages(
|
|||||||
context_config: "MessageContextConfig",
|
context_config: "MessageContextConfig",
|
||||||
) -> list[Msg]:
|
) -> list[Msg]:
|
||||||
from agentscope.message import Msg
|
from agentscope.message import Msg
|
||||||
from core.agentscope.caches.attachment_content_cache import (
|
|
||||||
create_attachment_content_cache,
|
|
||||||
)
|
|
||||||
from core.agentscope.caches.context_messages_cache import (
|
|
||||||
create_context_messages_cache,
|
|
||||||
)
|
|
||||||
from core.agentscope.services.context_service import AgentContextService
|
from core.agentscope.services.context_service import AgentContextService
|
||||||
from services.base.supabase import supabase_service
|
from services.base.supabase import supabase_service
|
||||||
from v1.agent.repository import AgentRepository
|
from v1.agent.repository import AgentRepository
|
||||||
@@ -348,12 +358,10 @@ async def _build_recent_context_messages(
|
|||||||
|
|
||||||
|
|
||||||
async def run_agentscope_task(command: dict[str, Any]) -> dict[str, object]:
|
async def run_agentscope_task(command: dict[str, Any]) -> dict[str, object]:
|
||||||
from core.agentscope.events import (
|
from core.agentscope.events.agui_codec import AgentScopeAgUiCodec
|
||||||
AgentScopeAgUiCodec,
|
from core.agentscope.events.pipeline import AgentScopeEventPipeline
|
||||||
AgentScopeEventPipeline,
|
from core.agentscope.events.redis_bus import RedisStreamBus
|
||||||
RedisStreamBus,
|
from core.agentscope.events.store import SqlAlchemyEventStore
|
||||||
SqlAlchemyEventStore,
|
|
||||||
)
|
|
||||||
from core.config.settings import config
|
from core.config.settings import config
|
||||||
from core.db.session import AsyncSessionLocal
|
from core.db.session import AsyncSessionLocal
|
||||||
from services.base.redis import get_or_init_redis_client
|
from services.base.redis import get_or_init_redis_client
|
||||||
|
|||||||
@@ -183,11 +183,6 @@ class DatabaseSettings(BaseModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class SensitiveWordSettings(BaseModel):
|
|
||||||
use_aliyun: bool = True
|
|
||||||
fallback_to_local: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class TestSettings(BaseModel):
|
class TestSettings(BaseModel):
|
||||||
email: str = ""
|
email: str = ""
|
||||||
code: str = ""
|
code: str = ""
|
||||||
@@ -232,12 +227,10 @@ class AppleIapSettings(BaseModel):
|
|||||||
bundle_id: str = Field(default="com.meeyao.qianwen", min_length=1)
|
bundle_id: str = Field(default="com.meeyao.qianwen", min_length=1)
|
||||||
root_cert_url: str = "https://www.apple.com/certificateauthority/AppleIncRootCertificate.cer"
|
root_cert_url: str = "https://www.apple.com/certificateauthority/AppleIncRootCertificate.cer"
|
||||||
jws_x5c_cert_url: str = "https://api.storekit.itunes.apple.com/v1/verificationKeys"
|
jws_x5c_cert_url: str = "https://api.storekit.itunes.apple.com/v1/verificationKeys"
|
||||||
|
environment: Literal["auto", "sandbox", "production"] = "auto"
|
||||||
server_api_issuer_id: str | None = None
|
server_api_issuer_id: str | None = None
|
||||||
server_api_key_id: str | None = None
|
server_api_key_id: str | None = None
|
||||||
server_api_private_key: SecretStr | None = None
|
server_api_private_key: SecretStr | None = None
|
||||||
sandbox_tester_email: str | None = None
|
|
||||||
sandbox_tester_password: SecretStr | None = None
|
|
||||||
server_notifications_url: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_env_files() -> list[str]:
|
def _resolve_env_files() -> list[str]:
|
||||||
@@ -283,7 +276,6 @@ class Settings(BaseSettings):
|
|||||||
storage: StorageSettings = StorageSettings()
|
storage: StorageSettings = StorageSettings()
|
||||||
llm: LlmSettings = LlmSettings()
|
llm: LlmSettings = LlmSettings()
|
||||||
database: DatabaseSettings = DatabaseSettings()
|
database: DatabaseSettings = DatabaseSettings()
|
||||||
sensitive_word: SensitiveWordSettings = Field(default_factory=SensitiveWordSettings)
|
|
||||||
test: TestSettings = Field(default_factory=TestSettings)
|
test: TestSettings = Field(default_factory=TestSettings)
|
||||||
taskiq: TaskiqSettings = Field(default_factory=TaskiqSettings)
|
taskiq: TaskiqSettings = Field(default_factory=TaskiqSettings)
|
||||||
agent_runtime: AgentRuntimeSettings = Field(default_factory=AgentRuntimeSettings)
|
agent_runtime: AgentRuntimeSettings = Field(default_factory=AgentRuntimeSettings)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from fastapi import Depends
|
|||||||
from redis.asyncio import Redis
|
from redis.asyncio import Redis
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from core.agentscope.events import RedisStreamBus
|
from core.agentscope.events.redis_bus import RedisStreamBus
|
||||||
from core.agentscope.tools.tool_result_storage import (
|
from core.agentscope.tools.tool_result_storage import (
|
||||||
create_tool_result_storage,
|
create_tool_result_storage,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from ag_ui.core import RunAgentInput
|
from ag_ui.core import RunAgentInput
|
||||||
from core.http.errors import ApiProblemError, problem_payload
|
from core.http.errors import ApiProblemError, problem_payload
|
||||||
from core.agentscope.events import to_sse_event
|
from core.agentscope.events.sse import to_sse_event
|
||||||
from core.agentscope.schemas.agui_input import (
|
from core.agentscope.schemas.agui_input import (
|
||||||
parse_run_input,
|
parse_run_input,
|
||||||
validate_run_request_messages_contract,
|
validate_run_request_messages_contract,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class AppleJwsVerifier:
|
|||||||
*,
|
*,
|
||||||
expected_bundle_id: str,
|
expected_bundle_id: str,
|
||||||
expected_product_id: str,
|
expected_product_id: str,
|
||||||
expected_environment: str,
|
|
||||||
) -> VerifiedTransaction | VerificationError:
|
) -> VerifiedTransaction | VerificationError:
|
||||||
try:
|
try:
|
||||||
unverified_header = jwt.get_unverified_header(signed_transaction_info)
|
unverified_header = jwt.get_unverified_header(signed_transaction_info)
|
||||||
@@ -148,17 +147,6 @@ class AppleJwsVerifier:
|
|||||||
detail=f"Invalid environment: {environment}",
|
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}",
|
|
||||||
)
|
|
||||||
|
|
||||||
revocation_date_raw = payload.get("revocationDate")
|
revocation_date_raw = payload.get("revocationDate")
|
||||||
revocation_date: int | None = (
|
revocation_date: int | None = (
|
||||||
int(revocation_date_raw) if revocation_date_raw is not None else None
|
int(revocation_date_raw) if revocation_date_raw is not None else None
|
||||||
|
|||||||
@@ -114,12 +114,10 @@ class PaymentService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
expected_bundle_id = config.apple_iap.bundle_id
|
expected_bundle_id = config.apple_iap.bundle_id
|
||||||
expected_environment = "Sandbox" if config.runtime.environment != "prod" else "Production"
|
|
||||||
result = self._verifier.verify_signed_transaction(
|
result = self._verifier.verify_signed_transaction(
|
||||||
request.signed_transaction_info,
|
request.signed_transaction_info,
|
||||||
expected_bundle_id=expected_bundle_id,
|
expected_bundle_id=expected_bundle_id,
|
||||||
expected_product_id=product_mapping.app_store_product_id,
|
expected_product_id=product_mapping.app_store_product_id,
|
||||||
expected_environment=expected_environment,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if isinstance(result, VerificationError):
|
if isinstance(result, VerificationError):
|
||||||
|
|||||||
@@ -82,10 +82,8 @@ class _FakeVerifier:
|
|||||||
*,
|
*,
|
||||||
expected_bundle_id: str,
|
expected_bundle_id: str,
|
||||||
expected_product_id: str,
|
expected_product_id: str,
|
||||||
expected_environment: str,
|
|
||||||
) -> VerifiedTransaction | VerificationError:
|
) -> VerifiedTransaction | VerificationError:
|
||||||
del signed_transaction_info, expected_bundle_id, expected_product_id
|
del signed_transaction_info, expected_bundle_id, expected_product_id
|
||||||
del expected_environment
|
|
||||||
return self._result
|
return self._result
|
||||||
|
|
||||||
|
|
||||||
@@ -290,6 +288,32 @@ class TestPaymentServiceSuccessfulGrant:
|
|||||||
assert len(points_repo.appended_ledger) == 1
|
assert len(points_repo.appended_ledger) == 1
|
||||||
assert len(payment_repo.inserted_transactions) == 1
|
assert len(payment_repo.inserted_transactions) == 1
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_grants_production_transaction_from_verified_payload(self) -> None:
|
||||||
|
payment_repo = _FakePaymentRepository()
|
||||||
|
service = PaymentService(
|
||||||
|
payment_repo=payment_repo,
|
||||||
|
points_repo=_FakePointsRepository(),
|
||||||
|
verifier=_FakeVerifier(
|
||||||
|
result=_make_verified_transaction(environment="Production")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
request = VerifyTransactionRequest(
|
||||||
|
productCode="starter_pack",
|
||||||
|
appStoreProductId="com.meeyao.qianwen.starter_pack",
|
||||||
|
transactionId="2000000123456789",
|
||||||
|
signedTransactionInfo="fake_jws",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await service.verify_and_grant(
|
||||||
|
user_id=uuid4(),
|
||||||
|
user_email="test@example.com",
|
||||||
|
request=request,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.status == "granted"
|
||||||
|
assert payment_repo.inserted_transactions[0].environment == "Production"
|
||||||
|
|
||||||
|
|
||||||
class TestPaymentServiceStarterPackIneligible:
|
class TestPaymentServiceStarterPackIneligible:
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -108,7 +108,6 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|
|||||||
|---|---:|---|---|
|
|---|---:|---|---|
|
||||||
| `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled | Refresh packages and show product-unavailable message |
|
| `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled | Refresh packages and show product-unavailable message |
|
||||||
| `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result | Block grant and prompt retry |
|
| `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result | Block grant and prompt retry |
|
||||||
| `PAYMENT_ENVIRONMENT_MISMATCH` | 422 | Transaction environment (Sandbox/Production) does not match server environment | Show purchase-verification-failed message |
|
|
||||||
| `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed | Show purchase-verification-failed message |
|
| `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed | Show purchase-verification-failed message |
|
||||||
| `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed | Show purchase-unavailable message |
|
| `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed | Show purchase-unavailable message |
|
||||||
| `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state | Prompt to contact support or refresh balance |
|
| `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state | Prompt to contact support or refresh balance |
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Protocol verification status:
|
|||||||
|
|
||||||
- Current strategy: additive evolution (`backward-compatible`).
|
- Current strategy: additive evolution (`backward-compatible`).
|
||||||
- Breaking change requires explicit migration + rollback notes (`requires-migration`).
|
- Breaking change requires explicit migration + rollback notes (`requires-migration`).
|
||||||
|
- Apple IAP environment is auto-detected from verified Apple transaction payloads; the verify API is not split by Sandbox/Production.
|
||||||
|
|
||||||
## Route overview
|
## Route overview
|
||||||
|
|
||||||
@@ -70,7 +71,6 @@ Verify and grant credits for an Apple IAP transaction.
|
|||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
| `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled |
|
| `PAYMENT_PRODUCT_NOT_FOUND` | 404 | `productCode` does not exist or is not enabled |
|
||||||
| `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result |
|
| `PAYMENT_PRODUCT_MISMATCH` | 422 | Client product ID does not match backend/Apple verification result |
|
||||||
| `PAYMENT_ENVIRONMENT_MISMATCH` | 422 | Transaction environment (Sandbox/Production) does not match server environment |
|
|
||||||
| `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed |
|
| `PAYMENT_TRANSACTION_INVALID` | 422 | Apple signed transaction invalid, signature verification failed, or payload malformed |
|
||||||
| `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed |
|
| `PAYMENT_TRANSACTION_REVOKED` | 409 | Transaction has been revoked or refunded, grant not allowed |
|
||||||
| `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state |
|
| `PAYMENT_TRANSACTION_CONFLICT` | 409 | Transaction already processed by another user or in conflicting state |
|
||||||
@@ -119,6 +119,14 @@ product_mappings:
|
|||||||
- Backend tracks purchase via `register_bonus_claims.has_purchased_starter_pack`
|
- Backend tracks purchase via `register_bonus_claims.has_purchased_starter_pack`
|
||||||
- If already purchased, returns `PAYMENT_STARTER_PACK_INELIGIBLE` (409)
|
- If already purchased, returns `PAYMENT_STARTER_PACK_INELIGIBLE` (409)
|
||||||
|
|
||||||
|
## Environment handling
|
||||||
|
|
||||||
|
- `signedTransactionInfo` is verified server-side and its Apple-provided `environment` field is the source of truth.
|
||||||
|
- Valid environments are `Sandbox` and `Production`.
|
||||||
|
- TestFlight and App Review Sandbox transactions are accepted when Apple signs them as `Sandbox`.
|
||||||
|
- App Store Production transactions are accepted when Apple signs them as `Production`.
|
||||||
|
- Backend configuration defaults to `apple_iap.environment=auto`; it must not force all verifications into one Apple environment.
|
||||||
|
|
||||||
## Ledger integration
|
## Ledger integration
|
||||||
|
|
||||||
- Successful purchases create a ledger entry with:
|
- Successful purchases create a ledger entry with:
|
||||||
|
|||||||
Reference in New Issue
Block a user