From 86062d5e78bab26e13d774a73f93ab773204fd3c Mon Sep 17 00:00:00 2001 From: ZL-Q Date: Tue, 28 Apr 2026 17:31:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20packages=20?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E8=AE=BF=E9=97=AE=E4=B8=8D=E5=AD=98=E5=9C=A8?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=E5=AF=BC=E8=87=B4=E7=9A=84=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 router 中对 result.region、result.currency、pkg.price 的访问 - 修正 pkg.type.value 为 pkg.type (type 是 Literal 不是 Enum) - 更新协议文档以反映实际实现 - 新增 Apple IAP 协议文档 - 标记未使用的错误码为 RESERVED --- backend/src/v1/points/router.py | 5 +- docs/protocols/auth/session-auth-protocol.md | 8 + docs/protocols/common/http-error-codes.md | 6 +- .../common/user-points-chat-data-protocol.md | 6 +- docs/protocols/payments/apple-iap-protocol.md | 138 ++++++++++++++++++ 5 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 docs/protocols/payments/apple-iap-protocol.md diff --git a/backend/src/v1/points/router.py b/backend/src/v1/points/router.py index 6925ea7..63ce42a 100644 --- a/backend/src/v1/points/router.py +++ b/backend/src/v1/points/router.py @@ -63,14 +63,11 @@ async def get_available_packages( ) return PackagesResponse( - region=result.region, - currency=result.currency, packages=[ PackageInfo( productCode=pkg.product_code, appStoreProductId=pkg.app_store_product_id, - type=pkg.type.value, - price=pkg.price, + type=pkg.type, credits=pkg.credits, isStarter=pkg.is_starter, starterEligible=pkg.starter_eligible, diff --git a/docs/protocols/auth/session-auth-protocol.md b/docs/protocols/auth/session-auth-protocol.md index 4e7bce7..8b575b7 100644 --- a/docs/protocols/auth/session-auth-protocol.md +++ b/docs/protocols/auth/session-auth-protocol.md @@ -73,10 +73,18 @@ Request: { "email": "user@example.com", "token": "123456" } ``` +Optional fields: + +```json +{ "email": "user@example.com", "token": "123456", "language": "zh-CN", "timezone": "Asia/Shanghai" } +``` + Validation (backend): - `email` must match `SUPABASE_EMAIL_PATTERN` - `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: diff --git a/docs/protocols/common/http-error-codes.md b/docs/protocols/common/http-error-codes.md index 3121e02..1a8bae7 100644 --- a/docs/protocols/common/http-error-codes.md +++ b/docs/protocols/common/http-error-codes.md @@ -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_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_APPLE_UNAVAILABLE` | 503 | 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_REFUND_INSUFFICIENT_BALANCE` | 409 | User has insufficient balance for refund clawback | Log for manual review; do not auto-clawback | +| `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 | (RESERVED) Verification succeeded but grant transaction failed | Show retry-later message; retain transaction for compensation | +| `PAYMENT_REFUND_INSUFFICIENT_BALANCE` | 409 | (RESERVED) User has insufficient balance for refund clawback | Log for manual review; do not auto-clawback | ## Global diff --git a/docs/protocols/common/user-points-chat-data-protocol.md b/docs/protocols/common/user-points-chat-data-protocol.md index 0a66f6c..ea5bf8b 100644 --- a/docs/protocols/common/user-points-chat-data-protocol.md +++ b/docs/protocols/common/user-points-chat-data-protocol.md @@ -277,7 +277,7 @@ Returns the authenticated user's points ledger in reverse chronological order. ### 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:** - Auth: Required (JWT) @@ -345,3 +345,7 @@ product_mappings: sort_order: 10 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. diff --git a/docs/protocols/payments/apple-iap-protocol.md b/docs/protocols/payments/apple-iap-protocol.md new file mode 100644 index 0000000..ee488d2 --- /dev/null +++ b/docs/protocols/payments/apple-iap-protocol.md @@ -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`.