From 1310f9a6c7d8cc5b43d1d5c785b9a80e4c0ca371 Mon Sep 17 00:00:00 2001 From: Alan Woodman Date: Wed, 20 Aug 2025 13:25:39 +0800 Subject: [PATCH] Initial push --- app.py | 64 + bin/Activate.ps1 | 247 ++++ bin/activate | 70 ++ bin/activate.csh | 27 + bin/activate.fish | 69 ++ bin/alembic | 8 + bin/flask | 8 + bin/mako-render | 8 + bin/normalizer | 8 + bin/pip | 8 + bin/pip3 | 8 + bin/pip3.12 | 8 + bin/python | 1 + bin/python3 | 1 + bin/python3.12 | 1 + bin/wheel | 8 + blueprints/__init__.py | 0 blueprints/auth.py | 74 ++ blueprints/main.py | 871 ++++++++++++++ config.py | 39 + include/site/python3.12/greenlet/greenlet.h | 164 +++ lib64 | 1 + migrations/README | 1 + migrations/alembic.ini | 50 + migrations/env.py | 113 ++ migrations/script.py.mako | 24 + .../versions/1b403a365765_add_new_features.py | 46 + .../versions/3252db86eaae_add_new_features.py | 42 + ..._initial_migration_with_users_payments_.py | 73 ++ .../versions/50157fcf55e4_add_new_features.py | 32 + .../versions/6a841af4c236_add_new_features.py | 32 + .../versions/906059746902_add_new_features.py | 38 + .../versions/9d9195d6b9a7_add_new_features.py | 48 + .../versions/ed07e785afd5_add_new_features.py | 48 + models.py | 102 ++ pyvenv.cfg | 5 + query_mysql - Copy.py | 532 +++++++++ query_mysql.py | 672 +++++++++++ requirements.txt | 7 + services.py | 229 ++++ splynx.py | 133 +++ static/css/custom.css | 332 ++++++ static/images/plutus3.JPG | Bin 0 -> 210957 bytes stripe_payment_processor.py | 1041 +++++++++++++++++ templates/auth/add_user.html | 76 ++ templates/auth/list_users.html | 58 + templates/auth/login.html | 46 + templates/base.html | 154 +++ templates/main/batch_detail.html | 609 ++++++++++ templates/main/batch_list.html | 91 ++ templates/main/index.html | 33 + templates/main/payment_plans_detail.html | 403 +++++++ templates/main/payment_plans_form.html | 551 +++++++++ templates/main/payment_plans_list.html | 267 +++++ templates/main/single_payment.html | 554 +++++++++ templates/main/single_payment_detail.html | 410 +++++++ templates/main/single_payments_list.html | 514 ++++++++ test_logging.py | 121 ++ 58 files changed, 9180 insertions(+) create mode 100644 app.py create mode 100644 bin/Activate.ps1 create mode 100644 bin/activate create mode 100644 bin/activate.csh create mode 100644 bin/activate.fish create mode 100755 bin/alembic create mode 100755 bin/flask create mode 100755 bin/mako-render create mode 100755 bin/normalizer create mode 100755 bin/pip create mode 100755 bin/pip3 create mode 100755 bin/pip3.12 create mode 120000 bin/python create mode 120000 bin/python3 create mode 120000 bin/python3.12 create mode 100755 bin/wheel create mode 100644 blueprints/__init__.py create mode 100644 blueprints/auth.py create mode 100644 blueprints/main.py create mode 100644 config.py create mode 100644 include/site/python3.12/greenlet/greenlet.h create mode 120000 lib64 create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/1b403a365765_add_new_features.py create mode 100644 migrations/versions/3252db86eaae_add_new_features.py create mode 100644 migrations/versions/455cbec206cf_initial_migration_with_users_payments_.py create mode 100644 migrations/versions/50157fcf55e4_add_new_features.py create mode 100644 migrations/versions/6a841af4c236_add_new_features.py create mode 100644 migrations/versions/906059746902_add_new_features.py create mode 100644 migrations/versions/9d9195d6b9a7_add_new_features.py create mode 100644 migrations/versions/ed07e785afd5_add_new_features.py create mode 100644 models.py create mode 100644 pyvenv.cfg create mode 100644 query_mysql - Copy.py create mode 100644 query_mysql.py create mode 100644 requirements.txt create mode 100644 services.py create mode 100644 splynx.py create mode 100644 static/css/custom.css create mode 100644 static/images/plutus3.JPG create mode 100644 stripe_payment_processor.py create mode 100644 templates/auth/add_user.html create mode 100644 templates/auth/list_users.html create mode 100644 templates/auth/login.html create mode 100644 templates/base.html create mode 100644 templates/main/batch_detail.html create mode 100644 templates/main/batch_list.html create mode 100644 templates/main/index.html create mode 100644 templates/main/payment_plans_detail.html create mode 100644 templates/main/payment_plans_form.html create mode 100644 templates/main/payment_plans_list.html create mode 100644 templates/main/single_payment.html create mode 100644 templates/main/single_payment_detail.html create mode 100644 templates/main/single_payments_list.html create mode 100644 test_logging.py diff --git a/app.py b/app.py new file mode 100644 index 0000000..e52cde4 --- /dev/null +++ b/app.py @@ -0,0 +1,64 @@ +from flask import Flask +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_login import LoginManager +import pymysql +from config import Config + +db = SQLAlchemy() +migrate = Migrate() +login_manager = LoginManager() + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + # Initialize extensions + db.init_app(app) + migrate.init_app(app, db) + login_manager.init_app(app) + login_manager.login_view = 'auth.login' + login_manager.login_message = 'Please log in to access this page.' + + # MySQL connection (read-only) - initialized when needed + def get_mysql_connection(): + if not hasattr(app, 'mysql_connection') or app.mysql_connection is None: + try: + app.mysql_connection = pymysql.connect( + host=app.config['MYSQL_CONFIG']['host'], + database=app.config['MYSQL_CONFIG']['database'], + user=app.config['MYSQL_CONFIG']['user'], + password=app.config['MYSQL_CONFIG']['password'], + port=app.config['MYSQL_CONFIG']['port'], + autocommit=False # Ensure read-only behavior + ) + except Exception as e: + print(f"MySQL connection failed: {e}") + app.mysql_connection = None + return app.mysql_connection + + # Make connection function available to app context + app.get_mysql_connection = get_mysql_connection + + # Register blueprints + from blueprints.auth import auth_bp + from blueprints.main import main_bp + + app.register_blueprint(auth_bp, url_prefix='/auth') + app.register_blueprint(main_bp) + + # User loader for Flask-Login + from models import Users + + @login_manager.user_loader + def load_user(user_id): + return Users.query.get(int(user_id)) + + # Note: Database tables will be managed by Flask-Migrate + # Use 'flask db init', 'flask db migrate', 'flask db upgrade' commands + + return app + +if __name__ == '__main__': + app = create_app() + app.run(debug=True) \ No newline at end of file diff --git a/bin/Activate.ps1 b/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/bin/activate b/bin/activate new file mode 100644 index 0000000..3479f15 --- /dev/null +++ b/bin/activate @@ -0,0 +1,70 @@ +# This file must be used with "source bin/activate" *from bash* +# You cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # Call hash to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + hash -r 2> /dev/null + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +# on Windows, a path can contain colons and backslashes and has to be converted: +if [ "${OSTYPE:-}" = "cygwin" ] || [ "${OSTYPE:-}" = "msys" ] ; then + # transform D:\path\to\venv to /d/path/to/venv on MSYS + # and to /cygdrive/d/path/to/venv on Cygwin + export VIRTUAL_ENV=$(cygpath /home/alan/python_projects/plutus/plutus) +else + # use the path as-is + export VIRTUAL_ENV=/home/alan/python_projects/plutus/plutus +fi + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(plutus) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(plutus) ' + export VIRTUAL_ENV_PROMPT +fi + +# Call hash to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +hash -r 2> /dev/null diff --git a/bin/activate.csh b/bin/activate.csh new file mode 100644 index 0000000..7ddaacb --- /dev/null +++ b/bin/activate.csh @@ -0,0 +1,27 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. + +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /home/alan/python_projects/plutus/plutus + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(plutus) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(plutus) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/bin/activate.fish b/bin/activate.fish new file mode 100644 index 0000000..2807203 --- /dev/null +++ b/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/). You cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /home/alan/python_projects/plutus/plutus + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(plutus) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(plutus) ' +end diff --git a/bin/alembic b/bin/alembic new file mode 100755 index 0000000..b66fa29 --- /dev/null +++ b/bin/alembic @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from alembic.config import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/bin/flask b/bin/flask new file mode 100755 index 0000000..261e444 --- /dev/null +++ b/bin/flask @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from flask.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/bin/mako-render b/bin/mako-render new file mode 100755 index 0000000..22fb403 --- /dev/null +++ b/bin/mako-render @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from mako.cmd import cmdline +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cmdline()) diff --git a/bin/normalizer b/bin/normalizer new file mode 100755 index 0000000..51e1b7d --- /dev/null +++ b/bin/normalizer @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli.cli_detect()) diff --git a/bin/pip b/bin/pip new file mode 100755 index 0000000..f9834e8 --- /dev/null +++ b/bin/pip @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/bin/pip3 b/bin/pip3 new file mode 100755 index 0000000..f9834e8 --- /dev/null +++ b/bin/pip3 @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/bin/pip3.12 b/bin/pip3.12 new file mode 100755 index 0000000..f9834e8 --- /dev/null +++ b/bin/pip3.12 @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/bin/python b/bin/python new file mode 120000 index 0000000..11b9d88 --- /dev/null +++ b/bin/python @@ -0,0 +1 @@ +python3.12 \ No newline at end of file diff --git a/bin/python3 b/bin/python3 new file mode 120000 index 0000000..11b9d88 --- /dev/null +++ b/bin/python3 @@ -0,0 +1 @@ +python3.12 \ No newline at end of file diff --git a/bin/python3.12 b/bin/python3.12 new file mode 120000 index 0000000..dc92e12 --- /dev/null +++ b/bin/python3.12 @@ -0,0 +1 @@ +/usr/bin/python3.12 \ No newline at end of file diff --git a/bin/wheel b/bin/wheel new file mode 100755 index 0000000..80fbf2d --- /dev/null +++ b/bin/wheel @@ -0,0 +1,8 @@ +#!/home/alan/python_projects/plutus/plutus/bin/python3.12 +# -*- coding: utf-8 -*- +import re +import sys +from wheel.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/blueprints/__init__.py b/blueprints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blueprints/auth.py b/blueprints/auth.py new file mode 100644 index 0000000..a5ab059 --- /dev/null +++ b/blueprints/auth.py @@ -0,0 +1,74 @@ +from flask import Blueprint, render_template, request, redirect, url_for, flash +from flask_login import login_user, logout_user, login_required, current_user +from werkzeug.security import generate_password_hash, check_password_hash +from models import Users +from app import db + +auth_bp = Blueprint('auth', __name__) + +@auth_bp.route('/login', methods=['GET', 'POST']) +def login(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + user = Users.query.filter_by(Username=username).first() + + if user and check_password_hash(user.Password, password) and user.Enabled: + login_user(user) + next_page = request.args.get('next') + return redirect(next_page) if next_page else redirect(url_for('main.index')) + else: + flash('Invalid username or password, or account is disabled.', 'error') + + return render_template('auth/login.html') + +@auth_bp.route('/logout') +@login_required +def logout(): + logout_user() + flash('You have been logged out.', 'success') + return redirect(url_for('auth.login')) + +@auth_bp.route('/add_user', methods=['GET', 'POST']) +@login_required +def add_user(): + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + full_name = request.form['full_name'] + email = request.form['email'] + permissions = request.form.get('permissions', '') + + # Check if username already exists + existing_user = Users.query.filter_by(Username=username).first() + if existing_user: + flash('Username already exists.', 'error') + return render_template('auth/add_user.html') + + # Create new user + new_user = Users( + Username=username, + Password=generate_password_hash(password), + FullName=full_name, + Email=email, + Permissions=permissions, + Enabled=True + ) + + try: + db.session.add(new_user) + db.session.commit() + flash('User created successfully.', 'success') + return redirect(url_for('auth.list_users')) + except Exception as e: + db.session.rollback() + flash('Error creating user.', 'error') + + return render_template('auth/add_user.html') + +@auth_bp.route('/list_users') +@login_required +def list_users(): + users = Users.query.all() + return render_template('auth/list_users.html', users=users) \ No newline at end of file diff --git a/blueprints/main.py b/blueprints/main.py new file mode 100644 index 0000000..322a2f5 --- /dev/null +++ b/blueprints/main.py @@ -0,0 +1,871 @@ +from flask import Blueprint, render_template, request, jsonify, flash, redirect, url_for +from flask_login import login_required, current_user +from sqlalchemy import func, case +import json +import pymysql +from app import db +from models import PaymentBatch, Payments, SinglePayments, PaymentPlans +from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET +from stripe_payment_processor import StripePaymentProcessor +from config import Config +from services import log_activity + +splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET) + +def processPaymentResult(pay_id, result, key): + """Process payment result and update database record.""" + from datetime import datetime + + if key == "pay": + payment = db.session.query(Payments).filter(Payments.id == pay_id).first() + elif key == "singlepay": + payment = db.session.query(SinglePayments).filter(SinglePayments.id == pay_id).first() + + try: + if result.get('error') and not result.get('needs_fee_update'): + payment.Error = f"Error Type: {result['error_type']}\nError: {result['error']}" + payment.Success = result['success'] + payment.PI_JSON = json.dumps(result) + else: + if result.get('needs_fee_update'): + payment.PI_FollowUp = True + payment.Payment_Intent = result['payment_intent_id'] + payment.Success = result['success'] + if result['success'] and Config.PROCESS_LIVE and key == "singlepay": + # Only update Splynx for successful single payments in live mode + find_pay_splynx_invoices(payment.Splynx_ID) + add_payment_splynx( + splynx_id=payment.Splynx_ID, + pi_id=result['payment_intent_id'], + pay_id=payment.id, + amount=payment.Payment_Amount + ) + if result.get('payment_method_type') == "card": + payment.Payment_Method = result['estimated_fee_details']['card_display_brand'] + elif result.get('payment_method_type') == "au_becs_debit": + payment.Payment_Method = result['payment_method_type'] + if payment.PI_JSON: + combined = {**json.loads(payment.PI_JSON), **result} + payment.PI_JSON = json.dumps(combined) + else: + payment.PI_JSON = json.dumps(result) + if result.get('fee_details'): + payment.Fee_Total = result['fee_details']['total_fee'] + for fee_type in result['fee_details']['fee_breakdown']: + if fee_type['type'] == "tax": + payment.Fee_Tax = fee_type['amount'] + elif fee_type['type'] == "stripe_fee": + payment.Fee_Stripe = fee_type['amount'] + except Exception as e: + print(f"processPaymentResult error: {e}\n{json.dumps(result)}") + payment.PI_FollowUp = True + +def find_pay_splynx_invoices(splynx_id): + """Mark Splynx invoices as paid for the given customer ID.""" + result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid") + + invoice_pay = { + "status": "paid" + } + + for pay in result: + res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) + return res + +def add_payment_splynx(splynx_id, pi_id, pay_id, amount): + """Add a payment record to Splynx.""" + from datetime import datetime + + stripe_pay = { + "customer_id": splynx_id, + "amount": amount, + "date": str(datetime.now().strftime('%Y-%m-%d')), + "field_1": pi_id, + "field_2": f"Single Payment_ID: {pay_id}" + } + + res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay) + if res: + return res['id'] + else: + return False + +def get_stripe_customer_id(splynx_id): + """Get Stripe customer ID from MySQL for a given Splynx customer ID.""" + connection = None + try: + # Connect to MySQL database + connection = pymysql.connect( + host=Config.MYSQL_CONFIG['host'], + database=Config.MYSQL_CONFIG['database'], + user=Config.MYSQL_CONFIG['user'], + password=Config.MYSQL_CONFIG['password'], + port=Config.MYSQL_CONFIG['port'], + autocommit=False, + cursorclass=pymysql.cursors.DictCursor + ) + + query = """ + SELECT + cb.customer_id, + cb.deposit, + cb.payment_method, + pad.field_1 AS stripe_customer_id + FROM customer_billing cb + LEFT OUTER JOIN payment_account_data pad ON cb.customer_id = pad.customer_id + WHERE cb.customer_id = %s + ORDER BY cb.payment_method ASC + LIMIT 1 + """ + + with connection.cursor() as cursor: + cursor.execute(query, (splynx_id,)) + result = cursor.fetchone() + + if result and result['stripe_customer_id']: + return result['stripe_customer_id'] + else: + return None + + except pymysql.Error as e: + print(f"MySQL Error in get_stripe_customer_id: {e}") + return None + except Exception as e: + print(f"Unexpected Error in get_stripe_customer_id: {e}") + return None + finally: + if connection: + connection.close() + +def get_stripe_payment_methods(stripe_customer_id): + """Get payment methods for a Stripe customer.""" + try: + # Initialize Stripe processor + if Config.PROCESS_LIVE: + api_key = "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" + else: + api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx" + + processor = StripePaymentProcessor(api_key=api_key, enable_logging=False) + + # Get payment methods from Stripe + stripe_customer_id = "cus_SoMyDihTxRsa7U" + payment_methods = processor.get_payment_methods(stripe_customer_id) + return payment_methods + + except Exception as e: + print(f"Error fetching payment methods: {e}") + return [] + +main_bp = Blueprint('main', __name__) + +@main_bp.app_template_filter('format_json') +def format_json_filter(json_string): + """Format JSON string with proper indentation.""" + if not json_string: + return '' + try: + # Parse the JSON string and format it with indentation + parsed = json.loads(json_string) + return json.dumps(parsed, indent=2, ensure_ascii=False) + except (json.JSONDecodeError, TypeError): + # If it's not valid JSON, return as-is + return json_string + +@main_bp.app_template_filter('currency') +def currency_filter(value): + """Format number as currency with digit grouping.""" + if value is None: + return '$0.00' + try: + # Convert to float if it's not already + num_value = float(value) + # Format with comma separators and 2 decimal places + return f"${num_value:,.2f}" + except (ValueError, TypeError): + return '$0.00' + +@main_bp.route('/') +@login_required +def index(): + return render_template('main/index.html') + +@main_bp.route('/batches') +@login_required +def batch_list(): + """Display list of all payment batches with summary information.""" + # Query all batches with summary statistics + batches = db.session.query( + PaymentBatch.id, + PaymentBatch.Created, + func.count(Payments.id).label('payment_count'), + func.sum(Payments.Payment_Amount).label('total_amount'), + func.sum(Payments.Fee_Stripe).label('total_fees'), + func.sum(case((Payments.Success == True, 1), else_=0)).label('successful_count'), + func.sum(case((Payments.Success == False, 1), else_=0)).label('failed_count'), + func.sum(case((Payments.Error.isnot(None), 1), else_=0)).label('error_count') + ).outerjoin(Payments, PaymentBatch.id == Payments.PaymentBatch_ID)\ + .group_by(PaymentBatch.id, PaymentBatch.Created)\ + .order_by(PaymentBatch.Created.desc()).all() + + return render_template('main/batch_list.html', batches=batches) + +@main_bp.route('/batch/') +@login_required +def batch_detail(batch_id): + """Display detailed view of a specific payment batch.""" + # Get batch information + batch = PaymentBatch.query.get_or_404(batch_id) + + # Get summary statistics for this batch + summary = db.session.query( + func.count(Payments.id).label('payment_count'), + func.sum(Payments.Payment_Amount).label('total_amount'), + func.sum(Payments.Fee_Stripe).label('total_fees'), + func.sum(case((Payments.Success == True, 1), else_=0)).label('successful_count'), + func.sum(case((Payments.Success == False, 1), else_=0)).label('failed_count'), + func.sum(case((Payments.Error.isnot(None), 1), else_=0)).label('error_count') + ).filter(Payments.PaymentBatch_ID == batch_id).first() + + # Get all payments for this batch ordered by Splynx_ID + payments = Payments.query.filter_by(PaymentBatch_ID=batch_id)\ + .order_by(Payments.Splynx_ID.asc()).all() + + return render_template('main/batch_detail.html', + batch=batch, + summary=summary, + payments=payments) + + +@main_bp.route('/single-payment') +@login_required +def single_payment(): + """Display single payment form page.""" + return render_template('main/single_payment.html') + +@main_bp.route('/single-payments') +@login_required +def single_payments_list(): + """Display list of all single payments with summary information.""" + # Query all single payments with user information + from models import Users + + payments = db.session.query( + SinglePayments.id, + SinglePayments.Splynx_ID, + SinglePayments.Stripe_Customer_ID, + SinglePayments.Payment_Intent, + SinglePayments.Payment_Method, + SinglePayments.Payment_Amount, + SinglePayments.Fee_Stripe, + SinglePayments.Fee_Total, + SinglePayments.Success, + SinglePayments.Error, + SinglePayments.PI_JSON, + SinglePayments.Created, + Users.FullName.label('processed_by') + ).outerjoin(Users, SinglePayments.Who == Users.id)\ + .order_by(SinglePayments.Created.desc()).all() + + # Calculate summary statistics + total_payments = len(payments) + successful_payments = sum(1 for p in payments if p.Success == True) + failed_payments = sum(1 for p in payments if p.Success == False) + pending_payments = sum(1 for p in payments if p.Success == None) + + total_amount = sum(p.Payment_Amount or 0 for p in payments if p.Success == True) + total_fees = sum(p.Fee_Stripe or 0 for p in payments if p.Success == True) + + summary = { + 'total_payments': total_payments, + 'successful_payments': successful_payments, + 'failed_payments': failed_payments, + 'pending_payments': pending_payments, + 'total_amount': total_amount, + 'total_fees': total_fees, + 'success_rate': (successful_payments / total_payments * 100) if total_payments > 0 else 0 + } + + return render_template('main/single_payments_list.html', payments=payments, summary=summary) + +@main_bp.route('/single-payment/detail/') +@login_required +def single_payment_detail(payment_id): + """Display detailed view of a specific single payment.""" + # Get payment information + from models import Users + + payment = db.session.query( + SinglePayments.id, + SinglePayments.Splynx_ID, + SinglePayments.Stripe_Customer_ID, + SinglePayments.Payment_Intent, + SinglePayments.PI_FollowUp, + SinglePayments.PI_Last_Check, + SinglePayments.Payment_Method, + SinglePayments.Fee_Tax, + SinglePayments.Fee_Stripe, + SinglePayments.Fee_Total, + SinglePayments.Payment_Amount, + SinglePayments.PI_JSON, + SinglePayments.PI_FollowUp_JSON, + SinglePayments.Error, + SinglePayments.Success, + SinglePayments.Created, + Users.FullName.label('processed_by') + ).outerjoin(Users, SinglePayments.Who == Users.id)\ + .filter(SinglePayments.id == payment_id).first() + + if not payment: + flash('Payment not found.', 'error') + return redirect(url_for('main.single_payments_list')) + + return render_template('main/single_payment_detail.html', payment=payment) + +@main_bp.route('/payment/detail/') +@login_required +def payment_detail(payment_id): + """Display detailed view of a specific single payment.""" + # Get payment information + + payment = db.session.query( + Payments.id, + Payments.Splynx_ID, + Payments.Stripe_Customer_ID, + Payments.Payment_Intent, + Payments.PI_FollowUp, + Payments.PI_Last_Check, + Payments.Payment_Method, + Payments.Fee_Tax, + Payments.Fee_Stripe, + Payments.Fee_Total, + Payments.Payment_Amount, + Payments.PI_JSON, + Payments.PI_FollowUp_JSON, + Payments.Error, + Payments.Success, + Payments.Created)\ + .filter(Payments.id == payment_id).first() + + if not payment: + flash('Payment not found.', 'error') + return redirect(url_for('main.single_payments_list')) + + return render_template('main/single_payment_detail.html', payment=payment) + +@main_bp.route('/single-payment/check-intent/', methods=['POST']) +@login_required +def check_payment_intent(payment_id): + """Check the status of a payment intent and update the record.""" + from datetime import datetime + + try: + # Get the payment record + payment = SinglePayments.query.get_or_404(payment_id) + + if not payment.Payment_Intent: + return jsonify({'success': False, 'error': 'No payment intent found'}), 400 + + # Initialize Stripe processor + if Config.PROCESS_LIVE: + api_key = "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" + else: + api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx" + + processor = StripePaymentProcessor(api_key=api_key, enable_logging=True) + + # Check payment intent status + intent_result = processor.check_payment_intent(payment.Payment_Intent) + + print(json.dumps(intent_result, indent=2)) + + if intent_result['status'] == "succeeded": + payment.PI_FollowUp_JSON = json.dumps(intent_result) + payment.PI_FollowUp = False + payment.PI_Last_Check = datetime.now() + processPaymentResult(pay_id=payment.id, result=intent_result, key="singlepay") + else: + payment.PI_FollowUp_JSON = json.dumps(intent_result) + payment.PI_Last_Check = datetime.now() + + db.session.commit() + + return jsonify({ + 'success': True, + 'status': intent_result['status'], + 'payment_succeeded': intent_result['status'] == "succeeded", + 'message': f'Payment intent status: {intent_result["status"]}' + }) + + except Exception as e: + db.session.rollback() + print(f"Check payment intent error: {e}") + return jsonify({'success': False, 'error': 'Failed to check payment intent'}), 500 + +@main_bp.route('/single-payment/process', methods=['POST']) +@login_required +def process_single_payment(): + """Process a single payment using Stripe.""" + try: + # Get form data + splynx_id = request.form.get('splynx_id') + amount = request.form.get('amount') + + # Validate inputs + if not splynx_id or not amount: + return jsonify({'success': False, 'error': 'Missing required fields'}), 400 + + try: + splynx_id = int(splynx_id) + amount = float(amount) + except (ValueError, TypeError): + return jsonify({'success': False, 'error': 'Invalid input format'}), 400 + + if amount <= 0: + return jsonify({'success': False, 'error': 'Amount must be greater than 0'}), 400 + + # Get customer details from Splynx + customer_data = splynx.Customer(splynx_id) + if not customer_data: + return jsonify({'success': False, 'error': 'Customer not found in Splynx'}), 404 + + # Get Stripe customer ID from MySQL + stripe_customer_id = get_stripe_customer_id(splynx_id) + if not stripe_customer_id: + return jsonify({'success': False, 'error': 'Customer does not have a valid Stripe payment method'}), 400 + + # Create payment record in database + payment_record = SinglePayments( + Splynx_ID=splynx_id, + Stripe_Customer_ID=stripe_customer_id, + Payment_Amount=amount, + Who=current_user.id + ) + db.session.add(payment_record) + db.session.commit() # Commit to get the payment ID + + # Initialize Stripe processor + if Config.PROCESS_LIVE: + print("LIVE Payment") + api_key = "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" + else: + print("SANDBOX Payment") + api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx" + # Use test customer for sandbox + import random + test_customers = ['cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoQedaG3q2ecKG', 'cus_SoMVPWxdYstYbr'] + stripe_customer_id = random.choice(test_customers) + + processor = StripePaymentProcessor(api_key=api_key, enable_logging=True) + print(f"stripe_customer_id: {stripe_customer_id}") + # Process payment + result = processor.process_payment( + customer_id=stripe_customer_id, + amount=amount, + currency="aud", + description=f"Single Payment - Splynx ID: {splynx_id} - Payment ID: {payment_record.id}" + ) + + # Update payment record with results + payment_record.Success = result.get('success', False) + payment_record.Payment_Intent = result.get('payment_intent_id') + payment_record.PI_JSON = json.dumps(result) + + if result.get('error') and not result.get('needs_fee_update'): + payment_record.Error = f"Error Type: {result.get('error_type', 'Unknown')}\nError: {result['error']}" + + if result.get('needs_fee_update'): + payment_record.PI_FollowUp = True + + if result.get('payment_method_type') == "card": + payment_record.Payment_Method = result.get('estimated_fee_details', {}).get('card_display_brand', 'card') + elif result.get('payment_method_type') == "au_becs_debit": + payment_record.Payment_Method = result['payment_method_type'] + + if result.get('fee_details'): + payment_record.Fee_Total = result['fee_details']['total_fee'] + for fee_type in result['fee_details']['fee_breakdown']: + if fee_type['type'] == "tax": + payment_record.Fee_Tax = fee_type['amount'] + elif fee_type['type'] == "stripe_fee": + payment_record.Fee_Stripe = fee_type['amount'] + + # Commit the updated payment record + db.session.commit() + + # Check if payment was actually successful + if result.get('success'): + # Payment succeeded - update Splynx if in live mode + if Config.PROCESS_LIVE: + try: + # Mark invoices as paid in Splynx + find_pay_splynx_invoices(splynx_id) + + # Add payment record to Splynx + splynx_payment_id = add_payment_splynx( + splynx_id=splynx_id, + pi_id=result.get('payment_intent_id'), + pay_id=payment_record.id, + amount=amount + ) + + if splynx_payment_id: + print(f"✅ Splynx payment record created: {splynx_payment_id}") + else: + print("⚠️ Failed to create Splynx payment record") + + except Exception as splynx_error: + print(f"❌ Error updating Splynx: {splynx_error}") + # Continue processing even if Splynx update fails + + # Log successful payment + log_activity( + current_user.id, + "PAYMENT_SUCCESS", + "SinglePayment", + payment_record.id, + details=f"Single payment successful: ${amount:,.2f} for customer {splynx_id} ({customer_data.get('name', 'Unknown')})" + ) + + # Payment succeeded + return jsonify({ + 'success': True, + 'payment_success': True, + 'payment_id': payment_record.id, + 'payment_intent': result.get('payment_intent_id'), + 'amount': amount, + 'customer_name': customer_data.get('name'), + 'message': f'Payment processed successfully for {customer_data.get("name")}' + }) + else: + # Payment failed - log the failure + log_activity( + current_user.id, + "PAYMENT_FAILED", + "SinglePayment", + payment_record.id, + details=f"Single payment failed: ${amount:,.2f} for customer {splynx_id} ({customer_data.get('name', 'Unknown')}) - {result.get('error', 'Unknown error')}" + ) + + # Payment failed - return the specific error + if result.get('needs_fee_update'): + fee_update = True + else: + fee_update = False + return jsonify({ + 'success': False, + 'payment_success': False, + 'fee_update': fee_update, + 'payment_id': payment_record.id, + 'error': result.get('error', 'Payment failed'), + 'error_type': result.get('error_type', 'unknown_error'), + 'stripe_error': result.get('error', 'Unknown payment error'), + 'customer_name': customer_data.get('name') + }), 422 # 422 Unprocessable Entity for business logic failures + + except Exception as e: + db.session.rollback() + print(f"Single payment processing error: {e}") + return jsonify({'success': False, 'error': 'Payment processing failed. Please try again.'}), 500 + +@main_bp.route('/payment-plans') +@login_required +def payment_plans_list(): + """Display list of all payment plans with summary information.""" + from models import Users + + # Query all payment plans with user information + plans = db.session.query( + PaymentPlans.id, + PaymentPlans.Splynx_ID, + PaymentPlans.Amount, + PaymentPlans.Frequency, + PaymentPlans.Start_Date, + PaymentPlans.Stripe_Payment_Method, + PaymentPlans.Enabled, + PaymentPlans.Created, + Users.FullName.label('created_by') + ).outerjoin(Users, PaymentPlans.Who == Users.id)\ + .order_by(PaymentPlans.Created.desc()).all() + + # Calculate summary statistics + total_plans = len(plans) + active_plans = sum(1 for p in plans if p.Enabled == True) + inactive_plans = sum(1 for p in plans if p.Enabled == False) + + total_recurring_amount = sum(p.Amount or 0 for p in plans if p.Enabled == True) + + summary = { + 'total_plans': total_plans, + 'active_plans': active_plans, + 'inactive_plans': inactive_plans, + 'total_recurring_amount': total_recurring_amount + } + + return render_template('main/payment_plans_list.html', plans=plans, summary=summary) + +@main_bp.route('/payment-plans/create') +@login_required +def payment_plans_create(): + """Display payment plan creation form.""" + return render_template('main/payment_plans_form.html', edit_mode=False) + +@main_bp.route('/payment-plans/create', methods=['POST']) +@login_required +def payment_plans_create_post(): + """Handle payment plan creation.""" + try: + # Get form data + splynx_id = request.form.get('splynx_id') + amount = request.form.get('amount') + frequency = request.form.get('frequency') + start_date = request.form.get('start_date') + stripe_payment_method = request.form.get('stripe_payment_method') + + # Validate inputs + if not all([splynx_id, amount, frequency, start_date, stripe_payment_method]): + flash('All fields are required.', 'error') + return redirect(url_for('main.payment_plans_create')) + + try: + splynx_id = int(splynx_id) + amount = float(amount) + from datetime import datetime + start_date = datetime.strptime(start_date, '%Y-%m-%d') + except (ValueError, TypeError): + flash('Invalid input format.', 'error') + return redirect(url_for('main.payment_plans_create')) + + if amount <= 0: + flash('Amount must be greater than 0.', 'error') + return redirect(url_for('main.payment_plans_create')) + + # Validate customer exists in Splynx + customer_data = splynx.Customer(splynx_id) + if not customer_data: + flash('Customer not found in Splynx.', 'error') + return redirect(url_for('main.payment_plans_create')) + + # Create payment plan record + payment_plan = PaymentPlans( + Splynx_ID=splynx_id, + Amount=amount, + Frequency=frequency, + Start_Date=start_date, + Stripe_Payment_Method=stripe_payment_method, + Who=current_user.id + ) + + db.session.add(payment_plan) + db.session.commit() + + # Log payment plan creation + log_activity( + current_user.id, + "PAYPLAN_CREATED", + "PaymentPlan", + payment_plan.id, + details=f"Payment plan created: ${amount:,.2f} {frequency} for customer {splynx_id} ({customer_data.get('name', 'Unknown')})" + ) + + flash(f'Payment plan created successfully for {customer_data.get("name", "customer")}.', 'success') + return redirect(url_for('main.payment_plans_detail', plan_id=payment_plan.id)) + + except Exception as e: + db.session.rollback() + print(f"Payment plan creation error: {e}") + flash('Failed to create payment plan. Please try again.', 'error') + return redirect(url_for('main.payment_plans_create')) + +@main_bp.route('/payment-plans/edit/') +@login_required +def payment_plans_edit(plan_id): + """Display payment plan edit form.""" + plan = PaymentPlans.query.get_or_404(plan_id) + return render_template('main/payment_plans_form.html', plan=plan, edit_mode=True) + +@main_bp.route('/payment-plans/edit/', methods=['POST']) +@login_required +def payment_plans_edit_post(plan_id): + """Handle payment plan updates.""" + try: + plan = PaymentPlans.query.get_or_404(plan_id) + + # Get form data + amount = request.form.get('amount') + frequency = request.form.get('frequency') + start_date = request.form.get('start_date') + stripe_payment_method = request.form.get('stripe_payment_method') + + # Validate inputs + if not all([amount, frequency, start_date, stripe_payment_method]): + flash('All fields are required.', 'error') + return redirect(url_for('main.payment_plans_edit', plan_id=plan_id)) + + try: + amount = float(amount) + from datetime import datetime + start_date = datetime.strptime(start_date, '%Y-%m-%d') + except (ValueError, TypeError): + flash('Invalid input format.', 'error') + return redirect(url_for('main.payment_plans_edit', plan_id=plan_id)) + + if amount <= 0: + flash('Amount must be greater than 0.', 'error') + return redirect(url_for('main.payment_plans_edit', plan_id=plan_id)) + + # Update payment plan + plan.Amount = amount + plan.Frequency = frequency + plan.Start_Date = start_date + plan.Stripe_Payment_Method = stripe_payment_method + + db.session.commit() + + # Log payment plan update + log_activity( + current_user.id, + "PAYPLAN_UPDATED", + "PaymentPlan", + plan.id, + details=f"Payment plan updated: ${amount:,.2f} {frequency} starting {start_date.strftime('%Y-%m-%d')}" + ) + + flash('Payment plan updated successfully.', 'success') + return redirect(url_for('main.payment_plans_detail', plan_id=plan.id)) + + except Exception as e: + db.session.rollback() + print(f"Payment plan update error: {e}") + flash('Failed to update payment plan. Please try again.', 'error') + return redirect(url_for('main.payment_plans_edit', plan_id=plan_id)) + +@main_bp.route('/payment-plans/delete/', methods=['POST']) +@login_required +def payment_plans_delete(plan_id): + """Handle payment plan deletion (soft delete).""" + try: + plan = PaymentPlans.query.get_or_404(plan_id) + + # Soft delete by setting Enabled to False + plan.Enabled = False + db.session.commit() + + flash('Payment plan has been disabled.', 'success') + return redirect(url_for('main.payment_plans_list')) + + except Exception as e: + db.session.rollback() + print(f"Payment plan deletion error: {e}") + flash('Failed to disable payment plan. Please try again.', 'error') + return redirect(url_for('main.payment_plans_detail', plan_id=plan_id)) + +@main_bp.route('/payment-plans/toggle/', methods=['POST']) +@login_required +def payment_plans_toggle(plan_id): + """Toggle payment plan enabled status.""" + try: + plan = PaymentPlans.query.get_or_404(plan_id) + + # Toggle enabled status + plan.Enabled = not plan.Enabled + db.session.commit() + + # Log payment plan toggle + action = "PAYPLAN_ENABLED" if plan.Enabled else "PAYPLAN_DISABLED" + log_activity( + current_user.id, + action, + "PaymentPlan", + plan.id, + details=f"Payment plan {'enabled' if plan.Enabled else 'disabled'}: ${plan.Amount:,.2f} {plan.Frequency}" + ) + + status = "enabled" if plan.Enabled else "disabled" + flash(f'Payment plan has been {status}.', 'success') + return redirect(url_for('main.payment_plans_detail', plan_id=plan_id)) + + except Exception as e: + db.session.rollback() + print(f"Payment plan toggle error: {e}") + flash('Failed to update payment plan status. Please try again.', 'error') + return redirect(url_for('main.payment_plans_detail', plan_id=plan_id)) + +@main_bp.route('/payment-plans/detail/') +@login_required +def payment_plans_detail(plan_id): + """Display detailed view of a specific payment plan.""" + from models import Users + + # Get payment plan with user information + plan = db.session.query( + PaymentPlans.id, + PaymentPlans.Splynx_ID, + PaymentPlans.Amount, + PaymentPlans.Frequency, + PaymentPlans.Start_Date, + PaymentPlans.Stripe_Payment_Method, + PaymentPlans.Enabled, + PaymentPlans.Created, + Users.FullName.label('created_by') + ).outerjoin(Users, PaymentPlans.Who == Users.id)\ + .filter(PaymentPlans.id == plan_id).first() + + if not plan: + flash('Payment plan not found.', 'error') + return redirect(url_for('main.payment_plans_list')) + + # Get associated single payments + associated_payments = db.session.query( + Payments.id, + Payments.Payment_Amount, + Payments.Success, + Payments.Error, + Payments.Created, + Payments.Payment_Intent)\ + .filter(Payments.PaymentPlan_ID == plan_id)\ + .order_by(Payments.Created.desc()).all() + + return render_template('main/payment_plans_detail.html', + plan=plan, + associated_payments=associated_payments) + +@main_bp.route('/api/stripe-payment-methods/') +@login_required +def api_stripe_payment_methods(stripe_customer_id): + """Get Stripe payment methods for a customer.""" + try: + payment_methods = get_stripe_payment_methods(stripe_customer_id) + return jsonify({'success': True, 'payment_methods': payment_methods}) + except Exception as e: + print(f"Error fetching payment methods: {e}") + return jsonify({'success': False, 'error': 'Failed to fetch payment methods'}), 500 + +@main_bp.route('/api/splynx/') +@login_required +def api_splynx_customer(id): + """ + Get Splynx customer information by ID + + Security: Restricted to operational and financial staff who need customer data access + """ + try: + log_activity(current_user.id, "API_ACCESS", "SplynxCustomer", id, + details=f"Accessed Splynx customer API for customer {id}") + + print(f"Splynx Customer API: {id}") + res = splynx.Customer(id) + + if res: + log_activity(current_user.id, "API_SUCCESS", "SplynxCustomer", id, + details=f"Successfully retrieved Splynx customer {id}") + return res + else: + log_activity(current_user.id, "API_NOT_FOUND", "SplynxCustomer", id, + details=f"Splynx customer {id} not found") + return {"error": "Customer not found"}, 404 + + except Exception as e: + log_activity(current_user.id, "API_ERROR", "SplynxCustomer", id, + details=f"Splynx customer API error: {str(e)}") + return {"error": "Internal server error"}, 500 \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..7120b15 --- /dev/null +++ b/config.py @@ -0,0 +1,39 @@ +import os + +class Config: + # Flask configuration + SECRET_KEY = os.environ.get('SECRET_KEY') or 'plutus-dev-secret-key-change-in-production' + + # PostgreSQL database configuration (Flask-SQLAlchemy) + SQLALCHEMY_DATABASE_URI = 'postgresql://flask:FR0u9312rad$swib13125@192.168.20.53/plutus' + SQLALCHEMY_TRACK_MODIFICATIONS = False + + # MySQL database configuration (read-only) + MYSQL_CONFIG = { + 'host': '103.210.154.25', + 'database': 'splynx', + 'user': 'splynximport', + 'password': 'splynxrocksbabyy', + 'port': 3306 + } + + # Query configuration + DEFAULT_QUERY_LIMIT = 3 + DEPOSIT_THRESHOLD = -5 + + # Payment Method Constants + PAYMENT_METHOD_DIRECT_DEBIT = 2 + PAYMENT_METHOD_CARD = 3 + PAYMENT_METHOD_PAYMENT_PLAN = 9 + + # Process live on Sandbox + # False = Sandbox - Default + PROCESS_LIVE = False + + # Threading configuration + MAX_PAYMENT_THREADS = 5 # Number of concurrent payment processing threads + THREAD_TIMEOUT = 60 # Timeout in seconds for payment processing threads + + # Stripe API Keys + STRIPE_LIVE_API_KEY = os.environ.get('STRIPE_LIVE_API_KEY') or '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' + STRIPE_TEST_API_KEY = os.environ.get('STRIPE_TEST_API_KEY') or 'sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx' \ No newline at end of file diff --git a/include/site/python3.12/greenlet/greenlet.h b/include/site/python3.12/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/include/site/python3.12/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/lib64 b/lib64 new file mode 120000 index 0000000..7951405 --- /dev/null +++ b/lib64 @@ -0,0 +1 @@ +lib \ No newline at end of file diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/1b403a365765_add_new_features.py b/migrations/versions/1b403a365765_add_new_features.py new file mode 100644 index 0000000..c99d227 --- /dev/null +++ b/migrations/versions/1b403a365765_add_new_features.py @@ -0,0 +1,46 @@ +"""Add new features + +Revision ID: 1b403a365765 +Revises: 455cbec206cf +Create Date: 2025-08-09 17:45:22.066241 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '1b403a365765' +down_revision = '455cbec206cf' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('PaymentBatch', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('Created', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('Payments', schema=None) as batch_op: + batch_op.add_column(sa.Column('PaymentBatch_ID', sa.Integer(), nullable=False)) + batch_op.alter_column('PI_Last_Check', + existing_type=postgresql.TIMESTAMP(), + nullable=True) + batch_op.create_foreign_key(None, 'PaymentBatch', ['PaymentBatch_ID'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('Payments', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.alter_column('PI_Last_Check', + existing_type=postgresql.TIMESTAMP(), + nullable=False) + batch_op.drop_column('PaymentBatch_ID') + + op.drop_table('PaymentBatch') + # ### end Alembic commands ### diff --git a/migrations/versions/3252db86eaae_add_new_features.py b/migrations/versions/3252db86eaae_add_new_features.py new file mode 100644 index 0000000..b4a541a --- /dev/null +++ b/migrations/versions/3252db86eaae_add_new_features.py @@ -0,0 +1,42 @@ +"""Add new features + +Revision ID: 3252db86eaae +Revises: 906059746902 +Create Date: 2025-08-14 15:02:47.519589 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '3252db86eaae' +down_revision = '906059746902' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('Payments', schema=None) as batch_op: + batch_op.add_column(sa.Column('PaymentPlan_ID', sa.Integer(), nullable=True)) + batch_op.create_foreign_key(None, 'PaymentPlans', ['PaymentPlan_ID'], ['id']) + + with op.batch_alter_table('SinglePayments', schema=None) as batch_op: + batch_op.drop_constraint(batch_op.f('SinglePayments_PaymentPlan_ID_fkey'), type_='foreignkey') + batch_op.drop_column('PaymentPlan_ID') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('SinglePayments', schema=None) as batch_op: + batch_op.add_column(sa.Column('PaymentPlan_ID', sa.INTEGER(), autoincrement=False, nullable=True)) + batch_op.create_foreign_key(batch_op.f('SinglePayments_PaymentPlan_ID_fkey'), 'PaymentPlans', ['PaymentPlan_ID'], ['id']) + + with op.batch_alter_table('Payments', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_column('PaymentPlan_ID') + + # ### end Alembic commands ### diff --git a/migrations/versions/455cbec206cf_initial_migration_with_users_payments_.py b/migrations/versions/455cbec206cf_initial_migration_with_users_payments_.py new file mode 100644 index 0000000..539546c --- /dev/null +++ b/migrations/versions/455cbec206cf_initial_migration_with_users_payments_.py @@ -0,0 +1,73 @@ +"""Initial migration with Users, Payments, and Logs tables + +Revision ID: 455cbec206cf +Revises: +Create Date: 2025-08-08 21:11:27.414842 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '455cbec206cf' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('Payments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('Splynx_ID', sa.Integer(), nullable=True), + sa.Column('Stripe_Customer_ID', sa.String(), nullable=True), + sa.Column('Payment_Intent', sa.String(), nullable=True), + sa.Column('PI_FollowUp', sa.Boolean(), nullable=False), + sa.Column('PI_Last_Check', sa.DateTime(), nullable=False), + sa.Column('Payment_Method', sa.String(), nullable=True), + sa.Column('Fee_Tax', sa.Float(), nullable=True), + sa.Column('Fee_Stripe', sa.Float(), nullable=True), + sa.Column('Fee_Total', sa.Float(), nullable=True), + sa.Column('Payment_Amount', sa.Float(), nullable=True), + sa.Column('PI_JSON', sa.Text(), nullable=True), + sa.Column('PI_FollowUp_JSON', sa.Text(), nullable=True), + sa.Column('Success', sa.Boolean(), nullable=False), + sa.Column('Created', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('Users', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('Username', sa.String(), nullable=True), + sa.Column('Password', sa.String(), nullable=True), + sa.Column('FullName', sa.String(), nullable=True), + sa.Column('Email', sa.String(), nullable=True), + sa.Column('PassResetCode', sa.String(), nullable=True), + sa.Column('PassResetRequest', sa.DateTime(), nullable=True), + sa.Column('Enabled', sa.Boolean(), nullable=False), + sa.Column('Permissions', sa.String(), nullable=True), + sa.Column('Created', sa.DateTime(), nullable=False), + sa.Column('LoginChangePass', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('Logs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('User_ID', sa.Integer(), nullable=False), + sa.Column('Log_Entry', sa.String(length=4000), nullable=True), + sa.Column('Added', sa.DateTime(), nullable=False), + sa.Column('Action', sa.String(length=50), nullable=True), + sa.Column('Entity_Type', sa.String(length=50), nullable=True), + sa.Column('Entity_ID', sa.Integer(), nullable=True), + sa.Column('IP_Address', sa.String(length=50), nullable=True), + sa.ForeignKeyConstraint(['User_ID'], ['Users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('Logs') + op.drop_table('Users') + op.drop_table('Payments') + # ### end Alembic commands ### diff --git a/migrations/versions/50157fcf55e4_add_new_features.py b/migrations/versions/50157fcf55e4_add_new_features.py new file mode 100644 index 0000000..6aef60e --- /dev/null +++ b/migrations/versions/50157fcf55e4_add_new_features.py @@ -0,0 +1,32 @@ +"""Add new features + +Revision ID: 50157fcf55e4 +Revises: ed07e785afd5 +Create Date: 2025-08-13 15:57:43.041740 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '50157fcf55e4' +down_revision = 'ed07e785afd5' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('PaymentPlans', schema=None) as batch_op: + batch_op.add_column(sa.Column('Stripe_Payment_Method', sa.String(length=50), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('PaymentPlans', schema=None) as batch_op: + batch_op.drop_column('Stripe_Payment_Method') + + # ### end Alembic commands ### diff --git a/migrations/versions/6a841af4c236_add_new_features.py b/migrations/versions/6a841af4c236_add_new_features.py new file mode 100644 index 0000000..ba0b329 --- /dev/null +++ b/migrations/versions/6a841af4c236_add_new_features.py @@ -0,0 +1,32 @@ +"""Add new features + +Revision ID: 6a841af4c236 +Revises: 50157fcf55e4 +Create Date: 2025-08-13 20:18:52.912339 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '6a841af4c236' +down_revision = '50157fcf55e4' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('PaymentPlans', schema=None) as batch_op: + batch_op.add_column(sa.Column('Stripe_Customer_ID', sa.String(length=50), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('PaymentPlans', schema=None) as batch_op: + batch_op.drop_column('Stripe_Customer_ID') + + # ### end Alembic commands ### diff --git a/migrations/versions/906059746902_add_new_features.py b/migrations/versions/906059746902_add_new_features.py new file mode 100644 index 0000000..87c6f48 --- /dev/null +++ b/migrations/versions/906059746902_add_new_features.py @@ -0,0 +1,38 @@ +"""Add new features + +Revision ID: 906059746902 +Revises: 6a841af4c236 +Create Date: 2025-08-13 20:25:29.561582 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '906059746902' +down_revision = '6a841af4c236' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('Payments', schema=None) as batch_op: + batch_op.add_column(sa.Column('Stripe_Payment_Method', sa.String(), nullable=True)) + + with op.batch_alter_table('SinglePayments', schema=None) as batch_op: + batch_op.add_column(sa.Column('Stripe_Payment_Method', sa.String(), nullable=True)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('SinglePayments', schema=None) as batch_op: + batch_op.drop_column('Stripe_Payment_Method') + + with op.batch_alter_table('Payments', schema=None) as batch_op: + batch_op.drop_column('Stripe_Payment_Method') + + # ### end Alembic commands ### diff --git a/migrations/versions/9d9195d6b9a7_add_new_features.py b/migrations/versions/9d9195d6b9a7_add_new_features.py new file mode 100644 index 0000000..9921c95 --- /dev/null +++ b/migrations/versions/9d9195d6b9a7_add_new_features.py @@ -0,0 +1,48 @@ +"""Add new features + +Revision ID: 9d9195d6b9a7 +Revises: 1b403a365765 +Create Date: 2025-08-12 16:22:21.329937 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '9d9195d6b9a7' +down_revision = '1b403a365765' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('SinglePayments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('Splynx_ID', sa.Integer(), nullable=True), + sa.Column('Stripe_Customer_ID', sa.String(), nullable=True), + sa.Column('Payment_Intent', sa.String(), nullable=True), + sa.Column('PI_FollowUp', sa.Boolean(), nullable=False), + sa.Column('PI_Last_Check', sa.DateTime(), nullable=True), + sa.Column('Payment_Method', sa.String(), nullable=True), + sa.Column('Fee_Tax', sa.Float(), nullable=True), + sa.Column('Fee_Stripe', sa.Float(), nullable=True), + sa.Column('Fee_Total', sa.Float(), nullable=True), + sa.Column('Payment_Amount', sa.Float(), nullable=True), + sa.Column('PI_JSON', sa.Text(), nullable=True), + sa.Column('PI_FollowUp_JSON', sa.Text(), nullable=True), + sa.Column('Error', sa.Text(), nullable=True), + sa.Column('Success', sa.Boolean(), nullable=True), + sa.Column('Created', sa.DateTime(), nullable=False), + sa.Column('Who', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['Who'], ['Users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('SinglePayments') + # ### end Alembic commands ### diff --git a/migrations/versions/ed07e785afd5_add_new_features.py b/migrations/versions/ed07e785afd5_add_new_features.py new file mode 100644 index 0000000..3dfc606 --- /dev/null +++ b/migrations/versions/ed07e785afd5_add_new_features.py @@ -0,0 +1,48 @@ +"""Add new features + +Revision ID: ed07e785afd5 +Revises: 9d9195d6b9a7 +Create Date: 2025-08-13 14:55:02.023809 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'ed07e785afd5' +down_revision = '9d9195d6b9a7' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('PaymentPlans', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('Splynx_ID', sa.Integer(), nullable=True), + sa.Column('Amount', sa.Float(), nullable=True), + sa.Column('Frequency', sa.String(length=50), nullable=True), + sa.Column('Day', sa.String(length=50), nullable=True), + sa.Column('Start_Date', sa.DateTime(), nullable=True), + sa.Column('Created', sa.DateTime(), nullable=False), + sa.Column('Who', sa.Integer(), nullable=False), + sa.Column('Enabled', sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint(['Who'], ['Users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + with op.batch_alter_table('SinglePayments', schema=None) as batch_op: + batch_op.add_column(sa.Column('PaymentPlan_ID', sa.Integer(), nullable=True)) + batch_op.create_foreign_key(None, 'PaymentPlans', ['PaymentPlan_ID'], ['id']) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('SinglePayments', schema=None) as batch_op: + batch_op.drop_constraint(None, type_='foreignkey') + batch_op.drop_column('PaymentPlan_ID') + + op.drop_table('PaymentPlans') + # ### end Alembic commands ### diff --git a/models.py b/models.py new file mode 100644 index 0000000..ce63b4e --- /dev/null +++ b/models.py @@ -0,0 +1,102 @@ +from datetime import datetime, timezone +from flask_login import UserMixin, current_user +from flask import redirect, url_for, flash +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import synonym, relationship +from app import db + +# S2qBHlGQxVDMQGYOO5Db + +class Users(UserMixin, db.Model): + __tablename__ = 'Users' + id = db.Column(db.Integer, primary_key=True) + Username = db.Column(db.String()) + Password = db.Column(db.String()) + FullName = db.Column(db.String()) + Email = db.Column(db.String()) + PassResetCode = db.Column(db.String()) + PassResetRequest = db.Column(db.DateTime) + Enabled = db.Column(db.Boolean, nullable=False, default=1) + Permissions = db.Column(db.String()) + Created = db.Column(db.DateTime, nullable=False, default=datetime.now(timezone.utc)) + LoginChangePass = db.Column(db.Boolean, nullable=False, default=0) + + def __repr__(self): + return '' % self.FullName + + def get_id(self): + return str(self.id) + +class PaymentBatch(db.Model): + __tablename__ = 'PaymentBatch' + id = db.Column(db.Integer, primary_key=True) + Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + + +class Payments(db.Model): + __tablename__ = 'Payments' + id = db.Column(db.Integer, primary_key=True) + Splynx_ID = db.Column(db.Integer) + PaymentBatch_ID = db.Column(db.Integer, db.ForeignKey('PaymentBatch.id'), nullable=False) + Stripe_Customer_ID = db.Column(db.String()) + Payment_Intent = db.Column(db.String()) + PI_FollowUp = db.Column(db.Boolean, nullable=False, default=0) + PI_Last_Check = db.Column(db.DateTime, nullable=True) + Payment_Method = db.Column(db.String()) + Stripe_Payment_Method = db.Column(db.String()) + Fee_Tax = db.Column(db.Float()) + Fee_Stripe = db.Column(db.Float()) + Fee_Total = db.Column(db.Float()) + Payment_Amount = db.Column(db.Float()) + PI_JSON = db.Column(db.Text()) + PI_FollowUp_JSON = db.Column(db.Text()) + Error = db.Column(db.Text()) + Success = db.Column(db.Boolean, nullable=True, default=None) + Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + PaymentPlan_ID = db.Column(db.Integer, db.ForeignKey('PaymentPlans.id'), nullable=True) + +class SinglePayments(db.Model): + __tablename__ = 'SinglePayments' + id = db.Column(db.Integer, primary_key=True) + Splynx_ID = db.Column(db.Integer) + Stripe_Customer_ID = db.Column(db.String()) + Payment_Intent = db.Column(db.String()) + PI_FollowUp = db.Column(db.Boolean, nullable=False, default=0) + PI_Last_Check = db.Column(db.DateTime, nullable=True) + Payment_Method = db.Column(db.String()) + Stripe_Payment_Method = db.Column(db.String()) + Fee_Tax = db.Column(db.Float()) + Fee_Stripe = db.Column(db.Float()) + Fee_Total = db.Column(db.Float()) + Payment_Amount = db.Column(db.Float()) + PI_JSON = db.Column(db.Text()) + PI_FollowUp_JSON = db.Column(db.Text()) + Error = db.Column(db.Text()) + Success = db.Column(db.Boolean, nullable=True, default=None) + Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False) + + +class Logs(db.Model): + __tablename__ = 'Logs' + id = db.Column(db.Integer, primary_key=True) + User_ID = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False) + Log_Entry = db.Column(db.String(4000)) + Added = db.Column(db.DateTime, nullable=False, default=datetime.now(timezone.utc)) + Action = db.Column(db.String(50)) + Entity_Type = db.Column(db.String(50)) + Entity_ID = db.Column(db.Integer) + IP_Address = db.Column(db.String(50)) + +class PaymentPlans(db.Model): + __tablename__ = 'PaymentPlans' + id = db.Column(db.Integer, primary_key=True) + Splynx_ID = db.Column(db.Integer) + Stripe_Customer_ID = db.Column(db.String(50)) + Amount = db.Column(db.Float) + Frequency = db.Column(db.String(50)) + Start_Date = db.Column(db.DateTime, nullable=True) + Stripe_Payment_Method = db.Column(db.String(50)) + Created = db.Column(db.DateTime, nullable=False, default=datetime.now()) + Who = db.Column(db.Integer, db.ForeignKey('Users.id'), nullable=False) + Enabled = db.Column(db.Boolean, nullable=True, default=True) \ No newline at end of file diff --git a/pyvenv.cfg b/pyvenv.cfg new file mode 100644 index 0000000..1a517eb --- /dev/null +++ b/pyvenv.cfg @@ -0,0 +1,5 @@ +home = /usr/bin +include-system-site-packages = false +version = 3.12.3 +executable = /usr/bin/python3.12 +command = /usr/bin/python3.12 -m venv /home/alan/python_projects/plutus/plutus diff --git a/query_mysql - Copy.py b/query_mysql - Copy.py new file mode 100644 index 0000000..ad347a2 --- /dev/null +++ b/query_mysql - Copy.py @@ -0,0 +1,532 @@ +#!/usr/bin/env python3 +""" +External script to query MySQL database (Splynx) for customer billing data. +This script runs independently of the Flask application. + +Usage: python query_mysql.py +""" + +import pymysql +import sys +import json +import random +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timedelta +from stripe_payment_processor import StripePaymentProcessor +from config import Config +from app import create_app, db +from models import Logs, Payments, PaymentBatch, SinglePayments, PaymentPlans +from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET + +# Initialize Splynx API +splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET) + +# Import constants from config +PAYMENT_METHOD_DIRECT_DEBIT = Config.PAYMENT_METHOD_DIRECT_DEBIT +PAYMENT_METHOD_CARD = Config.PAYMENT_METHOD_CARD +PAYMENT_METHOD_PAYMENT_PLAN = Config.PAYMENT_METHOD_PAYMENT_PLAN +PROCESS_LIVE = Config.PROCESS_LIVE + +if PROCESS_LIVE: + api_key = "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" +else: + api_key = "sk_test_51Rsi9gPfYyg6zE1S4ZpaPI1ehpbsHRLsGhysYXKwAWCZ7w6KYgVXy4pV095Nd8tyjUw9AkBhqfxqsIiiWJg5fexI00Dw36vnvx" + test_stripe_customers = ['cus_SoQqMGLmCjiBDZ', 'cus_SoQptxwe8hczGz', 'cus_SoQjeNXkKOdORI', 'cus_SoQiDcSrNRxbPF', 'cus_SoQedaG3q2ecKG', 'cus_SoQeTkzMA7AaLR', 'cus_SoQeijBTETQcGb', 'cus_SoQe259iKMgz7o', 'cus_SoQejTstdXEDTO', 'cus_SoQeQH2ORWBOWX', 'cus_SoQevtyWxqXtpC', 'cus_SoQekOFUHugf26', 'cus_SoPq6Zh0MCUR9W', 'cus_SoPovwUPJmvugz', 'cus_SoPnvGfejhpSR5', 'cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoMVPWxdYstYbr', 'cus_SoMVQ6Xj2dIrCR', 'cus_SoMVmBn1xipFEB', 'cus_SoMVNvZ2Iawb7Y', 'cus_SoMVZupj6wRy5e', 'cus_SoMVqjH7zkc5Qe', 'cus_SoMVkzj0ZUK0Ai', 'cus_SoMVFq3BUD3Njw', 'cus_SoLcrRrvoy9dJ4', 'cus_SoLcqHN1k0WD8j', 'cus_SoLcLtYDZGG32V', 'cus_SoLcG23ilNeMYt', 'cus_SoLcFhtUVzqumj', 'cus_SoLcPgMnuogINl', 'cus_SoLccGTY9mMV7T', 'cus_SoLRxqvJxuKFes', 'cus_SoKs7cjdcvW1oO'] + + +def find_pay_splynx_invoices(splynx_id): + result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid") + + invoice_pay = { + "status": "paid" + } + + for pay in result: + res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) + #print(json.dumps(res,indent=2)) + return res + +def add_payment_splynx(splynx_id, pi_id, pay_id, amount): + stripe_pay = { + "customer_id": splynx_id, + "amount": amount, + "date": str(datetime.now().strftime('%Y-%m-%d')), + "field_1": pi_id, + "field_2": f"Payment_ID (Batch): {pay_id}" + } + + res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay) + if res: + return res['id'] + else: + return False + +def handle_database_operation(operation_func, operation_name): + """ + Reusable function to handle database operations with consistent error handling. + + Args: + operation_func: Function that performs the database operation + operation_name: String description of the operation for error messages + + Returns: + Result of operation_func or None if failed + """ + try: + result = operation_func() + db.session.commit() + return result + except Exception as e: + db.session.rollback() + print(f"❌ {operation_name} failed: {e}") + return None + +def is_payment_day(start_date_string, payplan_schedule, date_format="%Y-%m-%d"): + """ + Check if today is a fortnightly payment day based on a start date. + + Args: + start_date_string (str): The first payment date + date_format (str): Format of the date string + + Returns: + bool: True if today is a payment day, False otherwise + """ + try: + if payplan_schedule == "Weekly": + num_days = 7 + elif payplan_schedule == "Fortnightly": + num_days = 14 + start_date = datetime.strptime(start_date_string, date_format) + today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) + + # Calculate days since start date + days_since_start = (today - start_date).days + + # Check if it's a multiple of 14 days (fortnight) + return days_since_start >= 0 and days_since_start % num_days == 0 + + except ValueError as e: + print(f"Error parsing date: {e}") + return False + + +def query_payplan_customers(): + """Query customer billing data from MySQL database and find Payment Plan customers.""" + to_return = [] + customers = db.session.query(PaymentPlans).filter(PaymentPlans.Enabled == True).all() + + for cust in customers: + if is_payment_day(start_date_string=str(cust.Start_Date.strftime('%Y-%m-%d')), payplan_schedule=cust.Frequency): + blah = { + "customer_id": cust.Splynx_ID, + "stripe_customer_id": cust.Stripe_Customer_ID, + "deposit": cust.Amount*-1, + "stripe_pm": cust.Stripe_Payment_Method, + "paymentplan_id": cust.id + } + to_return.append(blah) + + return to_return + + +def query_splynx_customers(pm): + """Query customer billing data from MySQL database.""" + + connection = None + try: + # Connect to MySQL database + connection = pymysql.connect( + host=Config.MYSQL_CONFIG['host'], + database=Config.MYSQL_CONFIG['database'], + user=Config.MYSQL_CONFIG['user'], + password=Config.MYSQL_CONFIG['password'], + port=Config.MYSQL_CONFIG['port'], + autocommit=False, + cursorclass=pymysql.cursors.DictCursor # Return results as dictionaries + ) + + print("✅ Connected to MySQL database successfully") + print(f"Database: {Config.MYSQL_CONFIG['database']} on {Config.MYSQL_CONFIG['host']}") + print("-" * 80) + + ## Payment Method: + ## 2 - Direct Debit (Automatic) + ## 3 - Card Payment (Automatic) + ## 9 - Payment Plan + + # Execute the query + query = """ + SELECT + cb.customer_id, + cb.deposit, + cb.payment_method, + pad.field_1 AS stripe_customer_id + FROM customer_billing cb + LEFT OUTER JOIN payment_account_data pad ON cb.customer_id = pad.customer_id + WHERE cb.payment_method = %s + AND cb.deposit < %s + ORDER BY cb.payment_method ASC + LIMIT %s + """ + + with connection.cursor() as cursor: + cursor.execute(query, (pm, Config.DEPOSIT_THRESHOLD, Config.DEFAULT_QUERY_LIMIT)) + #cursor.execute(query, (Config.DEFAULT_QUERY_LIMIT)) + results = cursor.fetchall() + + if results: + print(f"📊 Found {len(results)} rows:") + return results + else: + print("ℹ️ No rows found matching the criteria") + return False + + except pymysql.Error as e: + print(f"❌ MySQL Error: {e}") + sys.exit(1) + except Exception as e: + print(f"❌ Unexpected Error: {e}") + sys.exit(1) + finally: + if connection: + connection.close() + print("\n🔒 MySQL connection closed") + + +def addInitialPayments(customers, batch_id): + added = {"added": 0, "failed": 0} + payments_to_add = [] + + # Prepare all payments first + for cust in customers: + if PROCESS_LIVE: + stripe_customer_id = cust['stripe_customer_id'] + else: + #stripe_customer_id = cust['stripe_customer_id'] + stripe_customer_id = test_stripe_customers[random.randint(1, len(test_stripe_customers)-1)] + add_payer = Payments( + PaymentBatch_ID = batch_id, + Splynx_ID = cust['customer_id'], + Stripe_Customer_ID = stripe_customer_id, + Payment_Amount = float(cust['deposit'])*-1, + Stripe_Payment_Method = cust.get('stripe_pm', None), + PaymentPlan_ID = cust.get('paymentplan_id', None) + ) + payments_to_add.append(add_payer) + db.session.add(add_payer) + + # Atomic commit for entire batch + try: + db.session.commit() + added["added"] = len(payments_to_add) + print(f"✅ Successfully added {len(payments_to_add)} payments to batch {batch_id}") + except Exception as e: + db.session.rollback() + added["failed"] = len(payments_to_add) + print(f"❌ addInitialPayments failed for entire batch {batch_id}: {e}") + + print(f"Plutus DB: {json.dumps(added,indent=2)}\n") + +def addPaymentBatch(): + """Create a new payment batch and return its ID.""" + add_batch = PaymentBatch() + + try: + db.session.add(add_batch) + db.session.commit() + return add_batch.id + except Exception as e: + db.session.rollback() + print(f"❌ addPaymentBatch failed: {e}") + return None + +def processPaymentResult(pay_id, result, key): + if key == "pay": + payment = db.session.query(Payments).filter(Payments.id == pay_id).first() + elif key == "singlepay": + payment = db.session.query(SinglePayments).filter(SinglePayments.id == pay_id).first() + try: + if result.get('error') and not result.get('needs_fee_update'): + payment.Error = f"Error Type: {result['error_type']}\nError: {result['error']}" + payment.Success = result['success'] + payment.PI_JSON = json.dumps(result) + else: + if result.get('needs_fee_update'): + payment.PI_FollowUp = True + payment.Payment_Intent = result['payment_intent_id'] + payment.Success = result['success'] + if result['success'] and PROCESS_LIVE: + find_pay_splynx_invoices(payment.Splynx_ID) + add_payment_splynx( + splynx_id=payment.Splynx_ID, + pi_id=result['payment_intent_id'], + pay_id=payment.id, + amount=payment.Payment_Amount + ) + if result.get('payment_method_type') == "card": + payment.Payment_Method = result['estimated_fee_details']['card_display_brand'] + elif result.get('payment_method_type') == "au_becs_debit": + payment.Payment_Method = result['payment_method_type'] + if payment.PI_JSON: + combined = {**json.loads(payment.PI_JSON), **result} + payment.PI_JSON = json.dumps(combined) + else: + payment.PI_JSON = json.dumps(result) + if result.get('fee_details'): + payment.Fee_Total = result['fee_details']['total_fee'] + for fee_type in result['fee_details']['fee_breakdown']: + if fee_type['type'] == "tax": + payment.Fee_Tax = fee_type['amount'] + elif fee_type['type'] == "stripe_fee": + payment.Fee_Stripe = fee_type['amount'] + except Exception as e: + print(f"processPaymentResult: {e}\n{json.dumps(result)}") + payment.PI_FollowUp = True + + def _update_payment(): + return True # Just need to trigger commit, payment is already modified + + handle_database_operation(_update_payment, "processPaymentResult") + +# Thread lock for database operations +db_lock = threading.Lock() + +def process_single_payment(processor, payment_data): + """ + Thread-safe function to process a single payment. + + Args: + processor: StripePaymentProcessor instance + payment_data: Dict containing payment information + + Returns: + Dict with payment result and metadata + """ + try: + # Process payment with Stripe (thread-safe) + result = processor.process_payment( + customer_id=payment_data['customer_id'], + amount=payment_data['amount'], + currency=payment_data['currency'], + description=payment_data['description'], + stripe_pm=payment_data['stripe_pm'] + ) + + # Return result with payment ID for database update + return { + 'payment_id': payment_data['payment_id'], + 'result': result, + 'success': True + } + except Exception as e: + print(f"❌ Payment processing failed for payment ID {payment_data['payment_id']}: {e}") + return { + 'payment_id': payment_data['payment_id'], + 'result': None, + 'success': False, + 'error': str(e) + } + +def update_single_payment_result(payment_id, result): + """ + Thread-safe immediate update of single payment result to database. + Commits immediately to ensure data safety. + + Args: + payment_id: ID of the payment to update + result: Payment processing result + """ + with db_lock: + try: + if result: + processPaymentResult(pay_id=payment_id, result=result, key="pay") + print(f"✅ Payment {payment_id} result committed to database") + else: + print(f"⚠️ No result to commit for payment {payment_id}") + except Exception as e: + print(f"❌ Failed to update payment {payment_id}: {e}") + +def process_batch_mode(processor): + """Handle batch processing for Direct Debit and Card payments.""" + to_run_batches = [] + payment_methods = [PAYMENT_METHOD_DIRECT_DEBIT, PAYMENT_METHOD_CARD] + + for pm in payment_methods: + batch_id = addPaymentBatch() + if batch_id is not None: + to_run_batches.append(batch_id) + customers = query_splynx_customers(pm) + addInitialPayments(customers=customers, batch_id=batch_id) + else: + print(f"❌ Failed to create batch for payment method {pm}") + + return to_run_batches + +def process_payplan_mode(processor): + """Handle payment plan processing.""" + to_run_batches = [] + batch_id = addPaymentBatch() + if batch_id is not None: + to_run_batches.append(batch_id) + customers = query_payplan_customers() + addInitialPayments(customers=customers, batch_id=batch_id) + else: + print(f"❌ Failed to create batch for payment plan processing") + + return to_run_batches + +def execute_payment_batches(processor, batch_ids): + """Execute payments for all provided batch IDs using safe threading with immediate commits.""" + if not batch_ids: + print("⚠️ No valid batches to process") + return + + max_threads = Config.MAX_PAYMENT_THREADS + + for batch in batch_ids: + if batch is None: + print("⚠️ Skipping None batch ID") + continue + + cust_pay = db.session.query(Payments).filter(Payments.PaymentBatch_ID == batch).all() + if not cust_pay: + print(f"ℹ️ No payments found for batch {batch}") + continue + + print(f"🔄 Processing {len(cust_pay)} payments in batch {batch} using {max_threads} threads") + print(f"📊 Safety Mode: Each payment will be committed immediately to database") + + # Process payments in smaller chunks to avoid timeout issues + processed_count = 0 + failed_count = 0 + + # Process payments in chunks + chunk_size = max_threads * 2 # Process 2x thread count at a time + for i in range(0, len(cust_pay), chunk_size): + chunk = cust_pay[i:i + chunk_size] + print(f"🔄 Processing chunk {i//chunk_size + 1}: payments {i+1}-{min(i+chunk_size, len(cust_pay))}") + + # Prepare payment data for this chunk + payment_tasks = [] + + for pay in chunk: + if PROCESS_LIVE: + customer_id = pay.Stripe_Customer_ID + else: + customer_id = pay.Stripe_Customer_ID + #customer_id = test_stripe_customers[random.randint(1, len(test_stripe_customers)-1)] + payment_data = { + 'payment_id': pay.id, + 'customer_id': customer_id, + 'amount': pay.Payment_Amount, + 'currency': "aud", + 'description': f"Payment ID: {pay.id} - Splynx ID: {pay.Splynx_ID}", + 'stripe_pm': pay.Stripe_Payment_Method + } + print(f"payment_data: {json.dumps(payment_data,indent=2)}") + payment_tasks.append(payment_data) + + # Process this chunk with ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_threads) as executor: + # Submit tasks for this chunk + future_to_payment = { + executor.submit(process_single_payment, processor, task): task + for task in payment_tasks + } + + # Process results as they complete (NO TIMEOUT on as_completed) + for future in as_completed(future_to_payment): + try: + result = future.result(timeout=60) # Individual payment timeout + + if result['success'] and result['result']: + # IMMEDIATELY commit each successful payment to database + update_single_payment_result(result['payment_id'], result['result']) + processed_count += 1 + print(f"✅ Payment {result['payment_id']} processed and committed ({processed_count}/{len(cust_pay)})") + else: + failed_count += 1 + print(f"❌ Payment {result['payment_id']} failed ({failed_count} failures total)") + + except Exception as e: + payment_data = future_to_payment[future] + failed_count += 1 + print(f"❌ Thread exception for payment {payment_data['payment_id']}: {e}") + + print(f"📊 Chunk completed: {processed_count} processed, {failed_count} failed") + + print(f"✅ Batch {batch} completed: {processed_count}/{len(cust_pay)} payments processed successfully") + +def process_payintent_mode(processor): + """Handle payment intent follow-up processing.""" + to_check = { + "pay": db.session.query(Payments).filter(Payments.PI_FollowUp == True).all(), + "singlepay": db.session.query(SinglePayments).filter(SinglePayments.PI_FollowUp == True).all(), + } + #pis = db.session.query(Payments).filter(Payments.PI_FollowUp == True).all() + #to_check.append(pis) + #pis = db.session.query(SinglePayments).filter(SinglePayments.PI_FollowUp == True).all() + #to_check.append(pis) + for key, value in to_check.items(): + print(value) + for pi in value: + intent_result = processor.check_payment_intent(pi.Payment_Intent) + print(json.dumps(intent_result, indent=2)) + + if intent_result['status'] == "succeeded": + pi.PI_FollowUp_JSON = json.dumps(intent_result) + pi.PI_FollowUp = False + pi.PI_Last_Check = datetime.now() + processPaymentResult(pay_id=pi.id, result=intent_result, key=key) + else: + pi.PI_FollowUp_JSON = json.dumps(intent_result) + pi.PI_Last_Check = datetime.now() + + db.session.commit() + +if __name__ == "__main__": + ## Payment Method: + ## 2 - Direct Debit (Automatic) + ## 3 - Card Payment (Automatic) + ## 9 - Payment Plan + + ### Running Mode + ## batch = Monthly Direct Debit/Credit Cards + ## payintent = Check outstanding Payment Intents and update + ## payplan = Check for Payment Plans to run + + try: + if sys.argv[1] == "batch": + running_mode = "batch" + elif sys.argv[1] == "payintent": + running_mode = "payintent" + elif sys.argv[1] == "payplan": + running_mode = "payplan" + else: + print(f"❌ Invalid running mode: {sys.argv[1]}") + print("Valid modes: batch, payintent, payplan") + sys.exit(1) + try: + if sys.argv[2] == "live": + PROCESS_LIVE = True + except: + print("Processing payments against Sandbox") + except IndexError: + print("ℹ️ No running mode specified, defaulting to 'payintent'") + running_mode = "payintent" + # Create Flask application context + app = create_app() + processor = StripePaymentProcessor(api_key=api_key, enable_logging=True) + + with app.app_context(): + if running_mode == "batch": + batch_ids = process_batch_mode(processor) + execute_payment_batches(processor, batch_ids) + elif running_mode == "payplan": + batch_ids = process_payplan_mode(processor) + execute_payment_batches(processor, batch_ids) + elif running_mode == "payintent": + process_payintent_mode(processor) diff --git a/query_mysql.py b/query_mysql.py new file mode 100644 index 0000000..022202e --- /dev/null +++ b/query_mysql.py @@ -0,0 +1,672 @@ +#!/usr/bin/env python3 +""" +External script to query MySQL database (Splynx) for customer billing data. +This script runs independently of the Flask application. + +Usage: python query_mysql.py +""" + +import pymysql +import sys +import json +import random +import threading +import logging +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime +from typing import List, Dict, Union, Any +from stripe_payment_processor import StripePaymentProcessor +from config import Config +from app import create_app, db +from models import Payments, PaymentBatch, SinglePayments, PaymentPlans +from splynx import Splynx, SPLYNX_URL, SPLYNX_KEY, SPLYNX_SECRET +from services import ( + log_script_start, log_script_completion, log_batch_created, + log_payment_intent_followup +) + +# Configure logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('payment_processing.log'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + +# Initialize Splynx API +splynx = Splynx(url=SPLYNX_URL, key=SPLYNX_KEY, secret=SPLYNX_SECRET) + +# Import constants from config +PAYMENT_METHOD_DIRECT_DEBIT = Config.PAYMENT_METHOD_DIRECT_DEBIT +PAYMENT_METHOD_CARD = Config.PAYMENT_METHOD_CARD +PAYMENT_METHOD_PAYMENT_PLAN = Config.PAYMENT_METHOD_PAYMENT_PLAN +PROCESS_LIVE = Config.PROCESS_LIVE + +# Get Stripe API key from config +if PROCESS_LIVE: + api_key = Config.STRIPE_LIVE_API_KEY +else: + api_key = Config.STRIPE_TEST_API_KEY + test_stripe_customers = ['cus_SoQqMGLmCjiBDZ', 'cus_SoQptxwe8hczGz', 'cus_SoQjeNXkKOdORI', 'cus_SoQiDcSrNRxbPF', 'cus_SoQedaG3q2ecKG', 'cus_SoQeTkzMA7AaLR', 'cus_SoQeijBTETQcGb', 'cus_SoQe259iKMgz7o', 'cus_SoQejTstdXEDTO', 'cus_SoQeQH2ORWBOWX', 'cus_SoQevtyWxqXtpC', 'cus_SoQekOFUHugf26', 'cus_SoPq6Zh0MCUR9W', 'cus_SoPovwUPJmvugz', 'cus_SoPnvGfejhpSR5', 'cus_SoNAgAbkbFo8ZY', 'cus_SoMyDihTxRsa7U', 'cus_SoMVPWxdYstYbr', 'cus_SoMVQ6Xj2dIrCR', 'cus_SoMVmBn1xipFEB', 'cus_SoMVNvZ2Iawb7Y', 'cus_SoMVZupj6wRy5e', 'cus_SoMVqjH7zkc5Qe', 'cus_SoMVkzj0ZUK0Ai', 'cus_SoMVFq3BUD3Njw', 'cus_SoLcrRrvoy9dJ4', 'cus_SoLcqHN1k0WD8j', 'cus_SoLcLtYDZGG32V', 'cus_SoLcG23ilNeMYt', 'cus_SoLcFhtUVzqumj', 'cus_SoLcPgMnuogINl', 'cus_SoLccGTY9mMV7T', 'cus_SoLRxqvJxuKFes', 'cus_SoKs7cjdcvW1oO'] + + +def find_pay_splynx_invoices(splynx_id: int) -> List[Dict[str, Any]]: + result = splynx.get(url=f"/api/2.0/admin/finance/invoices?main_attributes[customer_id]={splynx_id}&main_attributes[status]=not_paid") + + invoice_pay = { + "status": "paid" + } + + updated_invoices = [] + for pay in result: + res = splynx.put(url=f"/api/2.0/admin/finance/invoices/{pay['id']}", params=invoice_pay) + if res: + updated_invoices.append(res) + return updated_invoices + +def add_payment_splynx(splynx_id: int, pi_id: str, pay_id: int, amount: float) -> Union[int, bool]: + stripe_pay = { + "customer_id": splynx_id, + "amount": amount, + "date": str(datetime.now().strftime('%Y-%m-%d')), + "field_1": pi_id, + "field_2": f"Payment_ID (Batch): {pay_id}" + } + + res = splynx.post(url="/api/2.0/admin/finance/payments", params=stripe_pay) + if res: + return res['id'] + else: + return False + +def handle_database_operation(operation_func: callable, operation_name: str) -> Any: + """ + Reusable function to handle database operations with consistent error handling. + + Args: + operation_func: Function that performs the database operation + operation_name: String description of the operation for error messages + + Returns: + Result of operation_func or None if failed + """ + try: + result = operation_func() + db.session.commit() + return result + except Exception as e: + db.session.rollback() + logger.error(f"{operation_name} failed: {e}") + return None + +def is_payment_day(start_date_string: str, payplan_schedule: str, date_format: str = "%Y-%m-%d") -> bool: + """ + Check if today is a payment day based on a start date and frequency. + + Args: + start_date_string (str): The first payment date + payplan_schedule (str): Payment frequency ("Weekly" or "Fortnightly") + date_format (str): Format of the date string + + Returns: + bool: True if today is a payment day, False otherwise + """ + try: + if not start_date_string or not payplan_schedule: + logger.error("Missing required parameters for payment day calculation") + return False + + if payplan_schedule == "Weekly": + num_days = 7 + elif payplan_schedule == "Fortnightly": + num_days = 14 + else: + logger.error(f"Unsupported payment schedule '{payplan_schedule}'") + return False + + start_date = datetime.strptime(start_date_string, date_format) + today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) + start_date = start_date.replace(hour=0, minute=0, second=0, microsecond=0) + + # Calculate days since start date + days_since_start = (today - start_date).days + + # Check if it's a multiple of the payment frequency + return days_since_start >= 0 and days_since_start % num_days == 0 + + except ValueError as e: + logger.error(f"Error parsing date '{start_date_string}' with format '{date_format}': {e}") + return False + except Exception as e: + logger.error(f"Unexpected error in is_payment_day: {e}") + return False + + +def query_payplan_customers() -> List[Dict[str, Any]]: + """Query customer billing data from MySQL database and find Payment Plan customers.""" + to_return = [] + customers = db.session.query(PaymentPlans).filter(PaymentPlans.Enabled == True).all() + + for cust in customers: + if cust.Start_Date and is_payment_day(start_date_string=str(cust.Start_Date.strftime('%Y-%m-%d')), payplan_schedule=cust.Frequency): + payment_data = { + "customer_id": cust.Splynx_ID, + "stripe_customer_id": cust.Stripe_Customer_ID, + "deposit": cust.Amount*-1, + "stripe_pm": cust.Stripe_Payment_Method, + "paymentplan_id": cust.id + } + to_return.append(payment_data) + + return to_return + + +def query_splynx_customers(pm: int) -> Union[List[Dict[str, Any]], bool]: + """Query customer billing data from MySQL database.""" + + connection = None + try: + # Connect to MySQL database + connection = pymysql.connect( + host=Config.MYSQL_CONFIG['host'], + database=Config.MYSQL_CONFIG['database'], + user=Config.MYSQL_CONFIG['user'], + password=Config.MYSQL_CONFIG['password'], + port=Config.MYSQL_CONFIG['port'], + autocommit=False, + cursorclass=pymysql.cursors.DictCursor # Return results as dictionaries + ) + + logger.info("Connected to MySQL database successfully") + logger.info(f"Database: {Config.MYSQL_CONFIG['database']} on {Config.MYSQL_CONFIG['host']}") + logger.info("-" * 80) + + ## Payment Method: + ## 2 - Direct Debit (Automatic) + ## 3 - Card Payment (Automatic) + ## 9 - Payment Plan + + # Execute the query + query = """ + SELECT + cb.customer_id, + cb.deposit, + cb.payment_method, + pad.field_1 AS stripe_customer_id + FROM customer_billing cb + LEFT OUTER JOIN payment_account_data pad ON cb.customer_id = pad.customer_id + WHERE cb.payment_method = %s + AND cb.deposit < %s + AND NOT EXISTS ( + SELECT 1 + FROM invoices i + WHERE i.customer_id = cb.customer_id + AND i.status = 'pending' + ) + ORDER BY cb.payment_method ASC + LIMIT %s + """ + + #query = """ + #SELECT + # cb.customer_id, + # cb.deposit, + # cb.payment_method, + # pad.field_1 AS stripe_customer_id + #FROM customer_billing cb + #LEFT OUTER JOIN payment_account_data pad ON cb.customer_id = pad.customer_id + #WHERE cb.payment_method = %s + #AND cb.deposit < %s + #ORDER BY cb.payment_method ASC + #LIMIT %s + #""" + + with connection.cursor() as cursor: + cursor.execute(query, (pm, Config.DEPOSIT_THRESHOLD, Config.DEFAULT_QUERY_LIMIT)) + results = cursor.fetchall() + + if results: + logger.info(f"Found {len(results)} rows") + return results + else: + logger.info("No rows found matching the criteria") + return False + + except pymysql.Error as e: + logger.error(f"MySQL Error: {e}") + sys.exit(1) + except Exception as e: + logger.error(f"Unexpected Error: {e}") + sys.exit(1) + finally: + if connection: + connection.close() + logger.info("MySQL connection closed") + + +def addInitialPayments(customers, batch_id): + added = {"added": 0, "failed": 0} + payments_to_add = [] + + # Prepare all payments first + for cust in customers: + if PROCESS_LIVE: + stripe_customer_id = cust['stripe_customer_id'] + else: + stripe_customer_id = test_stripe_customers[random.randint(1, len(test_stripe_customers)-1)] + add_payer = Payments( + PaymentBatch_ID = batch_id, + Splynx_ID = cust['customer_id'], + Stripe_Customer_ID = stripe_customer_id, + Payment_Amount = float(cust['deposit'])*-1, + Stripe_Payment_Method = cust.get('stripe_pm', None), + PaymentPlan_ID = cust.get('paymentplan_id', None) + ) + payments_to_add.append(add_payer) + db.session.add(add_payer) + + # Atomic commit for entire batch + try: + db.session.commit() + added["added"] = len(payments_to_add) + logger.info(f"Successfully added {len(payments_to_add)} payments to batch {batch_id}") + except Exception as e: + db.session.rollback() + added["failed"] = len(payments_to_add) + logger.error(f"addInitialPayments failed for entire batch {batch_id}: {e}") + + logger.info(f"Database operation result: {json.dumps(added,indent=2)}") + +def addPaymentBatch(): + """Create a new payment batch and return its ID.""" + add_batch = PaymentBatch() + + try: + db.session.add(add_batch) + db.session.commit() + return add_batch.id + except Exception as e: + db.session.rollback() + logger.error(f"addPaymentBatch failed: {e}") + return None + +def processPaymentResult(pay_id, result, key): + if key == "pay": + payment = db.session.query(Payments).filter(Payments.id == pay_id).first() + elif key == "singlepay": + payment = db.session.query(SinglePayments).filter(SinglePayments.id == pay_id).first() + try: + if result.get('error') and not result.get('needs_fee_update'): + payment.Error = f"Error Type: {result['error_type']}\nError: {result['error']}" + payment.Success = result['success'] + payment.PI_JSON = json.dumps(result) + else: + if result.get('needs_fee_update'): + payment.PI_FollowUp = True + payment.Payment_Intent = result['payment_intent_id'] + payment.Success = result['success'] + if result['success'] and PROCESS_LIVE: + find_pay_splynx_invoices(payment.Splynx_ID) + add_payment_splynx( + splynx_id=payment.Splynx_ID, + pi_id=result['payment_intent_id'], + pay_id=payment.id, + amount=payment.Payment_Amount + ) + if result.get('payment_method_type') == "card": + payment.Payment_Method = result['estimated_fee_details']['card_display_brand'] + elif result.get('payment_method_type') == "au_becs_debit": + payment.Payment_Method = result['payment_method_type'] + if payment.PI_JSON: + combined = {**json.loads(payment.PI_JSON), **result} + payment.PI_JSON = json.dumps(combined) + else: + payment.PI_JSON = json.dumps(result) + if result.get('fee_details'): + payment.Fee_Total = result['fee_details']['total_fee'] + for fee_type in result['fee_details']['fee_breakdown']: + if fee_type['type'] == "tax": + payment.Fee_Tax = fee_type['amount'] + elif fee_type['type'] == "stripe_fee": + payment.Fee_Stripe = fee_type['amount'] + except Exception as e: + logger.error(f"processPaymentResult: {e}\nResult: {json.dumps(result)}") + payment.PI_FollowUp = True + + def _update_payment(): + return True # Just need to trigger commit, payment is already modified + + handle_database_operation(_update_payment, "processPaymentResult") + +# Thread lock for database operations +db_lock = threading.Lock() + +def process_single_payment(processor, payment_data): + """ + Thread-safe function to process a single payment. + + Args: + processor: StripePaymentProcessor instance + payment_data: Dict containing payment information + + Returns: + Dict with payment result and metadata + """ + try: + # Process payment with Stripe (thread-safe) + result = processor.process_payment( + customer_id=payment_data['customer_id'], + amount=payment_data['amount'], + currency=payment_data['currency'], + description=payment_data['description'], + stripe_pm=payment_data['stripe_pm'] + ) + + # Return result with payment ID for database update + return { + 'payment_id': payment_data['payment_id'], + 'result': result, + 'success': True + } + except Exception as e: + logger.error(f"Payment processing failed for payment ID {payment_data['payment_id']}: {e}") + return { + 'payment_id': payment_data['payment_id'], + 'result': None, + 'success': False, + 'error': str(e) + } + +def update_single_payment_result(payment_id, result): + """ + Thread-safe immediate update of single payment result to database. + Commits immediately to ensure data safety. + + Args: + payment_id: ID of the payment to update + result: Payment processing result + """ + with db_lock: + try: + if result: + processPaymentResult(pay_id=payment_id, result=result, key="pay") + logger.info(f"Payment {payment_id} result committed to database") + else: + logger.warning(f"No result to commit for payment {payment_id}") + except Exception as e: + logger.error(f"Failed to update payment {payment_id}: {e}") + +def process_batch_mode(processor): + """Handle batch processing for Direct Debit and Card payments.""" + to_run_batches = [] + payment_methods = [PAYMENT_METHOD_DIRECT_DEBIT, PAYMENT_METHOD_CARD] + total_customers = 0 + + payment_method_names = { + PAYMENT_METHOD_DIRECT_DEBIT: "Direct Debit", + PAYMENT_METHOD_CARD: "Card Payment" + } + + for pm in payment_methods: + batch_id = addPaymentBatch() + if batch_id is not None: + to_run_batches.append(batch_id) + customers = query_splynx_customers(pm) + if customers: + customer_count = len(customers) + total_customers += customer_count + addInitialPayments(customers=customers, batch_id=batch_id) + + # Log batch creation + log_batch_created(batch_id, payment_method_names[pm], customer_count) + logger.info(f"Created batch {batch_id} for {payment_method_names[pm]} with {customer_count} customers") + else: + logger.info(f"No customers found for {payment_method_names[pm]}") + else: + logger.error(f"Failed to create batch for payment method {pm}") + + return to_run_batches, 0, 0, 0.0 # Success/failed counts will be updated during execution + +def process_payplan_mode(processor): + """Handle payment plan processing.""" + to_run_batches = [] + + # Get count of active payment plans for logging (if needed in future) + + batch_id = addPaymentBatch() + if batch_id is not None: + to_run_batches.append(batch_id) + customers = query_payplan_customers() + due_plans_count = len(customers) if customers else 0 + + if customers: + total_amount = sum(abs(c.get('deposit', 0)) for c in customers) + addInitialPayments(customers=customers, batch_id=batch_id) + + # Log batch creation for payment plans + log_batch_created(batch_id, "Payment Plan", due_plans_count) + logger.info(f"Created payment plan batch {batch_id} with {due_plans_count} due plans (${total_amount:,.2f} total)") + else: + logger.info("No payment plans due for processing today") + total_amount = 0.0 + else: + logger.error("Failed to create batch for payment plan processing") + due_plans_count = 0 + total_amount = 0.0 + + return to_run_batches, 0, 0, total_amount # Success/failed counts will be updated during execution + +def execute_payment_batches(processor, batch_ids): + """Execute payments for all provided batch IDs using safe threading with immediate commits.""" + if not batch_ids: + logger.warning("No valid batches to process") + return + + max_threads = Config.MAX_PAYMENT_THREADS + + for batch in batch_ids: + if batch is None: + logger.warning("Skipping None batch ID") + continue + + cust_pay = db.session.query(Payments).filter(Payments.PaymentBatch_ID == batch).all() + if not cust_pay: + logger.info(f"No payments found for batch {batch}") + continue + + logger.info(f"Processing {len(cust_pay)} payments in batch {batch} using {max_threads} threads") + logger.info("Safety Mode: Each payment will be committed immediately to database") + + # Process payments in smaller chunks to avoid timeout issues + processed_count = 0 + failed_count = 0 + + # Process payments in chunks + chunk_size = max_threads * 2 # Process 2x thread count at a time + for i in range(0, len(cust_pay), chunk_size): + chunk = cust_pay[i:i + chunk_size] + logger.info(f"Processing chunk {i//chunk_size + 1}: payments {i+1}-{min(i+chunk_size, len(cust_pay))}") + + # Prepare payment data for this chunk + payment_tasks = [] + + for pay in chunk: + if PROCESS_LIVE: + customer_id = pay.Stripe_Customer_ID + else: + customer_id = pay.Stripe_Customer_ID + payment_data = { + 'payment_id': pay.id, + 'customer_id': customer_id, + 'amount': pay.Payment_Amount, + 'currency': "aud", + 'description': f"Payment ID: {pay.id} - Splynx ID: {pay.Splynx_ID}", + 'stripe_pm': pay.Stripe_Payment_Method + } + logger.debug(f"payment_data: {json.dumps(payment_data,indent=2)}") + payment_tasks.append(payment_data) + + # Process this chunk with ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=max_threads) as executor: + # Submit tasks for this chunk + future_to_payment = { + executor.submit(process_single_payment, processor, task): task + for task in payment_tasks + } + + # Process results as they complete (NO TIMEOUT on as_completed) + for future in as_completed(future_to_payment): + try: + result = future.result(timeout=60) # Individual payment timeout + + if result['success'] and result['result']: + # IMMEDIATELY commit each successful payment to database + update_single_payment_result(result['payment_id'], result['result']) + processed_count += 1 + logger.info(f"Payment {result['payment_id']} processed and committed ({processed_count}/{len(cust_pay)})") + else: + failed_count += 1 + logger.warning(f"Payment {result['payment_id']} failed ({failed_count} failures total)") + + except Exception as e: + payment_data = future_to_payment[future] + failed_count += 1 + logger.error(f"Thread exception for payment {payment_data['payment_id']}: {e}") + + logger.info(f"Chunk completed: {processed_count} processed, {failed_count} failed") + + logger.info(f"Batch {batch} completed: {processed_count}/{len(cust_pay)} payments processed successfully") + +def process_payintent_mode(processor): + """Handle payment intent follow-up processing.""" + to_check = { + "pay": db.session.query(Payments).filter(Payments.PI_FollowUp == True).all(), + "singlepay": db.session.query(SinglePayments).filter(SinglePayments.PI_FollowUp == True).all(), + } + + total_pending = 0 + succeeded_count = 0 + failed_count = 0 + still_pending = 0 + + for key, value in to_check.items(): + logger.debug(f"Processing payment intent follow-up for {len(value)} {key} items") + total_pending += len(value) + + for pi in value: + try: + intent_result = processor.check_payment_intent(pi.Payment_Intent) + logger.debug(f"Intent result: {json.dumps(intent_result, indent=2)}") + + if intent_result['status'] == "succeeded": + pi.PI_FollowUp_JSON = json.dumps(intent_result) + pi.PI_FollowUp = False + pi.PI_Last_Check = datetime.now() + processPaymentResult(pay_id=pi.id, result=intent_result, key=key) + succeeded_count += 1 + elif intent_result['status'] == "failed": + pi.PI_FollowUp_JSON = json.dumps(intent_result) + pi.PI_FollowUp = False + pi.PI_Last_Check = datetime.now() + failed_count += 1 + else: + # Still pending + pi.PI_FollowUp_JSON = json.dumps(intent_result) + pi.PI_Last_Check = datetime.now() + still_pending += 1 + + db.session.commit() + except Exception as e: + logger.error(f"Error processing payment intent {pi.Payment_Intent}: {e}") + failed_count += 1 + + # Log payment intent follow-up results + if total_pending > 0: + log_payment_intent_followup(total_pending, succeeded_count, failed_count, still_pending) + logger.info(f"Payment intent follow-up completed: {succeeded_count} succeeded, {failed_count} failed, {still_pending} still pending") + else: + logger.info("No payment intents requiring follow-up") + + return succeeded_count, failed_count + +if __name__ == "__main__": + ## Payment Method: + ## 2 - Direct Debit (Automatic) + ## 3 - Card Payment (Automatic) + ## 9 - Payment Plan + + ### Running Mode + ## batch = Monthly Direct Debit/Credit Cards + ## payintent = Check outstanding Payment Intents and update + ## payplan = Check for Payment Plans to run + + start_time = datetime.now() + success_count = 0 + failed_count = 0 + total_amount = 0.0 + batch_ids = [] + errors = [] + + try: + if sys.argv[1] == "batch": + running_mode = "batch" + elif sys.argv[1] == "payintent": + running_mode = "payintent" + elif sys.argv[1] == "payplan": + running_mode = "payplan" + else: + logger.error(f"Invalid running mode: {sys.argv[1]}") + logger.info("Valid modes: batch, payintent, payplan") + sys.exit(1) + try: + if sys.argv[2] == "live": + PROCESS_LIVE = True + except IndexError: + logger.info("Processing payments against Sandbox") + except IndexError: + logger.info("No running mode specified, defaulting to 'payintent'") + running_mode = "payintent" + + # Create Flask application context + app = create_app() + processor = StripePaymentProcessor(api_key=api_key, enable_logging=True) + + with app.app_context(): + # Log script start + environment = "live" if PROCESS_LIVE else "sandbox" + log_script_start("query_mysql.py", running_mode, environment) + logger.info(f"Starting query_mysql.py in {running_mode} mode ({environment})") + + try: + if running_mode == "batch": + batch_ids, success_count, failed_count, total_amount = process_batch_mode(processor) + execute_payment_batches(processor, batch_ids) + elif running_mode == "payplan": + batch_ids, success_count, failed_count, total_amount = process_payplan_mode(processor) + execute_payment_batches(processor, batch_ids) + elif running_mode == "payintent": + success_count, failed_count = process_payintent_mode(processor) + except Exception as e: + logger.error(f"Script execution failed: {e}") + errors.append(str(e)) + failed_count += 1 + + # Calculate execution time and log completion + end_time = datetime.now() + duration_seconds = (end_time - start_time).total_seconds() + + log_script_completion( + script_name="query_mysql.py", + mode=running_mode, + success_count=success_count, + failed_count=failed_count, + total_amount=total_amount, + batch_ids=batch_ids if batch_ids else None, + duration_seconds=duration_seconds, + errors=errors if errors else None + ) + + logger.info(f"Script completed in {duration_seconds:.1f}s: {success_count} successful, {failed_count} failed") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..2859735 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask==2.3.3 +Flask-SQLAlchemy==3.1.1 +Flask-Migrate==4.0.5 +Flask-Login==0.6.3 +PyMySQL==1.1.0 +psycopg2-binary==2.9.8 +Werkzeug==2.3.7 \ No newline at end of file diff --git a/services.py b/services.py new file mode 100644 index 0000000..1114227 --- /dev/null +++ b/services.py @@ -0,0 +1,229 @@ +""" +Logging and utility services for Plutus payment processing application. + +This module provides database logging functionality for tracking application activities, +particularly automated script executions and payment processing operations. +""" + +from datetime import datetime, timezone +from typing import Optional, Union +from app import db +from models import Logs + +# System user ID for automated processes +SYSTEM_USER_ID = 1 + +def log_activity( + user_id: int, + action: str, + entity_type: str, + entity_id: Optional[int] = None, + details: Optional[str] = None, + ip_address: Optional[str] = None +) -> Optional[int]: + """ + Log an activity to the database. + + Args: + user_id (int): ID of the user performing the action (use SYSTEM_USER_ID for automated processes) + action (str): Type of action performed (e.g., 'BATCH_RUN', 'PAYMENT_PROCESSED', 'API_ACCESS') + entity_type (str): Type of entity involved (e.g., 'Batch', 'Payment', 'PaymentPlan') + entity_id (int, optional): ID of the specific entity involved + details (str, optional): Detailed description of the activity + ip_address (str, optional): IP address of the request (for web requests) + + Returns: + int: ID of the created log entry, or None if failed + """ + try: + log_entry = Logs( + User_ID=user_id, + Action=action, + Entity_Type=entity_type, + Entity_ID=entity_id, + Log_Entry=details, + IP_Address=ip_address, + Added=datetime.now(timezone.utc) + ) + + db.session.add(log_entry) + db.session.commit() + + return log_entry.id + + except Exception as e: + db.session.rollback() + print(f"Failed to log activity: {e}") + return None + +def log_script_start(script_name: str, mode: str, environment: str) -> Optional[int]: + """ + Log the start of a script execution. + + Args: + script_name (str): Name of the script being executed + mode (str): Running mode (batch, payintent, payplan) + environment (str): Environment (live, sandbox) + + Returns: + int: Log entry ID or None if failed + """ + details = f"{script_name} started in {mode} mode ({environment} environment)" + return log_activity( + user_id=SYSTEM_USER_ID, + action="SCRIPT_START", + entity_type="Script", + details=details + ) + +def log_script_completion( + script_name: str, + mode: str, + success_count: int = 0, + failed_count: int = 0, + total_amount: float = 0.0, + batch_ids: Optional[list] = None, + duration_seconds: Optional[float] = None, + errors: Optional[list] = None +) -> Optional[int]: + """ + Log the completion of a script execution with summary statistics. + + Args: + script_name (str): Name of the script that completed + mode (str): Running mode that was executed + success_count (int): Number of successful operations + failed_count (int): Number of failed operations + total_amount (float): Total amount processed + batch_ids (list, optional): List of batch IDs created + duration_seconds (float, optional): Execution time in seconds + errors (list, optional): List of error messages encountered + + Returns: + int: Log entry ID or None if failed + """ + total_operations = success_count + failed_count + success_rate = (success_count / total_operations * 100) if total_operations > 0 else 0 + + details_parts = [ + f"{script_name} completed in {mode} mode", + f"Total operations: {total_operations}", + f"Successful: {success_count}", + f"Failed: {failed_count}", + f"Success rate: {success_rate:.1f}%" + ] + + if total_amount > 0: + details_parts.append(f"Total amount: ${total_amount:,.2f}") + + if batch_ids: + details_parts.append(f"Batch IDs: {', '.join(map(str, batch_ids))}") + + if duration_seconds: + details_parts.append(f"Duration: {duration_seconds:.1f}s") + + if errors: + details_parts.append(f"Errors encountered: {len(errors)}") + if len(errors) <= 3: + details_parts.extend([f"- {error}" for error in errors]) + else: + details_parts.extend([f"- {error}" for error in errors[:3]]) + details_parts.append(f"... and {len(errors) - 3} more errors") + + details = "\\n".join(details_parts) + + action = "SCRIPT_SUCCESS" if failed_count == 0 else "SCRIPT_PARTIAL" if success_count > 0 else "SCRIPT_FAILED" + + return log_activity( + user_id=SYSTEM_USER_ID, + action=action, + entity_type="Script", + details=details + ) + +def log_batch_created(batch_id: int, payment_method: str, customer_count: int) -> Optional[int]: + """ + Log the creation of a payment batch. + + Args: + batch_id (int): ID of the created batch + payment_method (str): Payment method type (Direct Debit, Card, etc.) + customer_count (int): Number of customers in the batch + + Returns: + int: Log entry ID or None if failed + """ + details = f"Payment batch created for {payment_method} with {customer_count} customers" + return log_activity( + user_id=SYSTEM_USER_ID, + action="BATCH_CREATED", + entity_type="PaymentBatch", + entity_id=batch_id, + details=details + ) + +def log_payment_plan_run( + active_plans: int, + due_plans: int, + processed_count: int, + failed_count: int, + total_amount: float +) -> Optional[int]: + """ + Log the results of a payment plan execution. + + Args: + active_plans (int): Total number of active payment plans + due_plans (int): Number of plans due for payment today + processed_count (int): Number of payments successfully processed + failed_count (int): Number of failed payments + total_amount (float): Total amount processed + + Returns: + int: Log entry ID or None if failed + """ + details = ( + f"Payment plan execution: {active_plans} active plans, " + f"{due_plans} due today, {processed_count} successful, " + f"{failed_count} failed, ${total_amount:,.2f} total" + ) + + action = "PAYPLAN_SUCCESS" if failed_count == 0 else "PAYPLAN_PARTIAL" if processed_count > 0 else "PAYPLAN_FAILED" + + return log_activity( + user_id=SYSTEM_USER_ID, + action=action, + entity_type="PaymentPlan", + details=details + ) + +def log_payment_intent_followup( + pending_count: int, + succeeded_count: int, + failed_count: int, + still_pending: int +) -> Optional[int]: + """ + Log the results of payment intent follow-up processing. + + Args: + pending_count (int): Number of payment intents checked + succeeded_count (int): Number that succeeded + failed_count (int): Number that failed + still_pending (int): Number still pending + + Returns: + int: Log entry ID or None if failed + """ + details = ( + f"Payment intent follow-up: {pending_count} intents checked, " + f"{succeeded_count} succeeded, {failed_count} failed, " + f"{still_pending} still pending" + ) + + return log_activity( + user_id=SYSTEM_USER_ID, + action="PAYINTENT_FOLLOWUP", + entity_type="PaymentIntent", + details=details + ) \ No newline at end of file diff --git a/splynx.py b/splynx.py new file mode 100644 index 0000000..6b503e4 --- /dev/null +++ b/splynx.py @@ -0,0 +1,133 @@ +import time +import json +import requests # type: ignore +import hmac +import hashlib + +SPLYNX_URL = 'https://billing.interphone.com.au' +#SPLYNX_KEY = 'c189c78b155ee8e4d389bbcb34bebc05' +#SPLYNX_SECRET = '1454679ddf5c97ea347766709d3ca3bd' +SPLYNX_KEY = 'b4cd90cbea15e7692c940484a9637fc4' +SPLYNX_SECRET = '297ce5c6b7cd5aaf93d8c725fcb49f8f' + +class Splynx(): + def __init__(self, url, key, secret): + self.url = url + self.key = key + self.secret = secret + self.token = None + self.refresh = None + self.refreshtime = None + self.refreshexpire = None + + def _authenticate(self): + nonce = str(round(time.time() * 1000)) + sig = hmac.new(bytes(self.secret, 'UTF-8'),msg=bytes(nonce+self.key, 'UTF-8'), digestmod = hashlib.sha256).hexdigest().upper() + data = { 'auth_type': 'api_key', 'key': self.key, 'nonce': nonce, 'signature': sig} + headers = {'Content-Type': 'application/json'} + ret = requests.post(url=self.url+'/api/2.0/admin/auth/tokens', data=json.dumps(data), headers=headers) + jsonret = json.loads(ret.content) + self.token = jsonret['access_token'] + self.refresh = jsonret['refresh_token'] + self.refreshtime = jsonret['access_token_expiration'] + self.refreshexpire = jsonret['refresh_token_expiration'] + + def _refresh(self): + headers = {'Content-Type': 'application/json', 'Authorization': 'Splynx-EA (access_token={token})'.format(token=self.token)} + ret = requests.get(url=self.url+'/api/2.0/admin/auth/tokens/{refresh}'.format(refresh=self.refresh),headers=headers) + jsonret = json.loads(ret.content) + self.token = jsonret['access_token'] + self.refresh = jsonret['refresh_token'] + self.refreshtime = jsonret['access_token_expiration'] + self.refreshexpire = jsonret['refresh_token_expiration'] + + def getToken(self): + if self.token is None: + self._authenticate() + if self.token is not None and (self.refreshexpire <= int(time.time())): + self._authenticate() + if self.token is not None and (self.refreshtime <= int(time.time())): + self._refresh() + return self.token + + def get(self, url): + headers = {'Content-Type': 'application/json', 'Authorization': 'Splynx-EA (access_token={token})'.format(token=self.getToken())} + ret = requests.get(url=self.url+url, headers=headers) + return json.loads(ret.content) + + def put(self, url, params): + headers = {'Content-Type': 'application/json', 'Authorization': 'Splynx-EA (access_token={token})'.format(token=self.getToken())} + ret = requests.put(url=self.url+url, headers=headers, data=json.dumps(params)) + if ret.status_code == 202: + return True + return False + + def post(self, url, params): + headers = {'Content-Type': 'application/json', 'Authorization': 'Splynx-EA (access_token={token})'.format(token=self.getToken())} + ret = requests.post(url=self.url+url, headers=headers, data=json.dumps(params)) + if ret.status_code == 201: + return json.loads(ret.content) + return False + + def delete(self, url): + headers = {'Content-Type': 'application/json', 'Authorization': 'Splynx-EA (access_token={token})'.format(token=self.getToken())} + ret = requests.delete(url=self.url+url, headers=headers) + if ret.status_code == 204: + return True + return False + + + def ServiceStatus(self, service_login): + try: + s1 = self.get(url=f"/api/2.0/admin/customers/customer/0/internet-services?main_attributes[login]={service_login}") + + if not s1: + return { 'status': 'no service found', 'customer_name': 'none', 'customer_id': 'none' } + + service_status = s1[-1].get('status') + + if service_status == "active": + s2 = self.get(url="/api/2.0/admin/customers/customers-online") + + if s2: + online_services = [d for d in s2 if str(service_login) in d.values()] + #print(f"online_services: {json.dumps(online_services,indent=2)}") + if online_services: + detail = online_services[0] + cust = self.Customer(detail['customer_id']) + detail['status'] = "Online" + detail['customer_name'] = cust['name'] + return detail + else: + cust = self.Customer(s1[-1].get('customer_id')) + return { 'status': 'Offline', 'customer_name': cust['name'], 'customer_id': cust['id'] } + else: + # No online customers data available + return { 'status': 'Offline' } + else: + # Service exists but is not active (could be suspended, terminated, etc.) + cust = self.Customer(s1[-1].get('customer_id')) + return { 'status': service_status.capitalize(), 'customer_name': cust['name'], 'customer_id': cust['id'] } + + except Exception as e: + print(f"Error checking service status for {service_login}: {str(e)}") + return { 'status': 'no service found', 'customer_name': 'none', 'customer_id': 'none' } + + def Customer(self, customer_id): + try: + result = self.get(url=f"/api/2.0/admin/customers/customer/{customer_id}/") + #print(json.dumps(result,indent=2)) + return result + except: + return 'unknown' + + def GetInternetTariffs(self, tariff_id=None): + try: + if tariff_id: + tariffs = self.get(url=f"/api/2.0/admin/tariffs/internet/{tariff_id}") + else: + tariffs = self.get(url=f"/api/2.0/admin/tariffs/internet") + return tariffs + except Exception as e: + print(f"Error getting Internet Tariffs: {str(e)}") + return { 'status': 'no Internet Tariff found'} \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..e8174cc --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,332 @@ +/* Custom CSS for Plutus - God of Wealth Theme */ + +/* Plutus-inspired theme colors extracted from the god image */ +:root { + --plutus-gold: #d4af37; + --plutus-rich-gold: #b8860b; + --plutus-amber: #ffbf00; + --plutus-bronze: #cd7f32; + --plutus-dark-bronze: #8b4513; + --plutus-charcoal: #2c2c2c; + --plutus-deep-navy: #1a1a2e; + --plutus-warm-white: #faf8f0; + --plutus-cream: #f5e6d3; + --plutus-success: #228b22; + --plutus-warning: #ff8c00; + --plutus-danger: #dc143c; +} + +/* Custom navbar styling with Plutus theme */ +.navbar.is-dark { + background: linear-gradient(135deg, var(--plutus-deep-navy) 0%, var(--plutus-charcoal) 100%); + border-bottom: 2px solid var(--plutus-gold); + box-shadow: 0 2px 10px rgba(212, 175, 55, 0.3); +} + +.navbar-brand .navbar-item { + font-weight: 700; + font-size: 1.2rem; + color: var(--plutus-gold) !important; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); +} + +.navbar-item { + color: var(--plutus-warm-white) !important; + transition: color 0.3s ease, background-color 0.3s ease; +} + +.navbar-item:hover { + color: var(--plutus-amber) !important; + background-color: rgba(212, 175, 55, 0.1) !important; +} + +.navbar-link { + color: var(--plutus-warm-white) !important; +} + +.navbar-link:hover { + color: var(--plutus-amber) !important; + background-color: rgba(212, 175, 55, 0.1) !important; +} + +/* Navbar dropdown styling */ +.navbar-dropdown { + background-color: var(--plutus-deep-navy) !important; + border-color: var(--plutus-gold) !important; + box-shadow: 0 8px 16px rgba(212, 175, 55, 0.2) !important; +} + +.navbar-dropdown .navbar-item { + color: var(--plutus-warm-white) !important; + background-color: transparent !important; +} + +.navbar-dropdown .navbar-item:hover { + color: var(--plutus-amber) !important; + background-color: rgba(212, 175, 55, 0.1) !important; +} + +/* Hero section customization */ +.hero.is-primary { + background: linear-gradient(135deg, var(--plutus-gold) 0%, var(--plutus-amber) 100%); + color: var(--plutus-charcoal); +} + +/* Content boxes with Plutus theme */ +.box { + background-color: rgba(250, 248, 240, 0.95); + border: 1px solid rgba(212, 175, 55, 0.3); + box-shadow: 0 0.5em 1em -0.125em rgba(212, 175, 55, 0.2), 0 0px 0 1px rgba(212, 175, 55, 0.1); + backdrop-filter: blur(5px); +} + +.box:hover { + box-shadow: 0 0.5em 1.5em -0.125em rgba(212, 175, 55, 0.3), 0 0px 0 1px rgba(212, 175, 55, 0.2); + border-color: rgba(212, 175, 55, 0.5); + transition: all 0.3s ease; +} + +/* Button styling with Plutus theme */ +.button.is-primary { + background-color: var(--plutus-gold); + border-color: var(--plutus-rich-gold); + color: var(--plutus-charcoal); + font-weight: 600; +} + +.button.is-primary:hover { + background-color: var(--plutus-amber); + border-color: var(--plutus-gold); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(212, 175, 55, 0.4); +} + +.button:hover { + transform: translateY(-1px); + transition: all 0.2s ease; +} + +/* Table enhancements with Plutus theme */ +.table-container { + overflow-x: auto; + border-radius: 8px; + box-shadow: 0 4px 15px rgba(212, 175, 55, 0.2); + border: 1px solid rgba(212, 175, 55, 0.3); +} + +.table { + background-color: rgba(250, 248, 240, 0.95); + backdrop-filter: blur(5px); +} + +.table th { + background-color: var(--plutus-gold); + color: var(--plutus-charcoal); + font-weight: 700; + border-color: var(--plutus-rich-gold); +} + +.table td { + border-color: rgba(212, 175, 55, 0.2); +} + +.table tr:hover { + background-color: rgba(212, 175, 55, 0.1); +} + +/* Footer styling */ +.footer { + background: linear-gradient(135deg, var(--plutus-deep-navy) 0%, var(--plutus-charcoal) 100%); + color: var(--plutus-warm-white); + padding: 2rem 1.5rem; + margin-top: 2rem; + border-top: 2px solid var(--plutus-gold); +} + +/* Notification improvements with theme */ +.notification { + border-radius: 8px; + backdrop-filter: blur(5px); + border: 1px solid rgba(212, 175, 55, 0.3); +} + +.notification.is-success { + background-color: rgba(34, 139, 34, 0.9); + color: var(--plutus-warm-white); +} + +.notification.is-danger { + background-color: rgba(220, 20, 60, 0.9); + color: var(--plutus-warm-white); +} + +.notification.is-info { + background-color: rgba(212, 175, 55, 0.9); + color: var(--plutus-charcoal); +} + +/* Form styling with Plutus theme */ +.field .control .input:focus, +.field .control .textarea:focus { + border-color: var(--plutus-gold); + box-shadow: 0 0 0 0.125em rgba(212, 175, 55, 0.25); +} + +.field .control .input, +.field .control .textarea, +.field .control .select select { + background-color: rgba(250, 248, 240, 0.95); + border-color: rgba(212, 175, 55, 0.4); +} + +/* Tags with Plutus theme */ +.tag.is-success { + background-color: var(--plutus-success); + color: var(--plutus-warm-white); +} + +.tag.is-warning { + background-color: var(--plutus-warning); + color: var(--plutus-charcoal); +} + +.tag.is-danger { + background-color: var(--plutus-danger); + color: var(--plutus-warm-white); +} + +.tag.is-info { + background-color: var(--plutus-gold); + color: var(--plutus-charcoal); +} + +/* Plutus Background Implementation */ +body { + background-image: url('../images/plutus3.JPG'); + background-size: cover; + background-position: center; + background-attachment: fixed; + background-repeat: no-repeat; + position: relative; +} + +/* Dark overlay for better content readability */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient( + 135deg, + rgba(26, 26, 46, 0.85) 0%, + rgba(44, 44, 44, 0.75) 50%, + rgba(26, 26, 46, 0.85) 100% + ); + z-index: -1; + pointer-events: none; +} + +/* Content area styling for better readability over background */ +main.section { + position: relative; + z-index: 1; +} + +.container { + max-width: 1488px !important; /* 20% wider than Bulma's default 1240px */ + position: relative; + z-index: 2; +} + +/* Title styling with Plutus theme */ +.title { + color: var(--plutus-gold); + font-weight: 700; +} + +.subtitle { + color: var(--plutus-charcoal); +} + +/* Breadcrumb styling */ +.breadcrumb { + background-color: rgba(250, 248, 240, 0.9); + border-radius: 6px; + border: 1px solid rgba(212, 175, 55, 0.3); + padding: 0.75rem 1rem; + backdrop-filter: blur(5px); +} + +.breadcrumb a { + color: var(--plutus-bronze); +} + +.breadcrumb a:hover { + color: var(--plutus-gold); +} + +.breadcrumb .is-active a { + color: var(--plutus-charcoal); +} + +/* Level component styling */ +.level { + background-color: rgba(250, 248, 240, 0.85); + border-radius: 6px; + padding: 1rem; + margin-bottom: 1.5rem; + border: 1px solid rgba(212, 175, 55, 0.3); + backdrop-filter: blur(5px); +} + +/* Modal styling with Plutus theme */ +.modal-card-head { + background-color: var(--plutus-gold); + color: var(--plutus-charcoal); +} + +.modal-card-body { + background-color: var(--plutus-warm-white); +} + +.modal-card-foot { + background-color: var(--plutus-cream); +} + +/* Code blocks styling */ +pre { + background-color: var(--plutus-charcoal) !important; + color: var(--plutus-cream) !important; + border: 1px solid var(--plutus-gold); +} + +code { + background-color: rgba(44, 44, 44, 0.9) !important; + color: var(--plutus-amber) !important; + padding: 0.2em 0.4em; + border-radius: 3px; +} + +/* Dashboard-specific styling - remove background image */ +.dashboard-page body { + background-image: none !important; + background-color: var(--plutus-warm-white); +} + +.dashboard-page body::before { + display: none !important; +} + +/* Plutus image styling for dashboard */ +.plutus-image { + width: 100%; + height: auto; + max-width: 1000px; + border-radius: 12px; + box-shadow: 0 8px 32px rgba(212, 175, 55, 0.4); + border: 3px solid var(--plutus-gold); + margin: 2rem auto; + display: block; +} \ No newline at end of file diff --git a/static/images/plutus3.JPG b/static/images/plutus3.JPG new file mode 100644 index 0000000000000000000000000000000000000000..a7b5a7a116b79e5842dad0a25d4bd2f5e8944f0c GIT binary patch literal 210957 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�Z2 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%0DEt4pzomsv^F?i+{R6AY3EN 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 ZG>ZP;RHLT%f7S- Dict[str, Any]: + """ + Extract actual fee details from Stripe's balance transaction. + This provides real fee data instead of estimates. + + Args: + balance_transaction: Stripe balance transaction object + + Returns: + dict: Actual fee details from Stripe + """ + if not balance_transaction or not hasattr(balance_transaction, 'fee_details'): + return {'note': 'No fee details available'} + + fee_breakdown = [] + total_fee = getattr(balance_transaction, 'fee', 0) / 100 # Convert from cents + + for fee_detail in balance_transaction.fee_details: + fee_breakdown.append({ + 'type': getattr(fee_detail, 'type', 'unknown'), + 'description': getattr(fee_detail, 'description', 'Unknown fee'), + 'amount': getattr(fee_detail, 'amount', 0) / 100, # Convert from cents + 'currency': getattr(fee_detail, 'currency', 'unknown') + }) + + return { + 'source': 'stripe_actual', + 'total_fee': total_fee, + 'net_amount': getattr(balance_transaction, 'net', 0) / 100, # Convert from cents + 'fee_breakdown': fee_breakdown, + 'available_on': getattr(balance_transaction, 'available_on', None), + 'note': 'Actual fees from Stripe balance transaction' + } + + def calculate_stripe_fees(self, amount: float, payment_method_type: str, + payment_method_details: Optional[Any] = None, + transaction_successful: bool = True) -> Dict[str, Any]: + """ + DEPRECATED: Calculate estimated Stripe fees based on payment method type. + Use extract_actual_fees() with balance transaction for real fee data. + + This method provides fee estimates and is kept for fallback scenarios + where actual fee data is not available from Stripe. + + Args: + amount (float): Transaction amount in dollars + payment_method_type (str): Type of payment method ('card', 'au_becs_debit', etc.) + payment_method_details: Stripe PaymentMethod object with additional details + transaction_successful (bool): Whether transaction succeeded (affects BECS caps) + + Returns: + dict: Estimated fee calculation information + """ + fee_info = { + 'payment_method_type': payment_method_type, + 'percentage_fee': 0.0, + 'fixed_fee': 0.0, + 'total_fee': 0.0, + 'fee_description': 'Unknown payment method', + 'capped': False, + 'cap_amount': None, + 'international': False + } + + if payment_method_type == 'card': + # Default to domestic card rates + fee_info['percentage_fee'] = 1.7 + fee_info['fixed_fee'] = 0.30 + fee_info['fee_description'] = 'Domestic credit/debit card' + + # Check if it's an international card + if payment_method_details and hasattr(payment_method_details, 'card') and payment_method_details.card: + card_country = payment_method_details.card.country + self._log('info', f"Card country detected: {card_country}") + fee_info['card_brand'] = payment_method_details.get('card').get('brand') + fee_info['card_display_brand'] = payment_method_details.get('card').get('display_brand') + + if card_country and card_country != 'AU': + fee_info['percentage_fee'] = 3.5 + fee_info['fixed_fee'] = 0.30 + fee_info['fee_description'] = f'International credit/debit card ({card_country})' + fee_info['international'] = True + else: + self._log('info', f"Domestic card confirmed (country: {card_country})") + else: + # If we can't determine country, assume domestic for AU-based business + self._log('info', "Card country not available - assuming domestic") + + elif payment_method_type == 'au_becs_debit': + fee_info['percentage_fee'] = 1.0 + fee_info['fixed_fee'] = 0.30 + fee_info['fee_description'] = 'Australia BECS Direct Debit' + + # Apply BECS caps based on transaction outcome + if transaction_successful: + fee_info['cap_amount'] = 3.50 + fee_info['fee_description'] += ' (capped at $3.50)' + else: + fee_info['cap_amount'] = 2.50 + fee_info['fee_description'] += ' (failure/dispute - capped at $2.50)' + + # Calculate total fee + percentage_amount = amount * (fee_info['percentage_fee'] / 100) + calculated_fee = percentage_amount + fee_info['fixed_fee'] + + # Apply cap if applicable + if fee_info['cap_amount'] and calculated_fee > fee_info['cap_amount']: + fee_info['total_fee'] = fee_info['cap_amount'] + fee_info['capped'] = True + else: + fee_info['total_fee'] = round(calculated_fee, 2) + + return fee_info + + def process_payment(self, customer_id: str, amount: float, currency: str = 'aud', + description: Optional[str] = None, wait_for_completion: bool = True, + stripe_pm: Optional[str] = None) -> Dict[str, Any]: + """ + Process a single payment for a customer using their default payment method. + + Args: + customer_id (str): Stripe customer ID + amount (float): Amount in dollars (will be converted to cents internally) + currency (str): Currency code (default: 'aud') + description (str, optional): Payment description + wait_for_completion (bool): If True, will poll for 'processing' payments to complete (default: True) + + Returns: + dict: Comprehensive payment result with success status and details + """ + transaction_start = datetime.now() + + # Base response structure + response = { + 'success': False, + 'timestamp': transaction_start.isoformat(), + 'customer_id': customer_id, + 'amount': amount, + 'currency': currency.lower(), + 'description': description, + 'processing_time_seconds': 0.0, + 'test_mode': self.is_test_mode + } + + try: + # Validate inputs + if not customer_id or not isinstance(customer_id, str): + response['error'] = 'Invalid customer_id provided' + response['error_type'] = 'validation_error' + return response + + if amount <= 0: + response['error'] = 'Amount must be greater than 0' + response['error_type'] = 'validation_error' + return response + + # Convert dollars to cents + amount_cents = int(Decimal(str(amount)) * 100) + + self._log('info', f"Processing payment: {customer_id}, ${amount} {currency.upper()}") + + # Retrieve customer + customer = stripe.Customer.retrieve(customer_id) + print(f"customer: {json.dumps(customer,indent=2)}") + + if not customer: + response['error'] = f'Customer {customer_id} not found' + response['error_type'] = 'customer_not_found' + return response + + # Add customer details to response + response.update({ + 'customer_email': customer.email, + 'customer_name': customer.description or customer.name + }) + + if stripe_pm: + default_payment_method = stripe_pm + else: + # Get default payment method + default_payment_method = customer.invoice_settings.default_payment_method + + if not default_payment_method: + response['error'] = 'Customer has no default payment method set' + response['error_type'] = 'no_payment_method' + return response + + # Retrieve payment method details + payment_method = stripe.PaymentMethod.retrieve(default_payment_method) + payment_method_type = payment_method.type + + print(f"payment_method: {json.dumps(payment_method,indent=2)}") + + + response.update({ + 'payment_method_id': default_payment_method, + 'payment_method_type': payment_method_type + }) + + # Calculate estimated fees before payment + estimated_fee_details = self.calculate_stripe_fees( + amount, + payment_method_type, + payment_method, + transaction_successful=True # Will be updated if payment fails + ) + estimated_fee_details['source'] = 'estimated' + estimated_fee_details['note'] = 'Pre-payment estimate' + + response['estimated_fee_details'] = estimated_fee_details + + self._log('info', f"Payment method: {payment_method_type} - {estimated_fee_details['fee_description']}") + + # Prepare Payment Intent parameters + payment_intent_params = { + 'amount': amount_cents, + 'currency': currency, + 'customer': customer_id, + 'payment_method': default_payment_method, + 'description': description or f"Payment for {customer.description or customer.email}", + 'confirm': True, + 'return_url': 'https://your-website.com/payment-success', + 'off_session': True + } + + # Add mandate data for BECS Direct Debit + if payment_method_type == 'au_becs_debit': + payment_intent_params['mandate_data'] = { + 'customer_acceptance': { + 'type': 'offline' + } + } + self._log('info', "Added BECS mandate data for offline acceptance") + + # Create and confirm Payment Intent + payment_intent = stripe.PaymentIntent.create(**payment_intent_params) + + # Add payment intent details + response.update({ + 'payment_intent_id': payment_intent.id, + 'status': payment_intent.status + }) + + if payment_intent.status == 'succeeded': + response['success'] = True + self._log('info', f"✅ Payment successful: {payment_intent.id}") + + # Get actual fee details for successful payments + try: + # Re-retrieve with expanded balance transaction to get actual fees + time.sleep(3) + payment_intent_expanded = stripe.PaymentIntent.retrieve( + payment_intent.id, + expand=['latest_charge.balance_transaction'] + ) + + if (hasattr(payment_intent_expanded, 'latest_charge') and + payment_intent_expanded.latest_charge and + hasattr(payment_intent_expanded.latest_charge, 'balance_transaction') and + payment_intent_expanded.latest_charge.balance_transaction): + + balance_transaction = payment_intent_expanded.latest_charge.balance_transaction + actual_fees = self.extract_actual_fees(balance_transaction) + response['fee_details'] = actual_fees + self._log('info', f"Retrieved actual fees: ${actual_fees['total_fee']:.2f}") + else: + # Keep estimated fees if balance transaction not available + response['fee_details'] = estimated_fee_details + response['fee_details']['note'] = 'Balance transaction not yet available, showing estimate' + # Record this payment for later fee update + response['needs_fee_update'] = [customer_id, payment_intent.id] + self._log('info', f"Balance transaction not available, using estimates - marked for later update") + + except Exception as e: + # If we can't get actual fees, keep the estimates and mark for later + response['fee_details'] = estimated_fee_details + response['fee_details']['note'] = f'Could not retrieve actual fees: {str(e)}' + response['needs_fee_update'] = [customer_id, payment_intent.id] + self._log('warning', f"Failed to get actual fees: {str(e)} - marked for later update") + elif payment_intent.status == 'processing' and wait_for_completion: + # Payment is processing - wait for completion + self._log('info', f"💭 Payment is processing, waiting for completion...") + + # Use the polling method to wait for completion + polling_result = self.wait_for_payment_completion(payment_intent.id, customer_id=customer_id, max_wait_seconds=30) + + if polling_result['success']: + # Update our response with the final polling result + response.update(polling_result) + # The polling result already has all the details we need + + if polling_result['status'] == 'succeeded': + response['success'] = True + self._log('info', f"✅ Payment completed successfully after polling") + else: + response['success'] = False + response['error'] = f'Payment completed with status: {polling_result["status"]}' + response['error_type'] = 'payment_not_succeeded' + else: + # Polling failed - update with polling error info + response.update(polling_result) + response['success'] = False + if 'error' not in response: + response['error'] = 'Payment polling failed' + response['error_type'] = 'polling_failed' + else: + # For failed payments or processing without polling + if payment_method_type == 'au_becs_debit': + # Recalculate BECS fees with failure cap for failed payments + failed_fee_details = self.calculate_stripe_fees( + amount, + payment_method_type, + payment_method, + transaction_successful=False + ) + failed_fee_details['source'] = 'estimated' + failed_fee_details['note'] = 'Estimated fees for failed BECS payment' + response['fee_details'] = failed_fee_details + else: + # Use estimated fees for other payment types + response['fee_details'] = estimated_fee_details + + if payment_intent.status == 'processing': + response['error'] = f'Payment is processing (polling disabled). Check status later.' + response['error_type'] = 'payment_processing' + response['next_action'] = f'Use check_payment_intent("{payment_intent.id}") or wait_for_payment_completion("{payment_intent.id}") to check status' + else: + response['error'] = f'Payment not completed. Status: {payment_intent.status}' + response['error_type'] = 'payment_incomplete' + + self._log('warning', f"⚠️ Payment incomplete: {payment_intent.id} - {payment_intent.status}") + + # Calculate processing time + processing_time = (datetime.now() - transaction_start).total_seconds() + response['processing_time_seconds'] = round(processing_time, 2) + response['pi_status'] = payment_intent.status + return response + + except stripe.CardError as e: + # Card-specific error (declined, etc.) + processing_time = (datetime.now() - transaction_start).total_seconds() + #print(f"stripe.CardError: {str(e)}\n{e.user_message}\n{e.request_id}\n{e.code}") + #print(json.dumps(e, indent=2)) + response.update({ + 'error': f'Card declined: {e.user_message}', + 'error_type': 'card_declined', + 'decline_code': e.code, + 'processing_time_seconds': round(processing_time, 2) + }) + self._log('error', f"❌ Card declined for {customer_id}: {e.user_message}") + return response + + except stripe.InvalidRequestError as e: + # Invalid parameters + processing_time = (datetime.now() - transaction_start).total_seconds() + response.update({ + 'error': f'Invalid request: {str(e)}', + 'error_type': 'invalid_request', + 'processing_time_seconds': round(processing_time, 2) + }) + self._log('error', f"❌ Invalid request for {customer_id}: {str(e)}") + return response + + except stripe.AuthenticationError as e: + # Authentication with Stripe failed + processing_time = (datetime.now() - transaction_start).total_seconds() + response.update({ + 'error': f'Authentication failed: {str(e)}', + 'error_type': 'authentication_error', + 'processing_time_seconds': round(processing_time, 2) + }) + self._log('error', f"❌ Authentication failed: {str(e)}") + return response + + except stripe.APIConnectionError as e: + # Network communication with Stripe failed + processing_time = (datetime.now() - transaction_start).total_seconds() + response.update({ + 'error': f'Network error: {str(e)}', + 'error_type': 'network_error', + 'processing_time_seconds': round(processing_time, 2) + }) + self._log('error', f"❌ Network error: {str(e)}") + return response + + except stripe.StripeError as e: + # Other Stripe-specific errors + processing_time = (datetime.now() - transaction_start).total_seconds() + response.update({ + 'error': f'Stripe error: {str(e)}', + 'error_type': 'stripe_error', + 'processing_time_seconds': round(processing_time, 2) + }) + self._log('error', f"❌ Stripe error: {str(e)}") + return response + + except Exception as e: + # Unexpected errors + processing_time = (datetime.now() - transaction_start).total_seconds() + response.update({ + 'error': f'Unexpected error: {str(e)}', + 'error_type': 'unexpected_error', + 'processing_time_seconds': round(processing_time, 2) + }) + self._log('error', f"❌ Unexpected error for {customer_id}: {str(e)}") + return response + + def get_customer_info(self, customer_id: str) -> Dict[str, Any]: + """ + Retrieve customer information including payment methods. + + Args: + customer_id (str): Stripe customer ID + + Returns: + dict: Customer information and payment method details + """ + try: + customer = stripe.Customer.retrieve(customer_id) + + customer_info = { + 'success': True, + 'customer_id': customer.id, + 'email': customer.email, + 'name': customer.description or customer.name, + 'created': customer.created, + 'default_payment_method': customer.invoice_settings.default_payment_method if customer.invoice_settings else None, + 'payment_methods': [] + } + + # Get payment methods + payment_methods = stripe.PaymentMethod.list( + customer=customer_id, + limit=10 + ) + + for pm in payment_methods.data: + pm_info = { + 'id': pm.id, + 'type': pm.type, + 'created': pm.created + } + + if pm.card: + pm_info['card'] = { + 'brand': pm.card.brand, + 'last4': pm.card.last4, + 'country': pm.card.country, + 'exp_month': pm.card.exp_month, + 'exp_year': pm.card.exp_year + } + elif pm.au_becs_debit: + pm_info['au_becs_debit'] = { + 'bsb_number': pm.au_becs_debit.bsb_number, + 'last4': pm.au_becs_debit.last4 + } + + customer_info['payment_methods'].append(pm_info) + + return customer_info + + except stripe.StripeError as e: + return { + 'success': False, + 'error': f'Stripe error: {str(e)}', + 'error_type': 'stripe_error' + } + except Exception as e: + return { + 'success': False, + 'error': f'Unexpected error: {str(e)}', + 'error_type': 'unexpected_error' + } + + def get_payment_methods(self, customer_id: str) -> list: + """ + Get all payment methods for a Stripe customer. + + Args: + customer_id (str): Stripe customer ID + + Returns: + list: List of payment methods with details + """ + try: + self._log('info', f"Retrieving payment methods for customer: {customer_id}") + + # Get payment methods for the customer + payment_methods = stripe.PaymentMethod.list( + customer=customer_id, + limit=10 + ) + + methods_list = [] + + for pm in payment_methods.data: + pm_info = { + 'id': pm.id, + 'type': pm.type, + 'created': pm.created + } + + if pm.card: + pm_info['card'] = { + 'brand': pm.card.brand, + 'last4': pm.card.last4, + 'country': pm.card.country, + 'exp_month': pm.card.exp_month, + 'exp_year': pm.card.exp_year + } + elif pm.au_becs_debit: + pm_info['au_becs_debit'] = { + 'bsb_number': pm.au_becs_debit.bsb_number, + 'last4': pm.au_becs_debit.last4 + } + + methods_list.append(pm_info) + + self._log('info', f"Found {len(methods_list)} payment methods") + return methods_list + + except stripe.StripeError as e: + self._log('error', f"Stripe error retrieving payment methods: {str(e)}") + return [] + except Exception as e: + self._log('error', f"Unexpected error retrieving payment methods: {str(e)}") + return [] + + def check_payment_intent(self, payment_intent_id: str) -> Dict[str, Any]: + """ + Check the status and details of a specific payment intent. + + Args: + payment_intent_id (str): Stripe Payment Intent ID (e.g., 'pi_1234567890') + + Returns: + dict: Payment intent status and comprehensive details + """ + try: + # Validate input + if not payment_intent_id or not isinstance(payment_intent_id, str): + return { + 'success': False, + 'error': 'Invalid payment_intent_id provided', + 'error_type': 'validation_error', + 'timestamp': datetime.now().isoformat() + } + + if not payment_intent_id.startswith('pi_'): + return { + 'success': False, + 'error': 'Payment Intent ID must start with "pi_"', + 'error_type': 'validation_error', + 'timestamp': datetime.now().isoformat() + } + + self._log('info', f"Checking payment intent: {payment_intent_id}") + + # Retrieve the payment intent with expanded balance transaction for fee details + payment_intent = stripe.PaymentIntent.retrieve( + payment_intent_id, + expand=['latest_charge.balance_transaction'] + ) + + self._log('info', f"Retrieved payment intent with expanded data") + + # Base response + response = { + 'success': True, + 'payment_intent_id': payment_intent.id, + 'status': payment_intent.status, + 'amount': payment_intent.amount / 100, # Convert from cents to dollars + 'currency': payment_intent.currency, + 'created': datetime.fromtimestamp(payment_intent.created).isoformat(), + 'description': payment_intent.description, + 'customer_id': payment_intent.customer, + 'payment_method_id': payment_intent.payment_method, + 'test_mode': self.is_test_mode, + 'timestamp': datetime.now().isoformat() + } + + # Add status-specific information + if payment_intent.status == 'succeeded': + response.update({ + 'success_date': datetime.fromtimestamp(payment_intent.created).isoformat() + }) + + # Add receipt URL if available + if hasattr(payment_intent, 'charges') and payment_intent.charges and payment_intent.charges.data: + first_charge = payment_intent.charges.data[0] + response['receipt_url'] = getattr(first_charge, 'receipt_url', None) + + # Get actual fee details from balance transaction if available + if (hasattr(payment_intent, 'latest_charge') and + payment_intent.latest_charge and + hasattr(payment_intent.latest_charge, 'balance_transaction') and + payment_intent.latest_charge.balance_transaction): + + # Use actual fee data from Stripe + balance_transaction = payment_intent.latest_charge.balance_transaction + actual_fees = self.extract_actual_fees(balance_transaction) + response['fee_details'] = actual_fees + self._log('info', f"Using actual fee data: ${actual_fees['total_fee']:.2f}") + + elif payment_intent.payment_method: + # Fallback to calculated fees if balance transaction not available + try: + payment_method = stripe.PaymentMethod.retrieve(payment_intent.payment_method) + estimated_fees = self.calculate_stripe_fees( + response['amount'], + payment_method.type, + payment_method, + transaction_successful=True + ) + estimated_fees['source'] = 'estimated' + estimated_fees['note'] = 'Estimated fees - actual fees not yet available' + response['fee_details'] = estimated_fees + self._log('info', f"Using estimated fee data: ${estimated_fees['total_fee']:.2f}") + except Exception as e: + # If we can't get payment method details, just note it + response['fee_details'] = { + 'note': 'Fee details unavailable - payment method not accessible', + 'error': str(e) + } + self._log('warning', f"Could not retrieve fee details: {str(e)}") + else: + response['fee_details'] = {'note': 'No payment method associated with this payment intent'} + + elif payment_intent.status == 'requires_payment_method': + response['next_action'] = 'Payment method required' + + elif payment_intent.status == 'requires_confirmation': + response['next_action'] = 'Payment requires confirmation' + + elif payment_intent.status == 'requires_action': + response['next_action'] = 'Additional action required (e.g., 3D Secure)' + if payment_intent.next_action: + response['next_action_details'] = { + 'type': payment_intent.next_action.type if hasattr(payment_intent.next_action, 'type') else 'unknown' + } + + elif payment_intent.status == 'processing': + response['next_action'] = 'Payment is being processed' + + if payment_intent.status in ['canceled', 'failed', 'requires_payment_method']: + response['success'] = False + response['failure_reason'] = 'Payment was canceled or failed' + + # Get failure details if available + if payment_intent.last_payment_error: + error = payment_intent.last_payment_error + response['failure_details'] = { + 'code': error.code, + 'message': error.message, + 'type': error.type, + 'decline_code': getattr(error, 'decline_code', None) + } + + # Add charges information if available + if hasattr(payment_intent, 'charges') and payment_intent.charges and payment_intent.charges.data: + charge = payment_intent.charges.data[0] + response['charge_details'] = { + 'charge_id': charge.id, + 'paid': getattr(charge, 'paid', False), + 'refunded': getattr(charge, 'refunded', False), + 'amount_refunded': getattr(charge, 'amount_refunded', 0) / 100, # Convert to dollars + 'failure_code': getattr(charge, 'failure_code', None), + 'failure_message': getattr(charge, 'failure_message', None) + } + + # Add outcome information if available + if hasattr(charge, 'outcome') and charge.outcome: + response['charge_details']['outcome'] = { + 'network_status': getattr(charge.outcome, 'network_status', None), + 'reason': getattr(charge.outcome, 'reason', None), + 'seller_message': getattr(charge.outcome, 'seller_message', None), + 'type': getattr(charge.outcome, 'type', None) + } + + # Add payment method details from charge if available + if hasattr(charge, 'payment_method_details') and charge.payment_method_details: + pm_details = charge.payment_method_details + response['payment_method_details'] = { + 'type': getattr(pm_details, 'type', 'unknown') + } + + if hasattr(pm_details, 'card') and pm_details.card: + response['payment_method_details']['card'] = { + 'brand': getattr(pm_details.card, 'brand', None), + 'country': getattr(pm_details.card, 'country', None), + 'last4': getattr(pm_details.card, 'last4', None), + 'funding': getattr(pm_details.card, 'funding', None) + } + elif hasattr(pm_details, 'au_becs_debit') and pm_details.au_becs_debit: + response['payment_method_details']['au_becs_debit'] = { + 'bsb_number': getattr(pm_details.au_becs_debit, 'bsb_number', None), + 'last4': getattr(pm_details.au_becs_debit, 'last4', None) + } + + self._log('info', f"Payment intent {payment_intent_id} status: {payment_intent.status}") + return response + + except stripe.InvalidRequestError as e: + return { + 'success': False, + 'error': f'Invalid request: {str(e)}', + 'error_type': 'invalid_request', + 'payment_intent_id': payment_intent_id, + 'timestamp': datetime.now().isoformat() + } + + except stripe.PermissionError as e: + return { + 'success': False, + 'error': f'Permission denied: {str(e)}', + 'error_type': 'permission_error', + 'payment_intent_id': payment_intent_id, + 'timestamp': datetime.now().isoformat() + } + + except stripe.StripeError as e: + return { + 'success': False, + 'error': f'Stripe error: {str(e)}', + 'error_type': 'stripe_error', + 'payment_intent_id': payment_intent_id, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + return { + 'success': False, + 'error': f'Unexpected error: {str(e)}', + 'error_type': 'unexpected_error', + 'payment_intent_id': payment_intent_id, + 'timestamp': datetime.now().isoformat() + } + + def wait_for_payment_completion(self, payment_intent_id: str, max_wait_seconds: int = 60, + check_interval: int = 5, customer_id: Optional[str] = None) -> Dict[str, Any]: + """ + Poll a payment intent until it completes or times out. + Useful for payments that start with 'processing' status. + + Args: + payment_intent_id (str): Stripe Payment Intent ID + max_wait_seconds (int): Maximum time to wait in seconds (default: 60) + check_interval (int): How often to check in seconds (default: 5) + customer_id (str, optional): Customer ID to include in needs_fee_update tracking + + Returns: + dict: Final payment intent status with polling metadata + """ + start_time = datetime.now() + attempts = 0 + max_attempts = max_wait_seconds // check_interval + + self._log('info', f"Starting payment polling for {payment_intent_id} (max {max_wait_seconds}s, every {check_interval}s)") + + # Check initial status + result = self.check_payment_intent(payment_intent_id) + + if not result['success']: + # If we can't even check the payment, return the error + return result + + initial_status = result['status'] + self._log('info', f"Initial payment status: {initial_status}") + + # If payment is already in a final state, return immediately + final_statuses = ['succeeded', 'failed', 'canceled'] + if initial_status in final_statuses: + result['polling_info'] = { + 'polling_needed': False, + 'initial_status': initial_status, + 'final_status': initial_status, + 'total_wait_time_seconds': 0, + 'attempts': 1 + } + return result + + # Start polling for non-final statuses + polling_statuses = ['processing', 'requires_action', 'requires_confirmation'] + + if initial_status not in polling_statuses: + # Status doesn't require polling + result['polling_info'] = { + 'polling_needed': False, + 'initial_status': initial_status, + 'final_status': initial_status, + 'total_wait_time_seconds': 0, + 'attempts': 1, + 'note': f'Status "{initial_status}" does not require polling' + } + return result + + # Polling loop + while attempts < max_attempts: + attempts += 1 + + # Wait before checking (except for first attempt which we already did) + if attempts > 1: + self._log('info', f"Waiting {check_interval} seconds before attempt {attempts}...") + time.sleep(check_interval) + + # Check current status + current_result = self.check_payment_intent(payment_intent_id) + + if not current_result['success']: + # Error occurred during polling + elapsed_time = (datetime.now() - start_time).total_seconds() + current_result['polling_info'] = { + 'polling_needed': True, + 'initial_status': initial_status, + 'final_status': 'error', + 'total_wait_time_seconds': round(elapsed_time, 2), + 'attempts': attempts, + 'polling_error': 'Failed to check payment status during polling' + } + return current_result + + current_status = current_result['status'] + elapsed_time = (datetime.now() - start_time).total_seconds() + + self._log('info', f"Attempt {attempts}: Status = {current_status} (elapsed: {elapsed_time:.1f}s)") + + # Check if we've reached a final status + if current_status in final_statuses: + # Payment completed (success or failure) + current_result['polling_info'] = { + 'polling_needed': True, + 'initial_status': initial_status, + 'final_status': current_status, + 'total_wait_time_seconds': round(elapsed_time, 2), + 'attempts': attempts, + 'completed': True + } + + if current_status == 'succeeded': + self._log('info', f"✅ Payment completed successfully after {elapsed_time:.1f}s ({attempts} attempts)") + else: + self._log('warning', f"❌ Payment completed with status '{current_status}' after {elapsed_time:.1f}s") + current_result['pi_status'] = current_status + return current_result + + # Check if status changed to something that doesn't need polling + if current_status not in polling_statuses: + current_result['polling_info'] = { + 'polling_needed': True, + 'initial_status': initial_status, + 'final_status': current_status, + 'total_wait_time_seconds': round(elapsed_time, 2), + 'attempts': attempts, + 'completed': False, + 'note': f'Status changed to "{current_status}" which does not require further polling' + } + self._log('info', f"Status changed to '{current_status}', stopping polling") + current_result['pi_status'] = current_status + return current_result + + # Timeout reached + elapsed_time = (datetime.now() - start_time).total_seconds() + final_result = self.check_payment_intent(payment_intent_id) + + if final_result['success']: + final_status = final_result['status'] + final_result['polling_info'] = { + 'polling_needed': True, + 'initial_status': initial_status, + 'final_status': final_status, + 'total_wait_time_seconds': round(elapsed_time, 2), + 'attempts': attempts, + 'completed': False, + 'timed_out': True, + 'timeout_reason': f'Reached maximum wait time of {max_wait_seconds} seconds' + } + + # If payment is still processing after timeout and we have customer_id, mark for later review + if final_status == 'processing' and customer_id: + final_result['needs_fee_update'] = [customer_id, payment_intent_id] + self._log('warning', f"⏰ Payment still processing after timeout - marked for later review") + + self._log('warning', f"⏰ Polling timed out after {max_wait_seconds}s. Final status: {final_status}") + else: + # Error on final check + final_result['polling_info'] = { + 'polling_needed': True, + 'initial_status': initial_status, + 'final_status': 'error', + 'total_wait_time_seconds': round(elapsed_time, 2), + 'attempts': attempts, + 'completed': False, + 'timed_out': True, + 'timeout_reason': 'Timeout reached and final status check failed' + } + + # If we have customer_id and this might be a processing payment, mark for later + if customer_id: + final_result['needs_fee_update'] = [customer_id, payment_intent_id] + self._log('warning', f"⏰ Final check failed - marked for later review") + current_result['pi_status'] = final_status + return final_result + + def update_payment_fees(self, needs_fee_update) -> Dict[str, Any]: + """ + Update fees for a payment that was previously marked as needing a fee update. + + Args: + needs_fee_update (list): List containing [customer_id, payment_intent_id] + + Returns: + dict: Updated payment information with actual fees if available + """ + try: + # Parse the identifier + if not isinstance(needs_fee_update, (list, tuple)) or len(needs_fee_update) != 2: + return { + 'success': False, + 'error': 'Invalid needs_fee_update format. Expected [customer_id, payment_intent_id]', + 'error_type': 'validation_error' + } + + customer_id, payment_intent_id = needs_fee_update + + self._log('info', f"Updating fees for {payment_intent_id} (customer: {customer_id})") + + # Get the current payment intent status with expanded data + current_result = self.check_payment_intent(payment_intent_id) + + if not current_result['success']: + return current_result + + # Check if we now have actual fees or if payment is now complete + has_actual_fees = (current_result.get('fee_details', {}).get('source') == 'stripe_actual') + is_complete = current_result['status'] in ['succeeded', 'failed', 'canceled'] + + update_result = { + 'success': True, + 'payment_intent_id': payment_intent_id, + 'customer_id': customer_id, + 'status': current_result['status'], + 'amount': current_result['amount'], + 'currency': current_result['currency'], + 'has_actual_fees': has_actual_fees, + 'is_complete': is_complete, + 'fee_details': current_result.get('fee_details', {}), + 'timestamp': datetime.now().isoformat() + } + + # Determine if this payment still needs future updates + if has_actual_fees and is_complete: + update_result['needs_further_updates'] = False + update_result['note'] = 'Payment complete with actual fees' + self._log('info', f"✅ Payment {payment_intent_id} now complete with actual fees") + elif is_complete and not has_actual_fees: + update_result['needs_further_updates'] = False + update_result['note'] = 'Payment complete but actual fees not available' + self._log('info', f"✅ Payment {payment_intent_id} complete but no actual fees") + elif has_actual_fees and not is_complete: + update_result['needs_further_updates'] = True + update_result['needs_fee_update'] = [customer_id, payment_intent_id] # Keep tracking + update_result['note'] = 'Has actual fees but payment still processing' + self._log('info', f"⏳ Payment {payment_intent_id} has fees but still processing") + else: + update_result['needs_further_updates'] = True + update_result['needs_fee_update'] = [customer_id, payment_intent_id] # Keep tracking + update_result['note'] = 'Payment still processing without actual fees' + self._log('info', f"⏳ Payment {payment_intent_id} still needs updates") + + return update_result + + except Exception as e: + return { + 'success': False, + 'error': f'Failed to update payment fees: {str(e)}', + 'error_type': 'update_error', + 'needs_fee_update': needs_fee_update, + 'timestamp': datetime.now().isoformat() + } \ No newline at end of file diff --git a/templates/auth/add_user.html b/templates/auth/add_user.html new file mode 100644 index 0000000..1aefacd --- /dev/null +++ b/templates/auth/add_user.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block title %}Add User - Plutus{% endblock %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/templates/auth/list_users.html b/templates/auth/list_users.html new file mode 100644 index 0000000..50105d0 --- /dev/null +++ b/templates/auth/list_users.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}Users - Plutus{% endblock %} + +{% block content %} +
+
+

