diff --git a/http-api/app/meili.py b/http-api/app/meili.py new file mode 100644 index 0000000..2c79c6b --- /dev/null +++ b/http-api/app/meili.py @@ -0,0 +1,62 @@ +import re + +OPERATORS = [ + '!=', + '>=', + '<=', + '=', + '>', + '<', + 'TO', + 'EXISTS', + 'IN', + 'NOT IN', +] + + +def remove_quotes(value: str) -> str: + if (value.startswith('"') and value.endswith('"')) or ( + value.startswith("'") and value.endswith("'") + ): + return value[1:-1] + return value + + +def parse_condition(condition: str) -> dict[str, str] | None: + for op in OPERATORS: + parts = condition.split(op) + + if len(parts) == 2: + attr, value = parts + attr = attr.strip() + value = remove_quotes(value.strip()) + + return { + 'attr': attr, + 'op': op, + 'value': value, + } + + return None + + +def parse(s: str) -> list[dict]: + filter_expr = re.sub(r'\s+', ' ', s).strip() + + if filter_expr == '': + return [] + + parts = re.split(r'\b(?:AND|OR)\b', filter_expr) + conditions = [] + + for part in parts: + condition = parse_condition(part.strip()) + + if condition: + conditions.append(condition) + + return conditions + + +def encode(conditions: list[dict]): + return ' AND '.join(f'{c["attr"]} {c["op"]} {c["value"]}' for c in conditions if c) diff --git a/http-api/app/routes/orders/__init__.py b/http-api/app/routes/orders/__init__.py index b61ca6b..babec82 100644 --- a/http-api/app/routes/orders/__init__.py +++ b/http-api/app/routes/orders/__init__.py @@ -1,15 +1,14 @@ import urllib.parse as parse from aws_lambda_powertools.event_handler.api_gateway import Router -from aws_lambda_powertools.event_handler.exceptions import ( - NotFoundError, -) from layercake.dynamodb import ( DynamoDBPersistenceLayer, - KeyPair, + SortKey, + TransactKey, ) from meilisearch import Client as Meilisearch +import meili from boto3clients import dynamodb_client from config import MEILISEARCH_API_KEY, MEILISEARCH_HOST, ORDER_TABLE, USER_TABLE from middlewares import Tenant, TenantMiddleware @@ -35,10 +34,16 @@ def get_orders(): sort = event.get_query_string_value('sort', 'create_date:desc') page = int(event.get_query_string_value('page', '1')) hits_per_page = int(event.get_query_string_value('hitsPerPage', '25')) - filter_ = parse.unquote(event.get_query_string_value('filter', '')) + filter_ = meili.parse(parse.unquote(event.get_query_string_value('filter', ''))) if tenant.id != '*': - filter_ = f'tenant = {tenant.id}' + filter_ = [ + { + 'attr': 'tenant', + 'op': '=', + 'value': tenant.id, + }, + ] + filter_ return meili_client.index(ORDER_TABLE).search( query, @@ -47,7 +52,7 @@ def get_orders(): 'locales': ['pt'], 'page': page, 'hitsPerPage': hits_per_page, - 'filter': filter_, + 'filter': meili.encode(filter_), }, ) @@ -59,7 +64,11 @@ def get_orders(): summary='Get order', ) def get_order(id: str): - return order_layer.collection.get_item( - KeyPair(id, '0'), - exc_cls=NotFoundError, + return order_layer.collection.get_items( + TransactKey(id) + + SortKey('0') + + SortKey('items', path_spec='items') + + SortKey('address') + + SortKey('nfse', path_spec='nfse') + + SortKey('fees', path_spec='fees'), ) diff --git a/http-api/seeds/test-orders.jsonl b/http-api/seeds/test-orders.jsonl index 6d2236f..99eb9bc 100644 --- a/http-api/seeds/test-orders.jsonl +++ b/http-api/seeds/test-orders.jsonl @@ -4,7 +4,7 @@ {"sk": {"S": "items"}, "id": {"S": "XhAgiMsDG9rLhkN9urFru3"}, "items": {"L": [{"M": {"name": {"S": "NR-10 B\u00e1sico"}, "id": {"S": "38"}, "quantity": {"N": "1"}, "unit_price": {"N": "149"}}}]}} {"qrcode": {"S": "https://faturas.iugu.com/qr_code/d61cb3b6-da4d-48af-8497-4e2ed1f2f92f-27f9"}, "sk": {"S": "pix"}, "id": {"S": "XhAgiMsDG9rLhkN9urFru3"}, "qrcode_text": {"S": "00020101021226840014br.gov.bcb.pix2562qr.iugu.com/public/payload/v2/D61CB3B6DA4D48AF84974E2ED1F2F92F5204000053039865406149.005802BR5922MACIEL DOS SANTOS CIA6013FLORIANOPOLIS62070503***6304068E"}, "create_date": {"S": "2023-12-28T10:19:49.867940-03:00"}} {"sk": {"S": "user"}, "user_id": {"S": "c57MXV4BByFZLajcvBHdnN"}, "id": {"S": "XhAgiMsDG9rLhkN9urFru3"}, "create_date": {"S": "2023-12-28T10:20:54.229700-03:00"}} -{"payment_method": {"S": "CREDIT_CARD"}, "status": {"S": "PAID"}, "assignee": {"M": {"name": {"S": "Alessandra Larivia"}}}, "total": {"N": "149"}, "installments": {"N": "1"}, "due_date": {"S": "2024-02-08T08:46:59.771233-03:00"}, "email": {"S": "financeiro@aquanobile.com.br"}, "name": {"S": "AQUA NOBILE SERVI\u00c7OS LTDA"}, "create_date": {"S": "2024-02-08T08:41:59.773135-03:00"}, "payment_date": {"S": "2024-02-08T08:42:10.910170-03:00"}, "phone_number": {"S": "+553130705599"}, "sk": {"S": "0"}, "cnpj": {"S": "11278500000106"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "update_date": {"S": "2024-02-08T08:42:10.910170-03:00"}} +{"payment_method": {"S": "CREDIT_CARD"}, "status": {"S": "PAID"}, "assignee": {"M": {"name": {"S": "Alessandra Larivia"}}}, "total": {"N": "149"}, "installments": {"N": "1"}, "due_date": {"S": "2024-02-08T08:46:59.771233-03:00"}, "email": {"S": "financeiro@aquanobile.com.br"}, "name": {"S": "AQUA NOBILE SERVI\u00c7OS LTDA"}, "create_date": {"S": "2024-02-08T08:41:59.773135-03:00"}, "payment_date": {"S": "2024-02-08T08:42:10.910170-03:00"}, "phone_number": {"S": "+553130705599"}, "sk": {"S": "0"}, "cnpj": {"S": "11278500000106"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "update_date": {"S": "2024-02-08T08:42:10.910170-03:00"}, "tenant": {"S": "123"}} {"city": {"S": "Nova Lima"}, "postcode": {"S": "34006065"}, "neighborhood": {"S": "Vila da Serra"}, "complement": {"S": "sala 211"}, "sk": {"S": "address"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "street_number": {"S": "1021"}, "street": {"S": "Alameda Oscar Niemeyer"}, "state": {"S": "MG"}} {"last4": {"S": "6188"}, "brand": {"S": "Mastercard"}, "sk": {"S": "credit_card"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}} {"sk": {"S": "fees"}, "fees": {"N": "5"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "create_date": {"S": "2024-02-08T08:42:13.440374-03:00"}} @@ -25,7 +25,6 @@ {"city": {"S": "Marab\u00e1"}, "postcode": {"S": "68501535"}, "neighborhood": {"S": "Cidade Nova"}, "complement": {"S": ""}, "sk": {"S": "address"}, "id": {"S": "3Gkayh3FBuCSMW5eE5NfH5"}, "street_number": {"S": "S/N"}, "street": {"S": "Rua Ivete Vargas"}, "state": {"S": "PA"}} {"sk": {"S": "items"}, "id": {"S": "3Gkayh3FBuCSMW5eE5NfH5"}, "items": {"L": [{"M": {"name": {"S": "NR-10 Complementar (SEP)"}, "id": {"S": "55"}, "quantity": {"N": "6"}, "unit_price": {"N": "149"}}}]}} {"sk": {"S": "lock"}, "id": {"S": "3Gkayh3FBuCSMW5eE5NfH5"}, "lock_type": {"S": "CNPJ"}, "create_date": {"S": "2024-02-08T07:52:14.397071-03:00"}} -{"sk": {"S": "nfse"}, "nfse": {"S": "10382"}, "id": {"S": "3Gkayh3FBuCSMW5eE5NfH5"}, "create_date": {"S": "2024-02-08T07:52:14.361212-03:00"}} {"sk": {"S": "user"}, "user_id": {"S": "EkvQwpmmL6vzWtJunM5dCJ"}, "id": {"S": "3Gkayh3FBuCSMW5eE5NfH5"}, "create_date": {"S": "2024-02-08T07:48:56.154952-03:00"}} {"payment_method": {"S": "CREDIT_CARD"}, "status": {"S": "DECLINED"}, "assignee": {"M": {"name": {"S": "ALEXANDRE ROZIN"}}}, "total": {"N": "605"}, "installments": {"N": "2"}, "due_date": {"S": "2024-02-05T09:30:16.163998-03:00"}, "email": {"S": "alexandre@hartinst.com.br"}, "name": {"S": "HARTINST MANUTEN\u00c7\u00c3O INDUSTRIAL"}, "create_date": {"S": "2024-02-05T09:25:16.166315-03:00"}, "payment_date": {"NULL": true}, "phone_number": {"S": "+551936010400"}, "sk": {"S": "0"}, "cnpj": {"S": "31958303000145"}, "id": {"S": "mFERanksjRywrAniUdVmoV"}, "update_date": {"S": "2024-02-05T09:25:28.662366-03:00"}} {"city": {"S": "Americana"}, "postcode": {"S": "13467600"}, "neighborhood": {"S": "Parque Novo Mundo"}, "complement": {"S": ""}, "sk": {"S": "address"}, "id": {"S": "mFERanksjRywrAniUdVmoV"}, "street_number": {"S": "3929"}, "street": {"S": "Avenida de Cillo"}, "state": {"S": "SP"}} @@ -41,3 +40,4 @@ {"sk": {"S": "user"}, "user_id": {"S": "mESNbpk4pMTASxtnstd37B"}, "id": {"S": "bTRW6w69aYYKjKy3FZeVjt"}, "create_date": {"S": "2024-02-06T10:28:06.906367-03:00"}} {"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"},"sk": {"S": "0"},"assignee": {"M": {"name": {"S": "Sérgio Rafael de Siqueira"}}},"cnpj": {"S": "15608435000190"},"create_date": {"S": "2024-11-02T19:39:57.174669-03:00"},"due_date": {"S": "2024-11-03T19:39:09.897000-03:00"},"email": {"S": "sergio@somosbeta.com.br"},"name": {"S": "Beta Educação"},"payment_date": {"S": "2024-11-02T19:40:12.143000-03:00"},"payment_method": {"S": "MANUAL"},"status": {"S": "PAID"},"total": {"N": "10"},"update_date": {"S": "2024-11-02T19:40:12.831120-03:00"}} {"id": {"S": "QV4sXY3DvSTUMGJ4QqsrwJ"},"sk": {"S": "generated_items#7bzQhaPEB9uR8jRkzkFe2h"},"create_date": {"S": "2025-04-11T15:08:00.256341-03:00"},"status": {"S": "SUCCESS"}} +{"id": {"S": "123"}, "sk": {"S": "0"}} \ No newline at end of file diff --git a/http-api/template.yaml b/http-api/template.yaml index 80a9a2f..b1af4e3 100644 --- a/http-api/template.yaml +++ b/http-api/template.yaml @@ -40,7 +40,7 @@ Globals: COURSE_TABLE: !Ref CourseTable ELASTIC_CLOUD_ID: "{{resolve:ssm:/betaeducacao/elastic/cloud_id/str}}" ELASTIC_AUTH_PASS: "{{resolve:ssm:/betaeducacao/elastic/auth_pass/str}}" - KONVIVA_API_URL: https://saladeaula.digital + KONVIVA_API_URL: https://lms.saladeaula.digital KONVIVA_SECRET_KEY: "{{resolve:ssm:/betaeducacao/konviva/secret_key/str}}" MEILISEARCH_HOST: https://meili.eduseg.com.br MEILISEARCH_API_KEY: "{{resolve:ssm:/saladeaula/meili_api_key}}" diff --git a/http-api/tests/conftest.py b/http-api/tests/conftest.py index ddab1ba..ec2e4e5 100644 --- a/http-api/tests/conftest.py +++ b/http-api/tests/conftest.py @@ -21,6 +21,7 @@ def pytest_configure(): os.environ['USER_TABLE'] = PYTEST_TABLE_NAME os.environ['COURSE_TABLE'] = PYTEST_TABLE_NAME os.environ['ENROLLMENT_TABLE'] = PYTEST_TABLE_NAME + os.environ['ORDER_TABLE'] = PYTEST_TABLE_NAME @dataclass @@ -40,6 +41,7 @@ class HttpApiProxy: *, headers: dict = {}, auth_flow_type: str = 'USER_AUTH', + queryStringParameters: dict = {}, **kwargs, ) -> dict: return { @@ -49,10 +51,7 @@ class HttpApiProxy: 'rawQueryString': 'parameter1=value1¶meter1=value2¶meter2=value', 'cookies': ['cookie1', 'cookie2'], 'headers': headers, - 'queryStringParameters': { - 'parameter1': 'value1,value2', - 'parameter2': 'value', - }, + 'queryStringParameters': queryStringParameters, 'requestContext': { 'accountId': '123456789012', 'apiId': 'api-id', diff --git a/http-api/tests/routes/test_orders.py b/http-api/tests/routes/test_orders.py new file mode 100644 index 0000000..000f32f --- /dev/null +++ b/http-api/tests/routes/test_orders.py @@ -0,0 +1,48 @@ +import json +import urllib.parse as parse +from http import HTTPMethod, HTTPStatus + +from ..conftest import HttpApiProxy, LambdaContext + + +def test_orders( + mock_app, + dynamodb_seeds, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + # This data was added from seeds + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/orders', + queryStringParameters={ + 'filter': parse.quote('status = PENDING AND due_date >= 202025-07-09'), + }, + method=HTTPMethod.GET, + headers={'x-tenant': 'cJtK9SsnJhKPyxESe7g3DG'}, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.OK + + +def test_get_order( + mock_app, + dynamodb_seeds, + http_api_proxy: HttpApiProxy, + lambda_context: LambdaContext, +): + # This data was added from seeds + r = mock_app.lambda_handler( + http_api_proxy( + raw_path='/orders/KpZTYvu4RzgMJW3A2DF6cC', + method=HTTPMethod.GET, + ), + lambda_context, + ) + + assert r['statusCode'] == HTTPStatus.OK + + body = json.loads(r['body']) + print(body) diff --git a/http-api/tests/seeds.jsonl b/http-api/tests/seeds.jsonl index c03f26d..80e3991 100644 --- a/http-api/tests/seeds.jsonl +++ b/http-api/tests/seeds.jsonl @@ -22,4 +22,13 @@ {"id": {"S": "cpf"}, "sk": {"S": "07879819908"}} {"id": {"S": "cpf"}, "sk": {"S": "08679004901"}} {"id": {"S": "lock"}, "sk": {"S": "c2116a43f8f1aed659a10c83dab17ed3"}} -{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "3CNrFB9dy2RLit2pdeUWy4#8c9b55ef-e988-43ee-b2da-8594850605d7"}} \ No newline at end of file +{"id": {"S": "vacancies#cJtK9SsnJhKPyxESe7g3DG"}, "sk": {"S": "3CNrFB9dy2RLit2pdeUWy4#8c9b55ef-e988-43ee-b2da-8594850605d7"}} +{"payment_method": {"S": "CREDIT_CARD"}, "status": {"S": "PAID"}, "assignee": {"M": {"name": {"S": "Alessandra Larivia"}}}, "total": {"N": "149"}, "installments": {"N": "1"}, "due_date": {"S": "2024-02-08T08:46:59.771233-03:00"}, "email": {"S": "financeiro@aquanobile.com.br"}, "name": {"S": "AQUA NOBILE SERVI\u00c7OS LTDA"}, "create_date": {"S": "2024-02-08T08:41:59.773135-03:00"}, "payment_date": {"S": "2024-02-08T08:42:10.910170-03:00"}, "phone_number": {"S": "+553130705599"}, "sk": {"S": "0"}, "cnpj": {"S": "11278500000106"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "update_date": {"S": "2024-02-08T08:42:10.910170-03:00"}, "tenant": {"S": "cJtK9SsnJhKPyxESe7g3DG"}} +{"city": {"S": "Nova Lima"}, "postcode": {"S": "34006065"}, "neighborhood": {"S": "Vila da Serra"}, "complement": {"S": "sala 211"}, "sk": {"S": "address"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "street_number": {"S": "1021"}, "street": {"S": "Alameda Oscar Niemeyer"}, "state": {"S": "MG"}} +{"last4": {"S": "6188"}, "brand": {"S": "Mastercard"}, "sk": {"S": "credit_card"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}} +{"sk": {"S": "fees"}, "fees": {"N": "5"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "create_date": {"S": "2024-02-08T08:42:13.440374-03:00"}} +{"sk": {"S": "invoice"}, "pdf": {"S": "https://faturas.iugu.com/cdc228a0-bd1a-44aa-aa0d-1b47bad74aed-873a.pdf"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "create_date": {"S": "2024-02-08T08:42:03.041809-03:00"}, "invoice_id": {"S": "cdc228a0-bd1a-44aa-aa0d-1b47bad74aed-873a"}} +{"sk": {"S": "items"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "items": {"L": [{"M": {"name": {"S": "NR-33 Supervisor em Espa\u00e7o Confinado"}, "id": {"S": "57"}, "quantity": {"N": "1"}, "unit_price": {"N": "149"}}}]}} +{"sk": {"S": "lock"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "lock_type": {"S": "CNPJ"}, "create_date": {"S": "2024-02-08T08:42:13.058916-03:00"}} +{"sk": {"S": "nfse"}, "nfse": {"S": "10384"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "create_date": {"S": "2024-02-08T09:05:03.879692-03:00"}} +{"sk": {"S": "user"}, "user_id": {"S": "5AZXXXCWa2bU4spsxfLznx"}, "id": {"S": "KpZTYvu4RzgMJW3A2DF6cC"}, "create_date": {"S": "2024-02-08T08:42:05.190415-03:00"}} diff --git a/http-api/tests/test_filter.py b/http-api/tests/test_filter.py new file mode 100644 index 0000000..7c67350 --- /dev/null +++ b/http-api/tests/test_filter.py @@ -0,0 +1,9 @@ +import json +import urllib.parse as parse + + +def test_filter(): + f = '[{%22attr%22%3A%22status%22%2C%22op%22%3A%22%3D%22%2C%22value%22%3A%22PENDING%22}]' + decoded = parse.unquote(f) + filtros = json.loads(decoded) + print(filtros) diff --git a/http-api/tests/test_meili.py b/http-api/tests/test_meili.py new file mode 100644 index 0000000..4d511b5 --- /dev/null +++ b/http-api/tests/test_meili.py @@ -0,0 +1,11 @@ +import meili + + +def test_parse(): + s = 'payment_date >= 2025-07-01 AND status = PAID' + r = meili.parse(s) + + assert r == [ + {'attr': 'payment_date', 'op': '>=', 'value': '2025-07-01'}, + {'attr': 'status', 'op': '=', 'value': 'PAID'}, + ]