From 7d493a3cc14f65aabe8ad01c98c5148027025e93 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Fri, 25 Apr 2025 02:29:23 +0000 Subject: [PATCH 1/5] chore(release): bump version to 1.2.0 and upload released assets [ci skip] --- CHANGELOG.md | 7 +++++++ VERSION | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e21a1e1..3856712 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.2.0](https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-service-hub/compare/v1.1.1...v1.2.0) (2025-04-25) + + +### Features + +* **log:** ensure parent directory exists before opening log file ([bb90b26](https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-service-hub/commit/bb90b2688344d028fb6d10323e362946be53612e)) + ## [1.1.1](https://dev.azure.com/freeleaps/freeleaps-service-hub/_git/freeleaps-service-hub/compare/v1.1.0...v1.1.1) (2025-03-17) diff --git a/VERSION b/VERSION index 524cb55..26aaba0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.1 +1.2.0 From 40e0fafc2cdd5360e61f7507e7f9cc4cda4e31dd Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 30 Apr 2025 02:32:26 +0000 Subject: [PATCH 2/5] chore(release): bump version to 1.2.1 and upload released assets [ci skip] --- CHANGELOG.md | 7 +++++++ VERSION | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3856712..0a88825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.2.1](https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-service-hub/compare/v1.2.0...v1.2.1) (2025-04-30) + + +### Bug Fixes + +* **cleaner:** update document cleaner job ([dd96819](https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-service-hub/commit/dd96819709b4e0eef46eefbd5004ff25f5cdd8cd)) + # [1.2.0](https://gitea.freeleaps.mathmast.com/freeleaps/freeleaps-service-hub/compare/v1.1.1...v1.2.0) (2025-04-25) diff --git a/VERSION b/VERSION index 26aaba0..6085e94 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.0 +1.2.1 From ccc995f599eb4002253502552a1c6091bf4e6284 Mon Sep 17 00:00:00 2001 From: sunhaolou Date: Mon, 21 Jul 2025 12:50:51 +0800 Subject: [PATCH 3/5] refractor: a current working version before cleaning up. --- .env | 24 ++ apps/payment/.env | 6 +- .../backend/application/payment_hub.py | 3 - .../backend/business/payment_manager.py | 22 +- .../backend/business/stripe_manager.py | 310 +++++++++++++++--- .../backend/infra/payment/constants.py | 1 - apps/payment/backend/infra/payment/models.py | 15 + apps/payment/backend/models/__init__.py | 4 +- apps/payment/backend/models/payment/models.py | 5 +- .../backend/services/payment/constants.py | 1 - .../backend/services/payment/models.py | 1 - apps/payment/common/log/base_logger.py | 15 +- .../payment/payment_manager_controller.py | 13 - .../payment/stripe_manager_controller.py | 24 +- 14 files changed, 360 insertions(+), 84 deletions(-) create mode 100644 .env diff --git a/.env b/.env new file mode 100644 index 0000000..1834048 --- /dev/null +++ b/.env @@ -0,0 +1,24 @@ +APP_NAME=payment +export SERVICE_API_ACCESS_HOST=0.0.0.0 +export SERVICE_API_ACCESS_PORT=8006 +export CONTAINER_APP_ROOT=/app +export LOG_BASE_PATH=$CONTAINER_APP_ROOT/log/$APP_NAME +export BACKEND_LOG_FILE_NAME=$APP_NAME +export APPLICATION_ACTIVITY_LOG=$APP_NAME-activity +export MONGODB_NAME=freeleaps2 +export MONGODB_PORT=27017 +GIT_REPO_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub +CODEBASE_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/apps/payment +SITE_DEPLOY_FOLDER=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/sites/payment/deploy +#!/bin/bash +export VENV_DIR=venv_t +export VENV_ACTIVATE=venv_t/bin/activate +export DOCKER_HOME=/var/lib/docker +export DOCKER_APP_HOME=$DOCKER_HOME/app +export DOCKER_BACKEND_HOME=$DOCKER_APP_HOME/$APP_NAME +export DOCKER_BACKEND_LOG_HOME=$DOCKER_BACKEND_HOME/log +export MONGODB_URI=mongodb://localhost:27017/ +export FREELEAPS_ENV=local +export SITE_URL_ROOT=http://localhost:5173/ +export LOG_BASE_PATH=${CODEBASE_ROOT}/log +export STRIPE_API_KEY=sk_test_51Ogsw5B0IyqaSJBrwczlr820jnmvA1qQQGoLZ2XxOsIzikpmXo4pRLjw4XVMTEBR8DdVTYySiAv1XX53Zv5xqynF00GfMqttFd diff --git a/apps/payment/.env b/apps/payment/.env index 6b56dca..1834048 100644 --- a/apps/payment/.env +++ b/apps/payment/.env @@ -7,9 +7,9 @@ export BACKEND_LOG_FILE_NAME=$APP_NAME export APPLICATION_ACTIVITY_LOG=$APP_NAME-activity export MONGODB_NAME=freeleaps2 export MONGODB_PORT=27017 -GIT_REPO_ROOT=/mnt/freeleaps/freeleaps-service-hub -CODEBASE_ROOT=/mnt/freeleaps/freeleaps-service-hub/apps/payment -SITE_DEPLOY_FOLDER=/mnt/freeleaps/freeleaps-service-hub/sites/payment/deploy +GIT_REPO_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub +CODEBASE_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/apps/payment +SITE_DEPLOY_FOLDER=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/sites/payment/deploy #!/bin/bash export VENV_DIR=venv_t export VENV_ACTIVATE=venv_t/bin/activate diff --git a/apps/payment/backend/application/payment_hub.py b/apps/payment/backend/application/payment_hub.py index 07cce96..8f18ba0 100644 --- a/apps/payment/backend/application/payment_hub.py +++ b/apps/payment/backend/application/payment_hub.py @@ -9,9 +9,6 @@ class PaymentHub: self.stripe_manager = StripeManager() return - async def fetch_wechat_qr_code(self, project_id: str) -> Optional[Dict[str, any]]: - return await self.payment_manager.fetch_wechat_qr_code(project_id) - async def fetch_stripe_account_id(self, user_id: str) -> Optional[str]: return await self.payment_manager.fetch_stripe_account_id(user_id) diff --git a/apps/payment/backend/business/payment_manager.py b/apps/payment/backend/business/payment_manager.py index 6a5d6d3..bb2b955 100644 --- a/apps/payment/backend/business/payment_manager.py +++ b/apps/payment/backend/business/payment_manager.py @@ -9,18 +9,6 @@ class PaymentManager: def __init__(self) -> None: self.module_logger = ModuleLogger(sender_id=PaymentManager) - async def fetch_wechat_qr_code(self, project_id: str) -> Optional[Dict[str, any]]: - project = await ProjectDoc.get(project_id) - proposer = project.proposer_id - income_profile = await IncomeProfileDoc.find_one( - IncomeProfileDoc.user_id == proposer - ) - if income_profile: - return income_profile.bank_account.money_collecting_methods[ - 0 - ].wechat_qr_code - return None - async def fetch_stripe_account_id(self, user_id: str) -> Optional[str]: income_profile = await IncomeProfileDoc.find_one(IncomeProfileDoc.user_id == user_id) if income_profile: @@ -44,7 +32,7 @@ class PaymentManager: } }} ) - + if not payment_profile: await self.module_logger.log_warning( warning="No payment profile found for Stripe account", @@ -54,7 +42,7 @@ class PaymentManager: } ) return False - + # Update the stripe method status updated = False # Need to check if money_collecting_methods exists and is not empty @@ -66,7 +54,7 @@ class PaymentManager: method.last_update_time = int(datetime.now().timestamp()) updated = True break # Exit loop once found and updated - + if updated: await payment_profile.save() await self.module_logger.log_info( @@ -79,7 +67,7 @@ class PaymentManager: } ) return True - + # Log warning with more context await self.module_logger.log_warning( warning="Stripe account not found in payment methods", @@ -91,7 +79,7 @@ class PaymentManager: } ) return False - + except Exception as e: await self.module_logger.log_exception( exception=e, diff --git a/apps/payment/backend/business/stripe_manager.py b/apps/payment/backend/business/stripe_manager.py index 656f884..92b28e4 100644 --- a/apps/payment/backend/business/stripe_manager.py +++ b/apps/payment/backend/business/stripe_manager.py @@ -8,6 +8,7 @@ from stripe.error import SignatureVerificationError from common.log.module_logger import ModuleLogger from decimal import Decimal import json +import httpx stripe.api_key = app_settings.STRIPE_API_KEY @@ -15,7 +16,7 @@ stripe.api_key = app_settings.STRIPE_API_KEY class StripeManager: def __init__(self) -> None: - self.site_url_root = app_settings.SITE_URL_ROOT.rstrip("/") + self.site_url_root = "http://localhost:8888" self.module_logger = ModuleLogger(sender_id="StripeManager") async def create_stripe_account(self) -> Optional[str]: @@ -25,26 +26,18 @@ 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, - redirect_url="{}/work".format(self.site_url_root) + redirect_url="http://localhost:8888/work" ) return login_link.url - + # Otherwise show onboarding account_link = stripe.AccountLink.create( account=account_id, - refresh_url="{}/front-door".format(self.site_url_root), - return_url="{}/work".format(self.site_url_root), + refresh_url="http://localhost:8888/front-door", + return_url="http://localhost:8888/work", type="account_onboarding", ) return account_link.url @@ -78,10 +71,20 @@ class StripeManager: async def __fetch_transaction_by_session_id( self, session_id: str ) -> Optional[StripeTransactionDoc]: + await self.module_logger.log_info( + f"Looking up transaction for session_id: {session_id}", + properties={"session_id": session_id} + ) + transactions = await StripeTransactionDoc.find( StripeTransactionDoc.stripe_checkout_session_id == session_id ).to_list() + await self.module_logger.log_info( + f"Found {len(transactions)} transactions for session_id: {session_id}", + properties={"session_id": session_id, "transaction_count": len(transactions)} + ) + if len(transactions) > 1: await self.module_logger.log_error( error="More than one transaction found for session_id: {}".format( @@ -90,9 +93,24 @@ class StripeManager: properties={"session_id": session_id}, ) elif len(transactions) == 0: + await self.module_logger.log_error( + error="No transaction found for session_id: {}".format(session_id), + properties={"session_id": session_id}, + ) return None - return transactions[0] + transaction = transactions[0] + await self.module_logger.log_info( + f"Found transaction: project_id={transaction.project_id}, milestone_index={transaction.milestone_index}, status={transaction.status}", + properties={ + "session_id": session_id, + "project_id": transaction.project_id, + "milestone_index": transaction.milestone_index, + "status": transaction.status + } + ) + + return transaction async def fetch_transaction_by_session_id( self, session_id: str @@ -203,19 +221,25 @@ class StripeManager: transaction.stripe_price_id = price.id await transaction.save() - payment_link = stripe.PaymentLink.create( - line_items=[ + # Prepare payment link parameters with conditional application_fee_amount + payment_link_params = { + "line_items": [ { "price": transaction.stripe_price_id, "quantity": 1, } ], - application_fee_amount=transaction.application_fee_amount, - on_behalf_of=transaction.to_stripe_account_id, - transfer_data={ + "on_behalf_of": transaction.to_stripe_account_id, + "transfer_data": { "destination": transaction.to_stripe_account_id, }, - ) + } + + # Only add application_fee_amount if it's greater than 0 + if transaction.application_fee_amount and transaction.application_fee_amount > 0: + payment_link_params["application_fee_amount"] = transaction.application_fee_amount + + payment_link = stripe.PaymentLink.create(**payment_link_params) if payment_link: transaction.stripe_payment_link = payment_link.url @@ -276,27 +300,37 @@ class StripeManager: transaction.stripe_price_id = price.id await transaction.save() - session = stripe.checkout.Session.create( - payment_method_types=["card"], - line_items=[ + # Prepare payment_intent_data with conditional application_fee_amount + payment_intent_data = { + "on_behalf_of": transaction.to_stripe_account_id, + "transfer_data": { + "destination": transaction.to_stripe_account_id, + }, + } + + # Only add application_fee_amount if it's greater than 0 + if transaction.application_fee_amount and transaction.application_fee_amount > 0: + payment_intent_data["application_fee_amount"] = transaction.application_fee_amount + + + + session_params = { + "payment_method_types": ["card"], + "line_items": [ { "price": transaction.stripe_price_id, "quantity": 1, } ], - payment_intent_data={ - "on_behalf_of": transaction.to_stripe_account_id, - "application_fee_amount": transaction.application_fee_amount, - "transfer_data": { - "destination": transaction.to_stripe_account_id, - }, - }, - mode="payment", - success_url="{}/projects".format( - self.site_url_root - ), # needs to be set, local: http://localhost/ - cancel_url="{}/projects".format(self.site_url_root), - ) + "payment_intent_data": payment_intent_data, + "mode": "payment", + "success_url": "http://localhost:8888/projects", + "cancel_url": "http://localhost:8888/projects", + } + + + + session = stripe.checkout.Session.create(**session_params) if session: transaction.stripe_checkout_session_id = session.id @@ -335,18 +369,220 @@ class StripeManager: # Handle the checkout.session.completed event if event["type"] == "checkout.session.completed": session = event["data"]["object"] + await self.module_logger.log_info( + f"Processing checkout.session.completed webhook for session_id: {session['id']}", + properties={"session_id": session["id"]} + ) + transaction = await self.__fetch_transaction_by_session_id(session["id"]) if not transaction: await self.module_logger.log_error( error="Transaction not found for session_id: {}".format(session["id"]), properties={"session_id": session["id"]}, ) - return False + return False, None, None + # Update transaction status transaction.status = TransactionStatus.COMPLETED transaction.updated_time = datetime.now(timezone.utc) await transaction.save() + await self.module_logger.log_info( + f"Successfully updated transaction status to COMPLETED for project_id: {transaction.project_id}, milestone_index: {transaction.milestone_index}", + properties={ + "project_id": transaction.project_id, + "milestone_index": transaction.milestone_index, + "session_id": session["id"] + } + ) + + # Save payment method information + payment_method_saved = False + try: + print("=" * 50) + print("STARTING PAYMENT METHOD PROCESSING") + print("=" * 50) + print(f"Starting payment method processing for session {session['id']}") + + # Get the Stripe session to extract payment method details + try: + stripe_session = stripe.checkout.Session.retrieve(session["id"]) + print(f"Successfully retrieved Stripe session: {session['id']}") + except Exception as session_error: + print(f"Failed to retrieve Stripe session {session['id']}: {session_error}") + raise session_error + + payment_intent_id = stripe_session.get('payment_intent') + print(f"Payment intent ID from session: {payment_intent_id}") + + if payment_intent_id: + try: + payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) + print(f"Successfully retrieved payment intent: {payment_intent_id}") + except Exception as pi_error: + print(f"Failed to retrieve payment intent {payment_intent_id}: {pi_error}") + raise pi_error + + payment_method_id = payment_intent.get('payment_method') + print(f"Payment method ID from payment intent: {payment_method_id}") + + if payment_method_id: + try: + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) + print(f"Successfully retrieved payment method: {payment_method_id}") + except Exception as pm_error: + print(f"Failed to retrieve payment method {payment_method_id}: {pm_error}") + raise pm_error + + card_details = payment_method.get('card', {}) + print(f"Card details: {card_details}") + + # Get user email (use a fallback since we don't have access to user profile) + user_email = f"user_{transaction.from_user}@freeleaps.com" + print(f"User email for customer creation: {user_email}") + + # Get or create customer for the user + # Try to find existing customer first + 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 + print(f"Found existing customer: {customer_id}") + else: + # Create new customer + customer = stripe.Customer.create( + email=user_email, + metadata={"user_id": transaction.from_user} + ) + customer_id = customer.id + print(f"Created new customer: {customer_id}") + except Exception as customer_error: + print(f"Error creating/finding customer: {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: + print(f"Payment method {payment_method_id} already attached to customer {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 + ) + print(f"Successfully attached payment method {payment_method_id} to 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: + print(f"Payment method {payment_method_id} already exists in database, skipping save") + 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 + print(f"Successfully saved payment method {payment_method_id} for user {transaction.from_user}") + except stripe.error.InvalidRequestError as attach_error: + if "already attached" in str(attach_error).lower(): + print(f"Payment method {payment_method_id} already attached to 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: + print(f"Payment method {payment_method_id} already exists in database, skipping save") + 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 + print(f"Successfully saved payment method {payment_method_id} for user {transaction.from_user}") + elif "may not be used again" in str(attach_error).lower(): + print(f"Payment method {payment_method_id} was already used and cannot be attached to customer") + print(f"This is normal for one-time payment methods. Saving card details to database anyway.") + # 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: + print(f"Payment method {payment_method_id} already exists in database, skipping save") + 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 + print(f"Successfully saved payment method {payment_method_id} for user {transaction.from_user} (one-time use)") + else: + print(f"Error attaching payment method: {attach_error}") + except Exception as save_error: + print(f"Error saving payment method to database: {save_error}") + else: + print(f"Could not create customer for user {transaction.from_user}, skipping payment method save") + else: + print(f"No payment method found in payment intent {payment_intent_id}") + else: + print(f"No payment intent found in session {session['id']}") + + except Exception as payment_method_error: + print(f"Error processing payment method: {payment_method_error}") + import traceback + print(f"Full traceback for payment method error:") + print(traceback.format_exc()) + # Don't fail the webhook if payment method saving fails, but log it + + print(f"Payment method saved: {payment_method_saved}") return True, transaction.project_id, transaction.milestone_index + await self.module_logger.log_info( + f"Received non-checkout.session.completed webhook event: {event['type']}", + properties={"event_type": event["type"]} + ) return False, None, None diff --git a/apps/payment/backend/infra/payment/constants.py b/apps/payment/backend/infra/payment/constants.py index 25d4254..310f8ff 100644 --- a/apps/payment/backend/infra/payment/constants.py +++ b/apps/payment/backend/infra/payment/constants.py @@ -12,7 +12,6 @@ class MoneyCollectionType(IntEnum): UNSPECIFIED = 0 MARKED_AS_PAID = 1 UPLOAD_PROOF = 2 - WECHAT_QR_CODE = 3 STRIPE_CHECKOUT = 4 diff --git a/apps/payment/backend/infra/payment/models.py b/apps/payment/backend/infra/payment/models.py index 8d556dc..e6a2df4 100644 --- a/apps/payment/backend/infra/payment/models.py +++ b/apps/payment/backend/infra/payment/models.py @@ -24,3 +24,18 @@ class StripeTransactionDoc(Document): class Settings: name = "stripe_transaction" + + +class StripePaymentMethodDoc(Document): + user_id: str + stripe_customer_id: str + stripe_payment_method_id: str + card_last4: Optional[str] = None + card_brand: Optional[str] = None + card_exp_month: Optional[int] = None + card_exp_year: Optional[int] = None + created_time: datetime + updated_time: datetime + + class Settings: + name = "stripe_payment_method" diff --git a/apps/payment/backend/models/__init__.py b/apps/payment/backend/models/__init__.py index 077c1f5..b5a31b5 100644 --- a/apps/payment/backend/models/__init__.py +++ b/apps/payment/backend/models/__init__.py @@ -5,9 +5,9 @@ # TODO: Add all models to backend_models from backend.services.payment.models import IncomeProfileDoc, PaymentProfileDoc from backend.services.project.models import ProjectDoc -from backend.infra.payment.models import StripeTransactionDoc +from backend.infra.payment.models import StripeTransactionDoc, StripePaymentMethodDoc -backend_models = [IncomeProfileDoc, PaymentProfileDoc, ProjectDoc, StripeTransactionDoc] +backend_models = [IncomeProfileDoc, PaymentProfileDoc, ProjectDoc, StripeTransactionDoc, StripePaymentMethodDoc] # backend_models.extend(code_models) # backend_models.extend(user_models) # backend_models.extend(profile_models) diff --git a/apps/payment/backend/models/payment/models.py b/apps/payment/backend/models/payment/models.py index 387c59e..5ad97e0 100644 --- a/apps/payment/backend/models/payment/models.py +++ b/apps/payment/backend/models/payment/models.py @@ -2,6 +2,7 @@ from typing import List, Dict, Optional from decimal import Decimal from beanie import Document from pydantic import BaseModel +from datetime import datetime from backend.services.payment.constants import PaymentGateway from backend.infra.payment.constants import MoneyCollectionType, PaymentLocation @@ -23,7 +24,6 @@ class MoneyCollectingMethod(BaseModel): location: Optional[PaymentLocation] priority: int = 0 # less number has high priority to be used. stripe_account_id: Optional[str] - wechat_qr_code: Optional[str] last_update_time: Optional[int] = None @@ -61,3 +61,6 @@ class PaymentProfileDoc(Document): class Settings: name = "payment_profile" + + + diff --git a/apps/payment/backend/services/payment/constants.py b/apps/payment/backend/services/payment/constants.py index 202cba6..1b5d568 100644 --- a/apps/payment/backend/services/payment/constants.py +++ b/apps/payment/backend/services/payment/constants.py @@ -3,4 +3,3 @@ from enum import IntEnum class PaymentGateway(IntEnum): STRIP = 1 - WECHAT = 2 diff --git a/apps/payment/backend/services/payment/models.py b/apps/payment/backend/services/payment/models.py index 606668e..05d79bf 100644 --- a/apps/payment/backend/services/payment/models.py +++ b/apps/payment/backend/services/payment/models.py @@ -27,7 +27,6 @@ class MoneyCollectingMethod(BaseModel): location: Optional[PaymentLocation] priority: int = 0 # less number has high priority to be used. stripe_account_id: Optional[str] - wechat_qr_code: Optional[str] last_update_time: Optional[int] = None diff --git a/apps/payment/common/log/base_logger.py b/apps/payment/common/log/base_logger.py index 24f7bb0..d87356b 100644 --- a/apps/payment/common/log/base_logger.py +++ b/apps/payment/common/log/base_logger.py @@ -30,7 +30,7 @@ class LoggerBase: guru_logger.remove() file_sink = JsonSink( - log_file_path=log_filename, + log_file_path=log_filename, rotation_size_bytes=rotation_bytes, max_backup_files=log_settings.MAX_BACKUP_FILES ) @@ -47,8 +47,17 @@ class LoggerBase: filter=lambda record: record["extra"].get("topic") == self.__logger_name, ) - host_name = socket.gethostname() - host_ip = socket.gethostbyname(host_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" self.logger = guru_logger.bind( topic=self.__logger_name, host_ip=host_ip, diff --git a/apps/payment/webapi/routes/payment/payment_manager_controller.py b/apps/payment/webapi/routes/payment/payment_manager_controller.py index e9500ce..eb21bd1 100644 --- a/apps/payment/webapi/routes/payment/payment_manager_controller.py +++ b/apps/payment/webapi/routes/payment/payment_manager_controller.py @@ -6,19 +6,6 @@ from fastapi.encoders import jsonable_encoder router = APIRouter() payment_hub = PaymentHub() -# Web API -# Fetch wechat qr code -@router.get( - "/fetch_wechat_qr_code/{project_id}", - operation_id="fetch_wechat_qr_code", - summary="Fetch wechat qr code", - description="Fetch wechat qr code", -) -async def fetch_wechat_qr_code( - project_id: str -): - return await payment_hub.fetch_wechat_qr_code(project_id) - # Web API # Fetch stripe account id @router.get( diff --git a/apps/payment/webapi/routes/payment/stripe_manager_controller.py b/apps/payment/webapi/routes/payment/stripe_manager_controller.py index ee95dea..c8c353a 100644 --- a/apps/payment/webapi/routes/payment/stripe_manager_controller.py +++ b/apps/payment/webapi/routes/payment/stripe_manager_controller.py @@ -204,8 +204,28 @@ async def handle_account_webhook( details_submitted=session["details_submitted"], payouts_enabled=session["payouts_enabled"], charges_enabled=session["charges_enabled"] - ) + ) except Exception as e: raise HTTPException(status_code=400, detail=str(e)) - + return JSONResponse(content={"status": "success"}) + + +# Web API +# Detach payment method +@router.delete( + "/detach_payment_method/{payment_method_id}", + operation_id="detach_payment_method", + summary="Detach payment method from customer", + description="Detach a payment method from a Stripe customer", +) +async def detach_payment_method(payment_method_id: str): + try: + # Detach the payment method from Stripe + stripe.PaymentMethod.detach(payment_method_id) + return JSONResponse(content={"success": True, "message": "Payment method detached successfully"}) + except Exception as e: + return JSONResponse( + status_code=400, + content={"success": False, "message": f"Failed to detach payment method: {str(e)}"} + ) From a8f0a714ae06b692638ba3d35d0fbb6e128dffcc Mon Sep 17 00:00:00 2001 From: sunhaolou Date: Mon, 21 Jul 2025 15:23:19 +0800 Subject: [PATCH 4/5] refractor: clean up the codes for online services --- .env | 24 ---- apps/payment/.env | 6 +- .../backend/business/stripe_manager.py | 123 +++--------------- 3 files changed, 21 insertions(+), 132 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index 1834048..0000000 --- a/.env +++ /dev/null @@ -1,24 +0,0 @@ -APP_NAME=payment -export SERVICE_API_ACCESS_HOST=0.0.0.0 -export SERVICE_API_ACCESS_PORT=8006 -export CONTAINER_APP_ROOT=/app -export LOG_BASE_PATH=$CONTAINER_APP_ROOT/log/$APP_NAME -export BACKEND_LOG_FILE_NAME=$APP_NAME -export APPLICATION_ACTIVITY_LOG=$APP_NAME-activity -export MONGODB_NAME=freeleaps2 -export MONGODB_PORT=27017 -GIT_REPO_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub -CODEBASE_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/apps/payment -SITE_DEPLOY_FOLDER=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/sites/payment/deploy -#!/bin/bash -export VENV_DIR=venv_t -export VENV_ACTIVATE=venv_t/bin/activate -export DOCKER_HOME=/var/lib/docker -export DOCKER_APP_HOME=$DOCKER_HOME/app -export DOCKER_BACKEND_HOME=$DOCKER_APP_HOME/$APP_NAME -export DOCKER_BACKEND_LOG_HOME=$DOCKER_BACKEND_HOME/log -export MONGODB_URI=mongodb://localhost:27017/ -export FREELEAPS_ENV=local -export SITE_URL_ROOT=http://localhost:5173/ -export LOG_BASE_PATH=${CODEBASE_ROOT}/log -export STRIPE_API_KEY=sk_test_51Ogsw5B0IyqaSJBrwczlr820jnmvA1qQQGoLZ2XxOsIzikpmXo4pRLjw4XVMTEBR8DdVTYySiAv1XX53Zv5xqynF00GfMqttFd diff --git a/apps/payment/.env b/apps/payment/.env index 1834048..6b56dca 100644 --- a/apps/payment/.env +++ b/apps/payment/.env @@ -7,9 +7,9 @@ export BACKEND_LOG_FILE_NAME=$APP_NAME export APPLICATION_ACTIVITY_LOG=$APP_NAME-activity export MONGODB_NAME=freeleaps2 export MONGODB_PORT=27017 -GIT_REPO_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub -CODEBASE_ROOT=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/apps/payment -SITE_DEPLOY_FOLDER=/Users/sunhaolou/Downloads/Freeleaps/freeleaps-service-hub/sites/payment/deploy +GIT_REPO_ROOT=/mnt/freeleaps/freeleaps-service-hub +CODEBASE_ROOT=/mnt/freeleaps/freeleaps-service-hub/apps/payment +SITE_DEPLOY_FOLDER=/mnt/freeleaps/freeleaps-service-hub/sites/payment/deploy #!/bin/bash export VENV_DIR=venv_t export VENV_ACTIVATE=venv_t/bin/activate diff --git a/apps/payment/backend/business/stripe_manager.py b/apps/payment/backend/business/stripe_manager.py index 92b28e4..8f7c892 100644 --- a/apps/payment/backend/business/stripe_manager.py +++ b/apps/payment/backend/business/stripe_manager.py @@ -16,7 +16,7 @@ stripe.api_key = app_settings.STRIPE_API_KEY class StripeManager: def __init__(self) -> None: - self.site_url_root = "http://localhost:8888" + self.site_url_root = app_settings.SITE_URL_ROOT.rstrip("/") self.module_logger = ModuleLogger(sender_id="StripeManager") async def create_stripe_account(self) -> Optional[str]: @@ -29,15 +29,15 @@ class StripeManager: if link_type == "account_update" and account.tos_acceptance.date: login_link = stripe.Account.create_login_link( account_id, - redirect_url="http://localhost:8888/work" + redirect_url="{}/work".format(self.site_url_root) ) return login_link.url # Otherwise show onboarding account_link = stripe.AccountLink.create( account=account_id, - refresh_url="http://localhost:8888/front-door", - return_url="http://localhost:8888/work", + refresh_url="{}/front-door".format(self.site_url_root), + return_url="{}/work".format(self.site_url_root), type="account_onboarding", ) return account_link.url @@ -71,20 +71,10 @@ class StripeManager: async def __fetch_transaction_by_session_id( self, session_id: str ) -> Optional[StripeTransactionDoc]: - await self.module_logger.log_info( - f"Looking up transaction for session_id: {session_id}", - properties={"session_id": session_id} - ) - transactions = await StripeTransactionDoc.find( StripeTransactionDoc.stripe_checkout_session_id == session_id ).to_list() - await self.module_logger.log_info( - f"Found {len(transactions)} transactions for session_id: {session_id}", - properties={"session_id": session_id, "transaction_count": len(transactions)} - ) - if len(transactions) > 1: await self.module_logger.log_error( error="More than one transaction found for session_id: {}".format( @@ -99,18 +89,7 @@ class StripeManager: ) return None - transaction = transactions[0] - await self.module_logger.log_info( - f"Found transaction: project_id={transaction.project_id}, milestone_index={transaction.milestone_index}, status={transaction.status}", - properties={ - "session_id": session_id, - "project_id": transaction.project_id, - "milestone_index": transaction.milestone_index, - "status": transaction.status - } - ) - - return transaction + return transactions[0] async def fetch_transaction_by_session_id( self, session_id: str @@ -324,8 +303,8 @@ class StripeManager: ], "payment_intent_data": payment_intent_data, "mode": "payment", - "success_url": "http://localhost:8888/projects", - "cancel_url": "http://localhost:8888/projects", + "success_url": "{}/projects".format(self.site_url_root), + "cancel_url": "{}/projects".format(self.site_url_root), } @@ -369,10 +348,6 @@ class StripeManager: # Handle the checkout.session.completed event if event["type"] == "checkout.session.completed": session = event["data"]["object"] - await self.module_logger.log_info( - f"Processing checkout.session.completed webhook for session_id: {session['id']}", - properties={"session_id": session["id"]} - ) transaction = await self.__fetch_transaction_by_session_id(session["id"]) if not transaction: @@ -387,69 +362,31 @@ class StripeManager: transaction.updated_time = datetime.now(timezone.utc) await transaction.save() - await self.module_logger.log_info( - f"Successfully updated transaction status to COMPLETED for project_id: {transaction.project_id}, milestone_index: {transaction.milestone_index}", - properties={ - "project_id": transaction.project_id, - "milestone_index": transaction.milestone_index, - "session_id": session["id"] - } - ) - # Save payment method information payment_method_saved = False try: - print("=" * 50) - print("STARTING PAYMENT METHOD PROCESSING") - print("=" * 50) - print(f"Starting payment method processing for session {session['id']}") - # Get the Stripe session to extract payment method details - try: - stripe_session = stripe.checkout.Session.retrieve(session["id"]) - print(f"Successfully retrieved Stripe session: {session['id']}") - except Exception as session_error: - print(f"Failed to retrieve Stripe session {session['id']}: {session_error}") - raise session_error - + stripe_session = stripe.checkout.Session.retrieve(session["id"]) payment_intent_id = stripe_session.get('payment_intent') - print(f"Payment intent ID from session: {payment_intent_id}") if payment_intent_id: - try: - payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) - print(f"Successfully retrieved payment intent: {payment_intent_id}") - except Exception as pi_error: - print(f"Failed to retrieve payment intent {payment_intent_id}: {pi_error}") - raise pi_error - + payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) payment_method_id = payment_intent.get('payment_method') - print(f"Payment method ID from payment intent: {payment_method_id}") if payment_method_id: - try: - payment_method = stripe.PaymentMethod.retrieve(payment_method_id) - print(f"Successfully retrieved payment method: {payment_method_id}") - except Exception as pm_error: - print(f"Failed to retrieve payment method {payment_method_id}: {pm_error}") - raise pm_error - + payment_method = stripe.PaymentMethod.retrieve(payment_method_id) card_details = payment_method.get('card', {}) - print(f"Card details: {card_details}") # Get user email (use a fallback since we don't have access to user profile) user_email = f"user_{transaction.from_user}@freeleaps.com" - print(f"User email for customer creation: {user_email}") # Get or create customer for the user - # Try to find existing customer first 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 - print(f"Found existing customer: {customer_id}") else: # Create new customer customer = stripe.Customer.create( @@ -457,9 +394,7 @@ class StripeManager: metadata={"user_id": transaction.from_user} ) customer_id = customer.id - print(f"Created new customer: {customer_id}") except Exception as customer_error: - print(f"Error creating/finding customer: {customer_error}") # Use a fallback customer ID or skip payment method saving customer_id = None @@ -468,7 +403,6 @@ class StripeManager: # Check if payment method is already attached to a customer payment_method_obj = stripe.PaymentMethod.retrieve(payment_method_id) if payment_method_obj.customer: - print(f"Payment method {payment_method_id} already attached to customer {payment_method_obj.customer}") # Use the existing customer ID customer_id = payment_method_obj.customer else: @@ -477,7 +411,6 @@ class StripeManager: payment_method_id, customer=customer_id ) - print(f"Successfully attached payment method {payment_method_id} to customer {customer_id}") # Check if payment method already exists in our database from backend.infra.payment.models import StripePaymentMethodDoc @@ -486,7 +419,6 @@ class StripeManager: ) if existing_payment_method: - print(f"Payment method {payment_method_id} already exists in database, skipping save") payment_method_saved = True else: # Save to our database only if it doesn't exist @@ -503,10 +435,8 @@ class StripeManager: ) await payment_method_doc.save() payment_method_saved = True - print(f"Successfully saved payment method {payment_method_id} for user {transaction.from_user}") except stripe.error.InvalidRequestError as attach_error: if "already attached" in str(attach_error).lower(): - print(f"Payment method {payment_method_id} already attached to 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( @@ -514,7 +444,6 @@ class StripeManager: ) if existing_payment_method: - print(f"Payment method {payment_method_id} already exists in database, skipping save") payment_method_saved = True else: # Still save to our database since it's already attached @@ -531,10 +460,7 @@ class StripeManager: ) await payment_method_doc.save() payment_method_saved = True - print(f"Successfully saved payment method {payment_method_id} for user {transaction.from_user}") elif "may not be used again" in str(attach_error).lower(): - print(f"Payment method {payment_method_id} was already used and cannot be attached to customer") - print(f"This is normal for one-time payment methods. Saving card details to database anyway.") # Check if payment method already exists in our database from backend.infra.payment.models import StripePaymentMethodDoc existing_payment_method = await StripePaymentMethodDoc.find_one( @@ -542,7 +468,6 @@ class StripeManager: ) if existing_payment_method: - print(f"Payment method {payment_method_id} already exists in database, skipping save") payment_method_saved = True else: # Save to our database even though it can't be attached @@ -559,30 +484,18 @@ class StripeManager: ) await payment_method_doc.save() payment_method_saved = True - print(f"Successfully saved payment method {payment_method_id} for user {transaction.from_user} (one-time use)") - else: - print(f"Error attaching payment method: {attach_error}") except Exception as save_error: - print(f"Error saving payment method to database: {save_error}") - else: - print(f"Could not create customer for user {transaction.from_user}, skipping payment method save") - else: - print(f"No payment method found in payment intent {payment_intent_id}") - else: - print(f"No payment intent found in session {session['id']}") + 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: - print(f"Error processing payment method: {payment_method_error}") - import traceback - print(f"Full traceback for payment method error:") - print(traceback.format_exc()) + 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 - - print(f"Payment method saved: {payment_method_saved}") return True, transaction.project_id, transaction.milestone_index - await self.module_logger.log_info( - f"Received non-checkout.session.completed webhook event: {event['type']}", - properties={"event_type": event["type"]} - ) return False, None, None From d01468f89a1ee7e489bf8f005be78d3cf1a0df51 Mon Sep 17 00:00:00 2001 From: sunhaolou Date: Tue, 22 Jul 2025 13:16:14 +0800 Subject: [PATCH 5/5] fix: refractor the length functions and add comments, and address the issues --- .../backend/business/stripe_manager.py | 338 +++++++++++------- apps/payment/common/log/base_logger.py | 13 +- 2 files changed, 202 insertions(+), 149 deletions(-) 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,