"""Snapshot the customer invoice fee at order creation, never on update/export.""" from decimal import Decimal, InvalidOperation from uuid import uuid4 from fastapi import HTTPException from app.core.database import execute_query_single from app.core.config import settings def _one(sql, params, cursor=None): if cursor is not None: cursor.execute(sql, params) row = cursor.fetchone() if row is not None and not isinstance(row, dict): return {column[0]: value for column, value in zip(cursor.description, row)} return row return execute_query_single(sql, params) def validate_fee_product(product_id, cursor=None): try: product_id = int(product_id) if product_id <= 0: raise ValueError() except (TypeError, ValueError): raise HTTPException(422, 'Vælg en gyldig vare til faktureringsgebyr') product = _one('SELECT * FROM products WHERE id = %s AND deleted_at IS NULL', (product_id,), cursor) if not product or product.get('is_active') is False or product.get('status', 'active') != 'active' or product.get('lifecycle_status') in ('inactive', 'phasing_out') or product.get('is_active_in_economic') is False: raise HTTPException(409, 'Standardvaren til faktureringsgebyr findes ikke eller er inaktiv') amount = _amount(product.get('sales_price')) if amount is None or amount <= 0: raise HTTPException(422, 'Gebyrvaren skal have en positiv salgspris') if not (product.get('economic_product_number') or product.get('sku_internal')): raise HTTPException(422, 'Gebyrvaren mangler varenummer til eksport') return product def _amount(value): if value is None: return None try: amount = Decimal(str(value)) if not amount.is_finite() or amount < 0: raise ValueError() return amount.quantize(Decimal('0.01')) except (InvalidOperation, ValueError): raise HTTPException(422, 'Faktureringsgebyret skal være et gyldigt beløb på mindst 0') def invoice_fee_line(customer_id, *, cursor=None, legacy=False, customer=None, currency='DKK', _setting=None): setting = _setting if _setting is not None else _one('SELECT value FROM settings WHERE key = %s', ('invoice_fee_product_id',), cursor) configured = (setting or {}).get('value') active = configured not in (None, '', 'null') if not active and not legacy: return None customer = customer or _one('SELECT * FROM customers WHERE id = %s', (customer_id,), cursor) if not customer: raise HTTPException(409, 'Ordren mangler en gyldig Hub-kunde til beregning af faktureringsgebyr') if customer.get('invoice_fee_exempt') or customer.get('supplier_service_enrolled'): return None override = _amount(customer.get('invoice_fee_amount')) if override == 0: return None if active: product = validate_fee_product(configured, cursor) product_currency = product.get('sales_currency') or 'DKK' if currency != product_currency: raise HTTPException(422, f'Gebyrvarens pris er i {product_currency}, men ordren er i {currency}') amount = override if override is not None else _amount(product['sales_price']) else: product = {} amount = override if override is not None else _amount(settings.CUSTOMER_DEFAULT_INVOICE_FEE) if not amount: return None product_number = product.get('economic_product_number') or product.get('sku_internal') description = product.get('name') or 'Faktureringsgebyr' return { 'line_key': f'invoice_fee:{uuid4().hex}', 'source_type': 'invoice_fee', 'source_id': None, 'product_id': product.get('id'), 'product_number': product_number, 'description': description, 'quantity': 1, 'unit_price': float(amount), 'discount_percentage': 0, 'unit': 'stk', 'selected': True, 'currency': currency, 'amount': float(amount), 'line_total': float(amount), 'economic_product_number': product_number, 'customer_id': customer_id, 'customer_name': customer.get('name'), 'sag_id': None, 'case_id': None, 'time_entry_ids': [], 'time_date': None, 'fee_snapshot': {'product_id': product.get('id'), 'product_number': product_number, 'amount': str(amount), 'currency': currency}, } def with_invoice_fee(lines, customer_id, *, cursor=None, legacy=False, customer=None, native=False): """Called only for a new order. Ignore incoming fee lines; derive fee server-side.""" setting = _one('SELECT value FROM settings WHERE key = %s', ('invoice_fee_product_id',), cursor) active = (setting or {}).get('value') not in (None, '', 'null') if not active and not legacy: return list(lines) business = [dict(line) for line in lines if line.get('source_type') != 'invoice_fee'] if not business: return business currencies = {line.get('currency') or 'DKK' for line in business} if len(currencies) != 1: raise HTTPException(422, 'En ordre med faktureringsgebyr skal have én fælles valuta') # Do not charge credits. Positive new orders are charged once. total = sum(Decimal(str(line.get('quantity', 1))) * Decimal(str(line.get('unit_price', line.get('unitNetPrice', 0)))) * (1 - Decimal(str(line.get('discount_percentage', line.get('discountPercentage', 0)))) / 100) for line in business if line.get('selected', True)) if total <= 0: return business fee = invoice_fee_line(customer_id, cursor=cursor, legacy=legacy, customer=customer, currency=currencies.pop(), _setting=setting or {}) if fee: if native: fee.update(product={'productNumber': fee['product_number'], 'description': fee['description']}, unitNetPrice=fee['unit_price'], totalNetAmount=fee['amount'], discountPercentage=0) business.append(fee) return business