Skip to content

Commit

Permalink
moneygram
Browse files Browse the repository at this point in the history
  • Loading branch information
domdinicola committed Oct 15, 2024
1 parent 78beda5 commit 778a70e
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 88 deletions.
138 changes: 78 additions & 60 deletions src/hope_payment_gateway/apps/fsp/moneygram/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
import phonenumbers
import requests
from phonenumbers import NumberParseException
from urllib3.exceptions import PoolError
from requests.exceptions import ConnectionError

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__)

Expand All @@ -20,44 +18,69 @@
"WILL_CALL": "WILL_CALL",
"DIRECT_TO_ACCT": "DIRECT_TO_ACCT",
"BANK_DEPOSIT": "DIRECT_TO_ACCT",
"WILLCALL_TO": "WILLCALL_TO",
"2_HOUR": "2_HOUR",
"OVERNIGHT": "OVERNIGHT",
"OVERNIGHT2ANY": "OVERNIGHT2ANY",
"24_HOUR": "24_HOUR",
"CARD_DEPOSIT": "CARD_DEPOSIT",
"HOME_DELIVERY": "HOME_DELIVERY",
}


class PayloadMissingKey(Exception):
pass


class InvalidToken(Exception):
pass


class MoneyGramClient(metaclass=Singleton):
token = ""
expires_in = None
token_response = None

def __init__(self):
self.get_token()
self.set_token()

def get_token(self):
def set_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 PoolError:
parsed_response = json.loads(response.text)
except ConnectionError:
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
error = parsed_response["error"]
raise InvalidToken(f"{error['category']}: {error['message']} [{error['code']}]")

def prepare_transaction(self, hope_payload):
def get_headers(self, request_id):
return {
"Content-Type": "application/json",
"X-MG-ClientRequestId": request_id,
"content-type": "application/json",
"Authorization": "Bearer " + self.token,
}

@staticmethod
def get_basic_payload():
return {
"targetAudience": "AGENT_FACING",
"agentPartnerId": settings.MONEYGRAM_PARTNER_ID,
"userLanguage": "en-US",
}

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)
Expand All @@ -77,8 +100,8 @@ def prepare_transaction(self, hope_payload):
]:
if not (key in hope_payload.keys() and hope_payload[key]):
raise PayloadMissingKey("InvalidPayload: {} is missing in the payload".format(key))

return {
transaction_id = hope_payload["payment_record_code"]
payload = {
"targetAudience": "AGENT_FACING",
"agentPartnerId": settings.MONEYGRAM_PARTNER_ID,
"userLanguage": "en-US",
Expand Down Expand Up @@ -124,58 +147,53 @@ def prepare_transaction(self, hope_payload):
}
},
}
return transaction_id, payload

def create_transaction(self, hope_payload):

if self.token:
endpoint = "/disbursement/v1/transactions"
transaction_id, payload = self.prepare_transaction(hope_payload)
return self.perform_request(endpoint, transaction_id, payload)

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))
def prepare_quote(self, hope_payload: dict):

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(
transaction_id = hope_payload["payment_record_code"]
payload = self.get_basic_payload()
payload.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"],
"destinationCountryCode": hope_payload["destination_country"],
"serviceOptionCode": hope_payload.get("delivery_services_code", None),
"beneficiaryTypeCode": "Consumer",
"sendAmount": {"currencyCode": hope_payload["origination_currency"], "value": hope_payload["amount"]},
}
)
flow = PaymentRecordFlow(pr)
flow.store()
return transaction_id, payload

def quote(self, hope_payload):

endpoint = "/disbursement/v1/transactions/quote"
transaction_id, payload = self.prepare_quote(hope_payload)
return self.perform_request(endpoint, transaction_id, payload)

def perform_request(self, endpoint, transaction_id, payload):
url = settings.MONEYGRAM_HOST + endpoint
headers = self.get_headers(transaction_id)
for _ in range(2):
try:
response = requests.post(url, json=payload, headers=headers)
break
except (requests.exceptions.RequestException, requests.exceptions.MissingSchema) as e:
print("An error occurred:", e)
response = dict
break
except Exception as e:
print("Token Expired:", e)
self.set_token()

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))
return response
15 changes: 15 additions & 0 deletions src/hope_payment_gateway/apps/fsp/moneygram/handlers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from hope_payment_gateway.apps.gateway.models import FinancialServiceProvider, FinancialServiceProviderConfig
from hope_payment_gateway.apps.gateway.registry import FSPProcessor


class WesternUnionHandler(FSPProcessor):

def get_configuration(self, config_key, delivery_mechanism):
wu = FinancialServiceProvider.objects.get(vision_vendor_number="1900723202")
try:
config = FinancialServiceProviderConfig.objects.get(
key=config_key, fsp=wu, delivery_mechanism__code=delivery_mechanism
).configuration
except FinancialServiceProviderConfig.DoesNotExist:
config = wu.configuration
return config
41 changes: 41 additions & 0 deletions src/hope_payment_gateway/apps/fsp/moneygram/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from rest_framework.response import Response
from rest_framework.status import HTTP_400_BAD_REQUEST
from viewflow.fsm import TransitionNotAllowed

from hope_payment_gateway.apps.fsp.moneygram.client import MoneyGramClient
from hope_payment_gateway.apps.gateway.flows import PaymentRecordFlow
from hope_payment_gateway.apps.gateway.models import PaymentRecord


def quote_transaction(payload):
client = MoneyGramClient()
response = client.quote(payload)
return response


