Commit 0dab1404 authored by Pavel Kuzmenko's avatar Pavel Kuzmenko

Merge branch 'kpn/5/endpoint_for_multiple_auctions' into 'master'

feat: #5 Add endpoint for multiple auctions

See merge request !26
parents 84b916fa 6947f445
Pipeline #22018 failed with stages
in 1 minute and 7 seconds
[flake8]
max-line-length = 120
\ No newline at end of file
max-line-length = 120
ignore = D, F
\ No newline at end of file
......@@ -9,4 +9,5 @@ coverage
pytest
pytest-aiohttp
pytest-cov
requests-mock
\ No newline at end of file
requests-mock
aioresponses
\ No newline at end of file
......@@ -4,7 +4,7 @@ from aiohttp import web
import prozorro_sale
from prozorro_sale.billing_service.data_calculator import Calculator
from prozorro_sale.billing_service.utils import get_procedure
from prozorro_sale.billing_service.utils import get_procedure, get_procedures_by_date, get_administrators
SWAGGER_DOC_AVAILABLE = os.getenv('SWAGGER_DOC', False)
......@@ -59,6 +59,34 @@ async def get_account_data(request: web.Request) -> web.Response:
"""
auction_id = request.match_info['auction_id']
date_contract = request.query.get('date_contract')
data_set = get_procedure(auction_id)
data_set = await get_procedure(auction_id)
calculator = Calculator(data_set, date_contract)
return web.json_response(calculator())
async def get_account_data_from_date(request: web.Request) -> web.Response:
"""
---
description: Endpoint in which you can transfer the date and correct the data
according to the wine city administrator of the ETS from the last auctions,
which were carried out for the specified date.
tags:
- auction
parameters:
- in: path
name: date
required: true
schema:
type: string
example:
"2020-03-16T09:00:00+00:00"
produces:
- application/json
responses:
"200":
description: successful operation. Return json
"""
date = request.match_info['date']
data_list = await get_procedures_by_date(date)
resp = get_administrators(data_list)
return web.json_response(resp)
......@@ -41,8 +41,8 @@ class Calculator:
def set_up(self) -> None:
self.calculate_codes = self.current_config['constants'] \
+ self.current_config['variables'] \
+ self.current_config['procedures']
+ self.current_config['variables'] \
+ self.current_config['procedures']
self.calculate_codes.sort()
self.glob['procedure'] = self.procedure
......@@ -106,6 +106,7 @@ class Calculator:
data[var_code.name]['amount'] = value
data[var_code.name]['description'] = var_code.description
data[var_code.name]['purposeDescription'] = var_code.purposeDescription
if self.current_config['exceptions']:
data['needManualCheck'] = True
......
......@@ -110,13 +110,15 @@ class BaseCode:
class CodeFunction(BaseCode):
__slots__ = ['name', 'value', 'description', 'minValue', 'valueAddedTaxIncluded']
__slots__ = ['name', 'value', 'description', 'minValue', 'valueAddedTaxIncluded', 'purposeDescription']
def __init__(self, name, value, description=None, min_value=None, value_added_tax_included=None):
def __init__(self, name, value, description=None, min_value=None, value_added_tax_included=None,
purposeDescription=None):
super(CodeFunction, self).__init__(name, value)
self.description = description
self.minValue = min_value
self.valueAddedTaxIncluded = value_added_tax_included
self.purposeDescription = purposeDescription
def __eq__(self, other):
if str(self.name) not in str(other.value) and str(other.name) not in str(self.value):
......@@ -267,6 +269,7 @@ class ProceduresVariables(list):
value=val['value'],
description=val['description'],
min_value=val.get('minValue'),
value_added_tax_included=val.get('valueAddedTaxIncluded')
value_added_tax_included=val.get('valueAddedTaxIncluded'),
purposeDescription=val.get('purposeDescription'),
)
)
......@@ -13,8 +13,8 @@ class BillingException(Exception):
class ProcedureNotFound(BillingException):
def __init__(self, object_id: str):
super().__init__(f'Procedure with auctionId {object_id} not found')
def __init__(self, _type: str, _value: str):
super().__init__(f'Procedure with {_type}: {_value} not found')
class WrongProcedureType(BillingException):
......@@ -28,9 +28,9 @@ class BrokenConfig(BillingException):
class RemoteServiceBroken(BillingException):
def __init__(self, service: str, object_id: str):
LOG.exception(f'Cannot get data with id {object_id} from {service}')
super().__init__(f'Cannot get data with id {object_id} from {service}')
def __init__(self, service: str, _type: str, _value: str):
LOG.exception(f'Cannot get data by {_type}: {_value} from {service}')
super().__init__(f'Cannot get data by {_type}: {_value} from {service}')
ERROR_DICT = {
......
from aiohttp import web, hdrs
from .api import version, ping, get_account_data
from .api import version, ping, get_account_data, get_account_data_from_date
def init_routes(app: web.Application) -> None:
......@@ -8,3 +8,5 @@ def init_routes(app: web.Application) -> None:
add_route(hdrs.METH_GET, '/api', version, name="version")
add_route(hdrs.METH_GET, '/api/ping', ping, name="ping")
add_route(hdrs.METH_GET, '/api/billing/accountData/{auction_id}', get_account_data, name="account_data")
add_route(hdrs.METH_GET, '/api/billing/accountData/fromDate/{date}', get_account_data_from_date,
name="account_data_from_date")
import os
from requests import get
import aiohttp
from prozorro_sale.billing_service import errors
from prozorro_sale.billing_service.data_calculator import Calculator
SEARCH_URL = f"http://{os.environ.get('SEARCH_URL')}"
def get_procedure(auction_id: str) -> dict:
resp = get(f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}')
if 400 <= resp.status_code < 500:
raise errors.ProcedureNotFound(auction_id)
elif resp.status_code >= 500:
raise errors.RemoteServiceBroken('search', auction_id)
return resp.json()
async def get_procedure(auction_id: str) -> dict:
async with aiohttp.ClientSession() as session:
resp = await session.get(f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}')
if 400 <= resp.status < 500:
raise errors.ProcedureNotFound('auctionId', auction_id)
elif resp.status >= 500:
raise errors.RemoteServiceBroken('search', 'auctionId', auction_id)
return await resp.json()
async def get_procedures_by_date(date) -> list:
async with aiohttp.ClientSession() as session:
resp = await session.get(f'{SEARCH_URL}/api/search/byDateModified/{date}')
if resp.status == 204 or 400 <= resp.status < 500:
raise errors.ProcedureNotFound('dateModified', date)
elif resp.status >= 500:
raise errors.RemoteServiceBroken('search', 'dateModified', date)
return await resp.json()
def get_administrators(data_list: list) -> dict:
admin_data = dict()
for data_set in data_list:
if admin_fee := Calculator(data_set)().get('administratorFee'):
data = {
'ownerName': data_set['owner'],
'amount': admin_fee.get('amount', None),
}
purpose = admin_fee.get('purposeDescription', '')
if purpose:
purpose = purpose.format(auctionId=data_set['auctionId'])
data['purposeDescription'] = purpose
if with_wat := admin_fee.get('withVAT'):
data['withVAT'] = with_wat
if need_manual := admin_fee.get('needManualCheck'):
data['needManualCheck'] = need_manual
admin_data[data_set['auctionId']] = data
return admin_data
......@@ -2,6 +2,7 @@ import asyncio
import json
import pathlib
from contextlib import contextmanager
from aioresponses import aioresponses
import pytest
from aiotask_context import task_factory
......@@ -88,3 +89,9 @@ def config_file():
@contextmanager
def does_not_raise():
yield
@pytest.fixture
def mock_aioresponse():
with aioresponses() as m:
yield m
from unittest.mock import patch
from unittest.mock import patch, MagicMock
import pytest
from requests_mock import Mocker
from prozorro_sale.billing_service.utils import SEARCH_URL
from aiohttp import test_utils
from prozorro_sale.billing_service import errors
def mock_get_procedure(_type, status, value):
if status in [400, 402, 422]:
return errors.ProcedureNotFound(_type, value)
if status in [500, 503]:
return errors.RemoteServiceBroken('search', _type, value)
class TestApi:
......@@ -37,8 +45,8 @@ class TestApi:
async def test_account_data(self, client, procedures, procedure_name, keys):
procedure_obj = procedures[procedure_name]
auction_id = procedure_obj['auctionId']
with Mocker(real_http=True) as m:
m.register_uri('GET', f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', json=procedure_obj)
with patch('prozorro_sale.billing_service.api.get_procedure',
return_value=procedure_obj):
resp = await client.get(f'/api/billing/accountData/{auction_id}')
assert resp.status == 200
obj_data = await resp.json()
......@@ -53,13 +61,12 @@ class TestApi:
procedure_obj = procedures['basicSell']
procedure_obj['datePublished'] = "1961-04-12T06:07:00.790000Z"
auction_id = procedure_obj['auctionId']
with Mocker(real_http=True) as m:
m.register_uri('GET', f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', json=procedure_obj)
with patch('prozorro_sale.billing_service.api.get_procedure',
return_value=procedure_obj):
resp = await client.get(f'/api/billing/accountData/{auction_id}')
assert resp.status == 422
@pytest.mark.parametrize("status, resp_status", [
(200, 200),
(400, 404),
(422, 404),
(500, 424),
......@@ -68,12 +75,26 @@ class TestApi:
async def test_get_procedure_errors(self, client, procedures, status, resp_status):
procedure_obj = procedures['timber']
auction_id = procedure_obj['auctionId']
with Mocker(real_http=True) as m:
m.register_uri('GET', f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}',
json=procedure_obj, status_code=status)
with patch('prozorro_sale.billing_service.api.get_procedure',
side_effect=mock_get_procedure('auctionId', status, auction_id)):
resp = await client.get(f'/api/billing/accountData/{auction_id}')
assert resp.status == resp_status
@pytest.mark.parametrize("status, resp_status", [
(400, 404),
(402, 404),
(422, 404),
(500, 424),
(503, 424)
])
async def test_get_procedures_by_date_errors(self, client, procedures, status, resp_status):
procedure_obj = procedures['timber']
dateModified = procedure_obj['dateModified']
with patch('prozorro_sale.billing_service.api.get_procedures_by_date',
side_effect=mock_get_procedure('dateModified', status, dateModified)):
resp = await client.get(f'/api/billing/accountData/fromDate/{dateModified}')
assert resp.status == resp_status
async def test_global_exception(self, client, procedures):
procedure_obj = procedures['basicSell']
auction_id = procedure_obj['auctionId']
......
import pytest
from requests_mock import Mocker
import re
from prozorro_sale.billing_service.data_calculator import Calculator
......@@ -20,43 +19,41 @@ class TestCalculator:
('railwayCargo', ['operatorFee']),
('subsoil', ['guaranteeFee', 'operatorFee'])
])
async def test_calculator(self, procedures, config_file, procedure_name, with_vat_fields):
async def test_calculator(self, mock_aioresponse, procedures, config_file, procedure_name, with_vat_fields):
procedure_obj = procedures[procedure_name]
auction_id = procedure_obj['auctionId']
with Mocker(real_http=True) as m:
m.register_uri('GET', f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', json=procedure_obj)
await Calculator.load_config(config_file())
data_set = get_procedure(auction_id)
calculator = Calculator(data_set)
data = calculator()
assert 'auctionId' in data
for field, val in data.items():
if isinstance(val, dict):
if field in with_vat_fields:
assert 'withVAT' in val
else:
assert 'withVAT' not in val
assert 'amount' in val
assert 'description' in val
amount = val.get('amount')
assert amount is not None
assert re.match(amount_pattern, str(amount))
mock_aioresponse.get(f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', payload=procedure_obj)
await Calculator.load_config(config_file())
data_set = await get_procedure(auction_id)
calculator = Calculator(data_set)
data = calculator()
assert 'auctionId' in data
for field, val in data.items():
if isinstance(val, dict):
if field in with_vat_fields:
assert 'withVAT' in val
else:
assert 'withVAT' not in val
assert 'amount' in val
assert 'description' in val
amount = val.get('amount')
assert amount is not None
assert re.match(amount_pattern, str(amount))
async def test_check_rule_exceptions(self, procedures, config_file):
async def test_check_rule_exceptions(self, mock_aioresponse, procedures, config_file):
procedure_obj = procedures['basicSell']
auction_id = procedure_obj['auctionId']
with Mocker(real_http=True) as m:
m.register_uri('GET', f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', json=procedure_obj)
await Calculator.load_config(config_file())
rules = ['mask', 'value', 'bottom', 'top']
for indx, val in enumerate(rules):
v = '1234563378'
if val == 'top':
v = '1234563379'
if indx > 0:
del Calculator.config.exceptions['basicSell'][0][rules[indx - 1]]
Calculator.config.exceptions['basicSell'][0][val] = v
data_set = get_procedure(auction_id)
calculator = Calculator(data_set)
data = calculator()
assert 'needManualCheck' in data
mock_aioresponse.get(f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', payload=procedure_obj)
data_set = await get_procedure(auction_id)
await Calculator.load_config(config_file())
rules = ['mask', 'value', 'bottom', 'top']
for indx, val in enumerate(rules):
v = '1234563378'
if val == 'top':
v = '1234563379'
if indx > 0:
del Calculator.config.exceptions['basicSell'][0][rules[indx - 1]]
Calculator.config.exceptions['basicSell'][0][val] = v
calculator = Calculator(data_set)
data = calculator()
assert 'needManualCheck' in data
import pytest
from box import Box
from requests_mock import Mocker
from prozorro_sale.billing_service import errors
from prozorro_sale.billing_service.utils import get_procedure, SEARCH_URL
from prozorro_sale.billing_service.utils import get_procedure, get_procedures_by_date, SEARCH_URL, get_administrators
from test.conftest import does_not_raise
......@@ -16,30 +15,84 @@ class TestUtils:
'basicSell-cancelled',
'legitimatePropertyLease-complete'
])
async def test_get_procedure(self, procedures, procedure_name):
async def test_get_procedure(self, mock_aioresponse, procedures, procedure_name):
procedure_obj = procedures[procedure_name]
auction_id = procedure_obj['auctionId']
with Mocker(real_http=True) as m:
m.register_uri('GET', f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', json=procedure_obj)
procedure = get_procedure(auction_id)
mock_aioresponse.get(f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}', payload=procedure_obj)
procedure = await get_procedure(auction_id)
assert procedure['_id'] == procedure_obj['_id']
@pytest.mark.parametrize("status, expected", [
(200, does_not_raise()),
(400, pytest.raises(errors.ProcedureNotFound)),
(422, pytest.raises(errors.ProcedureNotFound)),
(500, pytest.raises(errors.RemoteServiceBroken)),
(503, pytest.raises(errors.RemoteServiceBroken))
])
async def test_get_procedure_errors(self, mock_aioresponse, procedures, status, expected):
procedure_obj = procedures['timber']
auction_id = procedure_obj['auctionId']
mock_aioresponse.get(f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}',
payload=procedure_obj, status=status)
with expected:
procedure = await get_procedure(auction_id)
assert procedure['_id'] == procedure_obj['_id']
@pytest.mark.parametrize("procedure_name", [
'timber',
'timber-active_qualification',
'timber-unsuccessful',
'basicSell',
'basicSell-cancelled',
'legitimatePropertyLease-complete'
])
async def test_get_procedures_by_date(self, mock_aioresponse, procedures, procedure_name):
procedure_obj = procedures[procedure_name]
dateModified = procedure_obj['dateModified']
mock_aioresponse.get(f'{SEARCH_URL}/api/search/byDateModified/{dateModified}',
payload=[procedure_obj])
procedure = await get_procedures_by_date(dateModified)
assert procedure[0]['_id'] == procedure_obj['_id']
@pytest.mark.parametrize("status, expected", [
(200, does_not_raise()),
(400, pytest.raises(errors.ProcedureNotFound)),
(204, pytest.raises(errors.ProcedureNotFound)),
(422, pytest.raises(errors.ProcedureNotFound)),
(500, pytest.raises(errors.RemoteServiceBroken)),
(503, pytest.raises(errors.RemoteServiceBroken))
])
async def test_get_procedure_errors(self, procedures, status, expected):
async def test_get_procedures_by_date_errors(self, mock_aioresponse, procedures, status, expected):
procedure_obj = procedures['timber']
dateModified = procedure_obj['dateModified']
mock_aioresponse.get(f'{SEARCH_URL}/api/search/byDateModified/{dateModified}',
payload=[procedure_obj], status=status)
with expected:
procedure = await get_procedures_by_date(dateModified)
assert procedure[0]['_id'] == procedure_obj['_id']
@pytest.mark.parametrize("procedure_name, is_admin_fee, purposeDescription", [
('timber', True, False),
('timber-active_qualification', False, False),
('timber-unsuccessful', False, False),
('basicSell', True, False),
('basicSell-cancelled', False, False),
('legitimatePropertyLease-complete', True, False),
('railwayCargo', True, True),
('subsoil', True, False),
])
async def test_account_data_from_date(self, procedures, procedure_name, is_admin_fee,
purposeDescription):
procedure_obj = procedures[procedure_name]
auction_id = procedure_obj['auctionId']
with Mocker(real_http=True) as m:
m.register_uri('GET', f'{SEARCH_URL}/api/search/byAuctionId/{auction_id}',
json=procedure_obj, status_code=status)
with expected:
procedure = get_procedure(auction_id)
assert procedure['_id'] == procedure_obj['_id']
data = get_administrators([procedure_obj])
if is_admin_fee:
assert data[auction_id]
assert data[auction_id]['amount']
assert data[auction_id]['ownerName'] == procedure_obj['owner']
assert bool(data[auction_id].get('purposeDescription')) == purposeDescription
else:
assert not data
async def test_box_dict(self):
test_dict = {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment