diff --git a/backend/hct_mis_api/apps/payment/migrations/0137_migration.py b/backend/hct_mis_api/apps/payment/migrations/0137_migration.py new file mode 100644 index 0000000000..7aa27d8283 --- /dev/null +++ b/backend/hct_mis_api/apps/payment/migrations/0137_migration.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.25 on 2024-07-10 14:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0136_migration'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='status', + field=models.CharField(choices=[('Distribution Successful', 'Distribution Successful'), ('Not Distributed', 'Not Distributed'), ('Transaction Successful', 'Transaction Successful'), ('Transaction Erroneous', 'Transaction Erroneous'), ('Force failed', 'Force failed'), ('Partially Distributed', 'Partially Distributed'), ('Pending', 'Pending'), ('Sent to Payment Gateway', 'Sent to Payment Gateway'), ('Sent to FSP', 'Sent to FSP'), ('Manually Cancelled', 'Manually Cancelled')], default='Pending', max_length=255), + ), + migrations.AlterField( + model_name='paymentrecord', + name='status', + field=models.CharField(choices=[('Distribution Successful', 'Distribution Successful'), ('Not Distributed', 'Not Distributed'), ('Transaction Successful', 'Transaction Successful'), ('Transaction Erroneous', 'Transaction Erroneous'), ('Force failed', 'Force failed'), ('Partially Distributed', 'Partially Distributed'), ('Pending', 'Pending'), ('Sent to Payment Gateway', 'Sent to Payment Gateway'), ('Sent to FSP', 'Sent to FSP'), ('Manually Cancelled', 'Manually Cancelled')], default='Pending', max_length=255), + ), + ] diff --git a/backend/hct_mis_api/apps/payment/services/payment_gateway.py b/backend/hct_mis_api/apps/payment/services/payment_gateway.py index 46d2724ee2..9c3dd645a6 100644 --- a/backend/hct_mis_api/apps/payment/services/payment_gateway.py +++ b/backend/hct_mis_api/apps/payment/services/payment_gateway.py @@ -105,6 +105,16 @@ class Meta: ] +class PaymentPayloadSerializer(serializers.Serializer): + amount = serializers.DecimalField(max_digits=12, decimal_places=2, required=True) + phone_no = serializers.CharField(required=False) + last_name = serializers.CharField(required=False) + first_name = serializers.CharField(required=False) + full_name = serializers.CharField(required=False) + destination_currency = serializers.CharField(required=True) + service_provider_code = serializers.CharField(required=False) + + class PaymentSerializer(ReadOnlyModelSerializer): remote_id = serializers.CharField(source="id") record_code = serializers.CharField(source="unicef_id") @@ -115,21 +125,20 @@ def get_extra_data(self, obj: Payment) -> Dict: return {} def get_payload(self, obj: Payment) -> Dict: - """ - amount: int # 120000 - phone_no: str # "78933211" - last_name: str # "Arabic" - first_name: str # "Angelina" - destination_currency: str # "USD" - """ - return { - "amount": obj.entitlement_quantity, - "phone_no": str(obj.collector.phone_no), - "last_name": obj.collector.family_name, - "first_name": obj.collector.given_name, - "full_name": obj.full_name, - "destination_currency": obj.currency, - } + payload = PaymentPayloadSerializer( + data={ + "amount": obj.entitlement_quantity, + "phone_no": str(obj.collector.phone_no), + "last_name": obj.collector.family_name, + "first_name": obj.collector.given_name, + "full_name": obj.full_name, + "destination_currency": obj.currency, + "service_provider_code": obj.collector.flex_fields.get("service_provider_code", ""), + } + ) + if not payload.is_valid(): + raise serializers.ValidationError(payload.errors) + return payload.data class Meta: model = Payment diff --git a/backend/hct_mis_api/apps/payment/tests/test_payment_gateway_service.py b/backend/hct_mis_api/apps/payment/tests/test_payment_gateway_service.py index 6f80290eea..a67c721e76 100644 --- a/backend/hct_mis_api/apps/payment/tests/test_payment_gateway_service.py +++ b/backend/hct_mis_api/apps/payment/tests/test_payment_gateway_service.py @@ -7,6 +7,7 @@ import pytest from pytz import utc +from rest_framework.exceptions import ValidationError from hct_mis_api.apps.account.fixtures import UserFactory from hct_mis_api.apps.core.base_test_case import APITestCase @@ -76,7 +77,12 @@ def setUpTestData(cls) -> None: ) cls.payments = [] for _ in range(2): - collector = IndividualFactory(household=None) + collector = IndividualFactory( + household=None, + flex_fields={ + "service_provider_code": "123456789", + }, + ) hoh = IndividualFactory(household=None) hh = HouseholdFactory(head_of_household=hoh) IndividualRoleInHouseholdFactory(household=hh, individual=hoh, role=ROLE_PRIMARY) @@ -411,3 +417,44 @@ def test_add_records_to_payment_instructions_error(self, add_records_to_payment_ self.assertEqual(self.payments[1].status, Payment.STATUS_ERROR) self.assertEqual(self.payments[0].reason_for_unsuccessful_payment, "Error") self.assertEqual(self.payments[1].reason_for_unsuccessful_payment, "Error") + + @mock.patch("hct_mis_api.apps.payment.services.payment_gateway.PaymentGatewayAPI._post") + def test_api_add_records_to_payment_instruction(self, post_mock: Any) -> None: + post_mock.return_value = { + "remote_id": "123", + "records": { + "1": self.payments[0].id, + }, + "errors": None, + } + PaymentGatewayAPI().add_records_to_payment_instruction([self.payments[0]], "123") + post_mock.assert_called_once_with( + "payment_instructions/123/add_records/", + [ + { + "remote_id": str(self.payments[0].id), + "record_code": self.payments[0].unicef_id, + "payload": { + "amount": str(self.payments[0].entitlement_quantity), + "phone_no": str(self.payments[0].collector.phone_no), + "last_name": self.payments[0].collector.family_name, + "first_name": self.payments[0].collector.given_name, + "full_name": self.payments[0].collector.full_name, + "destination_currency": self.payments[0].currency, + "service_provider_code": self.payments[0].collector.flex_fields["service_provider_code"], + }, + "extra_data": {}, + } + ], + validate_response=True, + ) + + @mock.patch("hct_mis_api.apps.payment.services.payment_gateway.PaymentGatewayAPI._post") + def test_api_add_records_to_payment_instruction_validation_error(self, post_mock: Any) -> None: + payment = self.payments[0] + payment.entitlement_quantity = None + payment.save() + with self.assertRaisesMessage( + ValidationError, "{'amount': [ErrorDetail(string='This field may not be null.', code='null')]}" + ): + PaymentGatewayAPI().add_records_to_payment_instruction([payment], "123")