"""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.')