Files
eveAI/common/models/entitlements.py
Josako d2bb51a4a8 Extra commit for files in 'common'
- Add functionality to add a default dictionary for configuration fields
- Correct entitlement processing
- Remove get_template functionality from ModelVariables, define it directly with LLM model definition in configuration file.
2025-05-19 14:12:38 +02:00

529 lines
23 KiB
Python

from sqlalchemy.sql.expression import text
from common.extensions import db
from datetime import datetime as dt, timezone as tz
from enum import Enum
from sqlalchemy import event
from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.hybrid import hybrid_property
from dateutil.relativedelta import relativedelta
from common.utils.database import Database
class BusinessEventLog(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime, nullable=False)
event_type = db.Column(db.String(50), nullable=False)
tenant_id = db.Column(db.Integer, nullable=False)
trace_id = db.Column(db.String(50), nullable=False)
span_id = db.Column(db.String(50))
span_name = db.Column(db.String(255))
parent_span_id = db.Column(db.String(50))
document_version_id = db.Column(db.Integer)
document_version_file_size = db.Column(db.Float)
specialist_id = db.Column(db.Integer)
specialist_type = db.Column(db.String(50))
specialist_type_version = db.Column(db.String(20))
chat_session_id = db.Column(db.String(50))
interaction_id = db.Column(db.Integer)
environment = db.Column(db.String(20))
llm_metrics_total_tokens = db.Column(db.Integer)
llm_metrics_prompt_tokens = db.Column(db.Integer)
llm_metrics_completion_tokens = db.Column(db.Integer)
llm_metrics_total_time = db.Column(db.Float)
llm_metrics_nr_of_pages = db.Column(db.Integer)
llm_metrics_call_count = db.Column(db.Integer)
llm_interaction_type = db.Column(db.String(20))
message = db.Column(db.Text)
license_usage_id = db.Column(db.Integer, db.ForeignKey('public.license_usage.id'), nullable=True)
license_usage = db.relationship('LicenseUsage', backref='events')
class License(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
tier_id = db.Column(db.Integer, db.ForeignKey('public.license_tier.id'),nullable=False) # 'small', 'medium', 'custom'
start_date = db.Column(db.Date, nullable=False)
end_date = db.Column(db.Date, nullable=True)
nr_of_periods = db.Column(db.Integer, nullable=False)
currency = db.Column(db.String(20), nullable=False)
yearly_payment = db.Column(db.Boolean, nullable=False, default=False)
basic_fee = db.Column(db.Float, nullable=False)
max_storage_mb = db.Column(db.Integer, nullable=False)
additional_storage_price = db.Column(db.Float, nullable=False)
additional_storage_bucket = db.Column(db.Integer, nullable=False)
included_embedding_mb = db.Column(db.Integer, nullable=False)
additional_embedding_price = db.Column(db.Numeric(10, 4), nullable=False)
additional_embedding_bucket = db.Column(db.Integer, nullable=False)
included_interaction_tokens = db.Column(db.Integer, nullable=False)
additional_interaction_token_price = db.Column(db.Numeric(10, 4), nullable=False)
additional_interaction_bucket = db.Column(db.Integer, nullable=False)
overage_embedding = db.Column(db.Float, nullable=False, default=0)
overage_interaction = db.Column(db.Float, nullable=False, default=0)
additional_storage_allowed = db.Column(db.Boolean, nullable=False, default=False)
additional_embedding_allowed = db.Column(db.Boolean, nullable=False, default=False)
additional_interaction_allowed = db.Column(db.Boolean, nullable=False, default=False)
# Versioning Information
created_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now())
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
updated_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now(), onupdate=db.func.now())
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
tenant = db.relationship('Tenant', back_populates='licenses')
license_tier = db.relationship('LicenseTier', back_populates='licenses')
periods = db.relationship('LicensePeriod', back_populates='license',
order_by='LicensePeriod.period_number',
cascade='all, delete-orphan')
def calculate_end_date(start_date, nr_of_periods):
"""Utility functie om einddatum te berekenen"""
if start_date and nr_of_periods:
return start_date + relativedelta(months=nr_of_periods) - relativedelta(days=1)
return None
# Luister naar start_date wijzigingen
@event.listens_for(License.start_date, 'set')
def set_start_date(target, value, oldvalue, initiator):
"""Bijwerken van end_date wanneer start_date wordt aangepast"""
if value and target.nr_of_periods:
target.end_date = calculate_end_date(value, target.nr_of_periods)
# Luister naar nr_of_periods wijzigingen
@event.listens_for(License.nr_of_periods, 'set')
def set_nr_of_periods(target, value, oldvalue, initiator):
"""Bijwerken van end_date wanneer nr_of_periods wordt aangepast"""
if value and target.start_date:
target.end_date = calculate_end_date(target.start_date, value)
class LicenseTier(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(50), nullable=False)
version = db.Column(db.String(50), nullable=False)
start_date = db.Column(db.Date, nullable=False)
end_date = db.Column(db.Date, nullable=True)
basic_fee_d = db.Column(db.Float, nullable=True)
basic_fee_e = db.Column(db.Float, nullable=True)
max_storage_mb = db.Column(db.Integer, nullable=False)
additional_storage_price_d = db.Column(db.Numeric(10, 4), nullable=False)
additional_storage_price_e = db.Column(db.Numeric(10, 4), nullable=False)
additional_storage_bucket = db.Column(db.Integer, nullable=False)
included_embedding_mb = db.Column(db.Integer, nullable=False)
additional_embedding_price_d = db.Column(db.Numeric(10, 4), nullable=False)
additional_embedding_price_e = db.Column(db.Numeric(10, 4), nullable=False)
additional_embedding_bucket = db.Column(db.Integer, nullable=False)
included_interaction_tokens = db.Column(db.Integer, nullable=False)
additional_interaction_token_price_d = db.Column(db.Numeric(10, 4), nullable=False)
additional_interaction_token_price_e = db.Column(db.Numeric(10, 4), nullable=False)
additional_interaction_bucket = db.Column(db.Integer, nullable=False)
standard_overage_embedding = db.Column(db.Float, nullable=False, default=0)
standard_overage_interaction = db.Column(db.Float, nullable=False, default=0)
# Versioning Information
created_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now())
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
updated_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now(), onupdate=db.func.now())
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
licenses = db.relationship('License', back_populates='license_tier')
partner_services = db.relationship('PartnerServiceLicenseTier', back_populates='license_tier')
class PartnerServiceLicenseTier(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
partner_service_id = db.Column(db.Integer, db.ForeignKey('public.partner_service.id'), primary_key=True,
nullable=False)
license_tier_id = db.Column(db.Integer, db.ForeignKey('public.license_tier.id'), primary_key=True,
nullable=False)
# Versioning Information
created_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now())
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
updated_at = db.Column(db.DateTime, nullable=True, server_default=db.func.now(), onupdate=db.func.now())
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
license_tier = db.relationship('LicenseTier', back_populates='partner_services')
partner_service = db.relationship('PartnerService', back_populates='license_tiers')
class PeriodStatus(Enum):
UPCOMING = "UPCOMING" # The period is still in the future
PENDING = "PENDING" # The period is active, but prepaid is not yet received
ACTIVE = "ACTIVE" # The period is active and prepaid has been received
COMPLETED = "COMPLETED" # The period has been completed, but not yet invoiced
INVOICED = "INVOICED" # The period has been completed and invoiced, but overage payment still pending
CLOSED = "CLOSED" # The period has been closed, invoiced and fully paid
class LicensePeriod(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
license_id = db.Column(db.Integer, db.ForeignKey('public.license.id'), nullable=False)
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
# Period identification
period_number = db.Column(db.Integer, nullable=False)
period_start = db.Column(db.Date, nullable=False)
period_end = db.Column(db.Date, nullable=False)
# License configuration snapshot - copied from license when period is created
currency = db.Column(db.String(20), nullable=True)
basic_fee = db.Column(db.Float, nullable=True)
max_storage_mb = db.Column(db.Integer, nullable=True)
additional_storage_price = db.Column(db.Float, nullable=True)
additional_storage_bucket = db.Column(db.Integer, nullable=True)
included_embedding_mb = db.Column(db.Integer, nullable=True)
additional_embedding_price = db.Column(db.Numeric(10, 4), nullable=True)
additional_embedding_bucket = db.Column(db.Integer, nullable=True)
included_interaction_tokens = db.Column(db.Integer, nullable=True)
additional_interaction_token_price = db.Column(db.Numeric(10, 4), nullable=True)
additional_interaction_bucket = db.Column(db.Integer, nullable=True)
# Allowance flags - can be changed from False to True within a period
additional_storage_allowed = db.Column(db.Boolean, nullable=True, default=False)
additional_embedding_allowed = db.Column(db.Boolean, nullable=True, default=False)
additional_interaction_allowed = db.Column(db.Boolean, nullable=True, default=False)
# Status tracking
status = db.Column(db.Enum(PeriodStatus), nullable=False, default=PeriodStatus.UPCOMING)
# State transition timestamps
upcoming_at = db.Column(db.DateTime, nullable=True)
pending_at = db.Column(db.DateTime, nullable=True)
active_at = db.Column(db.DateTime, nullable=True)
completed_at = db.Column(db.DateTime, nullable=True)
invoiced_at = db.Column(db.DateTime, nullable=True)
closed_at = db.Column(db.DateTime, nullable=True)
# Standard audit fields
created_at = db.Column(db.DateTime, server_default=db.func.now())
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
# Relationships
license = db.relationship('License', back_populates='periods')
license_usage = db.relationship('LicenseUsage',
uselist=False, # This makes it one-to-one
back_populates='license_period',
cascade='all, delete-orphan')
payments = db.relationship('Payment', back_populates='license_period')
invoices = db.relationship('Invoice', back_populates='license_period',
cascade='all, delete-orphan')
def update_allowance(self, allowance_type, allow_value, user_id=None):
"""
Update an allowance flag within a period
Only allows transitioning from False to True
Args:
allowance_type: One of 'storage', 'embedding', or 'interaction'
allow_value: The new value (must be True)
user_id: User ID performing the update
Raises:
ValueError: If trying to change from True to False, or invalid allowance type
"""
field_name = f"additional_{allowance_type}_allowed"
# Verify valid field
if not hasattr(self, field_name):
raise ValueError(f"Invalid allowance type: {allowance_type}")
# Get current value
current_value = getattr(self, field_name)
# Only allow False -> True transition
if current_value is True and allow_value is True:
# Already True, no change needed
return
elif allow_value is False:
raise ValueError(f"Cannot change {field_name} from {current_value} to False")
# Update the field
setattr(self, field_name, True)
self.updated_at = dt.now(tz.utc)
if user_id:
self.updated_by = user_id
@property
def prepaid_invoice(self):
"""Get the prepaid invoice for this period"""
return Invoice.query.filter_by(
license_period_id=self.id,
invoice_type=PaymentType.PREPAID
).first()
@property
def overage_invoice(self):
"""Get the overage invoice for this period"""
return Invoice.query.filter_by(
license_period_id=self.id,
invoice_type=PaymentType.POSTPAID
).first()
@property
def prepaid_payment(self):
"""Get the prepaid payment for this period"""
return Payment.query.filter_by(
license_period_id=self.id,
payment_type=PaymentType.PREPAID
).first()
@property
def overage_payment(self):
"""Get the overage payment for this period"""
return Payment.query.filter_by(
license_period_id=self.id,
payment_type=PaymentType.POSTPAID
).first()
@property
def all_invoices(self):
"""Get all invoices for this period"""
return self.invoices
@property
def all_payments(self):
"""Get all payments for this period"""
return self.payments
def transition_status(self, new_status: PeriodStatus, user_id: int = None):
"""Transition to a new status with proper validation and logging"""
if not self.can_transition_to(new_status):
raise ValueError(f"Invalid status transition from {self.status} to {new_status}")
self.status = new_status
self.updated_at = dt.now(tz.utc)
if user_id:
self.updated_by = user_id
# Set appropriate timestamps
if new_status == PeriodStatus.ACTIVE and not self.prepaid_received_at:
self.prepaid_received_at = dt.now(tz.utc)
elif new_status == PeriodStatus.COMPLETED:
self.completed_at = dt.now(tz.utc)
elif new_status == PeriodStatus.INVOICED:
self.invoiced_at = dt.now(tz.utc)
elif new_status == PeriodStatus.CLOSED:
self.closed_at = dt.now(tz.utc)
@property
def is_overdue(self):
"""Check if a prepaid payment is overdue"""
return (self.status == PeriodStatus.PENDING and
self.period_start <= dt.now(tz.utc).date())
def can_transition_to(self, new_status: PeriodStatus) -> bool:
"""Check if a status transition is valid"""
valid_transitions = {
PeriodStatus.UPCOMING: [PeriodStatus.ACTIVE, PeriodStatus.PENDING],
PeriodStatus.PENDING: [PeriodStatus.ACTIVE],
PeriodStatus.ACTIVE: [PeriodStatus.COMPLETED],
PeriodStatus.COMPLETED: [PeriodStatus.INVOICED, PeriodStatus.CLOSED],
PeriodStatus.INVOICED: [PeriodStatus.CLOSED],
PeriodStatus.CLOSED: []
}
return new_status in valid_transitions.get(self.status, [])
def __repr__(self):
return f'<LicensePeriod {self.id}: License {self.license_id}, Period {self.period_number}>'
class LicenseUsage(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
storage_mb_used = db.Column(db.Float, default=0)
embedding_mb_used = db.Column(db.Float, default=0)
embedding_prompt_tokens_used = db.Column(db.Integer, default=0)
embedding_completion_tokens_used = db.Column(db.Integer, default=0)
embedding_total_tokens_used = db.Column(db.Integer, default=0)
interaction_prompt_tokens_used = db.Column(db.Integer, default=0)
interaction_completion_tokens_used = db.Column(db.Integer, default=0)
interaction_total_tokens_used = db.Column(db.Integer, default=0)
license_period_id = db.Column(db.Integer, db.ForeignKey('public.license_period.id'), nullable=False)
# Standard audit fields
created_at = db.Column(db.DateTime, server_default=db.func.now())
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
license_period = db.relationship('LicensePeriod', back_populates='license_usage')
def recalculate_storage(self):
Database(self.tenant_id).switch_schema()
# Perform a SUM operation to get the total file size from document_versions
total_storage = db.session.execute(text(f"""
SELECT SUM(file_size)
FROM document_version
""")).scalar()
self.storage_mb_used = total_storage
class PaymentType(Enum):
PREPAID = "PREPAID"
POSTPAID = "POSTPAID"
class PaymentStatus(Enum):
PENDING = "PENDING"
PAID = "PAID"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
class Payment(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
license_period_id = db.Column(db.Integer, db.ForeignKey('public.license_period.id'), nullable=True)
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
# Payment details
payment_type = db.Column(db.Enum(PaymentType), nullable=False)
amount = db.Column(db.Numeric(10, 2), nullable=False)
currency = db.Column(db.String(3), nullable=False)
description = db.Column(db.Text, nullable=True)
# Status tracking
status = db.Column(db.Enum(PaymentStatus), nullable=False, default=PaymentStatus.PENDING)
# External provider information
external_payment_id = db.Column(db.String(255), nullable=True)
payment_method = db.Column(db.String(50), nullable=True) # credit_card, bank_transfer, etc.
provider_data = db.Column(JSONB, nullable=True) # Provider-specific data
# Payment information
paid_at = db.Column(db.DateTime, nullable=True)
# Standard audit fields
created_at = db.Column(db.DateTime, server_default=db.func.now())
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
# Relationships
license_period = db.relationship('LicensePeriod', back_populates='payments')
invoice = db.relationship('Invoice', back_populates='payment', uselist=False)
@property
def is_overdue(self):
"""Check if payment is overdue"""
if self.status != PaymentStatus.PENDING:
return False
# For prepaid payments, check if period start has passed
if (self.payment_type == PaymentType.PREPAID and
self.license_period_id):
return self.license_period.period_start <= dt.now(tz.utc).date()
# For postpaid, check against due date (would be on invoice)
return False
def __repr__(self):
return f'<Payment {self.id}: {self.payment_type} {self.amount} {self.currency}>'
class InvoiceStatus(Enum):
DRAFT = "DRAFT"
SENT = "SENT"
PAID = "PAID"
OVERDUE = "OVERDUE"
CANCELLED = "CANCELLED"
class Invoice(db.Model):
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
license_period_id = db.Column(db.Integer, db.ForeignKey('public.license_period.id'), nullable=False)
payment_id = db.Column(db.Integer, db.ForeignKey('public.payment.id'), nullable=True)
tenant_id = db.Column(db.Integer, db.ForeignKey('public.tenant.id'), nullable=False)
# Invoice details
invoice_type = db.Column(db.Enum(PaymentType), nullable=False)
invoice_number = db.Column(db.String(50), unique=True, nullable=False)
invoice_date = db.Column(db.Date, nullable=False)
due_date = db.Column(db.Date, nullable=False)
# Financial details
amount = db.Column(db.Numeric(10, 2), nullable=False)
currency = db.Column(db.String(3), nullable=False)
tax_amount = db.Column(db.Numeric(10, 2), default=0)
# Descriptive fields
description = db.Column(db.Text, nullable=True)
status = db.Column(db.Enum(InvoiceStatus), nullable=False, default=InvoiceStatus.DRAFT)
# Timestamps
sent_at = db.Column(db.DateTime, nullable=True)
paid_at = db.Column(db.DateTime, nullable=True)
# Standard audit fields
created_at = db.Column(db.DateTime, server_default=db.func.now())
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now())
updated_by = db.Column(db.Integer, db.ForeignKey('public.user.id'))
# Relationships
license_period = db.relationship('LicensePeriod', back_populates='invoices')
payment = db.relationship('Payment', back_populates='invoice')
def __repr__(self):
return f'<Invoice {self.invoice_number}: {self.amount} {self.currency}>'
class LicenseChangeLog(db.Model):
"""
Log of changes to license configurations
Used for auditing and tracking when/why license details changed
"""
__bind_key__ = 'public'
__table_args__ = {'schema': 'public'}
id = db.Column(db.Integer, primary_key=True)
license_id = db.Column(db.Integer, db.ForeignKey('public.license.id'), nullable=False)
changed_at = db.Column(db.DateTime, nullable=False, default=lambda: dt.now(tz.utc))
# What changed
field_name = db.Column(db.String(100), nullable=False)
old_value = db.Column(db.String(255), nullable=True)
new_value = db.Column(db.String(255), nullable=False)
# Why it changed
reason = db.Column(db.Text, nullable=True)
# Standard audit fields
created_by = db.Column(db.Integer, db.ForeignKey('public.user.id'), nullable=True)
# Relationships
license = db.relationship('License', backref=db.backref('change_logs', order_by='LicenseChangeLog.changed_at'))
def __repr__(self):
return f'<LicenseChangeLog: {self.license_id} {self.field_name} {self.old_value} -> {self.new_value}>'