bmc_hub/app/modules/also/backend/billing.py

97 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Keep ALSO's approved period amount authoritative on order lines."""
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from datetime import datetime, timedelta
import calendar
import re
from fastapi import HTTPException
def _number(value):
try:
result = Decimal(str(value))
if not result.is_finite():
raise InvalidOperation
return result
except (InvalidOperation, TypeError, ValueError):
raise HTTPException(409, 'ALSO-linjen mangler et gyldigt godkendt periodebeløb')
def period_amount_line(line):
"""Return a copy; never rewrite saved history or normalize other sources."""
if line.get('source_type') != 'also_cloud':
return dict(line)
amount = _number(line.get('amount'))
if amount < 0 or amount != amount.quantize(Decimal('.01'), rounding=ROUND_HALF_UP):
raise HTTPException(409, 'ALSO-periodebeløbet kræver afklaring før eksport')
if _number(line.get('discount_percentage', 0)) != 0:
raise HTTPException(409, 'ALSO-periodebeløbet må ikke kombineres med linjerabat')
result = dict(line)
meta = dict(line.get('meta') or {})
if meta.get('also_period_amount_version') != 1:
quantity = _number(line.get('quantity'))
monthly_price = _number(line.get('unit_price'))
if quantity <= 0 or monthly_price < 0:
raise HTTPException(409, 'ALSO-linjen mangler gyldigt licensantal eller månedspris')
meta.update(also_license_quantity=str(quantity), also_monthly_unit_price=str(monthly_price),
also_period_amount_version=1)
quantity_text = format(quantity, 'f').rstrip('0').rstrip('.') if '.' in str(quantity) else str(quantity)
price_text = f'{monthly_price:.2f}'.replace('.', ',')
license_text = 'licens' if quantity == 1 else 'licenser'
description = result.get('description') or 'Cloud abonnement'
title, separator, rest = description.partition('\n')
currency_text = 'kr.' if result.get('currency', 'DKK') == 'DKK' else result['currency']
explanation = f'{quantity_text} {license_text} á {price_text} {currency_text}/måned.'
interval = meta.get('billing_period')
period_text = _period_text(interval)
result['description'] = title + '\n' + explanation + (
' ' + period_text if period_text else ('\n' + rest if separator else ''))
result['description'] = re.sub(r'\s*Reference:\s*[^\n]*', '', result.get('description', ''), flags=re.IGNORECASE).strip()
result.update(quantity=1.0, unit_price=float(amount), amount=float(amount),
discount_percentage=0.0, meta=meta)
return result
def _period_text(interval):
"""ALSO Marketplace intervals have an exclusive end date."""
match = re.fullmatch(r'\s*(\d{2})[./-](\d{2})[./-](\d{4})\s*-\s*(\d{2})[./-](\d{2})[./-](\d{4})\s*', str(interval or ''))
if not match:
return None
try:
day, month, year, end_day, end_month, end_year = map(int, match.groups())
start = datetime(year, month, day).date()
end = datetime(end_year, end_month, end_day).date()
except ValueError:
raise HTTPException(409, 'ALSO-linjen har en ugyldig periode')
if end <= start:
raise HTTPException(409, 'ALSO-periodens slutdato skal være efter startdatoen')
last = end - timedelta(days=1)
start_text = start.strftime('%d.%m.') if start.year == last.year else start.strftime('%d.%m.%Y')
text = f'Periode: {start_text}–{last:%d.%m.%Y}, {(end-start).days}'
if (start.year, start.month) == (last.year, last.month):
text += f' af {calendar.monthrange(start.year, start.month)[1]}'
return text + ' dage.'
def check_period_amounts(lines, saved_lines):
"""Reject stale browsers and altered amounts before either export backend runs."""
saved = {str(line.get('line_key')): line for line in saved_lines}
seen = set()
for line in lines:
old = saved.get(str(line.get('line_key')))
if line.get('source_type') != 'also_cloud' and (not old or old.get('source_type') != 'also_cloud'):
continue
if not old or old.get('source_type') != 'also_cloud' or line.get('source_type') != 'also_cloud':
raise HTTPException(409, 'ALSO-eksport kræver den gemte godkendte ordrekladde')
key = str(line.get('line_key'))
if key in seen or line.get('source_id') != old.get('source_id'):
raise HTTPException(409, 'ALSO-kildelinjen mangler entydig identitet eller er valgt flere gange')
seen.add(key)
expected = period_amount_line(old)
if (line.get('product_id') != old.get('product_id')
or line.get('description') != expected.get('description')
or line.get('currency', 'DKK') != old.get('currency', 'DKK')
or any(_number(line.get(key, 0)) != _number(expected[key])
for key in ('quantity', 'unit_price', 'discount_percentage'))):
raise HTTPException(409, 'ALSO-linjen matcher ikke det godkendte periodebeløb. Genindlæs kladden og kontrollér linjerne før eksport.')