From 644c3cf9ddbf218e365213d9ea819080263fbc73 Mon Sep 17 00:00:00 2001 From: Domenico Date: Mon, 10 Jun 2024 00:02:14 -0600 Subject: [PATCH 1/3] 203607-export-api (#61) --- src/hope_payment_gateway/api/fsp/filters.py | 10 ++++ .../api/fsp/serializers.py | 9 ++++ src/hope_payment_gateway/api/fsp/views.py | 30 +++++++++++ src/hope_payment_gateway/api/urls.py | 1 + .../apps/core/permissions.py | 2 - .../apps/fsp/western_union/apps.py | 6 +-- .../apps/fsp/western_union/endpoints/nis.py | 1 - .../apps/fsp/western_union/handlers.py | 6 +++ .../apps/gateway/admin.py | 7 +++ .../gateway/migrations/0019_exporttemplate.py | 51 +++++++++++++++++++ .../apps/gateway/models.py | 34 ++++++++++++- .../apps/gateway/registry.py | 7 +-- 12 files changed, 154 insertions(+), 10 deletions(-) create mode 100644 src/hope_payment_gateway/apps/gateway/migrations/0019_exporttemplate.py diff --git a/src/hope_payment_gateway/api/fsp/filters.py b/src/hope_payment_gateway/api/fsp/filters.py index 540e55c..37fba61 100644 --- a/src/hope_payment_gateway/api/fsp/filters.py +++ b/src/hope_payment_gateway/api/fsp/filters.py @@ -3,6 +3,7 @@ from hope_payment_gateway.apps.gateway.models import ( DeliveryMechanism, + ExportTemplate, FinancialServiceProvider, FinancialServiceProviderConfig, PaymentInstruction, @@ -65,3 +66,12 @@ class Meta: "parent__remote_id": ["exact", "in"], "status": ["exact", "in"], } + + +class ExportTemplateFilter(filters.FilterSet): + class Meta: + model = ExportTemplate + fields = { + "fsp": ["exact"], + "config_key": ["exact"], + } diff --git a/src/hope_payment_gateway/api/fsp/serializers.py b/src/hope_payment_gateway/api/fsp/serializers.py index 4387f0c..0c108e4 100644 --- a/src/hope_payment_gateway/api/fsp/serializers.py +++ b/src/hope_payment_gateway/api/fsp/serializers.py @@ -4,6 +4,7 @@ from hope_payment_gateway.apps.gateway.models import ( DeliveryMechanism, + ExportTemplate, FinancialServiceProvider, FinancialServiceProviderConfig, PaymentInstruction, @@ -179,3 +180,11 @@ class Meta: "payload", "payout_amount", ) + + +class ExportTemplateSerializer(serializers.ModelSerializer): + fsp = serializers.PrimaryKeyRelatedField(queryset=FinancialServiceProvider.objects.all()) + + class Meta: + model = ExportTemplate + fields = ("query", "fsp", "config_key") diff --git a/src/hope_payment_gateway/api/fsp/views.py b/src/hope_payment_gateway/api/fsp/views.py index d11f6d7..27b8e6d 100644 --- a/src/hope_payment_gateway/api/fsp/views.py +++ b/src/hope_payment_gateway/api/fsp/views.py @@ -8,6 +8,7 @@ from hope_api_auth.views import LoggingAPIViewSet from hope_payment_gateway.api.fsp.filters import ( DeliveryMechanismFilter, + ExportTemplateFilter, FinancialServiceProviderConfigFilter, FinancialServiceProviderFilter, PaymentInstructionFilter, @@ -15,6 +16,7 @@ ) from hope_payment_gateway.api.fsp.serializers import ( DeliveryMechanismSerializer, + ExportTemplateSerializer, FinancialServiceProviderConfigSerializer, FinancialServiceProviderSerializer, PaymentInstructionSerializer, @@ -23,8 +25,11 @@ ) from hope_payment_gateway.apps.core.models import System from hope_payment_gateway.apps.fsp.western_union.endpoints.cancel import cancel +from hope_payment_gateway.apps.gateway.actions import TemplateExportForm, export_as_template, export_as_template_impl +from hope_payment_gateway.apps.gateway.admin import PaymentInstructionAdmin from hope_payment_gateway.apps.gateway.models import ( DeliveryMechanism, + ExportTemplate, FinancialServiceProvider, FinancialServiceProviderConfig, PaymentInstruction, @@ -131,6 +136,24 @@ def add_records(self, request, remote_id=None): } return Response({"remote_id": obj.remote_id, "errors": error_dict}, status=HTTP_400_BAD_REQUEST) + @action(detail=True) # , methods=["post"]) + def download(self, request, remote_id=None): + + obj = self.get_object() + try: + dm = DeliveryMechanism.objects.get(code=obj.extra.get("delivery_mechanism", None)) + export = ExportTemplate.objects.get( + fsp=obj.fsp, config_key=obj.extra.get("config_key", None), delivery_mechanism=dm + ) + queryset = PaymentRecord.objects.filter(parent=obj).select_related("parent__fsp") + + return export_as_template_impl( + queryset, + export.query.split("\r\n"), + ) + except ExportTemplate.DoesNotExist as exc: + return Response({"status_error": str(exc)}, status=HTTP_400_BAD_REQUEST) + class PaymentRecordViewSet(ProtectedMixin, LoggingAPIViewSet): serializer_class = PaymentRecordSerializer @@ -152,3 +175,10 @@ def cancel(self, request): return Response({"message": "cancel triggered"}) except TransitionNotAllowed as exc: return Response({"status_error": str(exc)}, status=HTTP_400_BAD_REQUEST) + + +class ExportTemplateViewSet(ProtectedMixin, LoggingAPIViewSet): + serializer_class = ExportTemplateSerializer + queryset = ExportTemplate.objects.select_related("fsp") + filterset_class = ExportTemplateFilter + search_fields = ("config_key",) diff --git a/src/hope_payment_gateway/api/urls.py b/src/hope_payment_gateway/api/urls.py index 1a5ad21..4aee38a 100644 --- a/src/hope_payment_gateway/api/urls.py +++ b/src/hope_payment_gateway/api/urls.py @@ -11,6 +11,7 @@ router.register(r"fsp", views.FinancialServiceProviderViewSet, basename="fsp") router.register(r"payment_instructions", views.PaymentInstructionViewSet, basename="payment-instruction") router.register(r"payment_records", views.PaymentRecordViewSet, basename="payment-record") +router.register(r"export_templates", views.ExportTemplateViewSet, basename="export-template") router.register(r"config", views.ConfigurationViewSet, basename="config") router.register(r"wu/corridors", wu_views.CorridorViewSet, basename="wu-corridor") diff --git a/src/hope_payment_gateway/apps/core/permissions.py b/src/hope_payment_gateway/apps/core/permissions.py index 4c67ea4..0150df8 100644 --- a/src/hope_payment_gateway/apps/core/permissions.py +++ b/src/hope_payment_gateway/apps/core/permissions.py @@ -1,5 +1,3 @@ -from django.conf import settings - from constance import config from rest_framework import permissions diff --git a/src/hope_payment_gateway/apps/fsp/western_union/apps.py b/src/hope_payment_gateway/apps/fsp/western_union/apps.py index cd67378..be32221 100644 --- a/src/hope_payment_gateway/apps/fsp/western_union/apps.py +++ b/src/hope_payment_gateway/apps/fsp/western_union/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig as BaseAppConfig -from hope_payment_gateway.apps.gateway.registry import registry +from hope_payment_gateway.apps.gateway.registry import export_registry, registry class AppConfig(BaseAppConfig): @@ -8,8 +8,8 @@ class AppConfig(BaseAppConfig): verbose_name = "Western Union" def ready(self) -> None: - from .handlers import WesternUnionHandler + from .handlers import CSVExportStrategy, WesternUnionHandler registry.register(WesternUnionHandler) - + export_registry.register(CSVExportStrategy) from . import tasks # noqa diff --git a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/nis.py b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/nis.py index 5839f1c..c2f0401 100644 --- a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/nis.py +++ b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/nis.py @@ -2,7 +2,6 @@ import sentry_sdk from django_fsm import TransitionNotAllowed -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.status import HTTP_400_BAD_REQUEST from rest_framework.views import APIView diff --git a/src/hope_payment_gateway/apps/fsp/western_union/handlers.py b/src/hope_payment_gateway/apps/fsp/western_union/handlers.py index 690c171..133fe56 100644 --- a/src/hope_payment_gateway/apps/fsp/western_union/handlers.py +++ b/src/hope_payment_gateway/apps/fsp/western_union/handlers.py @@ -17,3 +17,9 @@ def get_configuration(self, config_key, delivery_mechanism): except FinancialServiceProviderConfig.DoesNotExist: config = wu.configuration return config + + +class CSVExportStrategy(FSPProcessor): + + def export(self): + pass diff --git a/src/hope_payment_gateway/apps/gateway/admin.py b/src/hope_payment_gateway/apps/gateway/admin.py index af815d2..49e68d1 100644 --- a/src/hope_payment_gateway/apps/gateway/admin.py +++ b/src/hope_payment_gateway/apps/gateway/admin.py @@ -27,6 +27,7 @@ from hope_payment_gateway.apps.gateway.actions import TemplateExportForm, export_as_template, export_as_template_impl from hope_payment_gateway.apps.gateway.models import ( DeliveryMechanism, + ExportTemplate, FinancialServiceProvider, FinancialServiceProviderConfig, PaymentInstruction, @@ -262,3 +263,9 @@ class DeliveryMechanismAdmin(ExtraButtonsMixin, admin.ModelAdmin): "name", ) search_fields = ("code", "name") + + +@admin.register(ExportTemplate) +class ExportTemplateAdmin(ExtraButtonsMixin, admin.ModelAdmin): + list_display = ("fsp", "config_key", "delivery_mechanism") + search_fields = ("config_key", "delivery_mechanism__name") diff --git a/src/hope_payment_gateway/apps/gateway/migrations/0019_exporttemplate.py b/src/hope_payment_gateway/apps/gateway/migrations/0019_exporttemplate.py new file mode 100644 index 0000000..448a38e --- /dev/null +++ b/src/hope_payment_gateway/apps/gateway/migrations/0019_exporttemplate.py @@ -0,0 +1,51 @@ +# Generated by Django 5.0.4 on 2024-06-10 05:47 + +import django.db.models.deletion +import strategy_field.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("gateway", "0018_alter_financialserviceproviderconfig_delivery_mechanism"), + ] + + operations = [ + migrations.CreateModel( + name="ExportTemplate", + fields=[ + ("id", models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("query", models.TextField()), + ("config_key", models.CharField(max_length=32)), + ("strategy", strategy_field.fields.StrategyField()), + ("header", models.BooleanField(default=True)), + ("delimiter", models.CharField(choices=[(",", ","), (";", ";"), ("|", "|"), (":", ":")], default=",")), + ("quotechar", models.CharField(choices=[("'", "'"), ('"', '"'), ("`", "`")], default="'")), + ( + "quoting", + models.IntegerField( + choices=[(1, "All"), (0, "Minimal"), (3, "None"), (2, "Non Numeric")], default=1 + ), + ), + ("escapechar", models.CharField(blank=True, choices=[("", ""), ("\\", "\\")], default="", null=True)), + ( + "delivery_mechanism", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="template", + to="gateway.deliverymechanism", + ), + ), + ( + "fsp", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="gateway.financialserviceprovider" + ), + ), + ], + options={ + "unique_together": {("fsp", "config_key")}, + }, + ), + ] diff --git a/src/hope_payment_gateway/apps/gateway/models.py b/src/hope_payment_gateway/apps/gateway/models.py index 26a6a63..a9b8bcf 100644 --- a/src/hope_payment_gateway/apps/gateway/models.py +++ b/src/hope_payment_gateway/apps/gateway/models.py @@ -1,11 +1,15 @@ +import csv + from django.db import models +from django.utils.translation import gettext_lazy as _ +from adminactions.api import delimiters, quotes from django_fsm import FSMField, transition from model_utils.models import TimeStampedModel from strategy_field.fields import StrategyField from hope_payment_gateway.apps.core.models import System -from hope_payment_gateway.apps.gateway.registry import registry +from hope_payment_gateway.apps.gateway.registry import export_registry, registry class DeliveryMechanism(TimeStampedModel): @@ -191,3 +195,31 @@ def cancel(self): @transition(field=status, source="*", target=ERROR, permission="western_union.change_paymentrecordlog") def fail(self): pass + + +class ExportTemplate(models.Model): + query = models.TextField() + fsp = models.ForeignKey(FinancialServiceProvider, on_delete=models.CASCADE) + config_key = models.CharField(max_length=32) + delivery_mechanism = models.ForeignKey(DeliveryMechanism, on_delete=models.CASCADE, related_name="template") + strategy = StrategyField(registry=export_registry) + + header = models.BooleanField(default=True) + delimiter = models.CharField(choices=list(zip(delimiters, delimiters)), default=",") + quotechar = models.CharField(choices=list(zip(quotes, quotes)), default="'") + quoting = models.IntegerField( + choices=( + (csv.QUOTE_ALL, _("All")), + (csv.QUOTE_MINIMAL, _("Minimal")), + (csv.QUOTE_NONE, _("None")), + (csv.QUOTE_NONNUMERIC, _("Non Numeric")), + ), + default=csv.QUOTE_ALL, + ) + escapechar = models.CharField(choices=(("", ""), ("\\", "\\")), default="", null=True, blank=True) + + def __str__(self): + return f"{self.fsp} / {self.config_key}" + + class Meta: + unique_together = ("fsp", "config_key") diff --git a/src/hope_payment_gateway/apps/gateway/registry.py b/src/hope_payment_gateway/apps/gateway/registry.py index 0cb7089..e5fe782 100644 --- a/src/hope_payment_gateway/apps/gateway/registry.py +++ b/src/hope_payment_gateway/apps/gateway/registry.py @@ -12,8 +12,9 @@ def notify(self): pass -registry = Registry(FSPProcessor) - - class DefaultProcessor(FSPProcessor): pass + + +registry = Registry(FSPProcessor) +export_registry = Registry(FSPProcessor) From 403e256c5e78bbfedbe1d79619c4f4c5b9c4888d Mon Sep 17 00:00:00 2001 From: Domenico Date: Mon, 10 Jun 2024 20:22:58 -0600 Subject: [PATCH 2/3] 203936 race condition (#62) --- .../apps/fsp/western_union/endpoints/send_money.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py index a434533..f4ffb93 100644 --- a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py +++ b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py @@ -141,12 +141,13 @@ def send_money(hope_payload): pr = PaymentRecord.objects.get(record_code=record_code, status=PaymentRecord.PENDING) except PaymentRecord.DoesNotExist: return None - try: payload = create_validation_payload(hope_payload) response = send_money_validation(payload) + pr.refresh_from_db() if response["code"] != 200: pr.message = f"Validation failed: {response['error']}" + pr.success = False pr.save() return pr smv_payload = serialize_object(response["content"]) @@ -185,6 +186,7 @@ def send_money(hope_payload): payload[key] = value response = send_money_store(payload) + pr.refresh_from_db() if response["code"] == 200: pr.message, pr.success = "Send Money Store: Success", True pr.store() From 9c0b0742faf69454c1895b9b6a0573fe15e094b7 Mon Sep 17 00:00:00 2001 From: Domenico DiNicola Date: Mon, 10 Jun 2024 18:51:36 -0600 Subject: [PATCH 3/3] logs --- .../apps/fsp/western_union/endpoints/cancel.py | 15 +++++++++++++++ .../fsp/western_union/endpoints/send_money.py | 16 ++++++++++++---- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/cancel.py b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/cancel.py index 87640dc..00fd3a0 100644 --- a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/cancel.py +++ b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/cancel.py @@ -71,3 +71,18 @@ def cancel(pk): pr.extra_data.update(extra_data) pr.save() return pr + + +def reset_mtcns(mtcns): + frm = {"counter_id": "US125QCUSD8P", "identifier": "WGQCUS1250P", "reference_no": "RCPT-7050-24-0.198.578"} + for mtcn in mtcns: + response = search_request(frm, mtcn) + payload = response["content"] + try: + database_key = payload["payment_transactions"]["payment_transaction"][0]["money_transfer_key"] + except TypeError: + database_key = None + print("ERROR", mtcn) + response = cancel_request(frm, mtcn, database_key) + if response["code"] != 200: + print(response["code"], mtcn) diff --git a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py index 9fd2cd8..112a8a6 100644 --- a/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py +++ b/src/hope_payment_gateway/apps/fsp/western_union/endpoints/send_money.py @@ -1,6 +1,7 @@ import random import phonenumbers +import sentry_sdk from constance import config from django_fsm import TransitionNotAllowed from phonenumbers.phonenumberutil import NumberParseException @@ -38,10 +39,10 @@ def create_validation_payload(hope_payload): receiver = { "name": { - # "first_name": hope_payload["first_name"], - # "last_name": hope_payload["last_name"], - "first_name": str(hope_payload["first_name"].encode("utf-8"))[2:-1], - "last_name": str(hope_payload["last_name"].encode("utf-8"))[2:-1], + "first_name": hope_payload["first_name"], + "last_name": hope_payload["last_name"], + # "first_name": str(hope_payload["first_name"].encode("utf-8"))[2:-1], + # "last_name": str(hope_payload["last_name"].encode("utf-8"))[2:-1], "name_type": "D", }, "contact_phone": phone_number, @@ -122,6 +123,8 @@ def create_validation_payload(hope_payload): def send_money_validation(payload): wu_env = config.WESTERN_UNION_WHITELISTED_ENV client = WesternUnionClient("SendMoneyValidation_Service_H2HService.wsdl") + sentry_sdk.capture_message("Western Union: Send Money Validation") + print(payload["foreign_remote_number"].get("reference_no", None)) return client.response_context( "sendmoneyValidation", payload, "SendmoneyValidation_Service_H2H", f"SOAP_HTTP_Port_{wu_env}" ) @@ -130,6 +133,8 @@ def send_money_validation(payload): def send_money_store(payload): wu_env = config.WESTERN_UNION_WHITELISTED_ENV client = WesternUnionClient("SendMoneyStore_Service_H2HService.wsdl") + sentry_sdk.capture_message("Western Union: Send Money Store") + print(payload.get("mtcn", None)) return client.response_context( "SendMoneyStore_H2H", payload, "SendMoneyStore_Service_H2H", f"SOAP_HTTP_Port_{wu_env}" ) @@ -145,8 +150,10 @@ def send_money(hope_payload): try: payload = create_validation_payload(hope_payload) response = send_money_validation(payload) + pr.refresh_from_db() if response["code"] != 200: pr.message = f"Validation failed: {response['error']}" + pr.success = False pr.save() return pr smv_payload = serialize_object(response["content"]) @@ -185,6 +192,7 @@ def send_money(hope_payload): payload[key] = value response = send_money_store(payload) + pr.refresh_from_db() if response["code"] == 200: pr.message, pr.success = "Send Money Store: Success", True pr.store()