bmc_hub/tests/test_also_period_billing.py

179 lines
8.1 KiB
Python
Raw Normal View History

"""Regression coverage for Marketplace period amounts; no external writes."""
from copy import deepcopy
from decimal import Decimal
import json
import pytest
from fastapi import HTTPException
from app.modules.also.backend.billing import period_amount_line, check_period_amounts, _period_text
from app.modules.orders.backend.router import OrdreLineInput
from app.products.backend.economic_documents import line_snapshot
CASES = [
(116, '67.08', '7028.25', '01.08.2026 - 29.08.2026', '28 af 31'),
(116, '67.08', '753.03', '29.08.2026 - 01.09.2026', '3 af 31'),
(108, '67.08', '6543.55', '01.08.2026 - 29.08.2026', '28 af 31'),
(108, '67.08', '701.09', '29.08.2026 - 01.09.2026', '3 af 31'),
(1, '27.90', '17.10', '13.08.2026 - 01.09.2026', '19 af 31'),
(1, '50.28', '19.46', '20.08.2026 - 01.09.2026', '12 af 31'),
]
def source(index=0):
quantity, price, amount, interval, _ = CASES[index]
return dict(line_key=f'also:{index+1}', source_type='also_cloud', source_id=index+1,
product_id=12, description='Microsoft abonnement\nPeriode: ' + interval,
quantity=quantity, unit_price=price, amount=amount,
discount_percentage=0, currency='DKK', meta={'billing_period': interval})
def test_norva_period_amounts_survive_request_model_and_catalog_snapshot():
total = Decimal(0)
product = dict(id=12, name='Microsoft abonnement', economic_product_number='MS-365',
economic_connection_id=1, economic_unit_number=1, status='active')
for index, (_, _, amount, _, days) in enumerate(CASES):
old = source(index)
before = deepcopy(old)
line = period_amount_line(old)
assert old == before
assert period_amount_line(line) == line
assert days + ' dage.' in line['description']
assert 'Reference:' not in line['description']
request_line = OrdreLineInput(**line).model_dump()
check_period_amounts([request_line], [old])
snapshot = line_snapshot(request_line, product, 1, 'DKK')
assert snapshot['quantity'] == '1.00'
assert snapshot['unit_price'] == amount
assert snapshot['net_total'] == amount
total += Decimal(snapshot['net_total'])
assert total == Decimal('15062.48')
def test_exact_customer_text_and_no_reference():
old = source()
old['description'] += '\nReference: sag-xx11.'
line = period_amount_line(old)
assert line['description'] == ('Microsoft abonnement\n116 licenser á 67,08 kr./måned. '
'Periode: 01.08.–28.08.2026, 28 af 31 dage.')
@pytest.mark.parametrize('interval,expected', [
('01.02.2024 - 01.03.2024', '29 af 29 dage.'),
('01.02.2026 - 01.03.2026', '28 af 28 dage.'),
('31.12.2026 - 02.01.2027', '31.12.2026–01.01.2027, 2 dage.'),
('31.08.2026 - 02.09.2026', '31.08.–01.09.2026, 2 dage.'),
])
def test_exclusive_period_end(interval, expected):
assert expected in _period_text(interval)
@pytest.mark.parametrize('changes', [dict(amount=None), dict(amount='NaN'), dict(amount='Infinity'),
dict(amount='-1'), dict(quantity=0), dict(quantity=None),
dict(unit_price=None), dict(discount_percentage=10)])
def test_invalid_source_blocks_instead_of_silent_fallback(changes):
with pytest.raises(HTTPException):
period_amount_line(dict(source(), **changes))
def test_zero_is_preserved():
line = period_amount_line(dict(source(), amount=0, unit_price=0))
assert line['unit_price'] == line['amount'] == 0
@pytest.mark.parametrize('changes', [dict(quantity=116), dict(unit_price=67.08),
dict(product_id=99), dict(currency='EUR'),
dict(discount_percentage=1), dict(source_type='manual'),
dict(description='changed')])
def test_stale_or_modified_export_is_blocked(changes):
line = dict(period_amount_line(source()), **changes)
with pytest.raises(HTTPException):
check_period_amounts([line], [source()])
def test_unsaved_also_line_cannot_bypass_approved_amount():
with pytest.raises(HTTPException):
check_period_amounts([period_amount_line(source())], [])
def test_other_line_types_are_unchanged():
line = dict(source(), source_type='manual')
assert period_amount_line(line) == line
check_period_amounts([line], [])
def test_repricing_skips_approved_also_period_amounts(monkeypatch):
from app.products.backend import economic_pricing
monkeypatch.setattr(economic_pricing, 'query', lambda *args, **kwargs: [])
assert economic_pricing.proposals({'id': 1, 'customer_id': 1, 'lines_json': [period_amount_line(source())]})['changes'] == []
@pytest.mark.parametrize('raw_line', [{}, {'Charge': '7028.25', 'Unit Price': '67.08'}])
def test_approval_stores_period_price_with_source_identity(monkeypatch, raw_line):
from app.modules.also.backend import service
raw = dict(id=1, import_job_id=5, matched_customer_id=8, matched_product_id=12,
matched_customer_name='Testkunde', product_name='Microsoft abonnement',
currency='DKK', billing_start='2026-08-01', billable_parameters=116,
unit_price=Decimal('67.08'), total_price=Decimal('7028.25'),
charge_interval='01.08.2026 - 29.08.2026', raw_line_json=raw_line)
writes = []
def query(sql, params=()):
if 'SELECT' in sql and 'FROM also_import_lines l' in sql:
return [raw]
writes.append((sql, params))
return []
def single(sql, params=()):
if 'INSERT INTO ordre_drafts' in sql:
writes.append((sql, params))
return {'id': 44}
return None
monkeypatch.setattr(service.AlsoService, '_assert_enabled', lambda self: None)
monkeypatch.setattr(service, 'execute_query', query)
monkeypatch.setattr(service, 'execute_query_single', single)
monkeypatch.setattr(service, 'with_invoice_fee', lambda lines, customer: lines)
result = service.AlsoService().approve_lines_to_drafts(5, [1], 3)
insert = next(params for sql, params in writes if 'INSERT INTO ordre_drafts' in sql)
line = json.loads(insert[2])[0]
assert line['quantity'] == 1
assert line['unit_price'] == line['amount'] == 7028.25
assert line['source_id'] == 1
assert line['meta']['also_license_quantity'] == '116'
assert line['meta']['also_monthly_unit_price'] == '67.08'
assert result['approved_lines'] == 1
def test_duplicate_source_line_cannot_double_the_charge():
line = period_amount_line(source())
with pytest.raises(HTTPException):
check_period_amounts([line, line], [source()])
def test_existing_pending_draft_is_presented_correctly_without_database_write(monkeypatch):
import asyncio
from app.core import database
from app.modules.orders.backend.router import get_ordre_draft
old = source()
row = dict(id=44, sync_status='pending', lines_json=[old], export_status_json={})
monkeypatch.setattr(database, 'execute_query_single', lambda *args: deepcopy(row))
def no_write(*args, **kwargs):
raise AssertionError('Reading a pending draft must not write to the database')
monkeypatch.setattr(database, 'execute_query', no_write)
result = asyncio.run(get_ordre_draft(44, None))
assert result['lines_json'][0]['quantity'] == 1
assert result['lines_json'][0]['unit_price'] == 7028.25
assert row['lines_json'][0] == old
@pytest.mark.parametrize('state', [dict(sync_status='exported', economic_order_number='123'),
dict(sync_status='uncertain'),
dict(sync_status='pending', export_idempotency_key='attempt-123')])
def test_exported_or_uncertain_history_is_not_rewritten(monkeypatch, state):
import asyncio
from app.core import database
from app.modules.orders.backend.router import get_ordre_draft
row = dict(id=44, lines_json=[source()], export_status_json={}, **state)
monkeypatch.setattr(database, 'execute_query_single', lambda *args: deepcopy(row))
result = asyncio.run(get_ordre_draft(44, None))
assert result['lines_json'] == row['lines_json']