refactor: code quality improvements across the backend
- Remove insecure SSL verification bypass in attendance.py - Add gunicorn as production WSGI server (Dockerfile + entrypoint) - Fix silent data loss in reconciliation (log + surface unmatched members) - Add required column validation in payment sheet parsing - Add input validation on /qr route (account format, amount bounds, SPD injection) - Centralize configuration into scripts/config.py - Extract credentials path to env-configurable constant - Hide unmatched transactions from reconcile-juniors page - Fix test mocks to bypass cache layer (all 8 tests now pass reliably) - Add pytest + pytest-cov dev dependencies - Fix typo "Inffering" in infer_payments.py - Update CLAUDE.md to reflect current project state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
38
app.py
38
app.py
@@ -17,8 +17,12 @@ logging.basicConfig(level=getattr(logging, log_level, logging.INFO), format='%(a
|
||||
scripts_dir = Path(__file__).parent / "scripts"
|
||||
sys.path.append(str(scripts_dir))
|
||||
|
||||
from attendance import get_members_with_fees, get_junior_members_with_fees, SHEET_ID as ATTENDANCE_SHEET_ID, JUNIOR_SHEET_GID, ADULT_MERGED_MONTHS, JUNIOR_MERGED_MONTHS
|
||||
from match_payments import reconcile, fetch_sheet_data, fetch_exceptions, normalize, DEFAULT_SPREADSHEET_ID as PAYMENTS_SHEET_ID
|
||||
from config import (
|
||||
ATTENDANCE_SHEET_ID, PAYMENTS_SHEET_ID, JUNIOR_SHEET_GID,
|
||||
BANK_ACCOUNT, CREDENTIALS_PATH,
|
||||
)
|
||||
from attendance import get_members_with_fees, get_junior_members_with_fees, ADULT_MERGED_MONTHS, JUNIOR_MERGED_MONTHS
|
||||
from match_payments import reconcile, fetch_sheet_data, fetch_exceptions, normalize
|
||||
from cache_utils import get_sheet_modified_time, read_cache, write_cache, _LAST_CHECKED
|
||||
|
||||
def get_cached_data(cache_key, sheet_id, fetch_func, *args, serialize=None, deserialize=None, **kwargs):
|
||||
@@ -53,9 +57,6 @@ def get_month_labels(sorted_months, merged_months):
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# Bank account for QR code payments (can be overridden by ENV)
|
||||
BANK_ACCOUNT = os.environ.get("BANK_ACCOUNT", "CZ8520100000002800359168")
|
||||
|
||||
@app.before_request
|
||||
def start_timer():
|
||||
g.start_time = time.perf_counter()
|
||||
@@ -110,7 +111,7 @@ def fees():
|
||||
monthly_totals = {m: 0 for m in sorted_months}
|
||||
|
||||
# Get exceptions for formatting
|
||||
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
||||
credentials_path = CREDENTIALS_PATH
|
||||
exceptions = get_cached_data(
|
||||
"exceptions_dict", PAYMENTS_SHEET_ID, fetch_exceptions,
|
||||
PAYMENTS_SHEET_ID, credentials_path,
|
||||
@@ -173,7 +174,7 @@ def fees_juniors():
|
||||
monthly_totals = {m: 0 for m in sorted_months}
|
||||
|
||||
# Get exceptions for formatting (reusing payments sheet)
|
||||
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
||||
credentials_path = CREDENTIALS_PATH
|
||||
exceptions = get_cached_data(
|
||||
"exceptions_dict", PAYMENTS_SHEET_ID, fetch_exceptions,
|
||||
PAYMENTS_SHEET_ID, credentials_path,
|
||||
@@ -241,7 +242,7 @@ def reconcile_view():
|
||||
payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
|
||||
|
||||
# Use hardcoded credentials path for now, consistent with other scripts
|
||||
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
||||
credentials_path = CREDENTIALS_PATH
|
||||
|
||||
members_data = get_cached_data("attendance_regular", ATTENDANCE_SHEET_ID, get_members_with_fees)
|
||||
record_step("fetch_members")
|
||||
@@ -339,7 +340,7 @@ def reconcile_juniors_view():
|
||||
attendance_url = f"https://docs.google.com/spreadsheets/d/{ATTENDANCE_SHEET_ID}/edit#gid={JUNIOR_SHEET_GID}"
|
||||
payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
|
||||
|
||||
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
||||
credentials_path = CREDENTIALS_PATH
|
||||
|
||||
junior_members_data = get_cached_data("attendance_juniors", ATTENDANCE_SHEET_ID, get_junior_members_with_fees)
|
||||
record_step("fetch_junior_members")
|
||||
@@ -429,9 +430,8 @@ def reconcile_juniors_view():
|
||||
# Format credits and debts
|
||||
credits = sorted([{"name": n, "amount": a["total_balance"]} for n, a in result["members"].items() if a["total_balance"] > 0], key=lambda x: x["name"])
|
||||
debts = sorted([{"name": n, "amount": abs(a["total_balance"])} for n, a in result["members"].items() if a["total_balance"] < 0], key=lambda x: x["name"])
|
||||
unmatched = result["unmatched"]
|
||||
import json
|
||||
|
||||
|
||||
record_step("process_data")
|
||||
|
||||
return render_template(
|
||||
@@ -443,7 +443,7 @@ def reconcile_juniors_view():
|
||||
month_labels_json=json.dumps(month_labels),
|
||||
credits=credits,
|
||||
debts=debts,
|
||||
unmatched=unmatched,
|
||||
unmatched=[],
|
||||
attendance_url=attendance_url,
|
||||
payments_url=payments_url,
|
||||
bank_account=BANK_ACCOUNT
|
||||
@@ -453,7 +453,7 @@ def reconcile_juniors_view():
|
||||
def payments():
|
||||
attendance_url = f"https://docs.google.com/spreadsheets/d/{ATTENDANCE_SHEET_ID}/edit"
|
||||
payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
|
||||
credentials_path = ".secret/fuj-management-bot-credentials.json"
|
||||
credentials_path = CREDENTIALS_PATH
|
||||
|
||||
transactions = get_cached_data("payments_transactions", PAYMENTS_SHEET_ID, fetch_sheet_data, PAYMENTS_SHEET_ID, credentials_path)
|
||||
record_step("fetch_payments")
|
||||
@@ -494,7 +494,11 @@ def qr_code():
|
||||
account = request.args.get("account", BANK_ACCOUNT)
|
||||
amount = request.args.get("amount", "0")
|
||||
message = request.args.get("message", "")
|
||||
|
||||
|
||||
# Validate account: allow IBAN (letters+digits) or Czech format (digits/digits)
|
||||
if not re.match(r'^[A-Z]{2}\d{2,34}$|^\d{1,16}/\d{4}$', account):
|
||||
account = BANK_ACCOUNT
|
||||
|
||||
# QR Platba standard: SPD*1.0*ACC:accountNumber*BC:bankCode*AM:amount*CC:CZK*MSG:message
|
||||
acc_parts = account.split('/')
|
||||
if len(acc_parts) == 2:
|
||||
@@ -504,12 +508,14 @@ def qr_code():
|
||||
|
||||
try:
|
||||
amt_val = float(amount)
|
||||
if amt_val < 0 or amt_val > 10_000_000:
|
||||
amt_val = 0
|
||||
amt_str = f"{amt_val:.2f}"
|
||||
except ValueError:
|
||||
amt_str = "0.00"
|
||||
|
||||
# Message max 60 characters
|
||||
msg_str = message[:60]
|
||||
# Message max 60 characters, strip SPD delimiters to prevent injection
|
||||
msg_str = message[:60].replace("*", "")
|
||||
|
||||
qr_data = f"SPD*1.0*ACC:{acc_str}*AM:{amt_str}*CC:CZK*MSG:{msg_str}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user