- Improvement of Entitlements Domain

- Introduction of LicensePeriod
  - Introduction of Payments
  - Introduction of Invoices
- Services definitions for Entitlements Domain
This commit is contained in:
Josako
2025-05-16 09:06:13 +02:00
parent 1b1eef0d2e
commit b4f7b210e0
15 changed files with 717 additions and 201 deletions

View File

@@ -37,16 +37,15 @@ class LicenseTierForm(FlaskForm):
additional_interaction_bucket = IntegerField('Additional Interaction Bucket Size (M Tokens)',
validators=[DataRequired(), NumberRange(min=1)])
standard_overage_embedding = FloatField('Standard Overage Embedding (%)',
validators=[DataRequired(), NumberRange(min=0)],
default=0)
validators=[DataRequired(), NumberRange(min=0)], default=0)
standard_overage_interaction = FloatField('Standard Overage Interaction (%)',
validators=[DataRequired(), NumberRange(min=0)],
default=0)
validators=[DataRequired(), NumberRange(min=0)], default=0)
class LicenseForm(FlaskForm):
start_date = DateField('Start Date', id='form-control datepicker', validators=[DataRequired()])
end_date = DateField('End Date', id='form-control datepicker', validators=[DataRequired()])
nr_of_periods = IntegerField('Number of Periods',
validators=[DataRequired(), NumberRange(min=1, max=12)], default=12)
currency = StringField('Currency', validators=[Optional(), Length(max=20)])
yearly_payment = BooleanField('Yearly Payment', default=False)
basic_fee = FloatField('Basic Fee', validators=[InputRequired(), NumberRange(min=0)])

View File

@@ -1,18 +1,16 @@
import uuid
from datetime import datetime as dt, timezone as tz
from flask import request, redirect, flash, render_template, Blueprint, session, current_app, jsonify
from flask_security import hash_password, roles_required, roles_accepted, current_user
from flask import request, redirect, flash, render_template, Blueprint, session, current_app
from flask_security import roles_accepted, current_user
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import or_, desc
import ast
from common.models.entitlements import License, LicenseTier, LicenseUsage, BusinessEventLog
from common.extensions import db, security, minio_client, simple_encryption
from common.models.entitlements import License, LicenseTier, LicenseUsage, LicensePeriod, PeriodStatus
from common.extensions import db, cache_manager
from common.services.entitlement_services import EntitlementServices
from common.services.partner_services import PartnerServices
from common.services.tenant_services import TenantServices
from common.services.user_services import UserServices
from common.services.entitlements.license_tier_services import LicenseTierServices
from common.services.user.partner_services import PartnerServices
from common.services.user.user_services import UserServices
from common.utils.eveai_exceptions import EveAIException
from common.utils.security_utils import current_user_has_role
from .entitlements_forms import LicenseTierForm, LicenseForm
@@ -109,7 +107,7 @@ def handle_license_tier_selection():
return redirect(prefixed_url_for('entitlements_bp.create_license',
license_tier_id=license_tier_id))
case 'associate_license_tier_to_partner':
EntitlementServices.associate_license_tier_with_partner(license_tier_id)
LicenseTierServices.associate_license_tier_with_partner(license_tier_id)
# Add more conditions for other actions
return redirect(prefixed_url_for('entitlements_bp.view_license_tiers'))
@@ -153,8 +151,8 @@ def create_license(license_tier_id):
currency = session.get('tenant').get('currency')
if current_user_has_role("Partner Admin"): # The Partner Admin can only set start & end dates, and allowed fields
readonly_fields = [field.name for field in form if (field.name != 'end_date' and field.name != 'start_date' and
not field.name.endswith('allowed'))]
readonly_fields = [field.name for field in form if (field.name != 'nr_of_periods' and field.name != 'start_date'
and not field.name.endswith('allowed'))]
if request.method == 'GET':
# Fetch the LicenseTier
@@ -221,11 +219,14 @@ def edit_license(license_id):
license = License.query.get_or_404(license_id) # This will return a 404 if no license tier is found
form = LicenseForm(obj=license)
readonly_fields = []
if len(license.usages) > 0: # There already are usage records linked to this license
if len(license.periods) > 0: # There already are usage records linked to this license
# Define which fields should be disabled
readonly_fields = [field.name for field in form if field.name != 'end_date']
if current_user_has_role("Partner Admin"): # The Partner Admin can only set the end date
readonly_fields = [field.name for field in form if field.name != 'end_date']
readonly_fields = [field.name for field in form if field.name != 'nr_of_periods']
if current_user_has_role("Partner Admin"): # The Partner Admin can only set the nr_of_periods and allowed fields
readonly_fields = [field.name for field in form if (field.name != 'nr_of_periods'
and not field.name.endswith('allowed'))]
cache_manager.license_cache.invalidate_tenant_license(license.tenant_id)
if form.validate_on_submit():
# Populate the license with form data
@@ -296,6 +297,7 @@ def view_licenses():
current_date = dt.now(tz=tz.utc).date()
# Query licenses for the tenant, with ordering and active status
# TODO - Check validity
query = (
License.query
.join(LicenseTier) # Join with LicenseTier
@@ -303,10 +305,9 @@ def view_licenses():
.add_columns(
License.id,
License.start_date,
License.end_date,
License.nr_of_periods,
LicenseTier.name.label('license_tier_name'), # Access name through LicenseTier
((License.start_date <= current_date) &
(or_(License.end_date.is_(None), License.end_date >= current_date))).label('active')
(License.start_date <= current_date).label('active')
)
.order_by(License.start_date.desc())
)
@@ -315,8 +316,8 @@ def view_licenses():
lics = pagination.items
# prepare table data
rows = prepare_table_for_macro(lics, [('id', ''), ('license_tier_name', ''), ('start_date', ''), ('end_date', ''),
('active', '')])
rows = prepare_table_for_macro(lics, [('id', ''), ('license_tier_name', ''), ('start_date', ''),
('nr_of_periods', ''), ('active', '')])
# Render the licenses in a template
return render_template('entitlements/view_licenses.html', rows=rows, pagination=pagination)
@@ -333,4 +334,62 @@ def handle_license_selection():
match action:
case 'edit_license':
return redirect(prefixed_url_for('entitlements_bp.edit_license', license_id=license_id))
return redirect(prefixed_url_for('entitlements_bp.edit_license', license_id=license_id))
case 'view_periods':
return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id))
@entitlements_bp.route('/license/<int:license_id>/periods')
@roles_accepted('Super User', 'Partner Admin', 'Tenant Admin')
def view_license_periods(license_id):
license = License.query.get_or_404(license_id)
# Verify user can access this license
if not current_user.has_role('Super User'):
tenant_id = session.get('tenant').get('id')
if license.tenant_id != tenant_id:
flash('Access denied to this license', 'danger')
return redirect(prefixed_url_for('entitlements_bp.view_licenses'))
# Get all periods for this license
periods = (LicensePeriod.query
.filter_by(license_id=license_id)
.order_by(LicensePeriod.period_number)
.all())
# Group related data for easy template access
usage_by_period = {}
payments_by_period = {}
invoices_by_period = {}
for period in periods:
usage_by_period[period.id] = period.license_usage
payments_by_period[period.id] = list(period.payments)
invoices_by_period[period.id] = list(period.invoices)
return render_template('entitlements/license_periods.html',
license=license,
periods=periods,
usage_by_period=usage_by_period,
payments_by_period=payments_by_period,
invoices_by_period=invoices_by_period)
@entitlements_bp.route('/license/<int:license_id>/periods/<int:period_id>/transition', methods=['POST'])
@roles_accepted('Super User', 'Partner Admin')
def transition_period_status(license_id, period_id):
"""Handle status transitions for license periods"""
period = LicensePeriod.query.get_or_404(period_id)
new_status = request.form.get('new_status')
try:
period.transition_status(PeriodStatus[new_status], current_user.id)
db.session.commit()
flash(f'Period {period.period_number} status updated to {new_status}', 'success')
except ValueError as e:
flash(f'Invalid status transition: {str(e)}', 'danger')
except Exception as e:
db.session.rollback()
flash(f'Error updating status: {str(e)}', 'danger')
return redirect(prefixed_url_for('entitlements_bp.view_license_periods', license_id=license_id))

