fix: 修复 packages 接口访问不存在字段导致的运行时错误
- 移除 router 中对 result.region、result.currency、pkg.price 的访问 - 修正 pkg.type.value 为 pkg.type (type 是 Literal 不是 Enum) - 更新协议文档以反映实际实现 - 新增 Apple IAP 协议文档 - 标记未使用的错误码为 RESERVED
This commit is contained in:
@@ -63,14 +63,11 @@ async def get_available_packages(
|
|||||||
)
|
)
|
||||||
|
|
||||||
return PackagesResponse(
|
return PackagesResponse(
|
||||||
region=result.region,
|
|
||||||
currency=result.currency,
|
|
||||||
packages=[
|
packages=[
|
||||||
PackageInfo(
|
PackageInfo(
|
||||||
productCode=pkg.product_code,
|
productCode=pkg.product_code,
|
||||||
appStoreProductId=pkg.app_store_product_id,
|
appStoreProductId=pkg.app_store_product_id,
|
||||||
type=pkg.type.value,
|
type=pkg.type,
|
||||||
price=pkg.price,
|
|
||||||
credits=pkg.credits,
|
credits=pkg.credits,
|
||||||
isStarter=pkg.is_starter,
|
isStarter=pkg.is_starter,
|
||||||
starterEligible=pkg.starter_eligible,
|
starterEligible=pkg.starter_eligible,
|
||||||
|
|||||||
@@ -73,10 +73,18 @@ Request:
|
|||||||
{ "email": "user@example.com", "token": "123456" }
|
{ "email": "user@example.com", "token": "123456" }
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Optional fields:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "email": "user@example.com", "token": "123456", "language": "zh-CN", "timezone": "Asia/Shanghai" }
|
||||||
|
```
|
||||||
|
|
||||||
Validation (backend):
|
Validation (backend):
|
||||||
|
|
||||||
- `email` must match `SUPABASE_EMAIL_PATTERN`
|
- `email` must match `SUPABASE_EMAIL_PATTERN`
|
||||||
- `token` must be exactly 6 chars
|
- `token` must be exactly 6 chars
|
||||||
|
- `language` (optional): max 20 chars, updates profile settings.preferences.language if provided
|
||||||
|
- `timezone` (optional): max 50 chars, updates profile settings.preferences.timezone if provided
|
||||||
|
|
||||||
Response:
|
Response:
|
||||||
|
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ This document is the source of truth for backend RFC7807 `code` values consumed
|
|||||||
| `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 |
|
||||||
| `PAYMENT_STARTER_PACK_INELIGIBLE` | 409 | Current email identity has already purchased starter pack | Refresh packages and hide starter pack |
|
| `PAYMENT_STARTER_PACK_INELIGIBLE` | 409 | Current email identity has already purchased starter pack | Refresh packages and hide starter pack |
|
||||||
| `PAYMENT_APPLE_UNAVAILABLE` | 503 | Apple Server API or certificate fetch unavailable | Show retry-later message; do NOT complete/finish transaction |
|
| `PAYMENT_APPLE_UNAVAILABLE` | 503 | (RESERVED) Apple Server API or certificate fetch unavailable | Show retry-later message; do NOT complete/finish transaction |
|
||||||
| `PAYMENT_GRANT_FAILED` | 500 | Verification succeeded but grant transaction failed | Show retry-later message; retain transaction for compensation |
|
| `PAYMENT_GRANT_FAILED` | 500 | (RESERVED) Verification succeeded but grant transaction failed | Show retry-later message; retain transaction for compensation |
|
||||||
| `PAYMENT_REFUND_INSUFFICIENT_BALANCE` | 409 | User has insufficient balance for refund clawback | Log for manual review; do not auto-clawback |
|
| `PAYMENT_REFUND_INSUFFICIENT_BALANCE` | 409 | (RESERVED) User has insufficient balance for refund clawback | Log for manual review; do not auto-clawback |
|
||||||
|
|
||||||
## Global
|
## Global
|
||||||
|
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ Returns the authenticated user's points ledger in reverse chronological order.
|
|||||||
|
|
||||||
### GET /api/v1/points/packages
|
### GET /api/v1/points/packages
|
||||||
|
|
||||||
Returns available purchase packages for the current user's region, including starter pack eligibility.
|
Returns available purchase packages for the current user, including starter pack eligibility.
|
||||||
|
|
||||||
**Request:**
|
**Request:**
|
||||||
- Auth: Required (JWT)
|
- Auth: Required (JWT)
|
||||||
@@ -345,3 +345,7 @@ product_mappings:
|
|||||||
sort_order: 10
|
sort_order: 10
|
||||||
enabled: true
|
enabled: true
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Compatibility Note:**
|
||||||
|
- Previous protocol version documented `region` and `currency` fields that were never implemented. These have been removed from the specification.
|
||||||
|
- Strategy: `backward-compatible` — clients that expect these fields should handle their absence gracefully.
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Apple IAP Protocol (Frontend <-> Backend)
|
||||||
|
|
||||||
|
This document defines the Apple In-App Purchase verification and grant contract for Eryao Flutter app.
|
||||||
|
|
||||||
|
Protocol verification status:
|
||||||
|
|
||||||
|
- Backend route source: `backend/src/v1/payments/router.py`
|
||||||
|
- Backend service source: `backend/src/v1/payments/service.py`
|
||||||
|
- Backend verifier source: `backend/src/v1/payments/apple_verifier.py`
|
||||||
|
- Backend schema source: `backend/src/v1/payments/schemas.py`
|
||||||
|
- Current status: aligned
|
||||||
|
|
||||||
|
## Compatibility strategy
|
||||||
|
|
||||||
|
- Current strategy: additive evolution (`backward-compatible`).
|
||||||
|
- Breaking change requires explicit migration + rollback notes (`requires-migration`).
|
||||||
|
|
||||||
|
## Route overview
|
||||||
|
|
||||||
|
- Verify transaction: `POST /api/v1/payments/apple/transactions/verify`
|
||||||
|
- Apple server notification: `POST /api/v1/payments/apple/notifications` (server-to-server)
|
||||||
|
|
||||||
|
## Verify transaction
|
||||||
|
|
||||||
|
### `POST /api/v1/payments/apple/transactions/verify`
|
||||||
|
|
||||||
|
Verify and grant credits for an Apple IAP transaction.
|
||||||
|
|
||||||
|
**Authorization**: Requires authenticated session. User identity from JWT `sub`.
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"productCode": "new_user_pack",
|
||||||
|
"appStoreProductId": "com.meeyao.qianwen.new_user_pack",
|
||||||
|
"transactionId": "2000000123456789",
|
||||||
|
"signedTransactionInfo": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQeF...",
|
||||||
|
"appAccountToken": "uuid-or-null"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fields:**
|
||||||
|
- `productCode` (required): string, max 32 chars, product code from packages API
|
||||||
|
- `appStoreProductId` (required): string, max 128 chars, Apple product ID
|
||||||
|
- `transactionId` (required): string, max 64 chars, Apple transaction ID
|
||||||
|
- `signedTransactionInfo` (required): string, JWS signed transaction info from Apple
|
||||||
|
- `appAccountToken` (optional): UUID, app account token for user association
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "granted",
|
||||||
|
"productCode": "new_user_pack",
|
||||||
|
"transactionId": "2000000123456789",
|
||||||
|
"creditsAdded": 60,
|
||||||
|
"newBalance": 160,
|
||||||
|
"ledgerEventId": "payment.apple_iap:2000000123456789"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status values:**
|
||||||
|
- `granted`: Transaction verified and credits added
|
||||||
|
- `already_granted`: Transaction already processed for this user
|
||||||
|
|
||||||
|
**Error codes:**
|
||||||
|
|
||||||
|
| code | status | meaning |
|
||||||
|
|---|---:|---|
|
||||||
|
| `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_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_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_STARTER_PACK_INELIGIBLE` | 409 | Current email identity has already purchased starter pack |
|
||||||
|
|
||||||
|
## Apple server notification
|
||||||
|
|
||||||
|
### `POST /api/v1/payments/apple/notifications`
|
||||||
|
|
||||||
|
Server-to-server notification from Apple for refund/revoke events.
|
||||||
|
|
||||||
|
**Authorization**: None (Apple server origin).
|
||||||
|
|
||||||
|
**Request:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"signedPayload": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsIng1YyI6WyJNSUlFTURDQ0E3YWdBd0lCQWdJUWFQeF..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200):** Empty success response.
|
||||||
|
|
||||||
|
**Behavior:**
|
||||||
|
- Parses notification type and transaction info
|
||||||
|
- For `REFUND`, `REVOKE`, `DID_FAIL_TO_RENEW` notifications, processes refund clawback
|
||||||
|
- Refund reduces user balance by original purchase amount (or remaining balance if insufficient)
|
||||||
|
|
||||||
|
## Product mapping
|
||||||
|
|
||||||
|
Products are configured in `backend/src/core/config/static/packages/mapping.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
product_mappings:
|
||||||
|
new_user_pack:
|
||||||
|
app_store_product_id: com.meeyao.qianwen.new_user_pack
|
||||||
|
credits: 60
|
||||||
|
type: starter
|
||||||
|
sort_order: 0
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Starter pack eligibility
|
||||||
|
|
||||||
|
- Starter pack (`type: starter`) can only be purchased once per email identity
|
||||||
|
- Backend tracks purchase via `register_bonus_claims.has_purchased_starter_pack`
|
||||||
|
- If already purchased, returns `PAYMENT_STARTER_PACK_INELIGIBLE` (409)
|
||||||
|
|
||||||
|
## Ledger integration
|
||||||
|
|
||||||
|
- Successful purchases create a ledger entry with:
|
||||||
|
- `change_type: purchase`
|
||||||
|
- `biz_type: payment`
|
||||||
|
- `biz_id: apple_iap_transactions.id`
|
||||||
|
- `metadata.ext` containing Apple IAP details
|
||||||
|
|
||||||
|
- Refunds create a ledger entry with:
|
||||||
|
- `change_type: refund`
|
||||||
|
- `biz_type: payment`
|
||||||
|
- `metadata.ext.original_event_id` referencing original purchase event
|
||||||
|
|
||||||
|
## Error contract linkage
|
||||||
|
|
||||||
|
- All errors use RFC7807 with extension `code` and optional `params`.
|
||||||
|
- Error code registry source: `docs/protocols/common/http-error-codes.md`.
|
||||||
Reference in New Issue
Block a user