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:

**Phase 1: Backend - Apple Receipt Validation**
1. Create `v1/payments/` module with Apple IAP endpoints
2. Implement receipt validation with Apple's servers
3. Integrate with existing PointsService for crediting
4. Add purchase tracking

**Phase 2: iOS - StoreKit2 Integration**
1. Create `features/payments/` module in Flutter
2. Implement StoreKit2 product fetching
3. Implement purchase flow
4. Connect to backend validation endpoint

**Phase 3: Error Handling & Testing**
1. Add error codes to http-error-codes.md
2. Handle edge cases (network, cancelled, pending)
3. Add integration tests

Let me start with Phase 1.


content:
from v1.payments.router import router as payments_router

__all__ = ["payments_router"]


content:
from __future__ import annotations

from pydantic import BaseModel, ConfigDict, Field

class ApplePurchaseRequest(BaseModel):
    model_config = ConfigDict(extra="forbid")

    product_id: str = Field(min_length=1, max_length=100)
    transaction_id: str = Field(min_length=1, max_length=200)
    receipt_data: str = Field(min_length=1)

class ApplePurchaseResponse(BaseModel):
    success: bool
    credits_added: int
    new_balance: int
    transaction_id: str


content:
from __future__ import annotations

import hashlib
from dataclasses import dataclass
from decimal import Decimal
from typing import TYPE_CHECKING
from uuid import UUID

from 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:
    pass

@dataclass(frozen=True)
class AppleReceiptValidationResult:
    valid: bool
    product_id: str
    transaction_id: str
    purchase_date_ms: int
    quantity: int = 1

class ApplePaymentService:
    def __init__(self, repository: PointsRepository) -> None:
        self._repository = repository

    async def validate_and_process_purchase(
        self,
        *,
        user_id: UUID,
        user_email: str,
        product_id: str,
        transaction_id: str,
        receipt_data: str,
        country: str = "US",
    ) -> ApplePurchaseResponse:
        """Validate Apple receipt and credit points."""
        # Step 1: Validate receipt with Apple
        validation = await self._validate_receipt_with_apple(
            receipt_data=receipt_data,
            product_id=product_id,
            transaction_id=transaction_id,
        )

        if not validation.valid:
            raise ApiProblemError(
                status_code=400,
                detail=problem_payload(
                    code="PAYMENT_RECEIPT_INVALID",
                    detail="Apple receipt validation failed",
                ),
            )

        # Step 2: Check for duplicate transaction
        event_id = f"apple.iap:{validation.transaction_id}"
        if await self._repository.has_ledger_event(
            user_id=user_id, event_id=event_id
        ):
            raise ApiProblemError(
                status_code=409,
                detail=problem_payload(
                    code="PAYMENT_ALREADY_PROCESSED",
                    detail="Transaction already processed",
                ),
            )

        # Step 3: Get credits for product
        pkg_config = get_packages_config_for_region(country)
        credits = self._get_credits_for_product(
            packages=pkg_config.packages,
            product_id=product_id,
        )

        if credits <= 0:
            raise ApiProblemError(
                status_code=400,
                detail=problem_payload(
                    code="PAYMENT_PRODUCT_NOT_FOUND",
                    detail="Product not found in catalog",
                ),
            )

        # Step 4: Credit points
        account = await self._repository.get_or_create_user_points_for_update(
            user_id=user_id
        )
        account.balance = int(account.balance) + credits
        account.lifetime_earned = int(account.lifetime_earned) + credits
        account.version = int(account.version) + 1

        # Step 5: Record ledger
        metadata = PurchaseLedgerMetadata(
            platform="apple",
            product_id=product_id,
            transaction_id=validation.transaction_id,
        )
        command = ApplyPointsChangeCommand(
            user_id=user_id,
            change_type=PointsChangeType.PURCHASE,
            biz_type=PointsBizType.PAYMENT,
            event_id=event_id,
            amount=credits,
            direction=1,
            operator_id=None,
            metadata=metadata,
        )
        await self._repository.append_ledger(
            command=command,
            balance_after=int(account.balance),
        )

        return ApplePurchaseResponse(
            success=True,
            credits_added=credits,
            new_balance=int(account.balance),
            transaction_id=validation.transaction_id,
        )

    async def _validate_receipt_with_apple(
        self,
        *,
        receipt_data: str,
        product_id: str,
        transaction_id: str,
    ) -> AppleReceiptValidationResult:
        """Validate receipt with Apple's servers."""
        # TODO: Implement actual Apple server verification
        # For now, return mock validation for development
        # Production: Call https://buy.itunes.apple.com/verifyReceipt
        # Sandbox: Call https://sandbox.itunes.apple.com/verifyReceipt
        return AppleReceiptValidationResult(
            valid=True,
            product_id=product_id,
            transaction_id=transaction_id,
            purchase_date_ms=0,
        )

    def _get_credits_for_product(
        self,
        packages: list,
        product_id: str,
    ) -> int:
        """Get credits amount for a product ID."""
        for pkg in packages:
            if pkg.product_code == product_id and pkg.enabled:
                return pkg.credits
        return 0