Files
eryao/docs/protocols/payments/apple-iap-protocol.md
T
ZL-Q 86062d5e78 fix: 修复 packages 接口访问不存在字段导致的运行时错误
- 移除 router 中对 result.region、result.currency、pkg.price 的访问
- 修正 pkg.type.value 为 pkg.type (type 是 Literal 不是 Enum)
- 更新协议文档以反映实际实现
- 新增 Apple IAP 协议文档
- 标记未使用的错误码为 RESERVED
2026-04-28 17:31:24 +08:00

139 lines
4.6 KiB
Markdown

# 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`.