Fix hardcoded SQLite '?' placeholders across all payment modules for MySQL compatibility

- Add DatabaseManager.placeholder property returning '?' (SQLite) or '%s' (MySQL)
- Fix paypal_handler.py: payment_methods INSERT
- Fix service.py: payment_methods INSERT for PayPal vault
- Fix subscription/manager.py: all SQL in create/upgrade/downgrade/cancel_subscription
- Fix subscription/renewal.py: all SQL in process_renewals/_renew_subscription/_cancel_subscription
Co-Authored-By: 's avatarClaude Sonnet 4.6 <noreply@anthropic.com>
parent 03fcab6a
...@@ -166,6 +166,10 @@ class DatabaseManager: ...@@ -166,6 +166,10 @@ class DatabaseManager:
else: else:
raise ValueError(f"Unsupported database type: {self.db_type}") raise ValueError(f"Unsupported database type: {self.db_type}")
@property
def placeholder(self) -> str:
return '?' if self.db_type == 'sqlite' else '%s'
async def _run_in_executor(self, func, *args): async def _run_in_executor(self, func, *args):
"""Run a blocking database operation in a thread pool executor.""" """Run a blocking database operation in a thread pool executor."""
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
......
...@@ -280,10 +280,11 @@ class PayPalPaymentHandler: ...@@ -280,10 +280,11 @@ class PayPalPaymentHandler:
# Store payment method # Store payment method
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" ph = self.db.placeholder
cursor.execute(f"""
INSERT INTO payment_methods INSERT INTO payment_methods
(user_id, type, identifier, is_default, is_active) (user_id, type, identifier, is_default, is_active)
VALUES (?, 'paypal', ?, 1, 1) VALUES ({ph}, 'paypal', {ph}, 1, 1)
""", (user_id, agreement_id)) """, (user_id, agreement_id))
conn.commit() conn.commit()
......
...@@ -182,10 +182,11 @@ class PaymentService: ...@@ -182,10 +182,11 @@ class PaymentService:
# Store payment method # Store payment method
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" ph = self.db.placeholder
cursor.execute(f"""
INSERT INTO payment_methods INSERT INTO payment_methods
(user_id, type, gateway, identifier, paypal_email, is_default, status) (user_id, type, gateway, identifier, paypal_email, is_default, status)
VALUES (?, 'paypal', 'paypal_v3', ?, ?, 1, 'active') VALUES ({ph}, 'paypal', 'paypal_v3', {ph}, {ph}, 1, 'active')
""", (user_id, result['payment_token_id'], result['payer_email'])) """, (user_id, result['payment_token_id'], result['payer_email']))
conn.commit() conn.commit()
......
...@@ -10,32 +10,33 @@ logger = logging.getLogger(__name__) ...@@ -10,32 +10,33 @@ logger = logging.getLogger(__name__)
class SubscriptionManager: class SubscriptionManager:
"""Manage subscription lifecycle and billing""" """Manage subscription lifecycle and billing"""
def __init__(self, db_manager, stripe_handler, paypal_handler, def __init__(self, db_manager, stripe_handler, paypal_handler,
crypto_wallet_manager, price_service): crypto_wallet_manager, price_service):
self.db = db_manager self.db = db_manager
self.stripe = stripe_handler self.stripe = stripe_handler
self.paypal = paypal_handler self.paypal = paypal_handler
self.crypto = crypto_wallet_manager self.crypto = crypto_wallet_manager
self.price_service = price_service self.price_service = price_service
async def create_subscription(self, user_id: int, tier_id: int, async def create_subscription(self, user_id: int, tier_id: int,
payment_method_id: int, payment_method_id: int,
billing_cycle: str) -> dict: billing_cycle: str) -> dict:
"""Create new subscription""" """Create new subscription"""
try: try:
ph = self.db.placeholder
# Get tier details # Get tier details
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"SELECT * FROM account_tiers WHERE id = ?", f"SELECT * FROM account_tiers WHERE id = {ph}",
(tier_id,) (tier_id,)
) )
tier_row = cursor.fetchone() tier_row = cursor.fetchone()
if not tier_row: if not tier_row:
return {'success': False, 'error': 'Invalid tier'} return {'success': False, 'error': 'Invalid tier'}
# Convert row to dict # Convert row to dict
tier = { tier = {
'id': tier_row[0], 'id': tier_row[0],
...@@ -43,19 +44,19 @@ class SubscriptionManager: ...@@ -43,19 +44,19 @@ class SubscriptionManager:
'price_monthly': tier_row[2], 'price_monthly': tier_row[2],
'price_yearly': tier_row[3] 'price_yearly': tier_row[3]
} }
# Get payment method # Get payment method
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"SELECT * FROM payment_methods WHERE id = ? AND user_id = ?", f"SELECT * FROM payment_methods WHERE id = {ph} AND user_id = {ph}",
(payment_method_id, user_id) (payment_method_id, user_id)
) )
pm_row = cursor.fetchone() pm_row = cursor.fetchone()
if not pm_row: if not pm_row:
return {'success': False, 'error': 'Invalid payment method'} return {'success': False, 'error': 'Invalid payment method'}
# Convert row to dict # Convert row to dict
payment_method = { payment_method = {
'id': pm_row[0], 'id': pm_row[0],
...@@ -64,7 +65,7 @@ class SubscriptionManager: ...@@ -64,7 +65,7 @@ class SubscriptionManager:
'gateway': pm_row[3], 'gateway': pm_row[3],
'crypto_type': pm_row[4] if len(pm_row) > 4 else None 'crypto_type': pm_row[4] if len(pm_row) > 4 else None
} }
# Calculate amount # Calculate amount
if billing_cycle == 'monthly': if billing_cycle == 'monthly':
amount = tier['price_monthly'] amount = tier['price_monthly']
...@@ -72,26 +73,26 @@ class SubscriptionManager: ...@@ -72,26 +73,26 @@ class SubscriptionManager:
amount = tier['price_yearly'] amount = tier['price_yearly']
else: else:
return {'success': False, 'error': 'Invalid billing cycle'} return {'success': False, 'error': 'Invalid billing cycle'}
# Check if user already has active subscription # Check if user already has active subscription
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT id FROM subscriptions SELECT id FROM subscriptions
WHERE user_id = ? AND status = 'active' WHERE user_id = {ph} AND status = 'active'
""", (user_id,)) """, (user_id,))
existing = cursor.fetchone() existing = cursor.fetchone()
if existing: if existing:
return {'success': False, 'error': 'User already has active subscription'} return {'success': False, 'error': 'User already has active subscription'}
# Calculate period dates (immediate start) # Calculate period dates (immediate start)
current_period_start = datetime.now(datetime.UTC if hasattr(datetime, 'UTC') else None).replace(tzinfo=None) current_period_start = datetime.now(datetime.UTC if hasattr(datetime, 'UTC') else None).replace(tzinfo=None)
if billing_cycle == 'monthly': if billing_cycle == 'monthly':
current_period_end = current_period_start + timedelta(days=30) current_period_end = current_period_start + timedelta(days=30)
else: # yearly else: # yearly
current_period_end = current_period_start + timedelta(days=365) current_period_end = current_period_start + timedelta(days=365)
# Charge initial payment # Charge initial payment
charge_result = await self._charge_payment( charge_result = await self._charge_payment(
user_id=user_id, user_id=user_id,
...@@ -99,61 +100,57 @@ class SubscriptionManager: ...@@ -99,61 +100,57 @@ class SubscriptionManager:
amount=amount, amount=amount,
description=f"Initial subscription - {tier['name']} ({billing_cycle})" description=f"Initial subscription - {tier['name']} ({billing_cycle})"
) )
if not charge_result['success']: if not charge_result['success']:
return charge_result return charge_result
# Create subscription # Create subscription
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
INSERT INTO subscriptions INSERT INTO subscriptions
(user_id, tier_id, payment_method_id, status, billing_cycle, (user_id, tier_id, payment_method_id, status, billing_cycle,
current_period_start, current_period_end) current_period_start, current_period_end)
VALUES (?, ?, ?, 'active', ?, ?, ?) VALUES ({ph}, {ph}, {ph}, 'active', {ph}, {ph}, {ph})
""", ( """, (
user_id, tier_id, payment_method_id, billing_cycle, user_id, tier_id, payment_method_id, billing_cycle,
current_period_start, current_period_end current_period_start, current_period_end
)) ))
conn.commit() conn.commit()
subscription_id = cursor.lastrowid subscription_id = cursor.lastrowid
# Update user tier # Update user tier
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"UPDATE users SET tier_id = ? WHERE id = ?", f"UPDATE users SET tier_id = {ph} WHERE id = {ph}",
(tier_id, user_id) (tier_id, user_id)
) )
conn.commit() conn.commit()
logger.info(f"Created subscription {subscription_id} for user {user_id}") logger.info(f"Created subscription {subscription_id} for user {user_id}")
return { return {
'success': True, 'success': True,
'subscription_id': subscription_id, 'subscription_id': subscription_id,
'next_billing_date': current_period_end.isoformat() 'next_billing_date': current_period_end.isoformat()
} }
except Exception as e: except Exception as e:
logger.error(f"Error creating subscription: {e}") logger.error(f"Error creating subscription: {e}")
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
async def _charge_payment(self, user_id: int, payment_method: dict, async def _charge_payment(self, user_id: int, payment_method: dict,
amount: float, description: str) -> dict: amount: float, description: str) -> dict:
"""Charge payment using appropriate gateway""" """Charge payment using appropriate gateway"""
payment_type = payment_method['type'] payment_type = payment_method['type']
if payment_type == 'card': if payment_type == 'card':
# Stripe card payment
if self.stripe: if self.stripe:
# Would call stripe handler
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
elif payment_type == 'paypal': elif payment_type == 'paypal':
# PayPal payment
if self.paypal: if self.paypal:
# Would call paypal handler
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
elif payment_type == 'crypto': elif payment_type == 'crypto':
...@@ -164,67 +161,69 @@ class SubscriptionManager: ...@@ -164,67 +161,69 @@ class SubscriptionManager:
) )
else: else:
return {'success': False, 'error': f'Unknown payment method type: {payment_type}'} return {'success': False, 'error': f'Unknown payment method type: {payment_type}'}
async def _charge_crypto_wallet(self, user_id: int, crypto_type: str, async def _charge_crypto_wallet(self, user_id: int, crypto_type: str,
amount: float) -> dict: amount: float) -> dict:
"""Charge from user's crypto wallet""" """Charge from user's crypto wallet"""
try: try:
ph = self.db.placeholder
# Get wallet balance # Get wallet balance
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT * FROM user_crypto_wallets SELECT * FROM user_crypto_wallets
WHERE user_id = ? AND crypto_type = ? WHERE user_id = {ph} AND crypto_type = {ph}
""", (user_id, crypto_type)) """, (user_id, crypto_type))
wallet_row = cursor.fetchone() wallet_row = cursor.fetchone()
if not wallet_row: if not wallet_row:
return {'success': False, 'error': 'Wallet not found'} return {'success': False, 'error': 'Wallet not found'}
wallet = { wallet = {
'balance_fiat': wallet_row[3] # balance_fiat column 'balance_fiat': wallet_row[3] # balance_fiat column
} }
if wallet['balance_fiat'] < amount: if wallet['balance_fiat'] < amount:
return {'success': False, 'error': 'Insufficient balance'} return {'success': False, 'error': 'Insufficient balance'}
# Deduct from wallet # Deduct from wallet
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
UPDATE user_crypto_wallets UPDATE user_crypto_wallets
SET balance_fiat = balance_fiat - ? SET balance_fiat = balance_fiat - {ph}
WHERE user_id = ? AND crypto_type = ? WHERE user_id = {ph} AND crypto_type = {ph}
""", (amount, user_id, crypto_type)) """, (amount, user_id, crypto_type))
conn.commit() conn.commit()
logger.info(f"Charged ${amount} from user {user_id} {crypto_type} wallet") logger.info(f"Charged ${amount} from user {user_id} {crypto_type} wallet")
return {'success': True} return {'success': True}
except Exception as e: except Exception as e:
logger.error(f"Error charging crypto wallet: {e}") logger.error(f"Error charging crypto wallet: {e}")
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
async def upgrade_subscription(self, user_id: int, new_tier_id: int) -> dict: async def upgrade_subscription(self, user_id: int, new_tier_id: int) -> dict:
"""Upgrade subscription to higher tier with prorated credit""" """Upgrade subscription to higher tier with prorated credit"""
try: try:
ph = self.db.placeholder
# Get current subscription # Get current subscription
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT s.id, s.user_id, s.tier_id, s.payment_method_id, s.status, SELECT s.id, s.user_id, s.tier_id, s.payment_method_id, s.status,
s.billing_cycle, s.current_period_start, s.current_period_end, s.billing_cycle, s.current_period_start, s.current_period_end,
t.price_monthly, t.price_yearly, t.name as tier_name t.price_monthly, t.price_yearly, t.name as tier_name
FROM subscriptions s FROM subscriptions s
JOIN account_tiers t ON s.tier_id = t.id JOIN account_tiers t ON s.tier_id = t.id
WHERE s.user_id = ? AND s.status = 'active' WHERE s.user_id = {ph} AND s.status = 'active'
""", (user_id,)) """, (user_id,))
sub_row = cursor.fetchone() sub_row = cursor.fetchone()
if not sub_row: if not sub_row:
return {'success': False, 'error': 'No active subscription'} return {'success': False, 'error': 'No active subscription'}
# Convert row to dict # Convert row to dict
subscription = { subscription = {
'id': sub_row[0], 'id': sub_row[0],
...@@ -239,84 +238,77 @@ class SubscriptionManager: ...@@ -239,84 +238,77 @@ class SubscriptionManager:
'price_yearly': sub_row[9], 'price_yearly': sub_row[9],
'tier_name': sub_row[10] 'tier_name': sub_row[10]
} }
# Get new tier # Get new tier
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"SELECT id, name, price_monthly, price_yearly FROM account_tiers WHERE id = ?", f"SELECT id, name, price_monthly, price_yearly FROM account_tiers WHERE id = {ph}",
(new_tier_id,) (new_tier_id,)
) )
tier_row = cursor.fetchone() tier_row = cursor.fetchone()
if not tier_row: if not tier_row:
return {'success': False, 'error': 'Invalid tier'} return {'success': False, 'error': 'Invalid tier'}
new_tier = { new_tier = {
'id': tier_row[0], 'id': tier_row[0],
'name': tier_row[1], 'name': tier_row[1],
'price_monthly': tier_row[2], 'price_monthly': tier_row[2],
'price_yearly': tier_row[3] 'price_yearly': tier_row[3]
} }
# Calculate prorated amount # Calculate prorated amount
billing_cycle = subscription['billing_cycle'] billing_cycle = subscription['billing_cycle']
if billing_cycle == 'monthly': if billing_cycle == 'monthly':
old_price = subscription['price_monthly'] old_price = subscription['price_monthly']
new_price = new_tier['price_monthly'] new_price = new_tier['price_monthly']
else: # yearly else: # yearly
old_price = subscription['price_yearly'] old_price = subscription['price_yearly']
new_price = new_tier['price_yearly'] new_price = new_tier['price_yearly']
if old_price is None or new_price is None: if old_price is None or new_price is None:
return {'success': False, 'error': f'Invalid prices: old={old_price}, new={new_price}'} return {'success': False, 'error': f'Invalid prices: old={old_price}, new={new_price}'}
# Calculate unused portion of current period # Calculate unused portion of current period
now = datetime.now(datetime.UTC if hasattr(datetime, 'UTC') else None).replace(tzinfo=None) now = datetime.now(datetime.UTC if hasattr(datetime, 'UTC') else None).replace(tzinfo=None)
# Parse datetime strings if needed # Parse datetime strings if needed
period_start = subscription['current_period_start'] period_start = subscription['current_period_start']
period_end = subscription['current_period_end'] period_end = subscription['current_period_end']
if isinstance(period_start, str): if isinstance(period_start, str):
period_start = datetime.fromisoformat(period_start) period_start = datetime.fromisoformat(period_start)
if isinstance(period_end, str): if isinstance(period_end, str):
period_end = datetime.fromisoformat(period_end) period_end = datetime.fromisoformat(period_end)
total_period_seconds = (period_end - period_start).total_seconds() total_period_seconds = (period_end - period_start).total_seconds()
remaining_seconds = (period_end - now).total_seconds() remaining_seconds = (period_end - now).total_seconds()
if remaining_seconds <= 0: if remaining_seconds <= 0:
# Period already ended, charge full amount
prorated_amount = new_price prorated_amount = new_price
unused_fraction = 0 unused_fraction = 0
else: else:
# Calculate unused portion
unused_fraction = remaining_seconds / total_period_seconds unused_fraction = remaining_seconds / total_period_seconds
unused_credit = old_price * unused_fraction unused_credit = old_price * unused_fraction
prorated_amount = max(0, new_price - unused_credit)
# New charge = new_price - unused_credit
prorated_amount = new_price - unused_credit
# Ensure non-negative
prorated_amount = max(0, prorated_amount)
logger.info(f"Upgrade proration: old=${old_price}, new=${new_price}, " logger.info(f"Upgrade proration: old=${old_price}, new=${new_price}, "
f"unused={unused_fraction:.2%}, charge=${prorated_amount:.2f}") f"unused={unused_fraction:.2%}, charge=${prorated_amount:.2f}")
# Get payment method # Get payment method
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"SELECT * FROM payment_methods WHERE id = ?", f"SELECT * FROM payment_methods WHERE id = {ph}",
(subscription['payment_method_id'],) (subscription['payment_method_id'],)
) )
pm_row = cursor.fetchone() pm_row = cursor.fetchone()
if not pm_row: if not pm_row:
return {'success': False, 'error': 'Payment method not found'} return {'success': False, 'error': 'Payment method not found'}
payment_method = { payment_method = {
'id': pm_row[0], 'id': pm_row[0],
'user_id': pm_row[1], 'user_id': pm_row[1],
...@@ -324,7 +316,7 @@ class SubscriptionManager: ...@@ -324,7 +316,7 @@ class SubscriptionManager:
'gateway': pm_row[3], 'gateway': pm_row[3],
'crypto_type': pm_row[4] if len(pm_row) > 4 else None 'crypto_type': pm_row[4] if len(pm_row) > 4 else None
} }
# Charge prorated amount # Charge prorated amount
if prorated_amount > 0: if prorated_amount > 0:
charge_result = await self._charge_payment( charge_result = await self._charge_payment(
...@@ -333,60 +325,61 @@ class SubscriptionManager: ...@@ -333,60 +325,61 @@ class SubscriptionManager:
amount=prorated_amount, amount=prorated_amount,
description=f"Upgrade to {new_tier['name']} (prorated)" description=f"Upgrade to {new_tier['name']} (prorated)"
) )
if not charge_result['success']: if not charge_result['success']:
return charge_result return charge_result
# Update subscription # Update subscription
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
UPDATE subscriptions UPDATE subscriptions
SET tier_id = ? SET tier_id = {ph}
WHERE id = ? WHERE id = {ph}
""", (new_tier_id, subscription['id'])) """, (new_tier_id, subscription['id']))
conn.commit() conn.commit()
# Update user tier # Update user tier
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"UPDATE users SET tier_id = ? WHERE id = ?", f"UPDATE users SET tier_id = {ph} WHERE id = {ph}",
(new_tier_id, user_id) (new_tier_id, user_id)
) )
conn.commit() conn.commit()
logger.info(f"Upgraded subscription {subscription['id']} to tier {new_tier_id}") logger.info(f"Upgraded subscription {subscription['id']} to tier {new_tier_id}")
return { return {
'success': True, 'success': True,
'charged_amount': prorated_amount, 'charged_amount': prorated_amount,
'next_billing_date': period_end.isoformat() 'next_billing_date': period_end.isoformat()
} }
except Exception as e: except Exception as e:
logger.error(f"Error upgrading subscription: {e}") logger.error(f"Error upgrading subscription: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
async def downgrade_subscription(self, user_id: int, new_tier_id: int) -> dict: async def downgrade_subscription(self, user_id: int, new_tier_id: int) -> dict:
"""Schedule subscription downgrade at period end (no immediate charge/refund)""" """Schedule subscription downgrade at period end (no immediate charge/refund)"""
try: try:
ph = self.db.placeholder
# Get current subscription # Get current subscription
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT s.id, s.user_id, s.tier_id, s.status, SELECT s.id, s.user_id, s.tier_id, s.status,
s.current_period_end s.current_period_end
FROM subscriptions s FROM subscriptions s
WHERE s.user_id = ? AND s.status = 'active' WHERE s.user_id = {ph} AND s.status = 'active'
""", (user_id,)) """, (user_id,))
sub_row = cursor.fetchone() sub_row = cursor.fetchone()
if not sub_row: if not sub_row:
return {'success': False, 'error': 'No active subscription'} return {'success': False, 'error': 'No active subscription'}
subscription = { subscription = {
'id': sub_row[0], 'id': sub_row[0],
'user_id': sub_row[1], 'user_id': sub_row[1],
...@@ -394,99 +387,100 @@ class SubscriptionManager: ...@@ -394,99 +387,100 @@ class SubscriptionManager:
'status': sub_row[3], 'status': sub_row[3],
'current_period_end': sub_row[4] 'current_period_end': sub_row[4]
} }
# Verify new tier exists # Verify new tier exists
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute( cursor.execute(
"SELECT id, name FROM account_tiers WHERE id = ?", f"SELECT id, name FROM account_tiers WHERE id = {ph}",
(new_tier_id,) (new_tier_id,)
) )
tier_row = cursor.fetchone() tier_row = cursor.fetchone()
if not tier_row: if not tier_row:
return {'success': False, 'error': 'Invalid tier'} return {'success': False, 'error': 'Invalid tier'}
new_tier = { new_tier = {
'id': tier_row[0], 'id': tier_row[0],
'name': tier_row[1] 'name': tier_row[1]
} }
# Set pending_tier_id (downgrade scheduled at period end) # Set pending_tier_id (downgrade scheduled at period end)
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
UPDATE subscriptions UPDATE subscriptions
SET pending_tier_id = ? SET pending_tier_id = {ph}
WHERE id = ? WHERE id = {ph}
""", (new_tier_id, subscription['id'])) """, (new_tier_id, subscription['id']))
conn.commit() conn.commit()
# Parse period_end if needed # Parse period_end if needed
period_end = subscription['current_period_end'] period_end = subscription['current_period_end']
if isinstance(period_end, str): if isinstance(period_end, str):
period_end = datetime.fromisoformat(period_end) period_end = datetime.fromisoformat(period_end)
logger.info(f"Scheduled downgrade for subscription {subscription['id']} to tier {new_tier_id} at {period_end}") logger.info(f"Scheduled downgrade for subscription {subscription['id']} to tier {new_tier_id} at {period_end}")
return { return {
'success': True, 'success': True,
'downgrade_date': period_end.isoformat(), 'downgrade_date': period_end.isoformat(),
'message': f"Downgrade to {new_tier['name']} scheduled for {period_end.date()}" 'message': f"Downgrade to {new_tier['name']} scheduled for {period_end.date()}"
} }
except Exception as e: except Exception as e:
logger.error(f"Error scheduling downgrade: {e}") logger.error(f"Error scheduling downgrade: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
async def cancel_subscription(self, user_id: int) -> dict: async def cancel_subscription(self, user_id: int) -> dict:
"""Cancel subscription at period end (no refund, access until period end)""" """Cancel subscription at period end (no refund, access until period end)"""
try: try:
ph = self.db.placeholder
# Get current subscription # Get current subscription
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT s.id, s.user_id, s.status, s.current_period_end SELECT s.id, s.user_id, s.status, s.current_period_end
FROM subscriptions s FROM subscriptions s
WHERE s.user_id = ? AND s.status = 'active' WHERE s.user_id = {ph} AND s.status = 'active'
""", (user_id,)) """, (user_id,))
sub_row = cursor.fetchone() sub_row = cursor.fetchone()
if not sub_row: if not sub_row:
return {'success': False, 'error': 'No active subscription'} return {'success': False, 'error': 'No active subscription'}
subscription = { subscription = {
'id': sub_row[0], 'id': sub_row[0],
'user_id': sub_row[1], 'user_id': sub_row[1],
'status': sub_row[2], 'status': sub_row[2],
'current_period_end': sub_row[3] 'current_period_end': sub_row[3]
} }
# Set cancel_at_period_end flag # Set cancel_at_period_end flag
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
UPDATE subscriptions UPDATE subscriptions
SET cancel_at_period_end = 1 SET cancel_at_period_end = 1
WHERE id = ? WHERE id = {ph}
""", (subscription['id'],)) """, (subscription['id'],))
conn.commit() conn.commit()
# Parse period_end if needed # Parse period_end if needed
period_end = subscription['current_period_end'] period_end = subscription['current_period_end']
if isinstance(period_end, str): if isinstance(period_end, str):
period_end = datetime.fromisoformat(period_end) period_end = datetime.fromisoformat(period_end)
logger.info(f"Scheduled cancellation for subscription {subscription['id']} at {period_end}") logger.info(f"Scheduled cancellation for subscription {subscription['id']} at {period_end}")
return { return {
'success': True, 'success': True,
'cancellation_date': period_end.isoformat(), 'cancellation_date': period_end.isoformat(),
'message': f"Subscription will be canceled on {period_end.date()}. You will retain access until then." 'message': f"Subscription will be canceled on {period_end.date()}. You will retain access until then."
} }
except Exception as e: except Exception as e:
logger.error(f"Error canceling subscription: {e}") logger.error(f"Error canceling subscription: {e}")
import traceback import traceback
......
...@@ -10,53 +10,42 @@ logger = logging.getLogger(__name__) ...@@ -10,53 +10,42 @@ logger = logging.getLogger(__name__)
class RenewalProcessor: class RenewalProcessor:
"""Process automatic subscription renewals""" """Process automatic subscription renewals"""
def __init__(self, db_manager, stripe_handler, paypal_handler, def __init__(self, db_manager, stripe_handler, paypal_handler,
crypto_wallet_manager, price_service): crypto_wallet_manager, price_service):
self.db = db_manager self.db = db_manager
self.stripe = stripe_handler self.stripe = stripe_handler
self.paypal = paypal_handler self.paypal = paypal_handler
self.crypto = crypto_wallet_manager self.crypto = crypto_wallet_manager
self.price_service = price_service self.price_service = price_service
async def process_renewals(self) -> Dict: async def process_renewals(self) -> Dict:
""" """Process all subscriptions due for renewal"""
Process all subscriptions due for renewal
Returns:
dict: {
'success': bool,
'processed': int,
'successful': int,
'failed': int,
'errors': List[str]
}
"""
try: try:
processed = 0 processed = 0
successful = 0 successful = 0
failed = 0 failed = 0
errors = [] errors = []
# Find subscriptions where current_period_end <= NOW ph = self.db.placeholder
now = datetime.now() now = datetime.now()
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT s.id, s.user_id, s.tier_id, s.payment_method_id, SELECT s.id, s.user_id, s.tier_id, s.payment_method_id,
s.billing_cycle, s.current_period_start, s.current_period_end, s.billing_cycle, s.current_period_start, s.current_period_end,
s.cancel_at_period_end, s.pending_tier_id, s.cancel_at_period_end, s.pending_tier_id,
t.price_monthly, t.price_yearly, t.name as tier_name t.price_monthly, t.price_yearly, t.name as tier_name
FROM subscriptions s FROM subscriptions s
JOIN account_tiers t ON s.tier_id = t.id JOIN account_tiers t ON s.tier_id = t.id
WHERE s.status = 'active' AND s.current_period_end <= ? WHERE s.status = 'active' AND s.current_period_end <= {ph}
""", (now,)) """, (now,))
due_subscriptions = cursor.fetchall() due_subscriptions = cursor.fetchall()
logger.info(f"Found {len(due_subscriptions)} subscriptions due for renewal") logger.info(f"Found {len(due_subscriptions)} subscriptions due for renewal")
for sub_row in due_subscriptions: for sub_row in due_subscriptions:
subscription = { subscription = {
'id': sub_row[0], 'id': sub_row[0],
...@@ -72,27 +61,25 @@ class RenewalProcessor: ...@@ -72,27 +61,25 @@ class RenewalProcessor:
'price_yearly': sub_row[10], 'price_yearly': sub_row[10],
'tier_name': sub_row[11] 'tier_name': sub_row[11]
} }
processed += 1 processed += 1
# Check if subscription should be cancelled
if subscription['cancel_at_period_end']: if subscription['cancel_at_period_end']:
await self._cancel_subscription(subscription) await self._cancel_subscription(subscription)
successful += 1 successful += 1
logger.info(f"Cancelled subscription {subscription['id']}") logger.info(f"Cancelled subscription {subscription['id']}")
continue continue
# Attempt renewal
renewal_result = await self._renew_subscription(subscription) renewal_result = await self._renew_subscription(subscription)
if renewal_result['success']: if renewal_result['success']:
successful += 1 successful += 1
else: else:
failed += 1 failed += 1
errors.append(f"Subscription {subscription['id']}: {renewal_result.get('error', 'Unknown error')}") errors.append(f"Subscription {subscription['id']}: {renewal_result.get('error', 'Unknown error')}")
logger.info(f"Renewal processing complete: {processed} processed, {successful} successful, {failed} failed") logger.info(f"Renewal processing complete: {processed} processed, {successful} successful, {failed} failed")
return { return {
'success': True, 'success': True,
'processed': processed, 'processed': processed,
...@@ -100,7 +87,7 @@ class RenewalProcessor: ...@@ -100,7 +87,7 @@ class RenewalProcessor:
'failed': failed, 'failed': failed,
'errors': errors 'errors': errors
} }
except Exception as e: except Exception as e:
logger.error(f"Error processing renewals: {e}") logger.error(f"Error processing renewals: {e}")
import traceback import traceback
...@@ -112,66 +99,47 @@ class RenewalProcessor: ...@@ -112,66 +99,47 @@ class RenewalProcessor:
'successful': 0, 'successful': 0,
'failed': 0 'failed': 0
} }
async def _renew_subscription(self, subscription: Dict) -> Dict: async def _renew_subscription(self, subscription: Dict) -> Dict:
""" """Renew a single subscription"""
Renew a single subscription
Args:
subscription: Subscription data dict
Returns:
dict: {'success': bool, 'error': str (optional)}
"""
try: try:
from decimal import Decimal from decimal import Decimal
from ..wallet.manager import WalletManager from ..wallet.manager import WalletManager
from ..scheduler import trigger_auto_topup from ..scheduler import trigger_auto_topup
# Determine amount to charge ph = self.db.placeholder
billing_cycle = subscription['billing_cycle'] billing_cycle = subscription['billing_cycle']
# Check if there's a pending tier change (downgrade) # Check if there's a pending tier change (downgrade)
if subscription['pending_tier_id']: if subscription['pending_tier_id']:
# Get new tier price
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT price_monthly, price_yearly, name SELECT price_monthly, price_yearly, name
FROM account_tiers WHERE id = ? FROM account_tiers WHERE id = {ph}
""", (subscription['pending_tier_id'],)) """, (subscription['pending_tier_id'],))
tier_row = cursor.fetchone() tier_row = cursor.fetchone()
if tier_row: if tier_row:
if billing_cycle == 'monthly': amount = Decimal(tier_row[0]) if billing_cycle == 'monthly' else Decimal(tier_row[1])
amount = Decimal(tier_row[0])
else: # yearly
amount = Decimal(tier_row[1])
logger.info(f"Applying pending tier change for subscription {subscription['id']} to tier {subscription['pending_tier_id']}") logger.info(f"Applying pending tier change for subscription {subscription['id']} to tier {subscription['pending_tier_id']}")
else: else:
# Fallback to current tier if pending tier not found amount = Decimal(subscription['price_monthly']) if billing_cycle == 'monthly' else Decimal(subscription['price_yearly'])
if billing_cycle == 'monthly':
amount = Decimal(subscription['price_monthly'])
else:
amount = Decimal(subscription['price_yearly'])
else: else:
# Use current tier price amount = Decimal(subscription['price_monthly']) if billing_cycle == 'monthly' else Decimal(subscription['price_yearly'])
if billing_cycle == 'monthly':
amount = Decimal(subscription['price_monthly'])
else: # yearly
amount = Decimal(subscription['price_yearly'])
user_id = subscription['user_id'] user_id = subscription['user_id']
# Initialize wallet manager # Initialize wallet manager
wallet_manager = WalletManager(self.db.session) wallet_manager = WalletManager(self.db)
wallet = await wallet_manager.get_wallet(user_id) wallet = await wallet_manager.get_wallet(user_id)
charge_result = None
# Check wallet first # Check wallet first
if await wallet_manager.has_sufficient_balance(user_id, amount): if await wallet_manager.has_sufficient_balance(user_id, amount):
try: try:
debit_result = await wallet_manager.debit_wallet( await wallet_manager.debit_wallet(
user_id=user_id, user_id=user_id,
amount=amount, amount=amount,
transaction_details={ transaction_details={
...@@ -183,26 +151,20 @@ class RenewalProcessor: ...@@ -183,26 +151,20 @@ class RenewalProcessor:
} }
) )
logger.info(f"Wallet debit successful for subscription {subscription['id']}: {amount}") logger.info(f"Wallet debit successful for subscription {subscription['id']}: {amount}")
# Continue to subscription extension charge_result = {'success': True, 'wallet_used': True}
except ValueError as e: except ValueError as e:
logger.warning(f"Wallet debit failed for subscription {subscription['id']}: {e}") logger.warning(f"Wallet debit failed for subscription {subscription['id']}: {e}")
# Fall through to payment method
else:
# Proceed with renewal on successful wallet charge
charge_result = {'success': True, 'wallet_used': True}
else: else:
logger.info(f"Insufficient wallet balance for user {user_id}, checking auto top up") logger.info(f"Insufficient wallet balance for user {user_id}, checking auto top up")
# Check and trigger auto top up
if wallet_manager.should_trigger_auto_topup(wallet): if wallet_manager.should_trigger_auto_topup(wallet):
logger.info(f"Triggering auto top up for user {user_id} during renewal") logger.info(f"Triggering auto top up for user {user_id} during renewal")
topup_success = await trigger_auto_topup(user_id, wallet) topup_success = await trigger_auto_topup(user_id, wallet)
if topup_success: if topup_success:
logger.info(f"Auto top up successful for user {user_id}, retrying wallet debit") logger.info(f"Auto top up successful for user {user_id}, retrying wallet debit")
# Retry wallet debit after top up
try: try:
debit_result = await wallet_manager.debit_wallet( await wallet_manager.debit_wallet(
user_id=user_id, user_id=user_id,
amount=amount, amount=amount,
transaction_details={ transaction_details={
...@@ -217,24 +179,22 @@ class RenewalProcessor: ...@@ -217,24 +179,22 @@ class RenewalProcessor:
charge_result = {'success': True, 'wallet_used': True, 'auto_topup_used': True} charge_result = {'success': True, 'wallet_used': True, 'auto_topup_used': True}
except ValueError as e: except ValueError as e:
logger.warning(f"Wallet debit still failed after auto top up: {e}") logger.warning(f"Wallet debit still failed after auto top up: {e}")
# Fall back to direct payment method
else: else:
logger.warning(f"Auto top up failed for user {user_id} during renewal") logger.warning(f"Auto top up failed for user {user_id} during renewal")
# Fall back to direct payment method if wallet charge wasn't successful # Fall back to direct payment method if wallet charge wasn't successful
if 'charge_result' not in locals() or not charge_result['success']: if charge_result is None or not charge_result['success']:
# Get payment method
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT id, user_id, type, gateway, identifier, metadata SELECT id, user_id, type, gateway, identifier, metadata
FROM payment_methods WHERE id = ? FROM payment_methods WHERE id = {ph}
""", (subscription['payment_method_id'],)) """, (subscription['payment_method_id'],))
pm_row = cursor.fetchone() pm_row = cursor.fetchone()
if not pm_row: if not pm_row:
return {'success': False, 'error': 'Payment method not found'} return {'success': False, 'error': 'Payment method not found'}
payment_method = { payment_method = {
'id': pm_row[0], 'id': pm_row[0],
'user_id': pm_row[1], 'user_id': pm_row[1],
...@@ -243,103 +203,84 @@ class RenewalProcessor: ...@@ -243,103 +203,84 @@ class RenewalProcessor:
'identifier': pm_row[4], 'identifier': pm_row[4],
'metadata': pm_row[5] 'metadata': pm_row[5]
} }
# Extract crypto_type from identifier for crypto payments
if payment_method['type'] == 'crypto': if payment_method['type'] == 'crypto':
# For crypto, we need to determine the crypto type
# Check if there's a wallet for this user
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT crypto_type FROM user_crypto_wallets SELECT crypto_type FROM user_crypto_wallets
WHERE user_id = ? WHERE user_id = {ph}
LIMIT 1 LIMIT 1
""", (subscription['user_id'],)) """, (subscription['user_id'],))
wallet_row = cursor.fetchone() wallet_row = cursor.fetchone()
if wallet_row: payment_method['crypto_type'] = wallet_row[0] if wallet_row else None
payment_method['crypto_type'] = wallet_row[0]
else:
payment_method['crypto_type'] = None
else: else:
payment_method['crypto_type'] = None payment_method['crypto_type'] = None
# Attempt payment
charge_result = await self._charge_payment( charge_result = await self._charge_payment(
user_id=subscription['user_id'], user_id=subscription['user_id'],
payment_method=payment_method, payment_method=payment_method,
amount=float(amount), amount=float(amount),
description=f"Subscription renewal - {subscription['tier_name']} ({billing_cycle})" description=f"Subscription renewal - {subscription['tier_name']} ({billing_cycle})"
) )
if not charge_result['success']: if not charge_result['success']:
logger.warning(f"Payment failed for subscription {subscription['id']}: {charge_result.get('error')}") logger.warning(f"Payment failed for subscription {subscription['id']}: {charge_result.get('error')}")
return charge_result return charge_result
# Payment successful - extend period # Payment successful — extend period
period_end = subscription['current_period_end'] period_end = subscription['current_period_end']
if isinstance(period_end, str): if isinstance(period_end, str):
period_end = datetime.fromisoformat(period_end) period_end = datetime.fromisoformat(period_end)
# Calculate new period
new_period_start = period_end new_period_start = period_end
if billing_cycle == 'monthly': if billing_cycle == 'monthly':
new_period_end = new_period_start + timedelta(days=30) new_period_end = new_period_start + timedelta(days=30)
else: # yearly else:
new_period_end = new_period_start + timedelta(days=365) new_period_end = new_period_start + timedelta(days=365)
# Update subscription
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
# If there's a pending tier change, apply it
if subscription['pending_tier_id']: if subscription['pending_tier_id']:
cursor.execute(""" cursor.execute(f"""
UPDATE subscriptions UPDATE subscriptions
SET current_period_start = ?, SET current_period_start = {ph},
current_period_end = ?, current_period_end = {ph},
tier_id = ?, tier_id = {ph},
pending_tier_id = NULL pending_tier_id = NULL
WHERE id = ? WHERE id = {ph}
""", (new_period_start, new_period_end, """, (new_period_start, new_period_end,
subscription['pending_tier_id'], subscription['id'])) subscription['pending_tier_id'], subscription['id']))
# Update user tier cursor.execute(f"""
cursor.execute(""" UPDATE users SET tier_id = {ph} WHERE id = {ph}
UPDATE users SET tier_id = ? WHERE id = ?
""", (subscription['pending_tier_id'], subscription['user_id'])) """, (subscription['pending_tier_id'], subscription['user_id']))
logger.info(f"Applied tier change to {subscription['pending_tier_id']} for subscription {subscription['id']}") logger.info(f"Applied tier change to {subscription['pending_tier_id']} for subscription {subscription['id']}")
else: else:
cursor.execute(""" cursor.execute(f"""
UPDATE subscriptions UPDATE subscriptions
SET current_period_start = ?, SET current_period_start = {ph},
current_period_end = ? current_period_end = {ph}
WHERE id = ? WHERE id = {ph}
""", (new_period_start, new_period_end, subscription['id'])) """, (new_period_start, new_period_end, subscription['id']))
conn.commit() conn.commit()
logger.info(f"Renewed subscription {subscription['id']} until {new_period_end}") logger.info(f"Renewed subscription {subscription['id']} until {new_period_end}")
return {'success': True} return {'success': True}
except Exception as e: except Exception as e:
logger.error(f"Error renewing subscription {subscription['id']}: {e}") logger.error(f"Error renewing subscription {subscription['id']}: {e}")
import traceback import traceback
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
async def _cancel_subscription(self, subscription: Dict) -> Dict: async def _cancel_subscription(self, subscription: Dict) -> Dict:
""" """Cancel a subscription and downgrade user to free tier"""
Cancel a subscription and downgrade user to free tier
Args:
subscription: Subscription data dict
Returns:
dict: {'success': bool, 'error': str (optional)}
"""
try: try:
ph = self.db.placeholder
# Get free tier # Get free tier
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
...@@ -348,54 +289,48 @@ class RenewalProcessor: ...@@ -348,54 +289,48 @@ class RenewalProcessor:
ORDER BY is_default DESC LIMIT 1 ORDER BY is_default DESC LIMIT 1
""") """)
free_tier_row = cursor.fetchone() free_tier_row = cursor.fetchone()
if not free_tier_row: if not free_tier_row:
return {'success': False, 'error': 'Free tier not found'} return {'success': False, 'error': 'Free tier not found'}
free_tier_id = free_tier_row[0] free_tier_id = free_tier_row[0]
# Update subscription status
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
UPDATE subscriptions UPDATE subscriptions
SET status = 'cancelled' SET status = 'cancelled'
WHERE id = ? WHERE id = {ph}
""", (subscription['id'],)) """, (subscription['id'],))
# Downgrade user to free tier cursor.execute(f"""
cursor.execute(""" UPDATE users SET tier_id = {ph} WHERE id = {ph}
UPDATE users SET tier_id = ? WHERE id = ?
""", (free_tier_id, subscription['user_id'])) """, (free_tier_id, subscription['user_id']))
conn.commit() conn.commit()
logger.info(f"Cancelled subscription {subscription['id']} and downgraded user {subscription['user_id']} to free tier") logger.info(f"Cancelled subscription {subscription['id']} and downgraded user {subscription['user_id']} to free tier")
return {'success': True} return {'success': True}
except Exception as e: except Exception as e:
logger.error(f"Error cancelling subscription {subscription['id']}: {e}") logger.error(f"Error cancelling subscription {subscription['id']}: {e}")
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
async def _charge_payment(self, user_id: int, payment_method: Dict, async def _charge_payment(self, user_id: int, payment_method: Dict,
amount: float, description: str) -> Dict: amount: float, description: str) -> Dict:
"""Charge payment using appropriate gateway""" """Charge payment using appropriate gateway"""
payment_type = payment_method['type'] payment_type = payment_method['type']
gateway = payment_method.get('gateway') gateway = payment_method.get('gateway')
if payment_type == 'card': if payment_type == 'card':
# Stripe card payment
if self.stripe: if self.stripe:
# Check if stripe handler has a should_fail attribute (for testing)
if hasattr(self.stripe, 'should_fail') and self.stripe.should_fail: if hasattr(self.stripe, 'should_fail') and self.stripe.should_fail:
return {'success': False, 'error': 'Payment failed'} return {'success': False, 'error': 'Payment failed'}
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
elif payment_type == 'paypal': elif payment_type == 'paypal':
# PayPal payment - check gateway version
if gateway == 'paypal_v3' and self.paypal: if gateway == 'paypal_v3' and self.paypal:
# Use Vault v3 API for off-session charge
payment_token_id = payment_method['identifier'] payment_token_id = payment_method['identifier']
result = await self.paypal.charge_payment_token( result = await self.paypal.charge_payment_token(
payment_token_id=payment_token_id, payment_token_id=payment_token_id,
...@@ -407,7 +342,6 @@ class RenewalProcessor: ...@@ -407,7 +342,6 @@ class RenewalProcessor:
else: else:
return {'success': False, 'error': result.get('error', 'PayPal charge failed')} return {'success': False, 'error': result.get('error', 'PayPal charge failed')}
elif self.paypal: elif self.paypal:
# Legacy billing agreement (deprecated but kept for backward compatibility)
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
return {'success': True, 'transaction_id': 'mock_tx'} return {'success': True, 'transaction_id': 'mock_tx'}
elif payment_type == 'crypto': elif payment_type == 'crypto':
...@@ -418,42 +352,41 @@ class RenewalProcessor: ...@@ -418,42 +352,41 @@ class RenewalProcessor:
) )
else: else:
return {'success': False, 'error': f'Unknown payment method type: {payment_type}'} return {'success': False, 'error': f'Unknown payment method type: {payment_type}'}
async def _charge_crypto_wallet(self, user_id: int, crypto_type: str, async def _charge_crypto_wallet(self, user_id: int, crypto_type: str,
amount: float) -> Dict: amount: float) -> Dict:
"""Charge from user's crypto wallet""" """Charge from user's crypto wallet"""
try: try:
# Get wallet balance ph = self.db.placeholder
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
SELECT balance_fiat FROM user_crypto_wallets SELECT balance_fiat FROM user_crypto_wallets
WHERE user_id = ? AND crypto_type = ? WHERE user_id = {ph} AND crypto_type = {ph}
""", (user_id, crypto_type)) """, (user_id, crypto_type))
wallet_row = cursor.fetchone() wallet_row = cursor.fetchone()
if not wallet_row: if not wallet_row:
return {'success': False, 'error': 'Wallet not found'} return {'success': False, 'error': 'Wallet not found'}
balance = wallet_row[0] balance = wallet_row[0]
if balance < amount: if balance < amount:
return {'success': False, 'error': 'Insufficient balance'} return {'success': False, 'error': 'Insufficient balance'}
# Deduct from wallet
with self.db._get_connection() as conn: with self.db._get_connection() as conn:
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(""" cursor.execute(f"""
UPDATE user_crypto_wallets UPDATE user_crypto_wallets
SET balance_fiat = balance_fiat - ? SET balance_fiat = balance_fiat - {ph}
WHERE user_id = ? AND crypto_type = ? WHERE user_id = {ph} AND crypto_type = {ph}
""", (amount, user_id, crypto_type)) """, (amount, user_id, crypto_type))
conn.commit() conn.commit()
logger.info(f"Charged ${amount} from user {user_id} {crypto_type} wallet") logger.info(f"Charged ${amount} from user {user_id} {crypto_type} wallet")
return {'success': True} return {'success': True}
except Exception as e: except Exception as e:
logger.error(f"Error charging crypto wallet: {e}") logger.error(f"Error charging crypto wallet: {e}")
return {'success': False, 'error': str(e)} return {'success': False, 'error': str(e)}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment