Cleanup .pyc and .DS_Store, add new modules, remove legacy services

This commit is contained in:
Josako
2025-05-17 18:46:17 +02:00
parent 5c982fcc2c
commit d2a9092f46
93 changed files with 1620 additions and 80 deletions

View File

@@ -1,4 +1,13 @@
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.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):
@@ -41,7 +50,7 @@ class License(db.Model):
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)
@@ -56,6 +65,9 @@ class License(db.Model):
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())
@@ -65,7 +77,58 @@ class License(db.Model):
tenant = db.relationship('Tenant', back_populates='licenses')
license_tier = db.relationship('LicenseTier', back_populates='licenses')
usages = db.relationship('LicenseUsage', order_by='LicenseUsage.period_start_date', back_populates='license')
periods = db.relationship('LicensePeriod', back_populates='license',
order_by='LicensePeriod.period_number',
cascade='all, delete-orphan')
@hybrid_property
def end_date(self):
"""
Berekent de einddatum van de licentie op basis van start_date en nr_of_periods.
Elke periode is 1 maand, dus einddatum = startdatum + nr_of_periods maanden - 1 dag
"""
if self.start_date and self.nr_of_periods:
return self.start_date + relativedelta(months=self.nr_of_periods) - relativedelta(days=1)
return None
@end_date.expression
def end_date(cls):
"""
SQL expressie versie van de end_date property voor gebruik in queries
"""
return db.func.date_add(
db.func.date_add(
cls.start_date,
db.text(f'INTERVAL cls.nr_of_periods MONTH')
),
db.text('INTERVAL -1 DAY')
)
def update_configuration(self, **changes):
"""
Update license configuration
These changes will only apply to future periods, not existing ones
Args:
**changes: Dictionary of changes to apply to the license
Returns:
None
"""
allowed_fields = [
'tier_id', 'currency', 'basic_fee', 'max_storage_mb',
'additional_storage_price', 'additional_storage_bucket',
'included_embedding_mb', 'additional_embedding_price', 'additional_embedding_bucket',
'included_interaction_tokens', 'additional_interaction_token_price',
'additional_interaction_bucket', 'overage_embedding', 'overage_interaction',
'additional_storage_allowed', 'additional_embedding_allowed',
'additional_interaction_allowed'
]
# Apply only allowed changes
for key, value in changes.items():
if key in allowed_fields:
setattr(self, key, value)
class LicenseTier(db.Model):
@@ -123,13 +186,198 @@ class PartnerServiceLicenseTier(db.Model):
partner_service = db.relationship('PartnerService', back_populates='license_tiers')
class LicenseUsage(db.Model):
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=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)
# Allowance flags - can be changed from False to True within a period
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)
# 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)
@@ -138,9 +386,170 @@ class LicenseUsage(db.Model):
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)
period_start_date = db.Column(db.Date, nullable=False)
period_end_date = db.Column(db.Date, nullable=False)
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}>'
license = db.relationship('License', back_populates='usages')