# πŸ— АрхитСктура Donation Service ## ΠžΠ±Π·ΠΎΡ€ Donation Service - это полноцСнная систСма ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ с ΠΈΠ½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠ΅ΠΉ Π½Π΅ΡΠΊΠΎΠ»ΡŒΠΊΠΈΡ… ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹Ρ… ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ² (Stripe, YooKassa), ΠΎΠ±Π΅ΡΠΏΠ΅Ρ‡ΠΈΠ²Π°ΡŽΡ‰Π°Ρ Π±Π΅Π·ΠΎΠΏΠ°ΡΠ½ΡƒΡŽ ΠΈ Π½Π°Π΄Π΅ΠΆΠ½ΡƒΡŽ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΡƒ Π΄ΠΎΠ½Π°Ρ‚ΠΎΠ² ΠΎΡ‚ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ Ρ‚Π΅Π»Π΅Π³Ρ€Π°ΠΌ-Π±ΠΎΡ‚Π°. ## 🎯 ΠžΡΠ½ΠΎΠ²Π½Ρ‹Π΅ трСбования - **Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹Ρ… систСм**: Stripe ΠΈ YooKassa - **ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ**: Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅, валидация, ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΉ - **Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ**: Π—Π°Ρ‰ΠΈΡ‚Π° ΠΎΡ‚ Ρ„Ρ€ΠΎΠ΄Π°, валидация webhook подписСй - **ΠΠ°Π΄Π΅ΠΆΠ½ΠΎΡΡ‚ΡŒ**: ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок, retry Π»ΠΎΠ³ΠΈΠΊΠ°, Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΈ - **УвСдомлСния**: ΠžΠΏΠΎΠ²Π΅Ρ‰Π΅Π½ΠΈΡ ΠΏΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»Π΅ΠΉ ΠΎ статусС ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ - **ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³**: ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ ΠΈ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹Ρ… ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ ## πŸ› АрхитСктурная Π΄ΠΈΠ°Π³Ρ€Π°ΠΌΠΌΠ° ```mermaid graph TB subgraph "ИспользованиС" User[ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ] Bot[Π’Π΅Π»Π΅Π³Ρ€Π°ΠΌ Π‘ΠΎΡ‚] Webhook[Webhook Handler] end subgraph "БСрвисы" DS[DonationService] PP[PaymentProvider
Interface] NS[NotificationService] end subgraph "ΠŸΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Ρ‹" SP[StripePaymentProvider] YP[YooKassaPaymentProvider] end subgraph "Π Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠΈ" PR[PaymentRepository] UR[UserRepository] end subgraph "Π’Π½Π΅ΡˆΠ½ΠΈΠ΅ систСмы" Stripe[(Stripe API)] YooKassa[(YooKassa API)] end User --> Bot Bot --> DS DS --> PP ### Π”ΠΈΠ°Π³Ρ€Π°ΠΌΠΌΡ‹ ΠΏΠΎΡΠ»Π΅Π΄ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ #### ΠŸΡ€ΠΎΡ†Π΅ΡΡ создания ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° ```mermaid sequenceDiagram participant U as ΠŸΠΎΠ»ΡŒΠ·ΠΎΠ²Π°Ρ‚Π΅Π»ΡŒ participant B as Π’Π΅Π»Π΅Π³Ρ€Π°ΠΌ Π‘ΠΎΡ‚ participant DS as DonationService participant PP as PaymentProvider participant PR as PaymentRepository participant Ext as Π’Π½Π΅ΡˆΠ½ΠΈΠΉ API U->>B: Команда /donate 100 B->>DS: create_donation(user_id=123, amount=100, provider='stripe') DS->>DS: Валидация Π΄Π°Π½Π½Ρ‹Ρ… DS->>PP: create_payment(amount=100, currency='RUB', user_id=123, metadata={}) PP->>Ext: API Π²Ρ‹Π·ΠΎΠ² create payment Ext-->>PP: PaymentIntent {id: 'pi_xxx', url: 'https://...'} PP-->>DS: PaymentIntent DS->>PR: create_payment({user_id: 123, amount: 100, external_id: 'pi_xxx', status: 'pending'}) PR-->>DS: Payment record {id: 456} DS-->>B: DonationResponse {payment_url: 'https://...', payment_id: 456} B-->>U: Инлайн ΠΊΠ»Π°Π²ΠΈΠ°Ρ‚ΡƒΡ€Π° с ссылкой Π½Π° ΠΎΠΏΠ»Π°Ρ‚Ρƒ ``` #### ΠŸΡ€ΠΎΡ†Π΅ΡΡ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ webhook ```mermaid sequenceDiagram participant Ext as ΠŸΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹ΠΉ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ participant WH as WebhookHandler participant DS as DonationService participant PP as PaymentProvider participant PR as PaymentRepository participant US as UserService participant NS as NotificationService Ext->>WH: POST /webhook/stripe (webhook data + signature) WH->>DS: process_payment_webhook('stripe', webhook_data, signature) DS->>PP: validate_webhook(webhook_data, signature) PP-->>DS: True DS->>PP: process_webhook(webhook_data) PP-->>DS: PaymentEvent {type: 'payment.succeeded', payment_id: 'pi_xxx', amount: 100} DS->>PR: update_payment_status('pi_xxx', 'succeeded') PR-->>DS: True DS->>US: add_donation(user_id=123, amount=100) US-->>DS: True DS->>NS: notify_payment_success(user_id=123, amount=100) NS-->>DS: True DS-->>WH: True WH-->>Ext: HTTP 200 ``` #### ΠŸΡ€ΠΎΡ†Π΅ΡΡ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ошибок ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° ```mermaid sequenceDiagram participant Ext as ΠŸΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹ΠΉ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ participant WH as WebhookHandler participant DS as DonationService participant PP as PaymentProvider participant EH as ErrorHandler participant NS as NotificationService Ext->>WH: POST /webhook/stripe (failed payment webhook) WH->>DS: process_payment_webhook('stripe', webhook_data, signature) DS->>PP: validate_webhook(webhook_data, signature) PP-->>DS: True DS->>PP: process_webhook(webhook_data) PP-->>DS: PaymentEvent {type: 'payment.failed', payment_id: 'pi_xxx', error: 'card_declined'} DS->>PR: update_payment_status('pi_xxx', 'failed') PR-->>DS: True DS->>EH: handle_payment_error(error, 'pi_xxx') EH->>EH: Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ошибки EH->>EH: Анализ Ρ‚ΠΈΠΏΠ° ошибки alt ВрСмСнная ошибка EH->>DS: schedule_retry('pi_xxx') else ΠŸΠΎΡΡ‚ΠΎΡΠ½Π½Π°Ρ ошибка EH->>NS: notify_payment_failed(user_id=123, reason='card_declined') EH->>PR: mark_payment_final('pi_xxx') end DS-->>WH: True WH-->>Ext: HTTP 200 ``` ## πŸ”§ Π”Π΅Ρ‚Π°Π»ΡŒΠ½Π°Ρ Π°Ρ€Ρ…ΠΈΡ‚Π΅ΠΊΡ‚ΡƒΡ€Π° PP --> SP PP --> YP SP --> Stripe YP --> YooKassa Webhook --> DS DS --> PR DS --> UR DS --> NS ``` ## πŸ”§ ΠšΠΎΠΌΠΏΠΎΠ½Π΅Π½Ρ‚Ρ‹ систСмы ### 1. PaymentProvider (Абстрактный интСрфСйс) ```python class PaymentProvider(ABC): """Абстрактный интСрфСйс для ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹Ρ… ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ²""" @abstractmethod def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict) -> PaymentIntent: """Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" pass @abstractmethod def confirm_payment(self, payment_id: str) -> bool: """ΠŸΠΎΠ΄Ρ‚Π²Π΅Ρ€ΠΆΠ΄Π΅Π½ΠΈΠ΅ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" pass @abstractmethod def cancel_payment(self, payment_id: str) -> bool: """ΠžΡ‚ΠΌΠ΅Π½Π° ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" pass @abstractmethod def get_payment_status(self, payment_id: str) -> PaymentStatus: """ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ статуса ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" pass @abstractmethod def validate_webhook(self, request_data: Dict, signature: str) -> bool: """Валидация webhook подписи""" pass @abstractmethod def process_webhook(self, webhook_data: Dict) -> PaymentEvent: """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° webhook события""" pass ``` ### 2. DonationService (Основной сСрвис) ```python class DonationService: """Основной сСрвис для ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ Π΄ΠΎΠ½Π°Ρ‚ΠΎΠ²""" def __init__(self, payment_repo: PaymentRepository, user_service: UserService, notification_service: NotificationService): self.payment_repo = payment_repo self.user_service = user_service self.notification_service = notification_service self.providers = {} # Π‘Π»ΠΎΠ²Π°Ρ€ΡŒ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ² def create_donation(self, user_id: int, amount: float, provider_name: str = 'stripe') -> DonationResponse: """Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ Π΄ΠΎΠ½Π°Ρ‚Π°""" # Валидация Π²Ρ…ΠΎΠ΄Π½Ρ‹Ρ… Π΄Π°Π½Π½Ρ‹Ρ… self._validate_donation_data(user_id, amount, provider_name) # ΠŸΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π° provider = self._get_provider(provider_name) # Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° Ρ‡Π΅Ρ€Π΅Π· ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π° payment_intent = provider.create_payment(amount, 'RUB', user_id, {}) # Π‘ΠΎΡ…Ρ€Π°Π½Π΅Π½ΠΈΠ΅ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° Π² Π‘Π” payment_record = self.payment_repo.create_payment({ 'user_id': user_id, 'amount': amount, 'currency': 'RUB', 'provider': provider_name, 'external_id': payment_intent.id, 'status': 'pending' }) return DonationResponse(payment_url=payment_intent.url, payment_id=payment_record.id) def process_payment_webhook(self, provider_name: str, webhook_data: Dict, signature: str) -> bool: """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° webhook ΠΎΡ‚ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ½ΠΎΠ³ΠΎ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π°""" provider = self._get_provider(provider_name) # Валидация подписи if not provider.validate_webhook(webhook_data, signature): raise PaymentSecurityError("Invalid webhook signature") # ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° события payment_event = provider.process_webhook(webhook_data) # ОбновлСниС статуса ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° self._handle_payment_event(payment_event) return True def _handle_payment_event(self, event: PaymentEvent): """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° события ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" if event.type == 'payment.succeeded': self._process_successful_payment(event.payment_id, event.amount) elif event.type == 'payment.failed': self._process_failed_payment(event.payment_id) elif event.type == 'payment.cancelled': self._process_cancelled_payment(event.payment_id) ``` ### 3. Π Π΅Π°Π»ΠΈΠ·Π°Ρ†ΠΈΠΈ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ² #### StripePaymentProvider ```python class StripePaymentProvider(PaymentProvider): """Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с Stripe""" def __init__(self, api_key: str, webhook_secret: str): self.stripe.api_key = api_key self.webhook_secret = webhook_secret def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict) -> PaymentIntent: """Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° Π² Stripe""" try: intent = stripe.PaymentIntent.create( amount=int(amount * 100), # Stripe Ρ€Π°Π±ΠΎΡ‚Π°Π΅Ρ‚ с ΠΊΠΎΠΏΠ΅ΠΉΠΊΠ°ΠΌΠΈ currency=currency.lower(), metadata={'user_id': str(user_id), **metadata}, payment_method_types=['card'], ) return PaymentIntent(id=intent.id, url=intent.client_secret) except stripe.error.StripeError as e: raise PaymentProviderError(f"Stripe error: {e}") def validate_webhook(self, request_data: Dict, signature: str) -> bool: """Валидация Stripe webhook подписи""" try: stripe.Webhook.construct_event(request_data, signature, self.webhook_secret) return True except ValueError: return False def process_webhook(self, webhook_data: Dict) -> PaymentEvent: """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° Stripe webhook""" event = webhook_data payment_intent = event.data.object return PaymentEvent( type=f"payment.{event.type.split('.')[1]}", payment_id=payment_intent.id, amount=payment_intent.amount / 100, currency=payment_intent.currency, metadata=payment_intent.metadata ) ``` #### YooKassaPaymentProvider ```python class YooKassaPaymentProvider(PaymentProvider): """Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΡ с YooKassa""" def __init__(self, shop_id: str, secret_key: str): self.client = Client(shop_id=shop_id, secret_key=secret_key) def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict) -> PaymentIntent: """Π‘ΠΎΠ·Π΄Π°Π½ΠΈΠ΅ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° Π² YooKassa""" payment = Payment.create({ "amount": { "value": f"{amount:.2f}", "currency": currency }, "metadata": {'user_id': str(user_id), **metadata}, "confirmation": { "type": "redirect", "return_url": "https://t.me/bot" # URL Π²ΠΎΠ·Π²Ρ€Π°Ρ‚Π° }, "capture": True }, self.client.id) return PaymentIntent(id=payment.id, url=payment.confirmation.confirmation_url) def validate_webhook(self, request_data: Dict, signature: str) -> bool: """Валидация YooKassa webhook""" # РСализация Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ подписи для YooKassa pass def process_webhook(self, webhook_data: Dict) -> PaymentEvent: """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° YooKassa webhook""" event = webhook_data payment = event.object return PaymentEvent( type=f"payment.{payment.status}", payment_id=payment.id, amount=float(payment.amount.value), currency=payment.amount.currency, metadata=payment.metadata or {} ) ``` ## πŸ“Š МодСли Π΄Π°Π½Π½Ρ‹Ρ… ### Payment (ΠŸΠ»Π°Ρ‚Π΅ΠΆ) ```python @dataclass class Payment: """МодСль ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" id: int user_id: int amount: float currency: str provider: str # 'stripe' ΠΈΠ»ΠΈ 'yookassa' external_id: str # ID ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ° Ρƒ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π° status: str # pending, succeeded, failed, cancelled created_at: datetime updated_at: datetime processed_at: Optional[datetime] = None metadata: Optional[Dict] = None ``` ### Transaction (Вранзакция) ```python @dataclass class Transaction: """МодСль Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΈ""" id: int payment_id: int type: str # payment, refund amount: float status: str external_transaction_id: Optional[str] = None created_at: datetime details: Optional[Dict] = None ``` ## πŸ” БистСма бСзопасности ### Валидация webhook подписСй ```python class PaymentSecurity: """Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ½Ρ‹Ρ… ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ""" @staticmethod def validate_stripe_signature(payload: bytes, signature: str, secret: str) -> bool: """Валидация подписи Stripe""" try: stripe.Webhook.construct_event(payload, signature, secret) return True except ValueError: return False @staticmethod def validate_yookassa_signature(request_body: str, signature: str, secret: str) -> bool: """Валидация подписи YooKassa""" # РСализация Π°Π»Π³ΠΎΡ€ΠΈΡ‚ΠΌΠ° HMAC-SHA256 pass @staticmethod def prevent_double_processing(payment_id: str, idempotency_key: str) -> bool: """ΠŸΡ€Π΅Π΄ΠΎΡ‚Π²Ρ€Π°Ρ‰Π΅Π½ΠΈΠ΅ Π΄Π²ΠΎΠΉΠ½ΠΎΠΉ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ""" # ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° Π² Redis/cache pass ``` ## πŸ“‹ Π‘Ρ…Π΅ΠΌΠ° Π±Π°Π·Ρ‹ Π΄Π°Π½Π½Ρ‹Ρ… ```sql -- Π’Π°Π±Π»ΠΈΡ†Π° ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ CREATE TABLE payments ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, amount REAL NOT NULL, currency TEXT DEFAULT 'RUB', provider TEXT NOT NULL, -- 'stripe' ΠΈΠ»ΠΈ 'yookassa' external_id TEXT UNIQUE NOT NULL, status TEXT DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, processed_at DATETIME, metadata TEXT, -- JSON FOREIGN KEY (user_id) REFERENCES users(id) ); -- Π’Π°Π±Π»ΠΈΡ†Π° Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΉ CREATE TABLE transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, payment_id INTEGER NOT NULL, type TEXT NOT NULL, -- 'payment', 'refund' amount REAL NOT NULL, status TEXT NOT NULL, external_transaction_id TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, details TEXT, -- JSON FOREIGN KEY (payment_id) REFERENCES payments(id) ); -- Π˜Π½Π΄Π΅ΠΊΡΡ‹ для ΠΏΡ€ΠΎΠΈΠ·Π²ΠΎΠ΄ΠΈΡ‚Π΅Π»ΡŒΠ½ΠΎΡΡ‚ΠΈ CREATE INDEX idx_payments_user_id ON payments(user_id); CREATE INDEX idx_payments_external_id ON payments(external_id); CREATE INDEX idx_payments_status ON payments(status); CREATE INDEX idx_transactions_payment_id ON transactions(payment_id); ``` ## πŸ”„ ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок ### Π˜Π΅Ρ€Π°Ρ€Ρ…ΠΈΡ ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ ``` PaymentError (Π±Π°Π·ΠΎΠ²ΠΎΠ΅) β”œβ”€β”€ PaymentProviderError # Ошибки ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π° β”œβ”€β”€ PaymentValidationError # Ошибки Π²Π°Π»ΠΈΠ΄Π°Ρ†ΠΈΠΈ β”œβ”€β”€ PaymentSecurityError # Ошибки бСзопасности β”œβ”€β”€ PaymentTimeoutError # Π’Π°ΠΉΠΌΠ°ΡƒΡ‚Ρ‹ └── PaymentDuplicateError # Π”ΡƒΠ±Π»ΠΈΠΊΠ°Ρ‚Ρ‹ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ ``` ### БтратСгия ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ошибок ```python class PaymentErrorHandler: """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚Ρ‡ΠΈΠΊ ошибок ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ""" def handle_payment_error(self, error: PaymentError, payment_id: str): """ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибки ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" # Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ошибки logger.error(f"Payment error for {payment_id}: {error}") # ΠžΠΏΡ€Π΅Π΄Π΅Π»Π΅Π½ΠΈΠ΅ стратСгии retry if isinstance(error, PaymentTimeoutError): self._schedule_retry(payment_id) elif isinstance(error, PaymentSecurityError): self._notify_security_team(payment_id) else: self._mark_payment_failed(payment_id) def _schedule_retry(self, payment_id: str): """ΠŸΠ»Π°Π½ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΠΎΠ²Ρ‚ΠΎΡ€Π½ΠΎΠΉ ΠΏΠΎΠΏΡ‹Ρ‚ΠΊΠΈ""" # Π”ΠΎΠ±Π°Π²Π»Π΅Π½ΠΈΠ΅ Π² ΠΎΡ‡Π΅Ρ€Π΅Π΄ΡŒ retry pass def _notify_security_team(self, payment_id: str): """Π£Π²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΊΠΎΠΌΠ°Π½Π΄Ρ‹ бСзопасности""" pass ``` ## πŸ“‘ БистСма ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ ### NotificationService ```python class NotificationService: """БСрвис ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ""" async def notify_payment_success(self, user_id: int, amount: float): """Π£Π²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΎΠ± ΡƒΡΠΏΠ΅ΡˆΠ½ΠΎΠΌ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅""" message = f"βœ… Бпасибо Π·Π° Π΄ΠΎΠ½Π°Ρ‚ {amount} β‚½! Π’Π°ΡˆΠΈ срСдства зачислСны." await self.bot.send_message(chat_id=user_id, text=message) async def notify_payment_failed(self, user_id: int, reason: str): """Π£Π²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ ΠΎ Π½Π΅ΡƒΠ΄Π°Ρ‡Π½ΠΎΠΌ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅""" message = f"❌ К соТалСнию, ΠΏΠ»Π°Ρ‚Π΅ΠΆ Π½Π΅ ΠΏΡ€ΠΎΡˆΠ΅Π». ΠŸΡ€ΠΈΡ‡ΠΈΠ½Π°: {reason}" await self.bot.send_message(chat_id=user_id, text=message) async def notify_admin_payment_issue(self, payment_id: str, error: str): """Π£Π²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠ΅ Π°Π΄ΠΌΠΈΠ½ΠΎΠ² ΠΎ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ΅ с ΠΏΠ»Π°Ρ‚Π΅ΠΆΠΎΠΌ""" # ΠžΡ‚ΠΏΡ€Π°Π²ΠΊΠ° Π² Π°Π΄ΠΌΠΈΠ½-Ρ‡Π°Ρ‚ pass ``` ## βš™οΈ ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ ### ΠŸΡ€ΠΈΠΌΠ΅Ρ€ ΠΊΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΠΈ ```python PAYMENT_CONFIG = { 'providers': { 'stripe': { 'enabled': True, 'api_key': os.getenv('STRIPE_API_KEY'), 'webhook_secret': os.getenv('STRIPE_WEBHOOK_SECRET'), 'currency': 'RUB' }, 'yookassa': { 'enabled': True, 'shop_id': os.getenv('YOOKASSA_SHOP_ID'), 'secret_key': os.getenv('YOOKASSA_SECRET_KEY'), 'currency': 'RUB' } }, 'validation': { 'min_amount': 10.0, 'max_amount': 10000.0, 'allowed_currencies': ['RUB', 'USD', 'EUR'] }, 'retry': { 'max_attempts': 3, 'delay_seconds': 60 }, 'webhook': { 'timeout': 30, 'max_retries': 5 } } ``` ## πŸ“ˆ ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ ΠΈ ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ ### ΠšΠ»ΡŽΡ‡Π΅Π²Ρ‹Π΅ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ - **ОбъСм ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ**: ΠžΠ±Ρ‰Π°Ρ сумма, количСство Ρ‚Ρ€Π°Π½Π·Π°ΠΊΡ†ΠΈΠΉ - **ΠšΠΎΠ½Π²Π΅Ρ€ΡΠΈΡ**: Доля ΡƒΡΠΏΠ΅ΡˆΠ½Ρ‹Ρ… ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ - **ВрСмя ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ**: Π‘Ρ€Π΅Π΄Π½Π΅Π΅ врСмя ΠΎΡ‚ создания Π΄ΠΎ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½ΠΈΡ - **Ошибки**: ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΠΈ Ρ‚ΠΈΠΏΡ‹ ошибок ΠΏΠΎ ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π°ΠΌ - **Π‘Π΅Π·ΠΎΠΏΠ°ΡΠ½ΠΎΡΡ‚ΡŒ**: ΠŸΠΎΠΏΡ‹Ρ‚ΠΊΠΈ Ρ„Ρ€ΠΎΠ΄Π°, Π²Π°Π»ΠΈΠ΄Π½Ρ‹Π΅/Π½Π΅Π²Π°Π»ΠΈΠ΄Π½Ρ‹Π΅ webhook ### Π˜Π½ΡΡ‚Ρ€ΡƒΠΌΠ΅Π½Ρ‚Ρ‹ ΠΌΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³Π° ```python class PaymentMetrics: """ΠœΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ""" def record_payment_created(self, amount: float, provider: str): """Π—Π°ΠΏΠΈΡΡŒ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ создания ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" # Prometheus counter pass def record_payment_completed(self, amount: float, duration: float): """Π—Π°ΠΏΠΈΡΡŒ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ Π·Π°Π²Π΅Ρ€ΡˆΠ΅Π½Π½ΠΎΠ³ΠΎ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" # Prometheus histogram pass def record_payment_failed(self, error_type: str): """Π—Π°ΠΏΠΈΡΡŒ ΠΌΠ΅Ρ‚Ρ€ΠΈΠΊΠΈ Π½Π΅ΡƒΠ΄Π°Ρ‡Π½ΠΎΠ³ΠΎ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°""" # Prometheus counter pass ``` ## πŸ§ͺ ВСстированиС ### Unit тСсты - **PaymentProvider**: ΠœΠΎΠΊΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ API Π²Ρ‹Π·ΠΎΠ²ΠΎΠ² - **DonationService**: ВСстированиС бизнСс-Π»ΠΎΠ³ΠΈΠΊΠΈ - **Валидация**: ВСстированиС всСх сцСнариСв - **ΠžΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠ° ошибок**: ВСстированиС ΠΈΡΠΊΠ»ΡŽΡ‡Π΅Π½ΠΈΠΉ ### Integration тСсты - **ΠŸΠΎΠ»Π½Ρ‹ΠΉ Ρ†ΠΈΠΊΠ» ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°**: ΠžΡ‚ создания Π΄ΠΎ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ webhook - **ВзаимодСйствиС с Π‘Π”**: ВСстированиС Ρ€Π΅ΠΏΠΎΠ·ΠΈΡ‚ΠΎΡ€ΠΈΠ΅Π² - **Π’Π½Π΅ΡˆΠ½ΠΈΠ΅ API**: ВСстированиС с тСстовыми Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚Π°ΠΌΠΈ ### Test doubles ```python class MockPaymentProvider(PaymentProvider): """Мок ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€Π° для тСстирования""" def __init__(self, responses: Dict = None): self.responses = responses or {} self.calls = [] def create_payment(self, amount: float, currency: str, user_id: int, metadata: Dict): self.calls.append(('create_payment', amount, currency, user_id, metadata)) return self.responses.get('create_payment', PaymentIntent('mock_id', 'mock_url')) ``` ## πŸš€ Π Π°Π·Π²Π΅Ρ€Ρ‚Ρ‹Π²Π°Π½ΠΈΠ΅ ΠΈ эксплуатация ### ΠŸΡ€ΠΎΡ†Π΅ΡΡ развСртывания 1. **Настройка ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ²**: РСгистрация Π°ΠΊΠΊΠ°ΡƒΠ½Ρ‚ΠΎΠ², ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ API ΠΊΠ»ΡŽΡ‡Π΅ΠΉ 2. **ΠšΠΎΠ½Ρ„ΠΈΠ³ΡƒΡ€Π°Ρ†ΠΈΡ webhook**: Настройка URL для получСния ΡƒΠ²Π΅Π΄ΠΎΠΌΠ»Π΅Π½ΠΈΠΉ 3. **ΠœΠΈΠ³Ρ€Π°Ρ†ΠΈΠΈ Π‘Π”**: ΠŸΡ€ΠΈΠΌΠ΅Π½Π΅Π½ΠΈΠ΅ схСмы ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ 4. **ВСстированиС**: Π˜Π½Ρ‚Π΅Π³Ρ€Π°Ρ†ΠΈΠΎΠ½Π½ΠΎΠ΅ тСстированиС с тСстовыми ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°ΠΌΠΈ 5. **ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³**: Настройка Π°Π»Π΅Ρ€Ρ‚ΠΎΠ² ΠΈ Π΄Π°ΡˆΠ±ΠΎΡ€Π΄ΠΎΠ² ### ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€ΠΈΠ½Π³ Π² эксплуатации - **Health checks**: ΠŸΡ€ΠΎΠ²Π΅Ρ€ΠΊΠ° доступности API ΠΏΡ€ΠΎΠ²Π°ΠΉΠ΄Π΅Ρ€ΠΎΠ² - **Alerting**: УвСдомлСния ΠΏΡ€ΠΈ ΠΏΡ€ΠΎΠ±Π»Π΅ΠΌΠ°Ρ… с ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ°ΠΌΠΈ - **Π›ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅**: Π”Π΅Ρ‚Π°Π»ΡŒΠ½ΠΎΠ΅ Π»ΠΎΠ³ΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ всСх ΠΎΠΏΠ΅Ρ€Π°Ρ†ΠΈΠΉ - **Backup**: Π Π΅Π·Π΅Ρ€Π²Π½ΠΎΠ΅ ΠΊΠΎΠΏΠΈΡ€ΠΎΠ²Π°Π½ΠΈΠ΅ Π΄Π°Π½Π½Ρ‹Ρ… ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ --- *АрхитСктура Ρ€Π°Π·Ρ€Π°Π±ΠΎΡ‚Π°Π½Π° для обСспСчСния Π½Π°Π΄Π΅ΠΆΠ½ΠΎΠΉ, бСзопасной ΠΈ ΠΌΠ°ΡΡˆΡ‚Π°Π±ΠΈΡ€ΡƒΠ΅ΠΌΠΎΠΉ ΠΎΠ±Ρ€Π°Π±ΠΎΡ‚ΠΊΠΈ ΠΏΠ»Π°Ρ‚Π΅ΠΆΠ΅ΠΉ Π² Ρ‚Π΅Π»Π΅Π³Ρ€Π°ΠΌ-Π±ΠΎΡ‚Π΅.*