View File

@@ -1,13 +1,12 @@
from flask import current_app, session
from flask_wtf import FlaskForm
from wtforms import (StringField, PasswordField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
SelectField, SelectMultipleField, FieldList, FormField, FloatField, TextAreaField)
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional, ValidationError
from wtforms import (StringField, BooleanField, SubmitField, EmailField, IntegerField, DateField,
SelectField, SelectMultipleField, FieldList, FormField, TextAreaField)
from wtforms.validators import DataRequired, Length, Email, NumberRange, Optional
import pytz
from flask_security import current_user
from common.models.user import Role
from common.services.user_services import UserServices
from common.services.user.user_services import UserServices
from config.type_defs.service_types import SERVICE_TYPES

View File

@@ -7,7 +7,6 @@ import ast
from common.models.user import User, Tenant, Role, TenantDomain, TenantProject, PartnerTenant
from common.extensions import db, security, minio_client, simple_encryption
from common.services.user_services import UserServices
from common.utils.security_utils import send_confirmation_email, send_reset_email
from config.type_defs.service_types import SERVICE_TYPES
from .user_forms import TenantForm, CreateUserForm, EditUserForm, TenantDomainForm, TenantSelectionForm, \
@@ -18,8 +17,8 @@ from common.utils.simple_encryption import generate_api_key
from common.utils.nginx_utils import prefixed_url_for
from common.utils.eveai_exceptions import EveAIException
from common.utils.document_utils import set_logging_information, update_logging_information
from common.services.tenant_services import TenantServices
from common.services.user_services import UserServices
from common.services.user.tenant_services import TenantServices
from common.services.user.user_services import UserServices
from common.utils.mail_utils import send_email
user_bp = Blueprint('user_bp', __name__, url_prefix='/user')