Users

+
+ +
+ +{% if users %} +
+ + + + + + + + + + + + + + {% for user in users %} + + + + + + + + + + {% endfor %} + +
IDUsernameFull NameEmailStatusCreatedPermissions
{{ user.id }}{{ user.Username }}{{ user.FullName }}{{ user.Email }} + + {{ 'Active' if user.Enabled else 'Disabled' }} + + {{ user.Created.strftime('%Y-%m-%d %H:%M') }}{{ user.Permissions or '-' }}
+
+{% else %} +
+

No users found. Add the first user.

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/auth/login.html b/templates/auth/login.html new file mode 100644 index 0000000..7512a32 --- /dev/null +++ b/templates/auth/login.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block title %}Login - Plutus{% endblock %} + +{% block content %} +
+
+
+

Login to Plutus

+ +
+
+ +
+ + + + +
+
+ +
+ +
+ + + + +
+
+ +
+
+ +
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..3b70b0d --- /dev/null +++ b/templates/base.html @@ -0,0 +1,154 @@ + + + + + + {% block title %}Plutus{% endblock %} + + + + {% block head %}{% endblock %} + + + + +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ + {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+
+ +
+
+

+ Plutus - Payment Processing System +

+
+
+ + + + \ No newline at end of file diff --git a/templates/main/batch_detail.html b/templates/main/batch_detail.html new file mode 100644 index 0000000..249d2d0 --- /dev/null +++ b/templates/main/batch_detail.html @@ -0,0 +1,609 @@ +{% extends "base.html" %} + +{% block title %}Batch #{{ batch.id }} - Plutus{% endblock %} + +{% block content %} + + +
+
+
+

Payment Batch #{{ batch.id }}

+

Created: {{ batch.Created.strftime('%Y-%m-%d %H:%M:%S') if batch.Created else 'Unknown' }}

+
+
+ +
+ + +
+
+
+
+
+
+

Total Payments

+

{{ summary.payment_count or 0 }}

+
+
+
+
+

Payment Amount

+

{{ summary.total_amount | currency }}

+
+
+
+
+

Stripe Fees

+

{{ summary.total_fees | currency }}

+
+
+
+
+
+
+ +
+
+
+
+

Successful

+

{{ summary.successful_count or 0 }}

+
+
+
+
+
+
+

Failed

+

{{ summary.failed_count or 0 }}

+
+
+
+
+
+
+

Errors

+

{{ summary.error_count or 0 }}

+
+
+
+
+
+
+

Success Rate

+ {% if summary.payment_count and summary.payment_count > 0 %} + {% set success_rate = (summary.successful_count or 0) / summary.payment_count * 100 %} +

+ {{ "%.1f"|format(success_rate) }}% +

+ {% else %} +

0%

+ {% endif %} +
+
+
+
+ + +
+
+
+

Payment Details

+
+
+
+

+ + + + +

+
+
+
+ + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + + + + {% if payments %} +
+ + + + + + + + + + + + + + + + + {% for payment in payments %} + {% set row_class = '' %} + {% if payment.Success == True %} + {% set row_class = 'has-background-success-light' %} + {% elif payment.Success == False and payment.PI_FollowUp %} + {% set row_class = 'has-background-warning-light' %} + {% elif payment.Success == False and payment.Error %} + {% set row_class = 'has-background-danger-light' %} + {% elif payment.Success == None %} + {% set row_class = 'has-background-info-light' %} + {% endif %} + + + + + + + + + + + + + + {% endfor %} + +
Splynx IDStripe CustomerPayment IntentFollow UpLast CheckPayment MethodStripe FeeAmountDataStatus
+ {% if payment.Splynx_ID %} + + {{ payment.Splynx_ID }} + + {% else %} + - + {% endif %} + + {% if payment.Success == True %} + {{ payment.Stripe_Customer_ID or '-' }} + {% elif payment.Success == False and payment.PI_FollowUp %} + {{ payment.Stripe_Customer_ID or '-' }} + {% elif payment.Success == False and payment.Error %} + {{ payment.Stripe_Customer_ID or '-' }} + {% elif payment.Success == None %} + {{ payment.Stripe_Customer_ID or '-' }} + {% else %} + {{ payment.Stripe_Customer_ID or '-' }} + {% endif %} + + {% if payment.Success == True %} + {{ payment.Payment_Intent or '-' }} + {% elif payment.Success == False and payment.PI_FollowUp %} + {{ payment.Payment_Intent or '-' }} + {% elif payment.Success == False and payment.Error %} + {{ payment.Payment_Intent or '-' }} + {% elif payment.Success == None %} + {{ payment.Payment_Intent or '-' }} + {% else %} + {{ payment.Payment_Intent or '-' }} + {% endif %} + + {% if payment.PI_FollowUp %} + Follow Up + {% else %} + No + {% endif %} + + {{ payment.PI_Last_Check.strftime('%Y-%m-%d %H:%M') if payment.PI_Last_Check else '-' }} + + {% if payment.Payment_Method %} + {{ payment.Payment_Method }} + {% else %} + - + {% endif %} + + {% if payment.Fee_Stripe %} + {{ payment.Fee_Stripe | currency }} + {% else %} + - + {% endif %} + + {% if payment.Payment_Amount %} + {{ payment.Payment_Amount | currency }} + {% else %} + - + {% endif %} + +
+ {% if payment.PI_JSON %} + + {% endif %} + + {% if payment.PI_FollowUp_JSON %} + + {% endif %} + + {% if payment.Error %} + + {% endif %} +
+
+ {% if payment.Success == True %} + Success + {% elif payment.Success == False %} + Failed + {% else %} + Pending + {% endif %} +
+
+ {% else %} +
+

No payments found in this batch.

+
+ {% endif %} +
+ + +{% for payment in payments %} + + {% if payment.PI_JSON %} + + {% endif %} + + + {% if payment.PI_FollowUp_JSON %} + + {% endif %} + + + {% if payment.Error %} + + {% endif %} +{% endfor %} + + +{% endblock %} \ No newline at end of file diff --git a/templates/main/batch_list.html b/templates/main/batch_list.html new file mode 100644 index 0000000..2a3b040 --- /dev/null +++ b/templates/main/batch_list.html @@ -0,0 +1,91 @@ +{% extends "base.html" %} + +{% block title %}Payment Batches - Plutus{% endblock %} + +{% block content %} +
+
+

Payment Batches

+
+
+ +{% if batches %} +
+ + + + + + + + + + + + + + + {% for batch in batches %} + + + + + + + + + + + {% endfor %} + +
Batch IDCreatedTotal PaymentsPayment AmountStripe FeesSuccess RateStatusActions
+ #{{ batch.id }} + {{ batch.Created.strftime('%Y-%m-%d %H:%M') if batch.Created else '-' }} + {{ batch.payment_count or 0 }} + + {{ batch.total_amount | currency }} + + {{ batch.total_fees | currency }} + + {% if batch.payment_count and batch.payment_count > 0 %} + {% set success_rate = (batch.successful_count or 0) / batch.payment_count * 100 %} + {% if success_rate >= 90 %} + {{ "%.1f"|format(success_rate) }}% + {% elif success_rate >= 70 %} + {{ "%.1f"|format(success_rate) }}% + {% else %} + {{ "%.1f"|format(success_rate) }}% + {% endif %} + {% else %} + 0% + {% endif %} + +
+ {% if batch.successful_count %} + {{ batch.successful_count }} Success + {% endif %} + {% if batch.failed_count %} + {{ batch.failed_count }} Failed + {% endif %} + {% if batch.error_count %} + {{ batch.error_count }} Errors + {% endif %} + {% if not batch.successful_count and not batch.failed_count %} + No Payments + {% endif %} +
+
+ + + + + View Details + +
+
+{% else %} +
+

No payment batches found. Return to dashboard.

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/main/index.html b/templates/main/index.html new file mode 100644 index 0000000..f25bfbe --- /dev/null +++ b/templates/main/index.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - Plutus{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+

+ Welcome to Plutus +

+

+ Payment Processing System +

+
+
+Plutus - God of Wealth +
+

Welcome, {{ current_user.FullName }}!

+

You are successfully logged into the Plutus payment processing system.

+
+{% endblock %} \ No newline at end of file diff --git a/templates/main/payment_plans_detail.html b/templates/main/payment_plans_detail.html new file mode 100644 index 0000000..bf7e5b0 --- /dev/null +++ b/templates/main/payment_plans_detail.html @@ -0,0 +1,403 @@ +{% extends "base.html" %} + +{% block title %}Payment Plan #{{ plan.id }} - Plutus{% endblock %} + +{% block content %} + + +
+
+
+

Payment Plan #{{ plan.id }}

+

Created: {{ plan.Created.strftime('%Y-%m-%d %H:%M:%S') if plan.Created else 'Unknown' }}

+
+
+
+
+
+
+ +
+
+ + +
+
+
+ + +
+
+
+
+ {% if plan.Enabled %} + + + + {% else %} + + + + {% endif %} +
+
+
+ {% if plan.Enabled %} +

Active Payment Plan

+

This payment plan is currently active and processing payments.

+ {% else %} +

Inactive Payment Plan

+

This payment plan is disabled and not processing payments.

+ {% endif %} +
+
+
+
+
+
+

{{ plan.Amount | currency }}

+

{{ plan.Frequency }}

+
+
+
+
+
+ + +
+
+
+

+ + Customer Information +

+ +
+
+ + + +

Loading customer details...

+
+
+
+
+ +
+
+

+ + Plan Configuration +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Plan ID#{{ plan.id }}
Payment Amount{{ plan.Amount | currency }}
Frequency + + {{ plan.Frequency }} + +
Start Date + {{ plan.Start_Date.strftime('%Y-%m-%d') if plan.Start_Date else '-' }} + {% if plan.Start_Date %} +
Payments occur every {{ plan.Frequency.lower() }} from this date + {% endif %} +
Payment Method + {{ plan.Stripe_Payment_Method[:20] }}{% if plan.Stripe_Payment_Method|length > 20 %}...{% endif %} +
Status + {% if plan.Enabled %} + Active + {% else %} + Inactive + {% endif %} +
Created{{ plan.Created.strftime('%Y-%m-%d %H:%M:%S') if plan.Created else '-' }}
Created By{{ plan.created_by or 'Unknown' }}
+
+
+
+ + +
+
+
+

+ + Associated Payments +

+
+
+
+

+ + + + +

+
+
+
+ + {% if associated_payments %} +
+ + + + + + + + + + + + + {% for payment in associated_payments %} + + + + + + + + + {% endfor %} + +
Payment IDAmountStatusPayment IntentProcessedActions
+ + #{{ payment.id }} + + + {{ payment.Payment_Amount | currency }} + + {% if payment.Success == True %} + Success + {% elif payment.Success == False %} + Failed + {% else %} + Pending + {% endif %} + + {% if payment.Payment_Intent %} + {{ payment.Payment_Intent[:20] }}... + {% else %} + - + {% endif %} + {{ payment.Created.strftime('%Y-%m-%d %H:%M') if payment.Created else '-' }} + + + +
+
+ + +
+
+
+
+

Payment Summary

+

Total: {{ associated_payments|length }} payments

+
+
+
+
+
+
+ + {{ associated_payments|selectattr('Success', 'equalto', True)|list|length }} Successful + +
+
+
+
+ + {{ associated_payments|selectattr('Success', 'equalto', False)|list|length }} Failed + +
+
+
+
+ + {{ associated_payments|selectattr('Success', 'equalto', None)|list|length }} Pending + +
+
+
+
+ {% else %} +
+ + + +

No Associated Payments

+

This payment plan hasn't processed any payments yet.

+
+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/main/payment_plans_form.html b/templates/main/payment_plans_form.html new file mode 100644 index 0000000..d7d2460 --- /dev/null +++ b/templates/main/payment_plans_form.html @@ -0,0 +1,551 @@ +{% extends "base.html" %} + +{% block title %}{% if edit_mode %}Edit Payment Plan{% else %}Create Payment Plan{% endif %} - Plutus{% endblock %} + +{% block content %} + + +
+
+
+

{% if edit_mode %}Edit Payment Plan{% else %}Create Payment Plan{% endif %}

+

{% if edit_mode %}Update recurring payment settings{% else %}Set up automated recurring payments{% endif %}

+
+
+
+ + +
+ {% if edit_mode %} + +
+

+ + Edit Payment Plan Details +

+ +
+

Customer Information

+
+
+
+
+ Customer ID:
+ {{ plan.Splynx_ID }} +
+
+ Name:
+ + + Loading... + +
+
+
+
+
+ +
+ + +
+
+
+ +
+ + + + +
+

Enter the recurring payment amount (maximum $10,000)

+
+
+ +
+
+ +
+
+ +
+
+

How often should the payment be processed

+
+
+
+ +
+ +
+ +
+

The first payment date - determines both when payments start and which day of the week they occur

+
+ +
+ +
+
+ +
+
+

Stripe payment method to use for recurring payments

+
+ +
+
+ +
+ +
+
+
+ {% else %} + + + +
+

+ + Customer Lookup +

+ +
+ +
+ +
+

Enter the Splynx customer ID to fetch customer details

+
+ + + + + + + +
+
+ +
+
+
+ + + + {% endif %} +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/templates/main/payment_plans_list.html b/templates/main/payment_plans_list.html new file mode 100644 index 0000000..d9361d5 --- /dev/null +++ b/templates/main/payment_plans_list.html @@ -0,0 +1,267 @@ +{% extends "base.html" %} + +{% block title %}Payment Plans - Plutus{% endblock %} + +{% block content %} + + +
+
+
+

Payment Plans

+

Recurring payment management

+
+
+ +
+ + +
+
+
+

{{ summary.active_plans }}

+

Active Plans

+
+
+
+
+

{{ summary.inactive_plans }}

+

Inactive Plans

+
+
+
+
+

{{ summary.total_plans }}

+

Total Plans

+
+
+
+
+

{{ summary.total_recurring_amount | currency }}

+

Monthly Recurring

+
+
+
+ + +
+
+
+

Payment Plans

+
+
+
+

+ + + + +

+
+
+
+ + +
+
+ +
+ +
+
+
+ +
+ +
+
+
+ + {% if plans %} +
+ + + + + + + + + + + + + + + + + {% for plan in plans %} + + + + + + + + + + + + + {% endfor %} + +
Plan IDCustomerSplynx IDAmountFrequencyStart DateStatusCreatedCreated ByActions
+ + #{{ plan.id }} + + + + + Loading... + + + {{ plan.Splynx_ID }} + + {{ plan.Amount | currency }} + + + {{ plan.Frequency }} + + {{ plan.Start_Date.strftime('%Y-%m-%d') if plan.Start_Date else '-' }} + {% if plan.Enabled %} + Active + {% else %} + Inactive + {% endif %} + {{ plan.Created.strftime('%Y-%m-%d %H:%M') if plan.Created else '-' }}{{ plan.created_by or 'Unknown' }} +
+
+ + + +
+
+ + + +
+
+
+
+ {% else %} +
+ + + +

No Payment Plans Found

+

Get started by creating your first payment plan.

+ + + Create Payment Plan + +
+ {% endif %} +
+ + +{% endblock %} \ No newline at end of file diff --git a/templates/main/single_payment.html b/templates/main/single_payment.html new file mode 100644 index 0000000..6fdf029 --- /dev/null +++ b/templates/main/single_payment.html @@ -0,0 +1,554 @@ +{% extends "base.html" %} + +{% block title %}Single Payment - Plutus{% endblock %} + +{% block content %} + + +
+
+
+

Single Payment Processing

+

Process individual customer payments through Stripe

+
+
+
+ + +
+ +
+

+ + Customer Lookup +

+ +
+ +
+ +
+

Enter the Splynx customer ID to fetch customer details

+
+ + + + + + + +
+
+ +
+
+
+ + + +
+ + + + + + + + + + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/main/single_payment_detail.html b/templates/main/single_payment_detail.html new file mode 100644 index 0000000..0f9148b --- /dev/null +++ b/templates/main/single_payment_detail.html @@ -0,0 +1,410 @@ +{% extends "base.html" %} + +{% block title %}Payment #{{ payment.id }} - Plutus{% endblock %} + +{% block content %} + + +
+
+
+

Single Payment #{{ payment.id }}

+

Processed: {{ payment.Created.strftime('%Y-%m-%d %H:%M:%S') if payment.Created else 'Unknown' }}

+
+
+ +
+ + +
+
+
+
+ {% if payment.Success == True %} + + + + {% elif payment.Success == False %} + + + + {% else %} + + + + {% endif %} +
+
+
+ {% if payment.Success == True %} +

Payment Successful

+

This payment has been completed successfully.

+ {% elif payment.Success == False %} +

Payment Failed

+

This payment could not be completed.

+ {% else %} +

Payment Pending

+

This payment is still being processed.

+ {% endif %} +
+
+
+
+ {% if payment.PI_FollowUp %} + + {% endif %} +
+
+
+ + +
+
+
+

+ + Payment Information +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Payment ID#{{ payment.id }}
Splynx Customer ID + {% if payment.Splynx_ID %} + {{ payment.Splynx_ID }} + {% else %} + - + {% endif %} +
Stripe Customer ID{{ payment.Stripe_Customer_ID or '-' }}
Payment Intent{{ payment.Payment_Intent or '-' }}
Payment Method + {% if payment.Payment_Method %} + {{ payment.Payment_Method }} + {% else %} + - + {% endif %} +
Created{{ payment.Created.strftime('%Y-%m-%d %H:%M:%S') if payment.Created else '-' }}
Processed By{{ payment.processed_by or 'Unknown' }}
+
+
+ +
+
+

+ + Financial Details +

+ + + + + + + + + + + + + + + + + + + + +
Payment Amount{{ payment.Payment_Amount | currency }}
Stripe Fee{{ payment.Fee_Stripe | currency if payment.Fee_Stripe else '-' }}
Tax Fee{{ payment.Fee_Tax | currency if payment.Fee_Tax else '-' }}
Total Fees{{ payment.Fee_Total | currency if payment.Fee_Total else '-' }}
+ + {% if payment.PI_FollowUp %} +
+ + Follow-up Required: This payment requires additional processing. + {% if payment.PI_Last_Check %} +
Last checked: {{ payment.PI_Last_Check.strftime('%Y-%m-%d %H:%M:%S') }} + {% endif %} +
+ {% endif %} +
+
+
+ + +{% if payment.Error %} +
+

+ + Error Information +

+ +
+
{{ payment.Error }}
+
+
+{% endif %} + + +
+ {% if payment.PI_JSON %} +
+
+

+ + Payment Intent JSON +

+ +
+
+ +
+
+ +
{{ payment.PI_JSON | format_json }}
+ +
+
+ {% endif %} + + {% if payment.PI_FollowUp_JSON %} +
+
+

+ + Follow-up JSON +

+ +
+
+ +
+
+ +
{{ payment.PI_FollowUp_JSON | format_json }}
+ +
+
+ {% endif %} +
+ + + + + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/main/single_payments_list.html b/templates/main/single_payments_list.html new file mode 100644 index 0000000..6e3b196 --- /dev/null +++ b/templates/main/single_payments_list.html @@ -0,0 +1,514 @@ +{% extends "base.html" %} + +{% block title %}Single Payments - Plutus{% endblock %} + +{% block content %} + + +
+
+
+

Single Payments

+

Individual payment processing history

+
+
+ +
+ + +
+
+
+

Payment History

+
+
+
+

+ + + + +

+
+
+
+ + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+
+ + + + + {% if payments %} +
+ + + + + + + + + + + + + + + + + + {% for payment in payments %} + {% set row_class = '' %} + {% if payment.Success == True %} + {% set row_class = 'has-background-success-light' %} + {% elif payment.Success == False and payment.Error %} + {% set row_class = 'has-background-danger-light' %} + {% elif payment.Success == None %} + {% set row_class = 'has-background-info-light' %} + {% endif %} + + + + + + + + + + + + + + + {% endfor %} + +
Payment IDDateSplynx IDStripe CustomerPayment IntentPayment MethodStripe FeeAmountProcessed ByDataStatus
+ #{{ payment.id }} + + {{ payment.Created.strftime('%Y-%m-%d') }}
+ {{ payment.Created.strftime('%H:%M:%S') }} +
+ {% if payment.Splynx_ID %} + + {{ payment.Splynx_ID }} + + {% else %} + - + {% endif %} + + {% if payment.Success == True %} + {{ payment.Stripe_Customer_ID or '-' }} + {% elif payment.Success == False and payment.Error %} + {{ payment.Stripe_Customer_ID or '-' }} + {% elif payment.Success == None %} + {{ payment.Stripe_Customer_ID or '-' }} + {% else %} + {{ payment.Stripe_Customer_ID or '-' }} + {% endif %} + + {% if payment.Payment_Intent %} + {% if payment.Success == True %} + {{ payment.Payment_Intent }} + {% elif payment.Success == False and payment.Error %} + {{ payment.Payment_Intent }} + {% elif payment.Success == None %} + {{ payment.Payment_Intent }} + {% else %} + {{ payment.Payment_Intent }} + {% endif %} + {% else %} + - + {% endif %} + + {% if payment.Payment_Method %} + {{ payment.Payment_Method }} + {% else %} + - + {% endif %} + + {% if payment.Fee_Stripe %} + {{ payment.Fee_Stripe | currency }} + {% else %} + - + {% endif %} + + {% if payment.Payment_Amount %} + {{ payment.Payment_Amount | currency }} + {% else %} + - + {% endif %} + + {{ payment.processed_by or 'Unknown' }} + +
+ {% if payment.PI_JSON %} + + {% endif %} + + {% if payment.Error %} + + {% endif %} +
+
+ {% if payment.Success == True %} + Success + {% elif payment.Success == False %} + Failed + {% else %} + Pending + {% endif %} +
+
+ {% else %} +
+

No single payments found. Process your first payment.

+
+ {% endif %} +
+ + +{% for payment in payments %} + + {% if payment.PI_JSON %} + + {% endif %} + + + {% if payment.Error %} + + {% endif %} +{% endfor %} + + +{% endblock %} \ No newline at end of file diff --git a/test_logging.py b/test_logging.py new file mode 100644 index 0000000..3fce854 --- /dev/null +++ b/test_logging.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Test script to verify the logging system is working correctly. +""" + +from app import create_app, db +from services import ( + log_script_start, log_script_completion, log_batch_created, + log_payment_plan_run, log_payment_intent_followup, log_activity +) +from models import Logs +import time + +def test_logging_functions(): + """Test all logging functions.""" + print("Testing logging system...") + + # Test basic log_activity function + log_id = log_activity( + user_id=1, # System user + action="TEST_LOGGING", + entity_type="Test", + entity_id=999, + details="Testing the logging system functionality" + ) + print(f"✓ Basic logging test - Log ID: {log_id}") + + # Test script start logging + start_log_id = log_script_start("test_logging.py", "test", "sandbox") + print(f"✓ Script start logging - Log ID: {start_log_id}") + + # Test batch creation logging + batch_log_id = log_batch_created(123, "Direct Debit", 45) + print(f"✓ Batch creation logging - Log ID: {batch_log_id}") + + # Test payment plan run logging + payplan_log_id = log_payment_plan_run( + active_plans=25, + due_plans=8, + processed_count=7, + failed_count=1, + total_amount=1247.50 + ) + print(f"✓ Payment plan run logging - Log ID: {payplan_log_id}") + + # Test payment intent follow-up logging + intent_log_id = log_payment_intent_followup( + pending_count=15, + succeeded_count=12, + failed_count=2, + still_pending=1 + ) + print(f"✓ Payment intent follow-up logging - Log ID: {intent_log_id}") + + # Test script completion logging + completion_log_id = log_script_completion( + script_name="test_logging.py", + mode="test", + success_count=5, + failed_count=1, + total_amount=1247.50, + batch_ids=[123, 124], + duration_seconds=2.5, + errors=["Test error message"] + ) + print(f"✓ Script completion logging - Log ID: {completion_log_id}") + + return True + +def verify_logs_in_database(): + """Verify that logs were actually written to the database.""" + print("\nVerifying logs in database...") + + # Query recent test logs + recent_logs = db.session.query(Logs).filter( + (Logs.Action.like('TEST_%')) | + (Logs.Entity_Type == 'Test') + ).order_by(Logs.Added.desc()).limit(10).all() + + print(f"Found {len(recent_logs)} test log entries:") + for log in recent_logs: + print(f" - ID: {log.id}, Action: {log.Action}, Entity: {log.Entity_Type}, Details: {log.Log_Entry[:50]}...") + + return len(recent_logs) > 0 + +if __name__ == "__main__": + app = create_app() + + with app.app_context(): + print("=" * 60) + print("PLUTUS LOGGING SYSTEM TEST") + print("=" * 60) + + start_time = time.time() + + # Test logging functions + test_success = test_logging_functions() + + # Verify logs were written to database + db_success = verify_logs_in_database() + + end_time = time.time() + duration = end_time - start_time + + print(f"\nTest completed in {duration:.2f} seconds") + + if test_success and db_success: + print("✅ All logging tests passed!") + + # Log this test completion to demonstrate the system + log_activity( + user_id=1, + action="TEST_COMPLETED", + entity_type="Test", + details=f"Logging system test completed successfully in {duration:.2f}s" + ) + + else: + print("❌ Some logging tests failed!") + + print("=" * 60) \ No newline at end of file