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