|
|
import os
|
|
|
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, current_app
|
|
|
from flask_login import login_required, current_user
|
|
|
import requests
|
|
|
import hashlib
|
|
|
import hmac
|
|
|
import json
|
|
|
from datetime import datetime, timedelta
|
|
|
import uuid
|
|
|
from models.subscription import Subscription
|
|
|
|
|
|
pricing_bp = Blueprint('pricing', __name__)
|
|
|
|
|
|
|
|
|
SSLCOMMERCE_STORE_ID = os.environ.get('SSLCOMMERCE_STORE_ID', 'your_store_id_here')
|
|
|
SSLCOMMERCE_STORE_PASS = os.environ.get('SSLCOMMERCE_STORE_PASS', 'your_store_password_here')
|
|
|
SSLCOMMERCE_API_URL = os.environ.get('SSLCOMMERCE_API_URL', 'https://sandbox.sslcommerz.com/gwprocess/v4/api.php')
|
|
|
SSLCOMMERCE_VALIDATION_URL = os.environ.get('SSLCOMMERCE_VALIDATION_URL', 'https://sandbox.sslcommerz.com/validator/api/validationserverAPI.php')
|
|
|
|
|
|
|
|
|
PRICING_PLANS = {
|
|
|
'basic': {
|
|
|
'id': 'basic',
|
|
|
'name': 'Basic Plan',
|
|
|
'price': 0.00,
|
|
|
'description': 'Perfect for individuals getting started',
|
|
|
'features': [
|
|
|
'Convert up to 100 images per month',
|
|
|
'Basic math equation recognition',
|
|
|
'Email support',
|
|
|
'Standard processing speed'
|
|
|
],
|
|
|
'is_free': True
|
|
|
},
|
|
|
'pro': {
|
|
|
'id': 'pro',
|
|
|
'name': 'Professional Plan',
|
|
|
'price': 24.99,
|
|
|
'description': 'Ideal for professionals and researchers',
|
|
|
'features': [
|
|
|
'Unlimited image conversions',
|
|
|
'Advanced math equation recognition',
|
|
|
'Handwritten table detection',
|
|
|
'Priority processing speed',
|
|
|
'24/7 customer support',
|
|
|
'Early access to new features'
|
|
|
],
|
|
|
'is_free': False
|
|
|
},
|
|
|
'enterprise': {
|
|
|
'id': 'enterprise',
|
|
|
'name': 'Enterprise Plan',
|
|
|
'price': 49.99,
|
|
|
'description': 'For teams and organizations',
|
|
|
'features': [
|
|
|
'Everything in Professional Plan',
|
|
|
'Team collaboration tools',
|
|
|
'Custom API access',
|
|
|
'Dedicated account manager',
|
|
|
'SLA guarantee',
|
|
|
'On-premise deployment option'
|
|
|
],
|
|
|
'is_free': False
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@pricing_bp.route('/pricing')
|
|
|
@login_required
|
|
|
def pricing():
|
|
|
"""Display pricing plans"""
|
|
|
return render_template('pricing.html', plans=PRICING_PLANS)
|
|
|
|
|
|
@pricing_bp.route('/checkout/<plan_id>')
|
|
|
@login_required
|
|
|
def checkout(plan_id):
|
|
|
"""Display checkout page for a plan"""
|
|
|
if plan_id not in PRICING_PLANS:
|
|
|
return jsonify({'error': 'Invalid plan selected'}), 400
|
|
|
|
|
|
plan = PRICING_PLANS[plan_id]
|
|
|
|
|
|
|
|
|
if plan.get('is_free', False):
|
|
|
|
|
|
start_date = datetime.now()
|
|
|
end_date = start_date + timedelta(days=30)
|
|
|
|
|
|
|
|
|
subscription = Subscription.create_or_update(
|
|
|
user_id=current_user.id,
|
|
|
plan_id=plan_id,
|
|
|
start_date=start_date.isoformat(),
|
|
|
end_date=end_date.isoformat(),
|
|
|
is_active=True
|
|
|
)
|
|
|
|
|
|
|
|
|
current_user.subscription = subscription.__dict__ if subscription else {}
|
|
|
|
|
|
return render_template('payment_success.html', plan=plan)
|
|
|
|
|
|
|
|
|
tran_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
session['transaction'] = {
|
|
|
'tran_id': tran_id,
|
|
|
'plan_id': plan_id,
|
|
|
'user_id': current_user.id,
|
|
|
'amount': plan['price'],
|
|
|
'timestamp': datetime.now().isoformat()
|
|
|
}
|
|
|
|
|
|
return render_template('checkout.html', plan=plan, transaction=session['transaction'])
|
|
|
|
|
|
@pricing_bp.route('/process-payment/<plan_id>', methods=['POST'])
|
|
|
@login_required
|
|
|
def process_payment(plan_id):
|
|
|
"""Process payment for a plan based on selected payment method"""
|
|
|
if plan_id not in PRICING_PLANS:
|
|
|
return jsonify({'error': 'Invalid plan selected'}), 400
|
|
|
|
|
|
plan = PRICING_PLANS[plan_id]
|
|
|
payment_method = request.form.get('payment_method', 'sslcommerce')
|
|
|
|
|
|
|
|
|
if plan.get('is_free', False):
|
|
|
|
|
|
start_date = datetime.now()
|
|
|
end_date = start_date + timedelta(days=30)
|
|
|
|
|
|
|
|
|
subscription = Subscription.create_or_update(
|
|
|
user_id=current_user.id,
|
|
|
plan_id=plan_id,
|
|
|
start_date=start_date.isoformat(),
|
|
|
end_date=end_date.isoformat(),
|
|
|
is_active=True
|
|
|
)
|
|
|
|
|
|
|
|
|
current_user.subscription = subscription.__dict__ if subscription else {}
|
|
|
|
|
|
return render_template('payment_success.html', plan=plan)
|
|
|
|
|
|
|
|
|
tran_id = str(uuid.uuid4())
|
|
|
|
|
|
|
|
|
session['transaction'] = {
|
|
|
'tran_id': tran_id,
|
|
|
'plan_id': plan_id,
|
|
|
'user_id': current_user.id,
|
|
|
'amount': plan['price'],
|
|
|
'timestamp': datetime.now().isoformat(),
|
|
|
'payment_method': payment_method
|
|
|
}
|
|
|
|
|
|
if payment_method == 'sslcommerce':
|
|
|
|
|
|
return process_sslcommerce_payment(plan, tran_id)
|
|
|
else:
|
|
|
|
|
|
return process_alternative_payment(plan, tran_id, payment_method)
|
|
|
|
|
|
def process_sslcommerce_payment(plan, tran_id):
|
|
|
"""Process payment through SSLCommerce"""
|
|
|
|
|
|
post_data = {
|
|
|
'store_id': SSLCOMMERCE_STORE_ID,
|
|
|
'store_passwd': SSLCOMMERCE_STORE_PASS,
|
|
|
'total_amount': plan['price'],
|
|
|
'currency': 'USD',
|
|
|
'tran_id': tran_id,
|
|
|
'success_url': url_for('pricing.payment_success', _external=True),
|
|
|
'fail_url': url_for('pricing.payment_failed', _external=True),
|
|
|
'cancel_url': url_for('pricing.payment_cancelled', _external=True),
|
|
|
'ipn_url': url_for('pricing.payment_ipn', _external=True),
|
|
|
'cus_name': current_user.name,
|
|
|
'cus_email': current_user.email,
|
|
|
'cus_add1': 'Dhaka',
|
|
|
'cus_add2': 'Dhaka',
|
|
|
'cus_city': 'Dhaka',
|
|
|
'cus_state': 'Dhaka',
|
|
|
'cus_postcode': '1000',
|
|
|
'cus_country': 'Bangladesh',
|
|
|
'cus_phone': '01711111111',
|
|
|
'cus_fax': '01711111111',
|
|
|
'ship_name': current_user.name,
|
|
|
'ship_add1': 'Dhaka',
|
|
|
'ship_add2': 'Dhaka',
|
|
|
'ship_city': 'Dhaka',
|
|
|
'ship_state': 'Dhaka',
|
|
|
'ship_postcode': '1000',
|
|
|
'ship_country': 'Bangladesh',
|
|
|
'product_name': plan['name'],
|
|
|
'product_category': 'Software',
|
|
|
'product_profile': 'non-physical-goods',
|
|
|
'multi_card_name': '',
|
|
|
'value_a': plan['id'],
|
|
|
'value_b': current_user.id,
|
|
|
'value_c': '',
|
|
|
'value_d': ''
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
|
|
|
response = requests.post(SSLCOMMERCE_API_URL, data=post_data)
|
|
|
response_data = response.json()
|
|
|
|
|
|
if response_data.get('status') == 'SUCCESS':
|
|
|
|
|
|
return redirect(response_data['GatewayPageURL'])
|
|
|
else:
|
|
|
return jsonify({'error': 'Payment gateway error', 'details': response_data}), 500
|
|
|
|
|
|
except Exception as e:
|
|
|
return jsonify({'error': 'Failed to initiate payment', 'details': str(e)}), 500
|
|
|
|
|
|
def process_alternative_payment(plan, tran_id, payment_method):
|
|
|
"""Process payment through alternative methods (Nagad, Rocket, Bank)"""
|
|
|
|
|
|
|
|
|
|
|
|
instructions = {
|
|
|
'nagad': {
|
|
|
'title': 'Nagad Payment Instructions',
|
|
|
'steps': [
|
|
|
'Dial *167# from your Nagad registered mobile number',
|
|
|
'Select "Send Money"',
|
|
|
'Enter the merchant number: 017XXXXXXXX',
|
|
|
f'Enter the amount: {plan["price"]} BDT',
|
|
|
f'Enter reference: {tran_id}',
|
|
|
'Confirm the transaction with your Nagad PIN',
|
|
|
'You will receive a confirmation SMS'
|
|
|
],
|
|
|
'confirmation': 'After completing the payment, please click the confirmation button below.'
|
|
|
},
|
|
|
'rocket': {
|
|
|
'title': 'Rocket Payment Instructions',
|
|
|
'steps': [
|
|
|
'Dial *322# from your Rocket registered mobile number',
|
|
|
'Select "Send Money"',
|
|
|
'Enter the merchant number: 017XXXXXXXX',
|
|
|
f'Enter the amount: {plan["price"]} BDT',
|
|
|
f'Enter reference: {tran_id}',
|
|
|
'Confirm the transaction with your Rocket PIN',
|
|
|
'You will receive a confirmation SMS'
|
|
|
],
|
|
|
'confirmation': 'After completing the payment, please click the confirmation button below.'
|
|
|
},
|
|
|
'bank': {
|
|
|
'title': 'Bank Transfer Instructions',
|
|
|
'steps': [
|
|
|
f'Transfer {plan["price"]} BDT to the following bank account:',
|
|
|
'Bank: ABC Bank Ltd.',
|
|
|
'Account Name: TexLab Services',
|
|
|
'Account Number: 1234567890',
|
|
|
'Routing Number: 123456789',
|
|
|
f'Include this reference in the transfer description: {tran_id}',
|
|
|
'Email the transaction receipt to payments@texlab.com'
|
|
|
],
|
|
|
'confirmation': 'After completing the payment, please click the confirmation button below.'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
instruction_data = instructions.get(payment_method, {})
|
|
|
return render_template('manual_payment.html',
|
|
|
instructions=instruction_data,
|
|
|
plan=plan,
|
|
|
tran_id=tran_id,
|
|
|
method=payment_method,
|
|
|
plan_id=plan['id'])
|
|
|
|
|
|
@pricing_bp.route('/payment/manual-confirm')
|
|
|
@login_required
|
|
|
def manual_payment_confirm():
|
|
|
"""Confirm manual payment (Nagad, Rocket, Bank)"""
|
|
|
payment_method = request.args.get('method')
|
|
|
plan_id = request.args.get('plan_id')
|
|
|
|
|
|
if not payment_method or not plan_id:
|
|
|
return redirect(url_for('pricing.pricing'))
|
|
|
|
|
|
if 'transaction' not in session:
|
|
|
return redirect(url_for('pricing.pricing'))
|
|
|
|
|
|
transaction = session['transaction']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
start_date = datetime.now()
|
|
|
end_date = start_date + timedelta(days=30)
|
|
|
|
|
|
|
|
|
subscription = Subscription.create_or_update(
|
|
|
user_id=transaction['user_id'],
|
|
|
plan_id=transaction['plan_id'],
|
|
|
start_date=start_date.isoformat(),
|
|
|
end_date=end_date.isoformat(),
|
|
|
is_active=True
|
|
|
)
|
|
|
|
|
|
|
|
|
current_user.subscription = subscription.__dict__ if subscription else {}
|
|
|
|
|
|
|
|
|
session.pop('transaction', None)
|
|
|
|
|
|
return render_template('payment_success.html', plan=PRICING_PLANS[transaction['plan_id']])
|
|
|
|
|
|
@pricing_bp.route('/payment/success')
|
|
|
@login_required
|
|
|
def payment_success():
|
|
|
"""Handle successful payment"""
|
|
|
|
|
|
if 'transaction' not in session:
|
|
|
return redirect(url_for('pricing.pricing'))
|
|
|
|
|
|
transaction = session['transaction']
|
|
|
|
|
|
|
|
|
params = request.args.to_dict()
|
|
|
|
|
|
|
|
|
if verify_payment(params):
|
|
|
|
|
|
start_date = datetime.now()
|
|
|
end_date = start_date + timedelta(days=30)
|
|
|
|
|
|
|
|
|
subscription = Subscription.create_or_update(
|
|
|
user_id=transaction['user_id'],
|
|
|
plan_id=transaction['plan_id'],
|
|
|
start_date=start_date.isoformat(),
|
|
|
end_date=end_date.isoformat(),
|
|
|
is_active=True
|
|
|
)
|
|
|
|
|
|
|
|
|
current_user.subscription = subscription.__dict__ if subscription else {}
|
|
|
|
|
|
|
|
|
session.pop('transaction', None)
|
|
|
|
|
|
return render_template('payment_success.html', plan=PRICING_PLANS[transaction['plan_id']])
|
|
|
else:
|
|
|
return redirect(url_for('pricing.payment_failed'))
|
|
|
|
|
|
@pricing_bp.route('/payment/failed')
|
|
|
@login_required
|
|
|
def payment_failed():
|
|
|
"""Handle failed payment"""
|
|
|
session.pop('transaction', None)
|
|
|
return render_template('payment_failed.html')
|
|
|
|
|
|
@pricing_bp.route('/payment/cancelled')
|
|
|
@login_required
|
|
|
def payment_cancelled():
|
|
|
"""Handle cancelled payment"""
|
|
|
session.pop('transaction', None)
|
|
|
return render_template('payment_cancelled.html')
|
|
|
|
|
|
@pricing_bp.route('/payment/ipn', methods=['POST'])
|
|
|
def payment_ipn():
|
|
|
"""Handle IPN (Instant Payment Notification) from SSLCommerce"""
|
|
|
|
|
|
|
|
|
data = request.form.to_dict()
|
|
|
|
|
|
|
|
|
if verify_payment(data):
|
|
|
|
|
|
|
|
|
pass
|
|
|
|
|
|
return jsonify({'status': 'success'})
|
|
|
|
|
|
def verify_payment(params):
|
|
|
"""Verify payment with SSLCommerce"""
|
|
|
if 'val_id' not in params:
|
|
|
return False
|
|
|
|
|
|
validation_url = f"{SSLCOMMERCE_VALIDATION_URL}?val_id={params['val_id']}&store_id={SSLCOMMERCE_STORE_ID}&store_passwd={SSLCOMMERCE_STORE_PASS}&v=1&format=json"
|
|
|
|
|
|
try:
|
|
|
response = requests.get(validation_url)
|
|
|
data = response.json()
|
|
|
|
|
|
if data.get('status') == 'VALID' or data.get('status') == 'VALIDATED':
|
|
|
return True
|
|
|
else:
|
|
|
return False
|
|
|
except Exception:
|
|
|
return False
|
|
|
|
|
|
def update_user_plan(user_id, plan_id):
|
|
|
"""Update user's subscription plan in database"""
|
|
|
|
|
|
start_date = datetime.now()
|
|
|
end_date = start_date + timedelta(days=30)
|
|
|
|
|
|
|
|
|
subscription = Subscription.create_or_update(
|
|
|
user_id=user_id,
|
|
|
plan_id=plan_id,
|
|
|
start_date=start_date.isoformat(),
|
|
|
end_date=end_date.isoformat(),
|
|
|
is_active=True
|
|
|
)
|
|
|
|
|
|
return subscription
|
|
|
|
|
|
@pricing_bp.route('/current-plan')
|
|
|
@login_required
|
|
|
def current_plan():
|
|
|
"""Get current user's plan"""
|
|
|
subscription = Subscription.get_by_user_id(current_user.id)
|
|
|
if subscription and subscription.is_active:
|
|
|
return jsonify({
|
|
|
'plan': subscription.plan_id,
|
|
|
'expires': subscription.end_date
|
|
|
})
|
|
|
else:
|
|
|
return jsonify({
|
|
|
'plan': 'basic',
|
|
|
'expires': None
|
|
|
}) |