97 lines
5.0 KiB
Python
97 lines
5.0 KiB
Python
"""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.')
|