13 lines
6.8 KiB
JSON
13 lines
6.8 KiB
JSON
|
|
{"role": "system", "content": "You are implementing iOS Apple Pay integration. Follow the task.json requirements. Reference existing code patterns from v1/points/ and features/points/."}
|
||
|
|
{"role": "user", "content": "Start implementing the iOS Apple Pay integration feature."}
|
||
|
|
{"role": "assistant", "content": "I'll implement this feature in phases:\n\n**Phase 1: Backend - Apple Receipt Validation**\n1. Create `v1/payments/` module with Apple IAP endpoints\n2. Implement receipt validation with Apple's servers\n3. Integrate with existing PointsService for crediting\n4. Add purchase tracking\n\n**Phase 2: iOS - StoreKit2 Integration**\n1. Create `features/payments/` module in Flutter\n2. Implement StoreKit2 product fetching\n3. Implement purchase flow\n4. Connect to backend validation endpoint\n\n**Phase 3: Error Handling & Testing**\n1. Add error codes to http-error-codes.md\n2. Handle edge cases (network, cancelled, pending)\n3. Add integration tests\n\nLet me start with Phase 1."}
|
||
|
|
{"role": "assistant", "action": "create_file", "path": "backend/src/v1/payments/__init__.py", "content": "from v1.payments.router import router as payments_router\n\n__all__ = [\"payments_router\"]"}
|
||
|
|
{"role": "assistant", "action": "create_file", "path": "backend/src/v1/payments/schemas.py", "content": "from __future__ import annotations\n\nfrom pydantic import BaseModel, ConfigDict, Field\n\n\nclass ApplePurchaseRequest(BaseModel):\n model_config = ConfigDict(extra=\"forbid\")\n\n product_id: str = Field(min_length=1, max_length=100)\n transaction_id: str = Field(min_length=1, max_length=200)\n receipt_data: str = Field(min_length=1)\n\n\nclass ApplePurchaseResponse(BaseModel):\n success: bool\n credits_added: int\n new_balance: int\n transaction_id: str"}
|
||
|
|
{"role": "assistant", "action": "create_file", "path": "backend/src/v1/payments/service.py", "content": "from __future__ import annotations\n\nimport hashlib\nfrom dataclasses import dataclass\nfrom decimal import Decimal\nfrom typing import TYPE_CHECKING\nfrom uuid import UUID\n\nfrom core.config.packages import get_packages_config_for_region
|
||
|
|
from core.http.errors import ApiProblemError, problem_payload
|
||
|
|
from schemas.enums import PointsChangeType, PointsBizType
|
||
|
|
from schemas.domain.points import ApplyPointsChangeCommand, PurchaseLedgerMetadata
|
||
|
|
from v1.points.repository import PointsRepository
|
||
|
|
|
||
|
|
if TYPE_CHECKING:\n pass\n\n\n@dataclass(frozen=True)\nclass AppleReceiptValidationResult:\n valid: bool\n product_id: str\n transaction_id: str\n purchase_date_ms: int\n quantity: int = 1\n\n\nclass ApplePaymentService:\n def __init__(self, repository: PointsRepository) -> None:\n self._repository = repository\n\n async def validate_and_process_purchase(\n self,\n *,\n user_id: UUID,\n user_email: str,\n product_id: str,\n transaction_id: str,\n receipt_data: str,\n country: str = \"US\",\n ) -> ApplePurchaseResponse:\n \"\"\"Validate Apple receipt and credit points.\"\"\"\n # Step 1: Validate receipt with Apple\n validation = await self._validate_receipt_with_apple(\n receipt_data=receipt_data,\n product_id=product_id,\n transaction_id=transaction_id,\n )\n \n if not validation.valid:\n raise ApiProblemError(\n status_code=400,\n detail=problem_payload(\n code=\"PAYMENT_RECEIPT_INVALID\",\n detail=\"Apple receipt validation failed\",\n ),\n )\n\n # Step 2: Check for duplicate transaction\n event_id = f\"apple.iap:{validation.transaction_id}\"\n if await self._repository.has_ledger_event(\n user_id=user_id, event_id=event_id\n ):\n raise ApiProblemError(\n status_code=409,\n detail=problem_payload(\n code=\"PAYMENT_ALREADY_PROCESSED\",\n detail=\"Transaction already processed\",\n ),\n )\n\n # Step 3: Get credits for product\n pkg_config = get_packages_config_for_region(country)\n credits = self._get_credits_for_product(\n packages=pkg_config.packages,\n product_id=product_id,\n )\n \n if credits <= 0:\n raise ApiProblemError(\n status_code=400,\n detail=problem_payload(\n code=\"PAYMENT_PRODUCT_NOT_FOUND\",\n detail=\"Product not found in catalog\",\n ),\n )\n\n # Step 4: Credit points\n account = await self._repository.get_or_create_user_points_for_update(\n user_id=user_id\n )\n account.balance = int(account.balance) + credits\n account.lifetime_earned = int(account.lifetime_earned) + credits\n account.version = int(account.version) + 1\n\n # Step 5: Record ledger\n metadata = PurchaseLedgerMetadata(\n platform=\"apple\",\n product_id=product_id,\n transaction_id=validation.transaction_id,\n )\n command = ApplyPointsChangeCommand(\n user_id=user_id,\n change_type=PointsChangeType.PURCHASE,\n biz_type=PointsBizType.PAYMENT,\n event_id=event_id,\n amount=credits,\n direction=1,\n operator_id=None,\n metadata=metadata,\n )\n await self._repository.append_ledger(\n command=command,\n balance_after=int(account.balance),\n )\n\n return ApplePurchaseResponse(\n success=True,\n credits_added=credits,\n new_balance=int(account.balance),\n transaction_id=validation.transaction_id,\n )\n\n async def _validate_receipt_with_apple(\n self,\n *,\n receipt_data: str,\n product_id: str,\n transaction_id: str,\n ) -> AppleReceiptValidationResult:\n \"\"\"Validate receipt with Apple's servers.\"\"\"\n # TODO: Implement actual Apple server verification\n # For now, return mock validation for development\n # Production: Call https://buy.itunes.apple.com/verifyReceipt\n # Sandbox: Call https://sandbox.itunes.apple.com/verifyReceipt\n return AppleReceiptValidationResult(\n vali
|