Initial commit
This commit is contained in:
commit
983cee0320
322 changed files with 57174 additions and 0 deletions
304
backup/first -fina app/app/routes/subscriptions.py
Normal file
304
backup/first -fina app/app/routes/subscriptions.py
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify
|
||||
from flask_login import login_required, current_user
|
||||
from app import db
|
||||
from app.models.subscription import Subscription, RecurringPattern
|
||||
from app.models.category import Category
|
||||
from app.smart_detection import (
|
||||
detect_recurring_expenses,
|
||||
save_detected_patterns,
|
||||
get_user_suggestions,
|
||||
convert_pattern_to_subscription,
|
||||
dismiss_pattern
|
||||
)
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
bp = Blueprint('subscriptions', __name__, url_prefix='/subscriptions')
|
||||
|
||||
@bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
"""View all subscriptions and suggestions"""
|
||||
subscriptions = Subscription.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
is_active=True
|
||||
).order_by(Subscription.next_due_date).all()
|
||||
|
||||
suggestions = get_user_suggestions(current_user.id)
|
||||
|
||||
# Calculate total monthly cost
|
||||
monthly_cost = sum(
|
||||
sub.amount if sub.frequency == 'monthly' else
|
||||
sub.amount / 4 if sub.frequency == 'quarterly' else
|
||||
sub.amount / 12 if sub.frequency == 'yearly' else
|
||||
sub.amount * 4 if sub.frequency == 'weekly' else
|
||||
sub.amount * 2 if sub.frequency == 'biweekly' else
|
||||
sub.amount
|
||||
for sub in subscriptions
|
||||
)
|
||||
|
||||
yearly_cost = sum(sub.get_annual_cost() for sub in subscriptions)
|
||||
|
||||
return render_template('subscriptions/index.html',
|
||||
subscriptions=subscriptions,
|
||||
suggestions=suggestions,
|
||||
monthly_cost=monthly_cost,
|
||||
yearly_cost=yearly_cost)
|
||||
|
||||
|
||||
@bp.route('/detect', methods=['POST'])
|
||||
@login_required
|
||||
def detect():
|
||||
"""Run detection algorithm to find recurring expenses"""
|
||||
patterns = detect_recurring_expenses(current_user.id)
|
||||
|
||||
if patterns:
|
||||
saved = save_detected_patterns(patterns)
|
||||
flash(f'Found {saved} potential subscription(s)!', 'success')
|
||||
else:
|
||||
flash('No recurring patterns detected. Add more expenses to improve detection.', 'info')
|
||||
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
|
||||
|
||||
@bp.route('/create', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def create():
|
||||
"""Manually create a subscription"""
|
||||
if request.method == 'POST':
|
||||
name = request.form.get('name')
|
||||
amount = float(request.form.get('amount', 0))
|
||||
frequency = request.form.get('frequency')
|
||||
custom_interval_days = request.form.get('custom_interval_days')
|
||||
category_id = request.form.get('category_id')
|
||||
start_date = request.form.get('start_date')
|
||||
end_date = request.form.get('end_date')
|
||||
total_occurrences = request.form.get('total_occurrences')
|
||||
auto_create_expense = request.form.get('auto_create_expense') == 'on'
|
||||
notes = request.form.get('notes')
|
||||
|
||||
# Validate custom interval
|
||||
if frequency == 'custom':
|
||||
if not custom_interval_days:
|
||||
flash('Custom interval is required when using custom frequency', 'error')
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('subscriptions/create.html', categories=categories)
|
||||
|
||||
interval_value = int(custom_interval_days)
|
||||
if interval_value < 1 or interval_value > 365:
|
||||
flash('Custom interval must be between 1 and 365 days', 'error')
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('subscriptions/create.html', categories=categories)
|
||||
|
||||
# Parse dates
|
||||
start_date_obj = datetime.strptime(start_date, '%Y-%m-%d').date() if start_date else datetime.now().date()
|
||||
end_date_obj = datetime.strptime(end_date, '%Y-%m-%d').date() if end_date else None
|
||||
|
||||
subscription = Subscription(
|
||||
name=name,
|
||||
amount=amount,
|
||||
frequency=frequency,
|
||||
custom_interval_days=int(custom_interval_days) if custom_interval_days and frequency == 'custom' else None,
|
||||
category_id=category_id,
|
||||
user_id=current_user.id,
|
||||
start_date=start_date_obj,
|
||||
next_due_date=start_date_obj,
|
||||
end_date=end_date_obj,
|
||||
total_occurrences=int(total_occurrences) if total_occurrences else None,
|
||||
auto_create_expense=auto_create_expense,
|
||||
notes=notes,
|
||||
is_confirmed=True,
|
||||
auto_detected=False
|
||||
)
|
||||
|
||||
db.session.add(subscription)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Subscription "{name}" added successfully!', 'success')
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('subscriptions/create.html', categories=categories)
|
||||
|
||||
|
||||
@bp.route('/<int:subscription_id>/edit', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit(subscription_id):
|
||||
"""Edit a subscription"""
|
||||
subscription = Subscription.query.filter_by(
|
||||
id=subscription_id,
|
||||
user_id=current_user.id
|
||||
).first_or_404()
|
||||
|
||||
if request.method == 'POST':
|
||||
frequency = request.form.get('frequency')
|
||||
custom_interval_days = request.form.get('custom_interval_days')
|
||||
|
||||
# Validate custom interval
|
||||
if frequency == 'custom':
|
||||
if not custom_interval_days:
|
||||
flash('Custom interval is required when using custom frequency', 'error')
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('subscriptions/edit.html', subscription=subscription, categories=categories)
|
||||
|
||||
interval_value = int(custom_interval_days)
|
||||
if interval_value < 1 or interval_value > 365:
|
||||
flash('Custom interval must be between 1 and 365 days', 'error')
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('subscriptions/edit.html', subscription=subscription, categories=categories)
|
||||
|
||||
subscription.name = request.form.get('name')
|
||||
subscription.amount = float(request.form.get('amount', 0))
|
||||
subscription.frequency = frequency
|
||||
subscription.custom_interval_days = int(custom_interval_days) if custom_interval_days and frequency == 'custom' else None
|
||||
subscription.category_id = request.form.get('category_id')
|
||||
subscription.auto_create_expense = request.form.get('auto_create_expense') == 'on'
|
||||
subscription.notes = request.form.get('notes')
|
||||
|
||||
next_due_date = request.form.get('next_due_date')
|
||||
if next_due_date:
|
||||
subscription.next_due_date = datetime.strptime(next_due_date, '%Y-%m-%d').date()
|
||||
|
||||
end_date = request.form.get('end_date')
|
||||
subscription.end_date = datetime.strptime(end_date, '%Y-%m-%d').date() if end_date else None
|
||||
|
||||
total_occurrences = request.form.get('total_occurrences')
|
||||
subscription.total_occurrences = int(total_occurrences) if total_occurrences else None
|
||||
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Subscription "{subscription.name}" updated!', 'success')
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
|
||||
categories = Category.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('subscriptions/edit.html',
|
||||
subscription=subscription,
|
||||
categories=categories)
|
||||
|
||||
|
||||
@bp.route('/<int:subscription_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
def delete(subscription_id):
|
||||
"""Delete a subscription"""
|
||||
subscription = Subscription.query.filter_by(
|
||||
id=subscription_id,
|
||||
user_id=current_user.id
|
||||
).first_or_404()
|
||||
|
||||
name = subscription.name
|
||||
db.session.delete(subscription)
|
||||
db.session.commit()
|
||||
|
||||
flash(f'Subscription "{name}" deleted!', 'success')
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
|
||||
|
||||
@bp.route('/<int:subscription_id>/toggle', methods=['POST'])
|
||||
@login_required
|
||||
def toggle(subscription_id):
|
||||
"""Toggle subscription active status"""
|
||||
subscription = Subscription.query.filter_by(
|
||||
id=subscription_id,
|
||||
user_id=current_user.id
|
||||
).first_or_404()
|
||||
|
||||
subscription.is_active = not subscription.is_active
|
||||
db.session.commit()
|
||||
|
||||
status = 'activated' if subscription.is_active else 'paused'
|
||||
flash(f'Subscription "{subscription.name}" {status}!', 'success')
|
||||
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
|
||||
|
||||
@bp.route('/suggestion/<int:pattern_id>/accept', methods=['POST'])
|
||||
@login_required
|
||||
def accept_suggestion(pattern_id):
|
||||
"""Accept a detected pattern and convert to subscription"""
|
||||
subscription = convert_pattern_to_subscription(pattern_id, current_user.id)
|
||||
|
||||
if subscription:
|
||||
flash(f'Subscription "{subscription.name}" added!', 'success')
|
||||
else:
|
||||
flash('Could not add subscription.', 'error')
|
||||
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
|
||||
|
||||
@bp.route('/suggestion/<int:pattern_id>/dismiss', methods=['POST'])
|
||||
@login_required
|
||||
def dismiss_suggestion(pattern_id):
|
||||
"""Dismiss a detected pattern"""
|
||||
if dismiss_pattern(pattern_id, current_user.id):
|
||||
flash('Suggestion dismissed.', 'info')
|
||||
else:
|
||||
flash('Could not dismiss suggestion.', 'error')
|
||||
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
|
||||
|
||||
@bp.route('/api/upcoming')
|
||||
@login_required
|
||||
def api_upcoming():
|
||||
"""API endpoint for upcoming subscriptions"""
|
||||
days = int(request.args.get('days', 30))
|
||||
|
||||
end_date = datetime.now().date() + timedelta(days=days)
|
||||
|
||||
upcoming = Subscription.query.filter(
|
||||
Subscription.user_id == current_user.id,
|
||||
Subscription.is_active == True,
|
||||
Subscription.next_due_date <= end_date
|
||||
).order_by(Subscription.next_due_date).all()
|
||||
|
||||
return jsonify({
|
||||
'subscriptions': [{
|
||||
'id': sub.id,
|
||||
'name': sub.name,
|
||||
'amount': float(sub.amount),
|
||||
'next_due_date': sub.next_due_date.isoformat(),
|
||||
'days_until': (sub.next_due_date - datetime.now().date()).days
|
||||
} for sub in upcoming]
|
||||
})
|
||||
|
||||
|
||||
@bp.route('/auto-create', methods=['POST'])
|
||||
@login_required
|
||||
def auto_create_expenses():
|
||||
"""Auto-create expenses for due subscriptions (can be run via cron)"""
|
||||
from app.models.category import Expense
|
||||
|
||||
subscriptions = Subscription.query.filter_by(
|
||||
user_id=current_user.id,
|
||||
is_active=True,
|
||||
auto_create_expense=True
|
||||
).all()
|
||||
|
||||
created_count = 0
|
||||
|
||||
for sub in subscriptions:
|
||||
if sub.should_create_expense_today():
|
||||
# Create the expense
|
||||
expense = Expense(
|
||||
amount=sub.amount,
|
||||
description=f"{sub.name} (Auto-created)",
|
||||
date=datetime.now().date(),
|
||||
category_id=sub.category_id,
|
||||
user_id=current_user.id
|
||||
)
|
||||
|
||||
db.session.add(expense)
|
||||
|
||||
# Update subscription
|
||||
sub.last_auto_created = datetime.now().date()
|
||||
sub.advance_next_due_date()
|
||||
|
||||
created_count += 1
|
||||
|
||||
db.session.commit()
|
||||
|
||||
if created_count > 0:
|
||||
flash(f'Auto-created {created_count} expense(s) from subscriptions!', 'success')
|
||||
else:
|
||||
flash('No expenses due for auto-creation today.', 'info')
|
||||
|
||||
return redirect(url_for('subscriptions.index'))
|
||||
Loading…
Add table
Add a link
Reference in a new issue