refractor: a current working version before cleaning up.

This commit is contained in:
sunhaolou 2025-07-21 12:50:51 +08:00
parent 40e0fafc2c
commit ccc995f599
14 changed files with 360 additions and 84 deletions

24
.env Normal file
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -12,7 +12,6 @@ class MoneyCollectionType(IntEnum):
UNSPECIFIED = 0
MARKED_AS_PAID = 1
UPLOAD_PROOF = 2
WECHAT_QR_CODE = 3
STRIPE_CHECKOUT = 4

View File

@ -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"

View File

@ -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)

View File

@ -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"

View File

@ -3,4 +3,3 @@ from enum import IntEnum
class PaymentGateway(IntEnum):
STRIP = 1
WECHAT = 2

View File

@ -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

View File

@ -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,

View File

@ -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(

View File

@ -209,3 +209,23 @@ async def handle_account_webhook(
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)}"}
)