diff --git a/.github/workflows/openmis-module-test.yml b/.github/workflows/openmis-module-test.yml new file mode 100644 index 0000000..6e6b63b --- /dev/null +++ b/.github/workflows/openmis-module-test.yml @@ -0,0 +1,100 @@ +name: Automated CI testing +# This workflow run automatically for every commit on github it checks the syntax and launch the tests. +# | grep . | uniq -c filters out empty lines and then groups consecutive lines together with the number of occurrences +on: + pull_request: + workflow_dispatch: + inputs: + comment: + description: Just a simple comment to know the purpose of the manual build + required: false + +jobs: + run_test: + runs-on: ubuntu-latest + services: + mssql: + image: mcr.microsoft.com/mssql/server:2017-latest + env: + ACCEPT_EULA: Y + SA_PASSWORD: GitHub999 + ports: + - 1433:1433 + # needed because the mssql container does not provide a health check + options: --health-interval=10s --health-timeout=3s --health-start-period=10s --health-retries=10 --health-cmd="/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P ${SA_PASSWORD} -Q 'SELECT 1' || exit 1" + + steps: + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: install linux packages + run: | + wget https://raw.githubusercontent.com/openimis/database_ms_sqlserver/main/Empty%20databases/openIMIS_ONLINE.sql -O openIMIS_ONLINE.sql + wget https://raw.githubusercontent.com/openimis/database_ms_sqlserver/main/Demo%20database/openIMIS_demo_ONLINE.sql -O openIMIS_demo_ONLINE.sql + curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add - + curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/msprod.list + sudo apt-get update + sudo ACCEPT_EULA=Y apt-get install -y mssql-tools build-essential dialog apt-utils unixodbc-dev -y + python -m pip install --upgrade pip + - name: pull openimis backend + run: | + rm ./openimis -rf + git clone --depth 1 --branch develop https://github.com/openimis/openimis-be_py.git ./openimis + - name: copy current branch + uses: actions/checkout@v2 + with: + path: './current-module' + - name: Update the configuration + working-directory: ./openimis + run: | + export MODULE_NAME="$(echo $GITHUB_REPOSITORY | sed 's#^openimis/openimis-be-\(.*\)_py$#\1#')" + echo "the local module called $MODULE_NAME will be injected in openIMIS .json" + jq --arg name "$MODULE_NAME" 'if [.modules[].name == ($name)]| max then (.modules[] | select(.name == ($name)) | .pip)|="../current-module" else .modules |= .+ [{name:($name), pip:"../current-module"}] end' openimis.json + echo $(jq --arg name "$MODULE_NAME" 'if [.modules[].name == ($name)]| max then (.modules[] | select(.name == ($name)) | .pip)|="../current-module" else .modules |= .+ [{name:($name), pip:"../current-module"}] end' openimis.json) > openimis.json + - name: Install openIMIS Python dependencies + working-directory: ./openimis + run: | + pip install -r requirements.txt + python modules-requirements.py openimis.json > modules-requirements.txt + cat modules-requirements.txt + pip install -r modules-requirements.txt + - name: Environment info + working-directory: ./openimis + run: | + pip list + - name: Initialize DB + run: | + /opt/mssql-tools/bin/sqlcmd -S localhost,1433 -U SA -P $SA_PASSWORD -Q 'DROP DATABASE IF EXISTS imis' + /opt/mssql-tools/bin/sqlcmd -S localhost,1433 -U SA -P $SA_PASSWORD -Q 'CREATE DATABASE imis' + /opt/mssql-tools/bin/sqlcmd -S localhost,1433 -U SA -P $SA_PASSWORD -d imis -i openIMIS_ONLINE.sql | grep . | uniq -c + /opt/mssql-tools/bin/sqlcmd -S localhost,1433 -U SA -P $SA_PASSWORD -d imis -i openIMIS_demo_ONLINE.sql | grep . | uniq -c + env: + SA_PASSWORD: GitHub999 + ACCEPT_EULA: Y + +# - name: Check formatting with black +# run: | +# black --check . + + - name: Django tests + working-directory: ./openimis/openIMIS + run: | + export MODULE_NAME="$(echo $GITHUB_REPOSITORY | sed 's#^openimis/openimis-be-\(.*\)_py$#\1#')" + python -V + ls -l + python manage.py migrate + python init_test_db.py | grep . | uniq -c + python manage.py test --keepdb $MODULE_NAME + env: + SECRET_KEY: secret + DEBUG: true + #DJANGO_SETTINGS_MODULE: hat.settings + DB_HOST: localhost + DB_PORT: 1433 + DB_NAME: imis + DB_USER: sa + DB_PASSWORD: GitHub999 + #DEV_SERVER: true + SITE_ROOT: api + REMOTE_USER_AUTHENTICATION: True diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 3bd2eb8..4b00014 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -13,6 +13,8 @@ jobs: runs-on: ubuntu-latest steps: + - uses: olegtarasov/get-tag@v2.1 + id: tagName - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 @@ -21,11 +23,16 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install setuptools wheel twine + pip install setuptools wheel twine jq + + - name: update setup.py + run: | + echo "tag to use $GIT_TAG_NAME" + sed -i "s/version='.*'/version='$GIT_TAG_NAME'/g" setup.py - name: Build and publish env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{secrets.PYPI_TOKEN}} run: | python setup.py sdist bdist_wheel - twine upload dist/* \ No newline at end of file + twine upload dist/* diff --git a/policy/schema.py b/policy/schema.py index ed7fc49..5be7016 100644 --- a/policy/schema.py +++ b/policy/schema.py @@ -1,3 +1,7 @@ +from core.schema import ( + OrderedDjangoFilterConnectionField, + signal_mutation_module_validate, +) import graphene from django.core.exceptions import PermissionDenied from django.db.models import Prefetch @@ -9,7 +13,7 @@ from django.utils.translation import gettext as _ import graphene_django_optimizer as gql_optimizer from graphene_django.filter import DjangoFilterConnectionField -from core.schema import signal_mutation_module_validate, OrderedDjangoFilterConnectionField +from core.models import Officer from .models import PolicyMutation from product.models import Product from contribution.models import Premium @@ -78,7 +82,9 @@ class Query(graphene.ObjectType): chfId=graphene.String(required=True), serviceCode=graphene.String(required=True), ) - policy_officers = DjangoFilterConnectionField(OfficerGQLType) + policy_officers = DjangoFilterConnectionField( + OfficerGQLType, search=graphene.String() + ) def resolve_policy_values(self, info, **kwargs): product = Product.objects.get(id=kwargs.get('product_id')) @@ -245,10 +251,22 @@ def resolve_policy_service_eligibility_by_insuree(self, info, **kwargs): req=req ) - def resolve_policy_officers(self, info, **kwargs): - if not info.context.user.has_perms(PolicyConfig.gql_query_policy_officers_perms): + def resolve_policy_officers(self, info, search=None, **kwargs): + if not info.context.user.has_perms( + PolicyConfig.gql_query_policy_officers_perms + ): raise PermissionDenied(_("unauthorized")) + qs = Officer.objects + if search is not None: + qs = qs.filter( + Q(code__icontains=search) + | Q(last_name__icontains=search) + | Q(other_names__icontains=search) + ) + + return qs + class Mutation(graphene.ObjectType): create_policy = CreatePolicyMutation.Field() diff --git a/policy/services.py b/policy/services.py index 7f9f58d..07809f7 100644 --- a/policy/services.py +++ b/policy/services.py @@ -105,6 +105,33 @@ def __init__(self, user): @staticmethod def _to_item(row): + ceiling = None + ceiling_ip = None + ceiling_op = None + if row.product.max_treatment: + ceiling = row.product.max_treatment + if row.product.max_ip_treatment: + ceiling_ip = row.product.max_ip_treatment + if row.product.max_op_treatment: + ceiling_op = row.product.max_ip_treatment + if row.product.max_insuree: + ceiling = row.product.max_insuree - (row.total_rem_g if row.total_rem_g else 0) + else: + if row.product.max_ip_insuree: + ceiling_ip = row.product.max_ip_insuree - (row.total_rem_ip if row.total_rem_ip else 0) + if row.product.max_op_insuree: + ceiling_op = row.product.max_op_insuree - (row.total_rem_op if row.total_rem_op else 0) + if row.product.max_policy: + ceiling = row.product.max_policy - (row.total_rem_g if row.total_rem_g else 0) + else: + if row.product.max_ip_policy: + ceiling_ip = row.product.max_ip_policy - (row.total_rem_ip if row.total_rem_ip else 0) + if row.product.max_op_policy: + ceiling_op = row.product.max_op_policy - (row.total_rem_op if row.total_rem_op else 0) + balance = row.value + if row.total_ded_g: + balance -= row.total_ded_g + return ByFamilyOrInsureeResponseItem( policy_id=row.id, policy_uuid=row.uuid, @@ -121,10 +148,10 @@ def _to_item(row): ded=row.total_ded_g, ded_in_patient=row.total_ded_ip, ded_out_patient=row.total_ded_op, - ceiling=0, # TODO: product.xxx - ceiling_in_patient=0, # TODO: product.xxx - ceiling_out_patient=0, # TODO: product.xxx - balance=0, # TODO: nullsafe calculation from value,... + ceiling=ceiling, + ceiling_in_patient=ceiling_ip, + ceiling_out_patient=ceiling_op, + balance=balance, validity_from=row.validity_from, validity_to=row.validity_to ) @@ -318,6 +345,14 @@ def str_none(x): self.is_item_ok == other.is_item_ok and self.is_service_ok == other.is_service_ok) + def __str__(self): + return f"Eligibility for {self.eligibility_request} gave product {self.prod_id} " \ + f"with item/svc ok {self.is_item_ok}/{self.is_service_ok} " \ + f" left: {self.item_left}/{self.service_left}" + + def __repr__(self): + return self.__str__() + signal_eligibility_service_before = dispatch.Signal(providing_args=["user", "request", "response"]) signal_eligibility_service_after = dispatch.Signal(providing_args=["user", "request", "response"]) @@ -396,21 +431,21 @@ def request(self, req, response): return EligibilityResponse( eligibility_request=req, prod_id=prod_id or None, - total_admissions_left=total_admissions_left or 0, - total_visits_left=total_visits_left or 0, - total_consultations_left=total_consultations_left or 0, - total_surgeries_left=total_surgeries_left or 0, - total_deliveries_left=total_deliveries_left or 0, - total_antenatal_left=total_antenatal_left or 0, - consultation_amount_left=consultation_amount_left or 0.0, - surgery_amount_left=surgery_amount_left or 0.0, - delivery_amount_left=delivery_amount_left or 0.0, - hospitalization_amount_left=hospitalization_amount_left or 0.0, - antenatal_amount_left=antenatal_amount_left or 0.0, + total_admissions_left=total_admissions_left, + total_visits_left=total_visits_left, + total_consultations_left=total_consultations_left, + total_surgeries_left=total_surgeries_left, + total_deliveries_left=total_deliveries_left, + total_antenatal_left=total_antenatal_left, + consultation_amount_left=consultation_amount_left, + surgery_amount_left=surgery_amount_left, + delivery_amount_left=delivery_amount_left, + hospitalization_amount_left=hospitalization_amount_left, + antenatal_amount_left=antenatal_amount_left, min_date_service=min_date_service, min_date_item=min_date_item, - service_left=service_left or 0, - item_left=item_left or 0, + service_left=service_left, + item_left=item_left, is_item_ok=is_item_ok is True, is_service_ok=is_service_ok is True ) @@ -456,9 +491,9 @@ def request(self, req, response): "policy__product_id", waiting_period=F(waiting_period_field), limit_no=F(limit_field))\ - .annotate(min_date=MonthsAdd(waiting_period_field, "effective_date"))\ + .annotate(min_date=MonthsAdd(Coalesce(F(waiting_period_field), 0), "effective_date"))\ .annotate(services_count=Count("policy__product__products__service_id"))\ - .annotate(services_left=Coalesce("limit_no", Value(0)) - F("services_count")) + .annotate(services_left=F("limit_no") - F("services_count")) min_date_qs = queryset_svc.aggregate( min_date_lte=Min("min_date", filter=Q(min_date__lte=now)), @@ -480,7 +515,7 @@ def request(self, req, response): services_left = None min_date_service = None eligibility.min_date_service = min_date_service - eligibility.service_left = services_left or 0 + eligibility.service_left = services_left # TODO remove code duplication between service and item if req.item_code: @@ -491,7 +526,7 @@ def request(self, req, response): waiting_period_field = "policy__product__items__waiting_period_child" limit_field = "policy__product__items__limit_no_child" - item = Item.get_queryset(None, self.user).get(code__iexact=req.item_code) # TODO validity is checked but should be optional in get_queryset + item = Item.get_queryset(None, self.user).get(code__iexact=req.item_code) queryset_item = InsureePolicy.objects\ .filter(validity_to__isnull=True)\ @@ -510,9 +545,9 @@ def request(self, req, response): "policy__product_id", waiting_period=F(waiting_period_field), limit_no=F(limit_field))\ - .annotate(min_date=MonthsAdd(waiting_period_field, "effective_date"))\ + .annotate(min_date=MonthsAdd(Coalesce(F(waiting_period_field), 0), "effective_date"))\ .annotate(items_count=Count("policy__product__items__item_id")) \ - .annotate(items_left=Coalesce("limit_no", Value(0)) - F("items_count")) + .annotate(items_left=F("limit_no") - F("items_count")) min_date_qs = queryset_item.aggregate( min_date_lte=Min("min_date", filter=Q(min_date__lte=now)), @@ -534,7 +569,7 @@ def request(self, req, response): items_left = None min_date_item = None eligibility.min_date_item = min_date_item - eligibility.item_left = items_left or 0 + eligibility.item_left = items_left def get_total_filter(category): return ( @@ -620,11 +655,11 @@ def get_total_filter(category): total_visits_left = result["total_visits_left"] \ if result["total_visits_left"] is None or result["total_visits_left"] >= 0 else 0 - eligibility.surgery_amount_left = result["policy__product__max_amount_surgery"] or 0.0 - eligibility.consultation_amount_left = result["policy__product__max_amount_consultation"] or 0.0 - eligibility.delivery_amount_left = result["policy__product__max_amount_delivery"] or 0.0 - eligibility.antenatal_amount_left = result["policy__product__max_amount_antenatal"] or 0.0 - eligibility.hospitalization_amount_left = result["policy__product__max_amount_hospitalization"] or 0.0 + eligibility.surgery_amount_left = result["policy__product__max_amount_surgery"] + eligibility.consultation_amount_left = result["policy__product__max_amount_consultation"] + eligibility.delivery_amount_left = result["policy__product__max_amount_delivery"] + eligibility.antenatal_amount_left = result["policy__product__max_amount_antenatal"] + eligibility.hospitalization_amount_left = result["policy__product__max_amount_hospitalization"] if service: if service.category == Service.CATEGORY_SURGERY: @@ -670,12 +705,12 @@ def get_total_filter(category): eligibility.is_item_ok = True # The process above uses the None type but the stored procedure service sets these to 0 - eligibility.total_admissions_left = total_admissions_left or 0 - eligibility.total_consultations_left = total_consultations_left or 0 - eligibility.total_surgeries_left = total_surgeries_left or 0 - eligibility.total_deliveries_left = total_deliveries_left or 0 - eligibility.total_antenatal_left = total_antenatal_left or 0 - eligibility.total_visits_left = total_visits_left or 0 + eligibility.total_admissions_left = total_admissions_left + eligibility.total_consultations_left = total_consultations_left + eligibility.total_surgeries_left = total_surgeries_left + eligibility.total_deliveries_left = total_deliveries_left + eligibility.total_antenatal_left = total_antenatal_left + eligibility.total_visits_left = total_visits_left return eligibility diff --git a/policy/signals.py b/policy/signals.py new file mode 100644 index 0000000..8def951 --- /dev/null +++ b/policy/signals.py @@ -0,0 +1,5 @@ +from core.signals import Signal + + +_check_formal_sector_for_policy_signal_params = ["user", "policy_id"] +signal_check_formal_sector_for_policy = Signal(providing_args=_check_formal_sector_for_policy_signal_params) diff --git a/policy/test_services.py b/policy/test_services.py index cae4447..16b0946 100644 --- a/policy/test_services.py +++ b/policy/test_services.py @@ -1,4 +1,4 @@ -from unittest import mock +from unittest import mock, skip from claim.test_helpers import create_test_claim, create_test_claimservice, create_test_claimitem from claim.validations import validate_claim, validate_assign_prod_to_claimitems_and_services, process_dedrem @@ -86,21 +86,21 @@ def test_eligibility_sp_call(self): expected = EligibilityResponse( eligibility_request=req, prod_id=4, - total_admissions_left=0, - total_visits_left=0, - total_consultations_left=0, - total_surgeries_left=0, - total_deliveries_left=0, - total_antenatal_left=0, - consultation_amount_left=0.0, - surgery_amount_left=0.0, - delivery_amount_left=0.0, - hospitalization_amount_left=0.0, - antenatal_amount_left=0.0, + total_admissions_left=None, + total_visits_left=None, + total_consultations_left=None, + total_surgeries_left=None, + total_deliveries_left=None, + total_antenatal_left=None, + consultation_amount_left=None, + surgery_amount_left=None, + delivery_amount_left=None, + hospitalization_amount_left=None, + antenatal_amount_left=None, min_date_service=None, min_date_item=None, - service_left=0, - item_left=0, + service_left=None, + item_left=None, is_item_ok=True, is_service_ok=True, ) @@ -189,6 +189,45 @@ def test_eligibility_stored_proc_item(self): product.delete() insuree.delete() + def test_eligibility_by_insuree(self): + insuree = create_test_insuree() + product = create_test_product("ELI1") + (policy, insuree_policy) = create_test_policy2(product, insuree) + item = create_test_item("A") + item_pl_detail = add_item_to_hf_pricelist(item) + product_item = create_test_product_item(product, item, custom_props={"limit_no_adult": 12}) + claim = create_test_claim(custom_props={"insuree_id": insuree.id}) + claim_item = create_test_claimitem(claim, "A", custom_props={"item_id": item.id}) + errors = validate_claim(claim, True) + errors += validate_assign_prod_to_claimitems_and_services(claim) + errors += process_dedrem(claim, -1, True) + self.assertEqual(len(errors), 0) + + sp_el_svc = StoredProcEligibilityService(self.user) + native_el_svc = NativeEligibilityService(self.user) + req = EligibilityRequest(chf_id=insuree.chf_id, item_code=item.code) + settings.ROW_SECURITY = False + native_response = EligibilityResponse(req) + native_response = native_el_svc.request(req, native_response) + sp_response = EligibilityResponse(req) + sp_response = sp_el_svc.request(req, sp_response) + self.assertIsNotNone(native_response) + self.assertIsNotNone(sp_response) + self.assertEquals(native_response, sp_response) + + claim.dedrems.all().delete() + claim_item.delete() + claim.delete() + product_item.delete() + item_pl_detail.delete() + item.delete() + policy.insuree_policies.all().delete() + policy.delete() + product.delete() + insuree.delete() + + @skip("Not sure what is the proper behaviour when an IP is not present, skipping for now so that the main case" + "can be fixed.") def test_eligibility_stored_proc_item_no_insuree_policy(self): insuree = create_test_insuree() product = create_test_product("ELI1") diff --git a/setup.py b/setup.py index 95b8535..f040777 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ setup( name='openimis-be-policy', - version='1.2.1', + version='1.3.0', packages=find_packages(), include_package_data=True, license='GNU AGPL v3',