From ce89ebe0d408707d9b784e310a187246749eba17 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Tue, 9 Nov 2021 14:08:14 +0100 Subject: [PATCH 1/4] OPL-34: added 'payment plan' with making model plan generic --- contribution_plan/apps.py | 36 ++++++++- .../contribution_plan_mutations.py | 2 +- .../gql/gql_mutations/input_types.py | 14 +++- .../gql_mutations/payment_plan_mutations.py | 70 ++++++++++++++++ contribution_plan/gql/gql_types.py | 29 ++++++- .../0008_historicalpaymentplan_paymentplan.py | 80 +++++++++++++++++++ contribution_plan/mixins.py | 28 +++++++ contribution_plan/models.py | 39 ++++----- contribution_plan/schema.py | 31 ++++++- contribution_plan/services.py | 77 +++++++++++++++++- .../tests/gql_tests/query_tests.py | 15 ++-- contribution_plan/tests/helpers.py | 26 +++++- 12 files changed, 412 insertions(+), 35 deletions(-) create mode 100644 contribution_plan/gql/gql_mutations/payment_plan_mutations.py create mode 100644 contribution_plan/migrations/0008_historicalpaymentplan_paymentplan.py create mode 100644 contribution_plan/mixins.py diff --git a/contribution_plan/apps.py b/contribution_plan/apps.py index 4f8dd8a..b5a887c 100644 --- a/contribution_plan/apps.py +++ b/contribution_plan/apps.py @@ -9,6 +9,8 @@ "gql_query_contributionplanbundle_admins_perms": [], "gql_query_contributionplan_perms": ["151201"], "gql_query_contributionplan_admins_perms": [], + "gql_query_paymentplan_perms": ["157101"], + "gql_query_paymentplan_admins_perms": [], "gql_mutation_create_contributionplanbundle_perms": ["151102"], "gql_mutation_update_contributionplanbundle_perms": ["151103"], @@ -19,6 +21,11 @@ "gql_mutation_update_contributionplan_perms": ["151203"], "gql_mutation_delete_contributionplan_perms": ["151204"], "gql_mutation_replace_contributionplan_perms": ["151206"], + + "gql_mutation_create_paymentplan_perms": ["157102"], + "gql_mutation_update_paymentplan_perms": ["157103"], + "gql_mutation_delete_paymentplan_perms": ["157104"], + "gql_mutation_replace_paymentplan_perms": ["157106"], } @@ -31,6 +38,9 @@ class ContributionPlanConfig(AppConfig): gql_query_contributionplan_perms = [] gql_query_contributionplan_admins_perms = [] + gql_query_paymentplan_perms = [] + gql_query_paymentplan_admins_perms = [] + gql_mutation_create_contributionplanbundle_perms = [] gql_mutation_update_contributionplanbundle_perms = [] gql_mutation_delete_contributionplanbundle_perms = [] @@ -41,6 +51,11 @@ class ContributionPlanConfig(AppConfig): gql_mutation_delete_contributionplan_perms = [] gql_mutation_replace_contributionplan_perms = [] + gql_mutation_create_paymentplan_perms = [] + gql_mutation_update_paymentplan_perms = [] + gql_mutation_delete_paymentplan_perms = [] + gql_mutation_replace_paymentplan_perms = [] + def _configure_permissions(selfself, cfg): ContributionPlanConfig.gql_query_contributionplanbundle_perms = cfg[ "gql_query_contributionplanbundle_perms"] @@ -54,6 +69,12 @@ def _configure_permissions(selfself, cfg): "gql_query_contributionplan_admins_perms" ] + ContributionPlanConfig.gql_query_paymentplan_perms = cfg[ + "gql_query_paymentplan_perms"] + ContributionPlanConfig.gql_query_paymentplan_admins_perms = cfg[ + "gql_query_paymentplan_admins_perms" + ] + ContributionPlanConfig.gql_mutation_create_contributionplanbundle_perms = cfg[ "gql_mutation_create_contributionplanbundle_perms" ] @@ -80,7 +101,20 @@ def _configure_permissions(selfself, cfg): "gql_mutation_replace_contributionplan_perms" ] + ContributionPlanConfig.gql_mutation_create_paymentplan_perms = cfg[ + "gql_mutation_create_paymentplan_perms" + ] + ContributionPlanConfig.gql_mutation_update_paymentplan_perms = cfg[ + "gql_mutation_update_paymentplan_perms" + ] + ContributionPlanConfig.gql_mutation_delete_paymentplan_perms = cfg[ + "gql_mutation_delete_paymentplan_perms" + ] + ContributionPlanConfig.gql_mutation_replace_paymentplan_perms = cfg[ + "gql_mutation_replace_paymentplan_perms" + ] + def ready(self): from core.models import ModuleConfiguration cfg = ModuleConfiguration.get_or_default(MODULE_NAME, DEFAULT_CFG) - self._configure_permissions(cfg) \ No newline at end of file + self._configure_permissions(cfg) diff --git a/contribution_plan/gql/gql_mutations/contribution_plan_mutations.py b/contribution_plan/gql/gql_mutations/contribution_plan_mutations.py index c10d55a..6b4799c 100644 --- a/contribution_plan/gql/gql_mutations/contribution_plan_mutations.py +++ b/contribution_plan/gql/gql_mutations/contribution_plan_mutations.py @@ -40,4 +40,4 @@ class ReplaceContributionPlanMutation(BaseHistoryModelReplaceMutationMixin, Base _model = ContributionPlan class Input(ContributionPlanReplaceInputType): - pass \ No newline at end of file + pass diff --git a/contribution_plan/gql/gql_mutations/input_types.py b/contribution_plan/gql/gql_mutations/input_types.py index b86d0f0..5e19ed6 100644 --- a/contribution_plan/gql/gql_mutations/input_types.py +++ b/contribution_plan/gql/gql_mutations/input_types.py @@ -84,4 +84,16 @@ class ContributionPlanBundleDetailsUpdateInputType(OpenIMISMutation.Input): class ContributionPlanBundleDetailsReplaceInputType(ReplaceInputType): contribution_plan_id = graphene.UUID(required=False) date_valid_from = graphene.Date(required=True) - date_valid_to = graphene.Date(required=False) \ No newline at end of file + date_valid_to = graphene.Date(required=False) + + +class PaymentPlanInputType(ContributionPlanInputType): + pass + + +class PaymentPlanUpdateInputType(ContributionPlanUpdateInputType): + pass + + +class PaymentPlanReplaceInputType(ContributionPlanReplaceInputType): + pass diff --git a/contribution_plan/gql/gql_mutations/payment_plan_mutations.py b/contribution_plan/gql/gql_mutations/payment_plan_mutations.py new file mode 100644 index 0000000..2fdc5a7 --- /dev/null +++ b/contribution_plan/gql/gql_mutations/payment_plan_mutations.py @@ -0,0 +1,70 @@ +from core.gql.gql_mutations import DeleteInputType +from core.gql.gql_mutations.base_mutation import BaseMutation, BaseDeleteMutation, BaseReplaceMutation, \ + BaseHistoryModelCreateMutationMixin, BaseHistoryModelUpdateMutationMixin, \ + BaseHistoryModelDeleteMutationMixin, BaseHistoryModelReplaceMutationMixin +from contribution_plan.gql.gql_mutations import PaymentPlanInputType, PaymentPlanUpdateInputType, \ + PaymentPlanReplaceInputType +from contribution_plan.apps import ContributionPlanConfig +from contribution_plan.models import PaymentPlan +from django.contrib.auth.models import AnonymousUser +from django.core.exceptions import ValidationError + + +class CreatePaymentPlanMutation(BaseHistoryModelCreateMutationMixin, BaseMutation): + _mutation_class = "PaymentPlanMutation" + _mutation_module = "contribution_plan" + _model = PaymentPlan + + @classmethod + def _validate_mutation(cls, user, **data): + if type(user) is AnonymousUser or not user.id or not user.has_perms( + ContributionPlanConfig.gql_mutation_create_paymentplan_perms): + raise ValidationError("mutation.authentication_required") + + class Input(PaymentPlanInputType): + pass + + +class UpdatePaymentPlanMutation(BaseHistoryModelUpdateMutationMixin, BaseMutation): + _mutation_class = "PaymentPlanMutation" + _mutation_module = "contribution_plan" + _model = PaymentPlan + + @classmethod + def _validate_mutation(cls, user, **data): + if type(user) is AnonymousUser or not user.id or not user.has_perms( + ContributionPlanConfig.gql_mutation_update_paymentplan_perms): + raise ValidationError("mutation.authentication_required") + + class Input(PaymentPlanUpdateInputType): + pass + + +class DeletePaymentPlanMutation(BaseHistoryModelDeleteMutationMixin, BaseDeleteMutation): + _mutation_class = "PaymentPlanMutation" + _mutation_module = "contribution_plan" + _model = PaymentPlan + + @classmethod + def _validate_mutation(cls, user, **data): + if type(user) is AnonymousUser or not user.id or not user.has_perms( + ContributionPlanConfig.gql_mutation_delete_paymentplan_perms): + raise ValidationError("mutation.authentication_required") + + class Input(DeleteInputType): + pass + + +class ReplacePaymentPlanMutation(BaseHistoryModelReplaceMutationMixin, BaseReplaceMutation): + _mutation_class = "PaymentPlanMutation" + _mutation_module = "contribution_plan" + _model = PaymentPlan + + @classmethod + def _validate_mutation(cls, user, **data): + if type(user) is AnonymousUser or not user.id or not user.has_perms( + ContributionPlanConfig.gql_mutation_replace_paymentplan_perms): + raise ValidationError("mutation.authentication_required") + + class Input(PaymentPlanReplaceInputType): + pass diff --git a/contribution_plan/gql/gql_types.py b/contribution_plan/gql/gql_types.py index 51e3068..5711f86 100644 --- a/contribution_plan/gql/gql_types.py +++ b/contribution_plan/gql/gql_types.py @@ -1,5 +1,6 @@ import graphene -from contribution_plan.models import ContributionPlanBundle, ContributionPlan, ContributionPlanBundleDetails +from contribution_plan.models import ContributionPlanBundle, ContributionPlan, \ + ContributionPlanBundleDetails, PaymentPlan from core import ExtendedConnection, prefix_filterset from graphene_django import DjangoObjectType from product.schema import ProductGQLType @@ -82,3 +83,29 @@ class Meta: def get_queryset(cls, queryset, info): return ContributionPlanBundleDetails.get_queryset(queryset, info) + +class PaymentPlanGQLType(DjangoObjectType): + + class Meta: + model = PaymentPlan + interfaces = (graphene.relay.Node,) + filter_fields = { + "id": ["exact"], + "version": ["exact"], + "code": ["exact", "istartswith", "icontains", "iexact"], + "name": ["exact", "istartswith", "icontains", "iexact"], + **prefix_filterset("benefit_plan__", ProductGQLType._meta.filter_fields), + "calculation": ["exact"], + "periodicity": ["exact", "lt", "lte", "gt", "gte"], + "date_created": ["exact", "lt", "lte", "gt", "gte"], + "date_updated": ["exact", "lt", "lte", "gt", "gte"], + "user_created": ["exact"], + "user_updated": ["exact"], + "is_deleted": ["exact"] + } + + connection_class = ExtendedConnection + + @classmethod + def get_queryset(cls, queryset, info): + return PaymentPlan.get_queryset(queryset, info) diff --git a/contribution_plan/migrations/0008_historicalpaymentplan_paymentplan.py b/contribution_plan/migrations/0008_historicalpaymentplan_paymentplan.py new file mode 100644 index 0000000..5ea5bfb --- /dev/null +++ b/contribution_plan/migrations/0008_historicalpaymentplan_paymentplan.py @@ -0,0 +1,80 @@ +# Generated by Django 3.0.14 on 2021-11-09 12:54 + +import contribution_plan.mixins +import core.fields +import datetime +import dirtyfields.dirtyfields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import jsonfallback.fields +import simple_history.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('product', '__first__'), + ('contribution_plan', '0007_auto_20210217_1302'), + ] + + operations = [ + migrations.CreateModel( + name='PaymentPlan', + fields=[ + ('id', models.UUIDField(db_column='UUID', default=None, editable=False, primary_key=True, serialize=False)), + ('is_deleted', models.BooleanField(db_column='isDeleted', default=False)), + ('json_ext', jsonfallback.fields.FallbackJSONField(blank=True, db_column='Json_ext', null=True)), + ('date_created', core.fields.DateTimeField(db_column='DateCreated', null=True)), + ('date_updated', core.fields.DateTimeField(db_column='DateUpdated', null=True)), + ('version', models.IntegerField(default=1)), + ('date_valid_from', core.fields.DateTimeField(db_column='DateValidFrom', default=datetime.datetime.now)), + ('date_valid_to', core.fields.DateTimeField(blank=True, db_column='DateValidTo', null=True)), + ('replacement_uuid', models.UUIDField(db_column='ReplacementUUID', null=True)), + ('code', models.CharField(blank=True, db_column='Code', max_length=255, null=True)), + ('name', models.CharField(blank=True, db_column='Name', max_length=255, null=True)), + ('calculation', models.UUIDField(db_column='calculationUUID')), + ('periodicity', models.IntegerField(db_column='Periodicity')), + ('benefit_plan', models.ForeignKey(db_column='BenefitPlanID', on_delete=django.db.models.deletion.DO_NOTHING, to='product.Product')), + ('user_created', models.ForeignKey(db_column='UserCreatedUUID', on_delete=django.db.models.deletion.DO_NOTHING, related_name='paymentplan_user_created', to=settings.AUTH_USER_MODEL)), + ('user_updated', models.ForeignKey(db_column='UserUpdatedUUID', on_delete=django.db.models.deletion.DO_NOTHING, related_name='paymentplan_user_updated', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'db_table': 'tblPaymentPlan', + }, + bases=(contribution_plan.mixins.GenericPlanQuerysetMixin, dirtyfields.dirtyfields.DirtyFieldsMixin, models.Model), + ), + migrations.CreateModel( + name='HistoricalPaymentPlan', + fields=[ + ('id', models.UUIDField(db_column='UUID', db_index=True, default=None, editable=False)), + ('is_deleted', models.BooleanField(db_column='isDeleted', default=False)), + ('json_ext', jsonfallback.fields.FallbackJSONField(blank=True, db_column='Json_ext', null=True)), + ('date_created', core.fields.DateTimeField(db_column='DateCreated', null=True)), + ('date_updated', core.fields.DateTimeField(db_column='DateUpdated', null=True)), + ('version', models.IntegerField(default=1)), + ('date_valid_from', core.fields.DateTimeField(db_column='DateValidFrom', default=datetime.datetime.now)), + ('date_valid_to', core.fields.DateTimeField(blank=True, db_column='DateValidTo', null=True)), + ('replacement_uuid', models.UUIDField(db_column='ReplacementUUID', null=True)), + ('code', models.CharField(blank=True, db_column='Code', max_length=255, null=True)), + ('name', models.CharField(blank=True, db_column='Name', max_length=255, null=True)), + ('calculation', models.UUIDField(db_column='calculationUUID')), + ('periodicity', models.IntegerField(db_column='Periodicity')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField()), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('benefit_plan', models.ForeignKey(blank=True, db_column='BenefitPlanID', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='product.Product')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user_created', models.ForeignKey(blank=True, db_column='UserCreatedUUID', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('user_updated', models.ForeignKey(blank=True, db_column='UserUpdatedUUID', db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical payment plan', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': 'history_date', + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + ] diff --git a/contribution_plan/mixins.py b/contribution_plan/mixins.py new file mode 100644 index 0000000..f890a88 --- /dev/null +++ b/contribution_plan/mixins.py @@ -0,0 +1,28 @@ +from django.conf import settings +from django.db import models +from graphql import ResolveInfo + +from core.models import HistoryModelManager + + +class GenericPlanManager(HistoryModelManager): + def filter(self, *args, **kwargs): + keys = [x for x in kwargs if "itemsvc" in x] + for key in keys: + new_key = key.replace("itemsvc", self.model.model_prefix) + kwargs[new_key] = kwargs.pop(key) + return super(GenericPlanManager, self).filter(*args, **kwargs) + + +class GenericPlanQuerysetMixin: + + @classmethod + def get_queryset(cls, queryset, user): + queryset = cls.filter_queryset(queryset) + if isinstance(user, ResolveInfo): + user = user.context.user + if settings.ROW_SECURITY and user.is_anonymous: + return queryset.filter(id=-1) + if settings.ROW_SECURITY: + pass + return queryset diff --git a/contribution_plan/models.py b/contribution_plan/models.py index dcf211b..9e09ea2 100644 --- a/contribution_plan/models.py +++ b/contribution_plan/models.py @@ -4,6 +4,20 @@ from core.signals import Signal from graphql import ResolveInfo from product.models import Product +from contribution_plan.mixins import GenericPlanQuerysetMixin, GenericPlanManager + + +class GenericPlan(GenericPlanQuerysetMixin, core_models.HistoryBusinessModel): + code = models.CharField(db_column="Code", max_length=255, blank=True, null=True) + name = models.CharField(db_column="Name", max_length=255, blank=True, null=True) + calculation = models.UUIDField(db_column="calculationUUID", null=False) + benefit_plan = models.ForeignKey(Product, db_column="BenefitPlanID", on_delete=models.deletion.DO_NOTHING) + periodicity = models.IntegerField(db_column="Periodicity", null=False) + + objects = GenericPlanManager() + + class Meta: + abstract = True _get_contribution_length_signal_params = ["grace_period"] @@ -50,35 +64,24 @@ def filter(self, *args, **kwargs): return super(ContributionPlanManager, self).filter(*args, **kwargs) -class ContributionPlan(core_models.HistoryBusinessModel): - code = models.CharField(db_column="Code", max_length=255, blank=True, null=True) - name = models.CharField(db_column="Name", max_length=255, blank=True, null=True) - calculation = models.UUIDField(db_column="calculationUUID", null=False) - benefit_plan = models.ForeignKey(Product, db_column="BenefitPlanID", on_delete=models.deletion.DO_NOTHING) - periodicity = models.IntegerField(db_column="Periodicity", null=False) +class ContributionPlan(GenericPlan): length: int = None - objects = ContributionPlanManager() def get_contribution_length(self): self.length = self.periodicity get_contribution_length_signal.send(sender=self.__class__, instance=self) return self.length - @classmethod - def get_queryset(cls, queryset, user): - queryset = cls.filter_queryset(queryset) - if isinstance(user, ResolveInfo): - user = user.context.user - if settings.ROW_SECURITY and user.is_anonymous: - return queryset.filter(id=None) - if settings.ROW_SECURITY: - pass - return queryset - class Meta: db_table = 'tblContributionPlan' +class PaymentPlan(GenericPlan): + + class Meta: + db_table = 'tblPaymentPlan' + + class ContributionPlanBundleDetailsManager(models.Manager): def filter(self, *args, **kwargs): keys = [x for x in kwargs if "itemsvc" in x] diff --git a/contribution_plan/schema.py b/contribution_plan/schema.py index f57c1e7..76b4308 100644 --- a/contribution_plan/schema.py +++ b/contribution_plan/schema.py @@ -3,7 +3,7 @@ from core.schema import signal_mutation_module_validate from contribution_plan.gql import ContributionPlanGQLType, ContributionPlanBundleGQLType, \ - ContributionPlanBundleDetailsGQLType + ContributionPlanBundleDetailsGQLType, PaymentPlanGQLType from core.utils import append_validity_filter from contribution_plan.gql.gql_mutations.contribution_plan_bundle_details_mutations import \ CreateContributionPlanBundleDetailsMutation, UpdateContributionPlanBundleDetailsMutation, \ @@ -12,7 +12,10 @@ UpdateContributionPlanBundleMutation, DeleteContributionPlanBundleMutation, ReplaceContributionPlanBundleMutation from contribution_plan.gql.gql_mutations.contribution_plan_mutations import CreateContributionPlanMutation, \ UpdateContributionPlanMutation, DeleteContributionPlanMutation, ReplaceContributionPlanMutation -from contribution_plan.models import ContributionPlanBundle, ContributionPlan, ContributionPlanBundleDetails +from contribution_plan.gql.gql_mutations.payment_plan_mutations import CreatePaymentPlanMutation, \ + UpdatePaymentPlanMutation, DeletePaymentPlanMutation, ReplacePaymentPlanMutation +from contribution_plan.models import ContributionPlanBundle, ContributionPlan, \ + ContributionPlanBundleDetails, PaymentPlan from core.schema import OrderedDjangoFilterConnectionField from .models import ContributionPlanMutation, ContributionPlanBundleMutation from .apps import ContributionPlanConfig @@ -45,6 +48,14 @@ class Query(graphene.ObjectType): applyDefaultValidityFilter=graphene.Boolean() ) + payment_plan = OrderedDjangoFilterConnectionField( + PaymentPlanGQLType, + orderBy=graphene.List(of_type=graphene.String), + dateValidFrom__Gte=graphene.DateTime(), + dateValidTo__Lte=graphene.DateTime(), + applyDefaultValidityFilter=graphene.Boolean() + ) + def resolve_contribution_plan(self, info, **kwargs): if not info.context.user.has_perms(ContributionPlanConfig.gql_query_contributionplan_perms): raise PermissionError("Unauthorized") @@ -86,23 +97,35 @@ def resolve_contribution_plan_bundle_details(self, info, **kwargs): return gql_optimizer.query(query.filter(*filters).all(), info) + def resolve_payment_plan(self, info, **kwargs): + if not info.context.user.has_perms(ContributionPlanConfig.gql_query_paymentplan_perms): + raise PermissionError("Unauthorized") + + filters = append_validity_filter(**kwargs) + query = PaymentPlan.objects + return gql_optimizer.query(query.filter(*filters).all(), info) + + class Mutation(graphene.ObjectType): create_contribution_plan_bundle = CreateContributionPlanBundleMutation.Field() create_contribution_plan = CreateContributionPlanMutation.Field() create_contribution_plan_bundle_details = CreateContributionPlanBundleDetailsMutation.Field() + create_payment_plan = CreatePaymentPlanMutation.Field() update_contribution_plan_bundle = UpdateContributionPlanBundleMutation.Field() update_contribution_plan = UpdateContributionPlanMutation.Field() update_contribution_plan_bundle_details = UpdateContributionPlanBundleDetailsMutation.Field() + update_payment_plan = UpdatePaymentPlanMutation.Field() delete_contribution_plan_bundle = DeleteContributionPlanBundleMutation.Field() delete_contribution_plan = DeleteContributionPlanMutation.Field() delete_contribution_plan_bundle_details = DeleteContributionPlanBundleDetailsMutation.Field() + delete_payment_plan = DeletePaymentPlanMutation.Field() replace_contribution_plan_bundle = ReplaceContributionPlanBundleMutation.Field() replace_contribution_plan = ReplaceContributionPlanMutation.Field() replace_contribution_plan_bundle_details = ReplaceContributionPlanBundleDetailsMutation.Field() - + replace_payment_plan = ReplacePaymentPlanMutation.Field() def on_contribution_plan_mutation(sender, **kwargs): uuid = kwargs['data'].get('uuid', None) @@ -120,4 +143,4 @@ def on_contribution_plan_mutation(sender, **kwargs): def bind_signals(): - signal_mutation_module_validate["contribution_plan"].connect(on_contribution_plan_mutation) \ No newline at end of file + signal_mutation_module_validate["contribution_plan"].connect(on_contribution_plan_mutation) diff --git a/contribution_plan/services.py b/contribution_plan/services.py index b2d469f..d781cc1 100644 --- a/contribution_plan/services.py +++ b/contribution_plan/services.py @@ -4,7 +4,7 @@ from django.contrib.auth.models import AnonymousUser from django.forms.models import model_to_dict from contribution_plan.models import ContributionPlan as ContributionPlanModel, ContributionPlanBundle as ContributionPlanBundleModel, \ - ContributionPlanBundleDetails as ContributionPlanBundleDetailsModel + ContributionPlanBundleDetails as ContributionPlanBundleDetailsModel, PaymentPlan as PaymentPlanModel def check_authentication(function): @@ -223,6 +223,79 @@ def delete(self, contribution_plan_bundle_details): return _output_exception(model_name="ContributionPlanBundleDetails", method="delete", exception=exc) +class PaymentPlan(object): + + def __init__(self, user): + self.user = user + + @check_authentication + def get_by_id(self, by_payment_plan): + try: + pp = PaymentPlanModel.objects.get(id=by_payment_plan.id) + uuid_string = str(pp.id) + dict_representation = model_to_dict(pp) + dict_representation["id"], dict_representation["uuid"] = (str(uuid_string), str(uuid_string)) + except Exception as exc: + return _output_exception(model_name="PaymentPlan", method="get", exception=exc) + return _output_result_success(dict_representation=dict_representation) + + @check_authentication + def create(self, payment_plan): + try: + pp = PaymentPlanModel(**payment_plan) + pp.save(username=self.user.username) + uuid_string = str(pp.id) + dict_representation = model_to_dict(pp) + dict_representation["id"], dict_representation["uuid"] = (str(uuid_string), str(uuid_string)) + except Exception as exc: + return _output_exception(model_name="PaymentPlan", method="create", exception=exc) + return _output_result_success(dict_representation=dict_representation) + + @check_authentication + def update(self, payment_plan): + try: + updated_pp = PaymentPlanModel.objects.filter(id=payment_plan['id']).first() + [setattr(updated_pp, key, payment_plan[key]) for key in payment_plan] + updated_pp.save(username=self.user.username) + uuid_string = str(updated_pp.id) + dict_representation = model_to_dict(updated_pp) + dict_representation["id"], dict_representation["uuid"] = (str(uuid_string), str(uuid_string)) + except Exception as exc: + return _output_exception(model_name="payment_plan", method="update", exception=exc) + return _output_result_success(dict_representation=dict_representation) + + @check_authentication + def delete(self, payment_plan): + try: + pp_to_delete = PaymentPlanModel.objects.filter(id=payment_plan['id']).first() + pp_to_delete.delete(username=self.user.username) + return { + "success": True, + "message": "Ok", + "detail": "", + } + except Exception as exc: + return _output_exception(model_name="PaymentPlanModel", method="delete", exception=exc) + + @check_authentication + def replace(self, payment_plan): + try: + pp_to_replace = PaymentPlanModel.objects.filter(id=payment_plan['uuid']).first() + pp_to_replace.replace_object(data=payment_plan, username=self.user.username) + uuid_string = str(pp_to_replace.id) + dict_representation = model_to_dict(pp_to_replace) + dict_representation["id"], dict_representation["uuid"] = (str(uuid_string), str(uuid_string)) + except Exception as exc: + return _output_exception(model_name="ContributionPlan", method="replace", exception=exc) + return { + "success": True, + "message": "Ok", + "detail": "", + "old_object": json.loads(json.dumps(dict_representation, cls=DjangoJSONEncoder)), + "uuid_new_object": str(pp_to_replace.replacement_uuid), + } + + def _output_exception(model_name, method, exception): return { "success": False, @@ -238,4 +311,4 @@ def _output_result_success(dict_representation): "message": "Ok", "detail": "", "data": json.loads(json.dumps(dict_representation, cls=DjangoJSONEncoder)), - } \ No newline at end of file + } diff --git a/contribution_plan/tests/gql_tests/query_tests.py b/contribution_plan/tests/gql_tests/query_tests.py index ea377c0..585abbb 100644 --- a/contribution_plan/tests/gql_tests/query_tests.py +++ b/contribution_plan/tests/gql_tests/query_tests.py @@ -27,6 +27,7 @@ def setUpClass(cls): custom_props={'code': 'SuperContributionPlan!'}) cls.test_contribution_plan = create_test_contribution_plan() cls.test_contribution_plan_details = create_test_contribution_plan_bundle_details() + cls.test_payment_plan = create_test_payment_plan() cls.schema = Schema( query=contribution_plan_schema.Query, @@ -40,6 +41,7 @@ def tearDownClass(cls): ContributionPlanBundleDetails.objects.filter(id=cls.test_contribution_plan_details.id).delete() ContributionPlan.objects.filter(id=cls.test_contribution_plan.id).delete() ContributionPlanBundle.objects.filter(id=cls.test_contribution_plan_bundle.id).delete() + PaymentPlan.objects.filter(id=cls.test_payment_plan.id).delete() def test_find_contribution_plan_bundle_existing(self): id = self.test_contribution_plan_bundle.id @@ -53,6 +55,12 @@ def test_find_contribution_plan_existing(self): converted_id = base64.b64decode(result[0]['node']['id']).decode('utf-8').split(':')[1] self.assertEqual(UUID(converted_id), id) + def test_find_payment_plan_existing(self): + id = self.test_payment_plan.id + result = self.find_by_id_query("paymentPlan", id) + converted_id = base64.b64decode(result[0]['node']['id']).decode('utf-8').split(':')[1] + self.assertEqual(UUID(converted_id), id) + def test_find_contribution_plan_details_existing(self): id = self.test_contribution_plan_details.id result = self.find_by_id_query("contributionPlanBundleDetails", id) @@ -74,14 +82,10 @@ def test_find_contribution_plan_bundle_existing_anonymous_user(self): result_cpb = self.find_by_id_query("contributionPlanBundle", self.test_contribution_plan_bundle.id, context=self.AnonymousUserContext()) - result_cp = self.find_by_id_query("contributionPlan", - self.test_contribution_plan.id, - context=self.AnonymousUserContext()) result_cpbd = self.find_by_id_query("contributionPlanBundleDetails", self.test_contribution_plan_details.id, context=self.AnonymousUserContext()) - self.assertEqual(len(result_cp), 0) self.assertEqual(len(result_cpb), 0) self.assertEqual(len(result_cpbd), 0) @@ -123,7 +127,6 @@ def find_by_id_query(self, query_type, id, context=None): }} }} ''' - query_result = self.execute_query(query, context=context) records = query_result[query_type]['edges'] @@ -172,4 +175,4 @@ def wrap_arg(v): return v # if isinstance(v, numbers.Number) else F'"{v}"' params_as_args = [f'{k}:{wrap_arg(v)}' for k, v in params.items()] - return ", ".join(params_as_args) \ No newline at end of file + return ", ".join(params_as_args) diff --git a/contribution_plan/tests/helpers.py b/contribution_plan/tests/helpers.py index 2ce9a73..8a19672 100644 --- a/contribution_plan/tests/helpers.py +++ b/contribution_plan/tests/helpers.py @@ -1,7 +1,8 @@ import json from functools import lru_cache -from contribution_plan.models import ContributionPlanBundle, ContributionPlan, ContributionPlanBundleDetails +from contribution_plan.models import ContributionPlanBundle, ContributionPlan, \ + ContributionPlanBundleDetails, PaymentPlan from datetime import date from core.models import InteractiveUser, User @@ -76,6 +77,29 @@ def create_test_contribution_plan_bundle_details(contribution_plan_bundle=None, return contribution_plan_bundle_details +def create_test_payment_plan(product=None, calculation=ContributionValuationRule.uuid, custom_props={}): + if not product: + product = create_test_product("PlanCode", custom_props={"insurance_period": 12,}) + + user = __get_or_create_simple_contribution_plan_user() + + object_data = { + 'is_deleted': False, + 'code': "Payment Plan Code", + 'name': "Payment Plan Name", + 'benefit_plan': product, + 'periodicity': 12, + 'calculation': calculation, + 'json_ext': json.dumps("{}"), + **custom_props + } + + payment_plan = PaymentPlan(**object_data) + payment_plan.save(username=user.username) + + return payment_plan + + def __get_or_create_simple_contribution_plan_user(): if not User.objects.filter(username='admin').exists(): User.objects.create_superuser(username='admin', password='S\/pe®Pąßw0rd™') From ae9d3dcf5bf833e3d2245389eba6fea9a9251850 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Tue, 9 Nov 2021 14:30:32 +0100 Subject: [PATCH 2/4] OPL-34: added tests for PaymentPlan mutations --- .../tests/gql_tests/mutations_pp_tests.py | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 contribution_plan/tests/gql_tests/mutations_pp_tests.py diff --git a/contribution_plan/tests/gql_tests/mutations_pp_tests.py b/contribution_plan/tests/gql_tests/mutations_pp_tests.py new file mode 100644 index 0000000..0714639 --- /dev/null +++ b/contribution_plan/tests/gql_tests/mutations_pp_tests.py @@ -0,0 +1,206 @@ +import datetime +import numbers +import base64 +from unittest import mock +from django.test import TestCase +from uuid import UUID + +import graphene +from contribution_plan.tests.helpers import * +from contribution_plan import schema as contribution_plan_schema +from calculation.calculation_rule import ContributionValuationRule +from core import datetime +from product.test_helpers import create_test_product +from graphene import Schema +from graphene.test import Client + + +class MutationTestPaymentPlan(TestCase): + + class BaseTestContext: + def __init__(self, user): + self.user = user + + class AnonymousUserContext: + user = mock.Mock(is_anonymous=True) + + @classmethod + def setUpClass(cls): + if not User.objects.filter(username='admin').exists(): + User.objects.create_superuser(username='admin', password='S\/pe®Pąßw0rd™') + cls.user = User.objects.filter(username='admin').first() + cls.test_payment_plan = create_test_payment_plan() + cls.test_calculation = ContributionValuationRule.uuid + cls.test_calculation2 = ContributionValuationRule.uuid + cls.test_product = create_test_product("PlanCode", custom_props={"insurance_period": 12, }) + cls.schema = Schema( + query=contribution_plan_schema.Query, + mutation=contribution_plan_schema.Mutation + ) + cls.graph_client = Client(cls.schema) + + @classmethod + def tearDownClass(cls): + PaymentPlan.objects.filter(id=cls.test_payment_plan.id).delete() + + def test_payment_plan_create(self): + time_stamp = datetime.datetime.now() + input_param = { + "code": "XYZ", + "name": "XYZ test name xyz - "+str(time_stamp), + "benefitPlanId": self.test_product.id, + "calculation": f"{self.test_calculation}", + "periodicity": 12, + } + + self.add_mutation("createPaymentPlan", input_param) + result = self.find_by_exact_attributes_query( + "paymentPlan", + params=input_param, + )["edges"] + + converted_id = base64.b64decode(result[0]['node']['id']).decode('utf-8').split(':')[1] + # tear down the test data + PaymentPlan.objects.filter(id=f"{converted_id}").delete() + + self.assertEqual( + ( + "XYZ test name xyz - "+str(time_stamp), + "XYZ", + 1, + 12 + ), + ( + result[0]['node']['name'], + result[0]['node']['code'], + result[0]['node']['version'], + result[0]['node']['periodicity'] + ) + ) + + def test_payment_plan_create_without_obligatory_fields(self): + time_stamp = datetime.datetime.now() + input_param = { + "name": "XYZ test name xyz - "+str(time_stamp), + } + result_mutation = self.add_mutation("createPaymentPlan", input_param) + self.assertEqual(True, 'errors' in result_mutation) + + def test_payment_plan_delete_single_deletion(self): + time_stamp = datetime.datetime.now() + input_param = { + "code": "XYZ deletion", + "name": "XYZ test deletion xyz - "+str(time_stamp), + "benefitPlanId": self.test_product.id, + "calculation": f"{self.test_calculation}", + "periodicity": 12, + } + self.add_mutation("createPaymentPlan", input_param) + result = self.find_by_exact_attributes_query("paymentPlan", {**input_param, 'isDeleted': False}) + converted_id = base64.b64decode(result["edges"][0]['node']['id']).decode('utf-8').split(':')[1] + input_param2 = { + "uuids": [f"{converted_id}"], + } + self.add_mutation("deletePaymentPlan", input_param2) + result2 = self.find_by_exact_attributes_query("paymentPlan", {**input_param, 'isDeleted': False}) + + # tear down the test data + PaymentPlan.objects.filter(id=f"{converted_id}").delete() + + self.assertEqual((1, 0), (result["totalCount"], result2["totalCount"])) + + def test_payment_plan_update_1_existing(self): + id = self.test_payment_plan.id + version = self.test_payment_plan.version + input_param = { + "id": f"{id}", + "name": "XYZ test name xxxxx", + } + self.add_mutation("updatePaymentPlan", input_param) + result = self.find_by_exact_attributes_query("paymentPlan", {**input_param})["edges"] + self.test_payment_plan.version = result[0]['node']['version'] + + self.assertEqual( + ("XYZ test name xxxxx", version+1), + (result[0]['node']['name'], result[0]['node']['version']) + ) + + def find_by_exact_attributes_query(self, query_type, params, context=None): + if "dateValidFrom" in params: + params.pop('dateValidFrom') + if "dateValidTo" in params: + params.pop('dateValidTo') + if "benefitPlanId" in params: + params.pop('benefitPlanId') + if "calculation" in params: + params.pop('calculation') + node_content_str = "\n".join(params.keys()) + query = F''' + {{ + {query_type}({self.build_params(params)}) {{ + totalCount + edges {{ + node {{ + {'id' if 'id' not in params else '' } + {node_content_str} + version + dateValidFrom + dateValidTo + replacementUuid + }} + cursor + }} + }} + }} + ''' + query_result = self.execute_query(query, context=context) + records = query_result[query_type] + return records + + def execute_query(self, query, context=None): + if context is None: + context = self.BaseTestContext(self.user) + + query_result = self.graph_client.execute(query, context=context) + query_data = query_result['data'] + return query_data + + def add_mutation(self, mutation_type, input_params, context=None): + mutation = f''' + mutation + {{ + {mutation_type}(input: {{ + {self.build_params(input_params)} + }}) + + {{ + internalId + clientMutationId + }} + }} + ''' + mutation_result = self.execute_mutation(mutation, context=context) + return mutation_result + + def execute_mutation(self, mutation, context=None): + if context is None: + context = self.BaseTestContext(self.user) + + mutation_result = self.graph_client.execute(mutation, context=context) + return mutation_result + + def build_params(self, params): + def wrap_arg(v): + if isinstance(v, str): + return F'"{v}"' + if isinstance(v, list): + return json.dumps(v) + if isinstance(v, bool): + return str(v).lower() + if isinstance(v, datetime.date): + return graphene.DateTime.serialize( + datetime.datetime.fromordinal(v.toordinal())) + return v + + params_as_args = [f'{k}:{wrap_arg(v)}' for k, v in params.items()] + return ", ".join(params_as_args) From 5c14a573559b8bc6ea223d4316ca4b3e26ccbbda Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Tue, 9 Nov 2021 14:41:43 +0100 Subject: [PATCH 3/4] OPL-34: added test for create/update payment plan service --- .../tests/services/services_cp_tests.py | 85 ++++++++++++++++++- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/contribution_plan/tests/services/services_cp_tests.py b/contribution_plan/tests/services/services_cp_tests.py index fa9591c..fbef4ae 100644 --- a/contribution_plan/tests/services/services_cp_tests.py +++ b/contribution_plan/tests/services/services_cp_tests.py @@ -2,8 +2,9 @@ from django.test import TestCase from contribution_plan.services import ContributionPlan as ContributionPlanService, ContributionPlanBundle as ContributionPlanBundleService, \ - ContributionPlanBundleDetails as ContributionPlanBundleDetailsService -from contribution_plan.models import ContributionPlan, ContributionPlanBundle, ContributionPlanBundleDetails + ContributionPlanBundleDetails as ContributionPlanBundleDetailsService, PaymentPlan as PaymentPlanService +from contribution_plan.models import ContributionPlan, ContributionPlanBundle, \ + ContributionPlanBundleDetails, PaymentPlan from calculation.calculation_rule import ContributionValuationRule from product.models import Product from core.models import User @@ -22,6 +23,7 @@ def setUpClass(cls): cls.contribution_plan_service = ContributionPlanService(cls.user) cls.contribution_plan_bundle_service = ContributionPlanBundleService(cls.user) cls.contribution_plan_bundle_details_service = ContributionPlanBundleDetailsService(cls.user) + cls.payment_plan_service = PaymentPlanService(cls.user) cls.test_product = create_test_product("PlanCode", custom_props={"insurance_period": 12,}) cls.test_product2 = create_test_product("PC", custom_props={"insurance_period": 6}) cls.contribution_plan_bundle = create_test_contribution_plan_bundle() @@ -175,7 +177,6 @@ def test_contribution_plan_create_update_benefit_plan(self): ) ) - def test_contribution_plan_update_without_changing_field(self): contribution_plan = { 'code': "CPUWCF", @@ -611,3 +612,81 @@ def test_contribution_plan_bundle_details_update(self): response['data']['contribution_plan_bundle'], ) ) + + def test_payment_plan_create(self): + payment_plan = { + 'code': "PP SERVICE", + 'name': "Payment Plan Name Service", + 'benefit_plan_id': self.test_product.id, + 'periodicity': 6, + 'calculation': str(self.calculation), + 'json_ext': json.dumps("{}"), + } + + response = self.payment_plan_service.create(payment_plan) + + # tear down the test data + PaymentPlan.objects.filter(id=response["data"]["id"]).delete() + + self.assertEqual( + ( + True, + "Ok", + "", + "PP SERVICE", + "Payment Plan Name Service", + 1, + 6, + self.test_product.id, + str(self.calculation), + ), + ( + response['success'], + response['message'], + response['detail'], + response['data']['code'], + response['data']['name'], + response['data']['version'], + response['data']['periodicity'], + response['data']['benefit_plan'], + response['data']['calculation'], + ) + ) + + def test_payment_plan_create_update(self): + payment_plan = { + 'code': "PP SERUPD", + 'name': "PP for update", + 'benefit_plan_id': self.test_product.id, + 'periodicity': 6, + 'calculation': str(self.calculation), + 'json_ext': json.dumps("{}"), + } + + response = self.payment_plan_service.create(payment_plan) + payment_plan_object = PaymentPlan.objects.get(id=response['data']['id']) + payment_plan_to_update = { + 'id': str(payment_plan_object.id), + 'periodicity': 12, + } + response = self.payment_plan_service.update(payment_plan_to_update) + + # tear down the test data + PaymentPlan.objects.filter(id=payment_plan_object.id).delete() + + self.assertEqual( + ( + True, + "Ok", + "", + 12, + 2, + ), + ( + response['success'], + response['message'], + response['detail'], + response['data']['periodicity'], + response['data']['version'], + ) + ) From dc8b254bec181009e7a762ba302b26a9bc6a7635 Mon Sep 17 00:00:00 2001 From: sniedzielski Date: Thu, 25 Nov 2021 11:57:53 +0100 Subject: [PATCH 4/4] OP-461: added migration script to apply roles for admin for that module --- .../0009_contributionplan_roles_for_admin.py | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 contribution_plan/migrations/0009_contributionplan_roles_for_admin.py diff --git a/contribution_plan/migrations/0009_contributionplan_roles_for_admin.py b/contribution_plan/migrations/0009_contributionplan_roles_for_admin.py new file mode 100644 index 0000000..5019b51 --- /dev/null +++ b/contribution_plan/migrations/0009_contributionplan_roles_for_admin.py @@ -0,0 +1,74 @@ +import logging + +from django.db import migrations + +logger = logging.getLogger(__name__) + + +MIGRATION_SQL = """ + /* Contribution plan and bundle*/ + DECLARE @SystemRole INT + SELECT @SystemRole = role.RoleID from tblRole role where IsSystem=256; + /* Contribution plan bundle*/ + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151101) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151101, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151102) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151102, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151103) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151103, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151104) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151104, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151106) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151106, CURRENT_TIMESTAMP) + END + /* Contribution plan */ + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151201) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151201, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151202) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151202, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151203) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151203, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151204) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151204, CURRENT_TIMESTAMP) + END + IF NOT EXISTS (SELECT * FROM [tblRoleRight] WHERE [RoleID] = @SystemRole AND [RightID] = 151206) + BEGIN + INSERT [dbo].[tblRoleRight] ([RoleID], [RightID], [ValidityFrom]) + VALUES (@SystemRole, 151206, CURRENT_TIMESTAMP) + END +""" + + +class Migration(migrations.Migration): + dependencies = [ + ('contribution_plan', '0008_historicalpaymentplan_paymentplan') + ] + + operations = [ + migrations.RunSQL(MIGRATION_SQL) + ]