def create_transaction(payload):
client = MoneyGramClient()
response = client.create_transaction(payload)
if response:
body = response.json()
record_code = payload["payment_record_code"]
pr = PaymentRecord.objects.get(record_code=record_code)
pr.fsp_code = body["referenceNumber"]
pr.success = True
pr.payout_amount = body["receiveAmount"]["amount"]["value"]
pr.extra_data.update(
{
"fee": body["receiveAmount"]["fees"]["value"],
"fee_currency": body["receiveAmount"]["fees"]["currencyCode"],
"taxes": body["receiveAmount"]["taxes"]["value"],
"taxes_currency": body["receiveAmount"]["taxes"]["currencyCode"],
"expectedPayoutDate": body["expectedPayoutDate"],
"transactionId": body["transactionId"],
}
)
try:
flow = PaymentRecordFlow(pr)
flow.store()
except TransitionNotAllowed as e:
response = Response({"transition_not_allowed": str(e)}, status=HTTP_400_BAD_REQUEST)

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
return response
87 changes: 60 additions & 27 deletions src/hope_payment_gateway/apps/gateway/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
from adminfilters.autocomplete import AutoCompleteFilter
from adminfilters.mixin import AdminFiltersMixin

from hope_payment_gateway.apps.fsp.moneygram.client import MoneyGramClient, PayloadMissingKey
from hope_payment_gateway.apps.fsp.moneygram.client import InvalidToken, MoneyGramClient, PayloadMissingKey
from hope_payment_gateway.apps.fsp.moneygram.views import create_transaction, quote_transaction
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 (
Expand Down Expand Up @@ -93,6 +94,7 @@ def wu_prepare_payload(self, request, pk) -> TemplateResponse:
payload = create_validation_payload(payload)
client = WesternUnionClient("SendMoneyValidation_Service_H2HService.wsdl")
_, data = client.prepare("sendmoneyValidation", payload)

context["title"] = "Western Union Payload"
context["content"] = data
return TemplateResponse(request, "request.html", context)
Expand Down Expand Up @@ -153,48 +155,44 @@ def mg_prepare_payload(self, request, pk) -> TemplateResponse:
try:
client = MoneyGramClient()
context["title"] = "Moneygram Payload"
context["format"] = "json"
context["content"] = client.prepare_transaction(obj.get_payload())
return TemplateResponse(request, "request.html", context)

except (PayloadException, InvalidCorridor, PayloadMissingKey) as e:
messages.add_message(request, messages.ERROR, str(e))
return obj
except InvalidToken as e:
logger.error(e)
self.message_user(request, str(e), messages.ERROR)

@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()
resp = client.create_transaction(obj.get_payload())

data = resp.json()
msgs = []
if resp.status_code == 200:
loglevel = messages.SUCCESS
elif 400 <= resp.status_code < 500:
loglevel = messages.WARNING
if "errors" in resp.json():
for error in data["errors"]:
msgs.append(f"{error['message']} ({error['code']})")
elif "error" in resp.json():
msgs.append(resp.json()["error"]["message"])
else:
msgs = [
"Error",
]
else:
loglevel = messages.ERROR
for error in data["errors"]:
msgs.append(f"{error['message']} ({error['code']})")

for msg in msgs:
messages.add_message(request, loglevel, msg)
obj = PaymentRecord.objects.get(pk=pk)
try:
resp = create_transaction(obj.get_payload())
return self.handle_mg_response(request, resp, pk)
except InvalidToken as e:
logger.error(e)
self.message_user(request, str(e), messages.ERROR)

@view(html_attrs={"style": "background-color:#88FF88;color:black"}, label="Quote")
def mg_quote_transaction(self, request, pk) -> TemplateResponse:
obj = PaymentRecord.objects.get(pk=pk)
try:
resp = quote_transaction(obj.get_payload())
return self.handle_mg_response(request, resp, pk)
except InvalidToken as e:
logger.error(e)
self.message_user(request, str(e), messages.ERROR)

@choice(change_list=False)
def moneygram(self, button):
button.choices = [
self.mg_prepare_payload,
self.mg_create_transaction,
self.mg_quote_transaction,
]
return button

Expand All @@ -208,6 +206,41 @@ def instruction(self, button: button) -> Optional[str]:
button.visible = False
return None

def handle_mg_response(self, request, resp, pk):
if resp:
data = resp.json()
msgs = []
if resp.status_code == 200:
context = self.get_common_context(request, pk)
context["title"] = "Moneygram Quote"
context["format"] = "json"
context["content"] = resp.json()
return TemplateResponse(request, "request.html", context)

elif 400 <= resp.status_code < 500:
loglevel = messages.WARNING
if "errors" in data:
for error in data["errors"]:
msgs.append(f"{error['message']} ({error['code']})")
if "offendingFields" in error:
for field in error["offendingFields"]:
msgs.append(f"Field: {field['field']}")
elif "error" in data:
msgs.append(data["error"]["message"])
else:
msgs = [
"Error",
]
else:
loglevel = messages.ERROR
for error in data["errors"]:
msgs.append(f"{error['message']} ({error['code']})")

for msg in msgs:
messages.add_message(request, loglevel, msg)
else:
messages.add_message(request, messages.ERROR, "Connection Error")


@admin.register(PaymentInstruction)
class PaymentInstructionAdmin(ExtraButtonsMixin, admin.ModelAdmin):
Expand Down
1 change: 0 additions & 1 deletion tests/moneygram/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ def test_get_token():
# @_recorder.record(file_path="tests/moneygram/responses/transaction.yaml")
@responses.activate
@pytest.mark.django_db
@pytest.mark.skip
@override_settings(MONEYGRAM_HOST="https://sandboxapi.moneygram.com")
def test_create_transaction():
responses._add_from_file(file_path="tests/moneygram/responses/token.yaml")
Expand Down

0 comments on commit 778a70e

Please sign in to comment.