diff --git a/apps/payment/backend/business/stripe_manager.py b/apps/payment/backend/business/stripe_manager.py index 8f7c892..4503370 100644 --- a/apps/payment/backend/business/stripe_manager.py +++ b/apps/payment/backend/business/stripe_manager.py @@ -26,6 +26,14 @@ class StripeManager: async def create_account_link(self, account_id: str, link_type: str = "account_onboarding") -> Optional[str]: account = stripe.Account.retrieve(account_id) # For account_update, try to show dashboard if TOS is accepted + + self.module_logger.log_info("create_account_link urls", + { + "redirect_url": "{}/work".format(self.site_url_root), + "refresh_url": "{}/front-door".format(self.site_url_root), + "return_url": "{}/work".format(self.site_url_root) + } + ) if link_type == "account_update" and account.tos_acceptance.date: login_link = stripe.Account.create_login_link( account_id, @@ -345,10 +353,14 @@ class StripeManager: async def invoke_checkout_session_webhook( self, event: dict ) -> Tuple[bool, Optional[str], Optional[str]]: - # Handle the checkout.session.completed event + """ + Handle checkout.session.completed webhook events from Stripe. + Updates transaction status and saves payment method information for future use. + """ if event["type"] == "checkout.session.completed": session = event["data"]["object"] + # Find and validate the transaction transaction = await self.__fetch_transaction_by_session_id(session["id"]) if not transaction: await self.module_logger.log_error( @@ -357,145 +369,195 @@ class StripeManager: ) return False, None, None - # Update transaction status - transaction.status = TransactionStatus.COMPLETED - transaction.updated_time = datetime.now(timezone.utc) - await transaction.save() + # Update transaction status to completed + await self.__update_transaction_status(transaction) - # Save payment method information - payment_method_saved = False - try: - # Get the Stripe session to extract payment method details - stripe_session = stripe.checkout.Session.retrieve(session["id"]) - payment_intent_id = stripe_session.get('payment_intent') + # Process and save payment method information + await self.__process_payment_method(session, transaction) - if payment_intent_id: - payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) - payment_method_id = payment_intent.get('payment_method') - - if payment_method_id: - payment_method = stripe.PaymentMethod.retrieve(payment_method_id) - card_details = payment_method.get('card', {}) - - # Get user email (use a fallback since we don't have access to user profile) - user_email = f"user_{transaction.from_user}@freeleaps.com" - - # Get or create customer for the user - customer_id = None - try: - # Search for existing customers by email - customers = stripe.Customer.list(email=user_email, limit=1) - if customers.data: - customer_id = customers.data[0].id - else: - # Create new customer - customer = stripe.Customer.create( - email=user_email, - metadata={"user_id": transaction.from_user} - ) - customer_id = customer.id - except Exception as customer_error: - # Use a fallback customer ID or skip payment method saving - customer_id = None - - if customer_id: - try: - # Check if payment method is already attached to a customer - payment_method_obj = stripe.PaymentMethod.retrieve(payment_method_id) - if payment_method_obj.customer: - # Use the existing customer ID - customer_id = payment_method_obj.customer - else: - # Try to attach payment method to customer in Stripe - stripe.PaymentMethod.attach( - payment_method_id, - customer=customer_id - ) - - # Check if payment method already exists in our database - from backend.infra.payment.models import StripePaymentMethodDoc - existing_payment_method = await StripePaymentMethodDoc.find_one( - StripePaymentMethodDoc.stripe_payment_method_id == payment_method_id - ) - - if existing_payment_method: - payment_method_saved = True - else: - # Save to our database only if it doesn't exist - payment_method_doc = StripePaymentMethodDoc( - user_id=transaction.from_user, - stripe_customer_id=customer_id, - stripe_payment_method_id=payment_method_id, - card_last4=card_details.get('last4'), - card_brand=card_details.get('brand'), - card_exp_month=card_details.get('exp_month'), - card_exp_year=card_details.get('exp_year'), - created_time=datetime.now(timezone.utc), - updated_time=datetime.now(timezone.utc), - ) - await payment_method_doc.save() - payment_method_saved = True - except stripe.error.InvalidRequestError as attach_error: - if "already attached" in str(attach_error).lower(): - # Check if payment method already exists in our database - from backend.infra.payment.models import StripePaymentMethodDoc - existing_payment_method = await StripePaymentMethodDoc.find_one( - StripePaymentMethodDoc.stripe_payment_method_id == payment_method_id - ) - - if existing_payment_method: - payment_method_saved = True - else: - # Still save to our database since it's already attached - payment_method_doc = StripePaymentMethodDoc( - user_id=transaction.from_user, - stripe_customer_id=customer_id, - stripe_payment_method_id=payment_method_id, - card_last4=card_details.get('last4'), - card_brand=card_details.get('brand'), - card_exp_month=card_details.get('exp_month'), - card_exp_year=card_details.get('exp_year'), - created_time=datetime.now(timezone.utc), - updated_time=datetime.now(timezone.utc), - ) - await payment_method_doc.save() - payment_method_saved = True - elif "may not be used again" in str(attach_error).lower(): - # Check if payment method already exists in our database - from backend.infra.payment.models import StripePaymentMethodDoc - existing_payment_method = await StripePaymentMethodDoc.find_one( - StripePaymentMethodDoc.stripe_payment_method_id == payment_method_id - ) - - if existing_payment_method: - payment_method_saved = True - else: - # Save to our database even though it can't be attached - payment_method_doc = StripePaymentMethodDoc( - user_id=transaction.from_user, - stripe_customer_id=customer_id, - stripe_payment_method_id=payment_method_id, - card_last4=card_details.get('last4'), - card_brand=card_details.get('brand'), - card_exp_month=card_details.get('exp_month'), - card_exp_year=card_details.get('exp_year'), - created_time=datetime.now(timezone.utc), - updated_time=datetime.now(timezone.utc), - ) - await payment_method_doc.save() - payment_method_saved = True - except Exception as save_error: - await self.module_logger.log_error( - error=f"Error saving payment method to database: {save_error}", - properties={"payment_method_id": payment_method_id, "user_id": transaction.from_user} - ) - - except Exception as payment_method_error: - await self.module_logger.log_error( - error=f"Error processing payment method: {payment_method_error}", - properties={"session_id": session["id"], "user_id": transaction.from_user} - ) - # Don't fail the webhook if payment method saving fails, but log it return True, transaction.project_id, transaction.milestone_index return False, None, None + + async def __update_transaction_status(self, transaction: StripeTransactionDoc) -> None: + """ + Update transaction status to completed and save to database. + """ + transaction.status = TransactionStatus.COMPLETED + transaction.updated_time = datetime.now(timezone.utc) + await transaction.save() + + async def __process_payment_method(self, session: dict, transaction: StripeTransactionDoc) -> None: + """ + Extract payment method details from Stripe session and save to database. + Creates or finds customer and attaches payment method for future use. + """ + try: + # Get payment method details from Stripe + payment_method_info = await self.__extract_payment_method_info(session) + if not payment_method_info: + return + + payment_method_id, card_details = payment_method_info + + # Get or create Stripe customer for the user + customer_id = await self.__get_or_create_customer(transaction.from_user) + if not customer_id: + return + + # Attach payment method to customer and save to database + await self.__attach_and_save_payment_method( + payment_method_id, card_details, customer_id, transaction.from_user + ) + + except Exception as payment_method_error: + await self.module_logger.log_error( + error=f"Error processing payment method: {payment_method_error}", + properties={"session_id": session["id"], "user_id": transaction.from_user} + ) + + async def __extract_payment_method_info(self, session: dict) -> Optional[Tuple[str, dict]]: + """ + Extract payment method ID and card details from Stripe session. + Returns tuple of (payment_method_id, card_details) or None if not found. + """ + try: + # Get the Stripe session to extract payment method details + stripe_session = stripe.checkout.Session.retrieve(session["id"]) + payment_intent_id = stripe_session.get('payment_intent') + + if not payment_intent_id: + return None + + payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) + payment_method_id = payment_intent.get('payment_method') + + if not payment_method_id: + return None + + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) + card_details = payment_method.get('card', {}) + + return payment_method_id, card_details + + except Exception as e: + await self.module_logger.log_error( + error=f"Error extracting payment method info: {e}", + properties={"session_id": session["id"]} + ) + return None + + async def __get_or_create_customer(self, user_id: str) -> Optional[str]: + """ + Find existing Stripe customer by email or create new one. + Returns customer ID or None if creation fails. + """ + try: + # Generate email for user (fallback since we don't have access to user profile) + user_email = f"user_{user_id}@freeleaps.com" + + # Search for existing customers by email + customers = stripe.Customer.list(email=user_email, limit=1) + if customers.data: + return customers.data[0].id + + # Create new customer if not found + customer = stripe.Customer.create( + email=user_email, + metadata={"user_id": user_id} + ) + return customer.id + + except Exception as customer_error: + await self.module_logger.log_error( + error=f"Error getting/creating customer: {customer_error}", + properties={"user_id": user_id} + ) + return None + + async def __attach_and_save_payment_method( + self, payment_method_id: str, card_details: dict, customer_id: str, user_id: str + ) -> None: + """ + Attach payment method to Stripe customer and save details to database. + Handles various error scenarios gracefully. + """ + try: + # Check if payment method is already attached to a customer + payment_method_obj = stripe.PaymentMethod.retrieve(payment_method_id) + if payment_method_obj.customer: + # Use the existing customer ID + customer_id = payment_method_obj.customer + else: + # Try to attach payment method to customer in Stripe + stripe.PaymentMethod.attach( + payment_method_id, + customer=customer_id + ) + + # Save to database + await self.__save_payment_method_to_db( + payment_method_id, card_details, customer_id, user_id + ) + + except stripe.error.InvalidRequestError as attach_error: + # Handle specific Stripe attachment errors + await self.__handle_attachment_error( + attach_error, payment_method_id, card_details, customer_id, user_id + ) + except Exception as save_error: + await self.module_logger.log_error( + error=f"Error attaching payment method: {save_error}", + properties={"payment_method_id": payment_method_id, "user_id": user_id} + ) + + async def __save_payment_method_to_db( + self, payment_method_id: str, card_details: dict, customer_id: str, user_id: str + ) -> None: + """ + Save payment method details to database if it doesn't already exist. + """ + from backend.infra.payment.models import StripePaymentMethodDoc + + # Check if payment method already exists in our database + existing_payment_method = await StripePaymentMethodDoc.find_one( + StripePaymentMethodDoc.stripe_payment_method_id == payment_method_id + ) + + if existing_payment_method: + return # Already saved + + # Save to our database + payment_method_doc = StripePaymentMethodDoc( + user_id=user_id, + stripe_customer_id=customer_id, + stripe_payment_method_id=payment_method_id, + card_last4=card_details.get('last4'), + card_brand=card_details.get('brand'), + card_exp_month=card_details.get('exp_month'), + card_exp_year=card_details.get('exp_year'), + created_time=datetime.now(timezone.utc), + updated_time=datetime.now(timezone.utc), + ) + await payment_method_doc.save() + + async def __handle_attachment_error( + self, attach_error: stripe.error.InvalidRequestError, + payment_method_id: str, card_details: dict, customer_id: str, user_id: str + ) -> None: + """ + Handle specific Stripe attachment errors and still save to database when possible. + """ + error_message = str(attach_error).lower() + + if "already attached" in error_message or "may not be used again" in error_message: + # Payment method can't be attached but we can still save to database + await self.__save_payment_method_to_db( + payment_method_id, card_details, customer_id, user_id + ) + else: + # Log other attachment errors + await self.module_logger.log_error( + error=f"Error attaching payment method: {attach_error}", + properties={"payment_method_id": payment_method_id, "user_id": user_id} + ) diff --git a/apps/payment/common/log/base_logger.py b/apps/payment/common/log/base_logger.py index d87356b..2470ebe 100644 --- a/apps/payment/common/log/base_logger.py +++ b/apps/payment/common/log/base_logger.py @@ -47,17 +47,8 @@ class LoggerBase: filter=lambda record: record["extra"].get("topic") == self.__logger_name, ) - try: - host_name = socket.gethostname() - host_ip = socket.gethostbyname(host_name) - except socket.gaierror: - # Fallback if hostname resolution fails - host_name = "localhost" - host_ip = "127.0.0.1" - except Exception: - # Generic fallback - host_name = "localhost" - host_ip = "127.0.0.1" + host_name = socket.gethostname() + host_ip = socket.gethostbyname(host_name) self.logger = guru_logger.bind( topic=self.__logger_name, host_ip=host_ip,