diff --git a/src/hope_payment_gateway/apps/fsp/moneygram/auth.py b/src/hope_payment_gateway/apps/fsp/moneygram/auth.py deleted file mode 100644 index a4c8ea2..0000000 --- a/src/hope_payment_gateway/apps/fsp/moneygram/auth.py +++ /dev/null @@ -1,112 +0,0 @@ -import base64 -import json -import logging - -from django.conf import settings - -import requests -from urllib3.connectionpool import HTTPSConnectionPool - -from hope_payment_gateway.apps.core.models import Singleton - -logger = logging.getLogger(__name__) - - -class PayloadMissingKey(Exception): - pass - - -class MoneyGramClient(metaclass=Singleton): - token = "" - expires_in = None - token_response = None - - def __init__(self): - self.get_token() - - def get_token(self): - url = settings.MONEYGRAM_HOST + "/oauth/accesstoken?grant_type=client_credentials" - credentials = f"{settings.MONEYGRAM_CLIENT_ID}:{settings.MONEYGRAM_CLIENT_SECRET}" - encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8") - headers = {"Content-Type": "application/json", "Authorization": "Basic " + encoded_credentials} - - try: - response = requests.get(url, headers=headers) - except HTTPSConnectionPool: - self.token = None - self.token_response = None - else: - if response.status_code == 200: - parsed_response = json.loads(response.text) - self.token = parsed_response["access_token"] - self.expires_in = parsed_response["expires_in"] - else: - logger.warning("Invalid token") - self.token = None - self.token_response = response - - def create_transaction(self, hope_payload): - - if self.token: - - url = settings.MONEYGRAM_HOST + "/disbursement/v1/transactions" - - for key in [ - "first_name", - "last_name", - "amount", - "destination_country", - "destination_currency", - "payment_record_code", - ]: - if not (key in hope_payload.keys() and hope_payload[key]): - raise PayloadMissingKey("InvalidPayload: {} is missing in the payload".format(key)) - - headers = { - "Content-Type": "application/json", - "X-MG-ClientRequestId": hope_payload["payment_record_code"], - "Authorization": "Bearer " + self.token, - } - - payload = { - "agentPartnerId": settings.MONEYGRAM_PARTNER_ID, - "targetAudience": "AGENT_FACING", - "userLanguage": "en-US", - "destinationCountryCode": "USA", - "destinationCountrySubdivisionCode": "US-MN", - "serviceOptionCode": "WILL_CALL", - "sendAmount": {"currencyCode": hope_payload["origination_currency"], "value": hope_payload["amount"]}, - "receiveCurrencyCode": hope_payload["destination_currency"], - "initiator": {"method": "batch", "userId": "abc", "userType": "consumer"}, - "autoCommit": True, - "receiver": { - "name": { - "firstName": hope_payload["first_name"], - "middleName": hope_payload.get("middle_name", ""), - "lastName": hope_payload["last_name"], - "secondLastName": "", - } - }, - } - response = self.perform_request(url, headers, payload) - return response - - else: - return self.token_response - - def perform_request(self, url, headers, payload=None): - try: - response = requests.post(url, json=payload, headers=headers) - - if response.status_code == 200: - parsed_response = json.dumps(json.loads(response.text), indent=2) - print(parsed_response) - else: - print("Request failed with status code:", response.status_code) - print(json.dumps(json.loads(response.text), indent=2)) - - except (requests.exceptions.RequestException, requests.exceptions.MissingSchema) as e: - print("An error occurred:", e) - response = None - - return response diff --git a/src/hope_payment_gateway/apps/fsp/moneygram/client.py b/src/hope_payment_gateway/apps/fsp/moneygram/client.py new file mode 100644 index 0000000..fb69b92 --- /dev/null +++ b/src/hope_payment_gateway/apps/fsp/moneygram/client.py @@ -0,0 +1,181 @@ +import base64 +import json +import logging + +from django.conf import settings + +import phonenumbers +import requests +from phonenumbers import NumberParseException +from urllib3.connectionpool import HTTPSConnectionPool + +from hope_payment_gateway.apps.core.models import Singleton +from hope_payment_gateway.apps.gateway.flows import PaymentRecordFlow +from hope_payment_gateway.apps.gateway.models import PaymentRecord + +logger = logging.getLogger(__name__) + + +MONEYGRAM_DM_MAPPING = { + "WILL_CALL": "WILL_CALL", + "DIRECT_TO_ACCT": "DIRECT_TO_ACCT", + "BANK_DEPOSIT": "DIRECT_TO_ACCT", +} + + +class PayloadMissingKey(Exception): + pass + + +class MoneyGramClient(metaclass=Singleton): + token = "" + expires_in = None + token_response = None + + def __init__(self): + self.get_token() + + def get_token(self): + url = settings.MONEYGRAM_HOST + "/oauth/accesstoken?grant_type=client_credentials" + credentials = f"{settings.MONEYGRAM_CLIENT_ID}:{settings.MONEYGRAM_CLIENT_SECRET}" + encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode("utf-8") + headers = {"Content-Type": "application/json", "Authorization": "Basic " + encoded_credentials} + + try: + response = requests.get(url, headers=headers) + except HTTPSConnectionPool: + self.token = None + self.token_response = None + else: + if response.status_code == 200: + parsed_response = json.loads(response.text) + self.token = parsed_response["access_token"] + self.expires_in = parsed_response["expires_in"] + else: + logger.warning("Invalid token") + self.token = None + self.token_response = response + + def prepare_transaction(self, hope_payload): + + raw_phone_no = hope_payload.get("phone_no", "N/A") + try: + phone_no = phonenumbers.parse(raw_phone_no, None) + phone_number = phone_no.national_number + country_code = phone_no.country_code + except NumberParseException: + phone_number = raw_phone_no + country_code = None + + for key in [ + "first_name", + "last_name", + "amount", + "destination_country", + "destination_currency", + "payment_record_code", + ]: + if not (key in hope_payload.keys() and hope_payload[key]): + raise PayloadMissingKey("InvalidPayload: {} is missing in the payload".format(key)) + + return { + "targetAudience": "AGENT_FACING", + "agentPartnerId": settings.MONEYGRAM_PARTNER_ID, + "userLanguage": "en-US", + "destinationCountryCode": hope_payload["destination_country"], + # "destinationCountrySubdivisionCode": "US-NY", + "receiveCurrencyCode": hope_payload["destination_currency"], + "serviceOptionCode": hope_payload["delivery_services_code", "WILL_CALL"], + # "serviceOptionRoutingCode": "74261037", # TODO + "autoCommit": "true", + "sendAmount": {"currencyCode": hope_payload["origination_currency"], "value": hope_payload["amount"]}, + "sender": { + "business": { + "businessName": "UNICEF", + "legalEntityName": "UNICEF", + "businessType": "ACCOMMODATION_HOTELS", + "businessRegistrationNumber": settings.MONEYGRAM_REGISTRATION_NUMBER, + "businessIssueDate": "2024-04-29", + "businessCountryOfRegistration": "USA", + "address": { + "line1": "3 United Nations Plaza", + "city": "NEW YORK", + "countrySubdivisionCode": "US-NY", + "countryCode": "USA", + "postalCode": 10017, + }, + "contactDetails": {"phone": {"number": 2123267000, "countryDialCode": 1}}, + } + }, + "beneficiary": { + "consumer": { + "name": { + "firstName": hope_payload["first_name"], + "middleName": hope_payload.get("middle_name", ""), + "lastName": hope_payload["last_name"], + }, + "address": { + "line1": hope_payload.get("address", "Via di Acilia"), + "city": hope_payload.get("city", "Roma"), + "countryCode": hope_payload["destination_country"], + "postalCode": 55442, + }, + "mobilePhone": {"number": phone_number, "countryDialCode": country_code}, + } + }, + } + + def create_transaction(self, hope_payload): + + if self.token: + + url = settings.MONEYGRAM_HOST + "/disbursement/v1/transactions" + payload = self.prepare_transaction(hope_payload) + headers = { + "Content-Type": "application/json", + "X-MG-ClientRequestId": hope_payload["payment_record_code"], + "Authorization": "Bearer " + self.token, + } + + response = self.perform_request(url, headers, payload) + self.transaction_callback(hope_payload, response.json()) + return response + + else: + return self.token_response + + def perform_request(self, url, headers, payload=None): + try: + response = requests.post(url, json=payload, headers=headers) + + if response.status_code == 200: + parsed_response = json.dumps(json.loads(response.text), indent=2) + print(parsed_response) + else: + print("Request failed with status code:", response.status_code) + print(json.dumps(json.loads(response.text), indent=2)) + + except (requests.exceptions.RequestException, requests.exceptions.MissingSchema) as e: + print("An error occurred:", e) + response = dict + + return response + + def transaction_callback(self, hope_payload, response): + record_code = hope_payload["payment_record_code"] + pr = PaymentRecord.objects.get(record_code=record_code) + pr.fsp_code = response["referenceNumber"] + pr.success = True + pr.payout_amount = response["receiveAmount"]["amount"]["value"] + pr.extra_data.update( + { + "fee": response["receiveAmount"]["fees"]["value"], + "fee_currency": response["receiveAmount"]["fees"]["currencyCode"], + "taxes": response["receiveAmount"]["taxes"]["value"], + "taxes_currency": response["receiveAmount"]["taxes"]["currencyCode"], + "expectedPayoutDate": response["expectedPayoutDate"], + "transactionId": response["transactionId"], + } + ) + flow = PaymentRecordFlow(pr) + flow.store() diff --git a/src/hope_payment_gateway/apps/fsp/moneygram/tasks.py b/src/hope_payment_gateway/apps/fsp/moneygram/tasks.py index 915babc..bec55f9 100644 --- a/src/hope_payment_gateway/apps/fsp/moneygram/tasks.py +++ b/src/hope_payment_gateway/apps/fsp/moneygram/tasks.py @@ -3,7 +3,7 @@ from constance import config -from hope_payment_gateway.apps.fsp.moneygram.auth import MoneyGramClient +from hope_payment_gateway.apps.fsp.moneygram.client import MoneyGramClient from hope_payment_gateway.apps.gateway.models import ( FinancialServiceProvider, PaymentInstruction, diff --git a/src/hope_payment_gateway/apps/gateway/admin.py b/src/hope_payment_gateway/apps/gateway/admin.py index 42a5120..fb9c074 100644 --- a/src/hope_payment_gateway/apps/gateway/admin.py +++ b/src/hope_payment_gateway/apps/gateway/admin.py @@ -16,7 +16,7 @@ from adminfilters.autocomplete import AutoCompleteFilter from adminfilters.mixin import AdminFiltersMixin -from hope_payment_gateway.apps.fsp.moneygram.auth import MoneyGramClient +from hope_payment_gateway.apps.fsp.moneygram.client import MoneyGramClient, PayloadMissingKey from hope_payment_gateway.apps.fsp.western_union.endpoints.cancel import cancel, search_request from hope_payment_gateway.apps.fsp.western_union.endpoints.client import WesternUnionClient from hope_payment_gateway.apps.fsp.western_union.endpoints.send_money import ( @@ -76,16 +76,16 @@ def get_queryset(self, request): @choice(change_list=False) def western_union(self, button): button.choices = [ - self.prepare_payload, - self.send_money_validation, - self.send_money, - self.search_request, - self.cancel, + self.wu_prepare_payload, + self.wu_send_money_validation, + self.wu_send_money, + self.wu_search_request, + self.wu_cancel, ] return button - @view(html_attrs={"style": "background-color:#88FF88;color:black"}) - def prepare_payload(self, request, pk) -> TemplateResponse: + @view(html_attrs={"style": "background-color:#88FF88;color:black"}, label="Prepare Payload") + def wu_prepare_payload(self, request, pk) -> TemplateResponse: context = self.get_common_context(request, pk) obj = PaymentRecord.objects.get(pk=pk) payload = obj.get_payload() @@ -93,16 +93,16 @@ def prepare_payload(self, request, pk) -> TemplateResponse: payload = create_validation_payload(payload) client = WesternUnionClient("SendMoneyValidation_Service_H2HService.wsdl") _, data = client.prepare("sendmoneyValidation", payload) - context["title"] = "Payload" + context["title"] = "Western Union Payload" context["content"] = data return TemplateResponse(request, "western_union.html", context) - except (PayloadException, InvalidCorridor) as e: + except (PayloadException, InvalidCorridor, PayloadMissingKey) as e: messages.add_message(request, messages.ERROR, str(e)) return obj - @view(html_attrs={"style": "background-color:#88FF88;color:black"}) - def send_money_validation(self, request, pk) -> TemplateResponse: + @view(html_attrs={"style": "background-color:#88FF88;color:black"}, label="Send Money Validation") + def wu_send_money_validation(self, request, pk) -> TemplateResponse: context = self.get_common_context(request, pk) obj = PaymentRecord.objects.get(pk=pk) payload = obj.get_payload() @@ -115,8 +115,8 @@ def send_money_validation(self, request, pk) -> TemplateResponse: messages.add_message(request, messages.ERROR, str(e)) return obj - @view(html_attrs={"style": "background-color:#88FF88;color:black"}) - def send_money(self, request, pk) -> TemplateResponse: + @view(html_attrs={"style": "background-color:#88FF88;color:black"}, label="Send Money") + def wu_send_money(self, request, pk) -> TemplateResponse: obj = PaymentRecord.objects.get(pk=pk) log = send_money(obj.get_payload()) if log is None: @@ -125,8 +125,8 @@ def send_money(self, request, pk) -> TemplateResponse: loglevel = messages.SUCCESS if log.success else messages.ERROR messages.add_message(request, loglevel, log.message) - @view(html_attrs={"style": "background-color:yellow;color:blue"}) - def search_request(self, request, pk) -> TemplateResponse: + @view(html_attrs={"style": "background-color:yellow;color:blue"}, label="Search Request") + def wu_search_request(self, request, pk) -> TemplateResponse: context = self.get_common_context(request, pk) obj = PaymentRecord.objects.get(pk=pk) if mtcn := obj.extra_data.get("mtcn", None): @@ -136,8 +136,8 @@ def search_request(self, request, pk) -> TemplateResponse: return TemplateResponse(request, "western_union.html", context) messages.warning(request, "Missing MTCN") - @view(html_attrs={"style": "background-color:#88FF88;color:black"}) - def cancel(self, request, pk) -> TemplateResponse: + @view(html_attrs={"style": "background-color:#88FF88;color:black"}, label="Cancel") + def wu_cancel(self, request, pk) -> TemplateResponse: context = self.get_common_context(request, pk) obj = PaymentRecord.objects.get(pk=pk) if mtcn := obj.extra_data.get("mtcn", None): @@ -146,15 +146,22 @@ def cancel(self, request, pk) -> TemplateResponse: loglevel = messages.SUCCESS if log.success else messages.ERROR messages.add_message(request, loglevel, log.message) - @choice(change_list=False) - def moneygram(self, button): - button.choices = [ - self.create_transaction, - ] - return button + @view(html_attrs={"style": "background-color:#88FF88;color:black"}, label="Prepare Payload") + def mg_prepare_payload(self, request, pk) -> TemplateResponse: + context = self.get_common_context(request, pk) + obj = PaymentRecord.objects.get(pk=pk) + try: + client = MoneyGramClient() + context["title"] = "Moneygram Payload" + context["content"] = client.prepare_transaction(obj.get_payload()) + return TemplateResponse(request, "western_union.html", context) - @view(html_attrs={"style": "background-color:#88FF88;color:black"}) - def create_transaction(self, request, pk) -> TemplateResponse: + except (PayloadException, InvalidCorridor, PayloadMissingKey) as e: + messages.add_message(request, messages.ERROR, str(e)) + return obj + + @view(html_attrs={"style": "background-color:#88FF88;color:black"}, label="Create Transaction") + def mg_create_transaction(self, request, pk) -> TemplateResponse: obj = PaymentRecord.objects.get(pk=pk) client = MoneyGramClient() @@ -175,7 +182,7 @@ def create_transaction(self, request, pk) -> TemplateResponse: msgs = [ "Error", ] - elif resp.status_code >= 500: + else: loglevel = messages.ERROR for error in data["errors"]: msgs.append(f"{error['message']} ({error['code']})") @@ -183,6 +190,14 @@ def create_transaction(self, request, pk) -> TemplateResponse: for msg in msgs: messages.add_message(request, loglevel, msg) + @choice(change_list=False) + def moneygram(self, button): + button.choices = [ + self.mg_prepare_payload, + self.mg_create_transaction, + ] + return button + @link() def instruction(self, button: button) -> Optional[str]: if "original" in button.context: diff --git a/src/hope_payment_gateway/config/fragments/moneygram.py b/src/hope_payment_gateway/config/fragments/moneygram.py index 577e5e8..f20d827 100644 --- a/src/hope_payment_gateway/config/fragments/moneygram.py +++ b/src/hope_payment_gateway/config/fragments/moneygram.py @@ -7,3 +7,4 @@ MONEYGRAM_CLIENT_ID = env("MONEYGRAM_CLIENT_ID", default="") MONEYGRAM_CLIENT_SECRET = env("MONEYGRAM_CLIENT_SECRET", default="") MONEYGRAM_PARTNER_ID = env("MONEYGRAM_PARTNER_ID", default="") +MONEYGRAM_REGISTRATION_NUMBER = env("MONEYGRAM_REGISTRATION_NUMBER", default="") diff --git a/tests/moneygram/test_client.py b/tests/moneygram/test_client.py index d1a7a83..926b7f1 100644 --- a/tests/moneygram/test_client.py +++ b/tests/moneygram/test_client.py @@ -2,7 +2,7 @@ import responses -from hope_payment_gateway.apps.fsp.moneygram.auth import MoneyGramClient +from hope_payment_gateway.apps.fsp.moneygram.client import MoneyGramClient # from responses import _recorder # @_recorder.record(file_path="tests/moneygram/responses/token.yaml")