diff --git a/backend/hct_mis_api/apps/core/tests/test_utils.py b/backend/hct_mis_api/apps/core/tests/test_utils.py
index 74ca8ab582..7b0f848b0a 100644
--- a/backend/hct_mis_api/apps/core/tests/test_utils.py
+++ b/backend/hct_mis_api/apps/core/tests/test_utils.py
@@ -19,7 +19,7 @@ def test_get_count_and_percentage(self) -> None:
def test_get_payment_delivered_quantity_status_and_value(self) -> None:
with self.assertRaisesMessage(Exception, "Invalid delivered quantity"):
- get_payment_delivered_quantity_status_and_value(None, Decimal(10.00)) # type: ignore
+ get_payment_delivered_quantity_status_and_value(None, Decimal(10.00))
with self.assertRaisesMessage(Exception, "Invalid delivered quantity"):
get_payment_delivered_quantity_status_and_value("", Decimal(10.00))
self.assertEqual(
diff --git a/backend/hct_mis_api/apps/payment/models.py b/backend/hct_mis_api/apps/payment/models.py
index 900b000798..dfe67acece 100644
--- a/backend/hct_mis_api/apps/payment/models.py
+++ b/backend/hct_mis_api/apps/payment/models.py
@@ -923,7 +923,11 @@ def is_reconciled(self) -> bool:
return (
self.eligible_payments.exclude(
- status__in=[GenericPayment.STATUS_PENDING, GenericPayment.STATUS_SENT_TO_PG]
+ status__in=[
+ GenericPayment.STATUS_PENDING,
+ GenericPayment.STATUS_SENT_TO_PG,
+ GenericPayment.STATUS_SENT_TO_FSP,
+ ]
).count()
== self.eligible_payments.count()
)
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..aa5f20e737 100644
--- a/backend/hct_mis_api/apps/payment/services/payment_gateway.py
+++ b/backend/hct_mis_api/apps/payment/services/payment_gateway.py
@@ -150,10 +150,9 @@ class PaymentRecordData(FlexibleArgumentsDataclassMixin):
record_code: str
parent: str
status: str
- hope_status: str
auth_code: str
- payout_amount: float
fsp_code: str
+ payout_amount: Optional[float] = None
message: Optional[str] = None
def get_hope_status(self, entitlement_quantity: Decimal) -> str:
@@ -163,9 +162,8 @@ def get_transferred_status_based_on_delivery_amount() -> str:
self.payout_amount, entitlement_quantity
)
except Exception:
- raise PaymentGatewayAPI.PaymentGatewayAPIException(
- f"Invalid delivered_quantity {self.payout_amount} for Payment {self.remote_id}"
- )
+ logger.error(f"Invalid delivered_quantity {self.payout_amount} for Payment {self.remote_id}")
+ _hope_status = Payment.STATUS_ERROR
return _hope_status
mapping = {
@@ -180,7 +178,8 @@ def get_transferred_status_based_on_delivery_amount() -> str:
hope_status = mapping.get(self.status)
if not hope_status:
- raise PaymentGatewayAPI.PaymentGatewayAPIException(f"Invalid Payment status: {self.status}")
+ logger.error(f"Invalid Payment status: {self.status}")
+ hope_status = Payment.STATUS_ERROR
return hope_status() if callable(hope_status) else hope_status
@@ -435,8 +434,16 @@ def update_payment(
_payment.fsp_auth_code = matching_pg_payment.auth_code
update_fields = ["status", "status_date", "fsp_auth_code"]
- if _payment.status not in Payment.ALLOW_CREATE_VERIFICATION and matching_pg_payment.message:
- _payment.reason_for_unsuccessful_payment = matching_pg_payment.message
+ if _payment.status in [
+ Payment.STATUS_ERROR,
+ Payment.STATUS_MANUALLY_CANCELLED,
+ ]:
+ if matching_pg_payment.message:
+ _payment.reason_for_unsuccessful_payment = matching_pg_payment.message
+ elif matching_pg_payment.payout_amount:
+ _payment.reason_for_unsuccessful_payment = f"Delivered amount: {matching_pg_payment.payout_amount}"
+ else:
+ _payment.reason_for_unsuccessful_payment = "Unknown error"
update_fields.append("reason_for_unsuccessful_payment")
delivered_quantity = matching_pg_payment.payout_amount
@@ -449,7 +456,7 @@ def update_payment(
try:
_payment.delivered_quantity = to_decimal(delivered_quantity)
_payment.delivered_quantity_usd = get_quantity_in_usd(
- amount=Decimal(delivered_quantity),
+ amount=Decimal(delivered_quantity), # type: ignore
currency=_payment_plan.currency,
exchange_rate=Decimal(_exchange_rate),
currency_exchange_date=_payment_plan.currency_exchange_date,
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..70c606fdeb 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
@@ -33,7 +33,6 @@
)
from hct_mis_api.apps.payment.services.payment_gateway import (
AddRecordsResponseData,
- PaymentGatewayAPI,
PaymentGatewayService,
PaymentInstructionStatus,
PaymentRecordData,
@@ -131,7 +130,6 @@ def test_sync_records_for_split(
record_code="1",
parent="1",
status="TRANSFERRED_TO_BENEFICIARY",
- hope_status=Payment.STATUS_DISTRIBUTION_SUCCESS,
auth_code="1",
payout_amount=float(self.payments[0].entitlement_quantity),
fsp_code="1",
@@ -158,7 +156,6 @@ def test_sync_records_for_split(
record_code="2",
parent="2",
status="ERROR",
- hope_status=Payment.STATUS_ERROR,
auth_code="2",
payout_amount=0.0,
fsp_code="2",
@@ -188,6 +185,24 @@ def test_sync_records_for_split(
def test_sync_records(
self, get_quantity_in_usd_mock: Any, get_records_for_payment_instruction_mock: Any, get_exchange_rate_mock: Any
) -> None:
+ collector = IndividualFactory(household=None)
+ hoh = IndividualFactory(household=None)
+ hh = HouseholdFactory(head_of_household=hoh)
+ IndividualRoleInHouseholdFactory(household=hh, individual=hoh, role=ROLE_PRIMARY)
+ IndividualFactory.create_batch(2, household=hh)
+ self.payments.append(
+ PaymentFactory(
+ parent=self.pp,
+ household=hh,
+ status=Payment.STATUS_PENDING,
+ currency="PLN",
+ collector=collector,
+ delivered_quantity=None,
+ delivered_quantity_usd=None,
+ financial_service_provider=self.pg_fsp,
+ )
+ )
+
self.dm.sent_to_payment_gateway = True
self.dm.save()
@@ -199,10 +214,74 @@ def test_sync_records(
modified="2023-10-11",
record_code="1",
parent="1",
- status="TRANSFERRED_TO_BENEFICIARY",
- hope_status=Payment.STATUS_DISTRIBUTION_SUCCESS,
+ status="ERROR",
+ auth_code="1",
+ fsp_code="1",
+ message="Error",
+ ),
+ PaymentRecordData(
+ id=2,
+ remote_id=str(self.payments[1].id),
+ created="2023-10-10",
+ modified="2023-10-11",
+ record_code="2",
+ parent="2",
+ status="ERROR",
+ auth_code="2",
+ fsp_code="2",
+ payout_amount=1.23,
+ ),
+ PaymentRecordData(
+ id=3,
+ remote_id=str(self.payments[2].id),
+ created="2023-10-10",
+ modified="2023-10-11",
+ record_code="3",
+ parent="3",
+ status="CANCELLED",
+ auth_code="3",
+ fsp_code="3",
+ ),
+ ]
+
+ pg_service = PaymentGatewayService()
+ pg_service.api.get_records_for_payment_instruction = get_records_for_payment_instruction_mock # type: ignore
+
+ pg_service.sync_records()
+ assert get_records_for_payment_instruction_mock.call_count == 1
+ self.pp.refresh_from_db()
+ self.payments[0].refresh_from_db()
+ self.payments[1].refresh_from_db()
+ self.payments[2].refresh_from_db()
+ assert self.payments[0].status == Payment.STATUS_ERROR
+ assert self.payments[1].status == Payment.STATUS_ERROR
+ assert self.payments[2].status == Payment.STATUS_MANUALLY_CANCELLED
+ assert self.payments[0].reason_for_unsuccessful_payment == "Error"
+ assert self.payments[1].reason_for_unsuccessful_payment == "Delivered amount: 1.23"
+ assert self.payments[2].reason_for_unsuccessful_payment == "Unknown error"
+ assert self.pp.is_reconciled
+
+ @mock.patch("hct_mis_api.apps.payment.models.PaymentPlan.get_exchange_rate", return_value=2.0)
+ @mock.patch(
+ "hct_mis_api.apps.payment.services.payment_gateway.PaymentGatewayAPI.get_records_for_payment_instruction"
+ )
+ @mock.patch("hct_mis_api.apps.payment.services.payment_gateway.get_quantity_in_usd", return_value=100.00)
+ def test_sync_records_error_messages(
+ self, get_quantity_in_usd_mock: Any, get_records_for_payment_instruction_mock: Any, get_exchange_rate_mock: Any
+ ) -> None:
+ self.dm.sent_to_payment_gateway = True
+ self.dm.save()
+
+ get_records_for_payment_instruction_mock.return_value = [
+ PaymentRecordData(
+ id=1,
+ remote_id=str(self.payments[0].id),
+ created="2023-10-10",
+ modified="2023-10-11",
+ record_code="1",
+ parent="1",
+ status="PENDING",
auth_code="1",
- payout_amount=float(self.payments[0].entitlement_quantity),
fsp_code="1",
),
PaymentRecordData(
@@ -213,7 +292,6 @@ def test_sync_records(
record_code="2",
parent="2",
status="TRANSFERRED_TO_BENEFICIARY",
- hope_status=Payment.STATUS_DISTRIBUTION_SUCCESS,
auth_code="2",
payout_amount=float(self.payments[1].entitlement_quantity) - 10.00,
fsp_code="2",
@@ -228,14 +306,53 @@ def test_sync_records(
pg_service.sync_records()
assert get_records_for_payment_instruction_mock.call_count == 1
+ self.pp.refresh_from_db()
self.payments[0].refresh_from_db()
self.payments[1].refresh_from_db()
- assert self.payments[0].status == Payment.STATUS_DISTRIBUTION_SUCCESS
+ assert self.payments[0].status == Payment.STATUS_SENT_TO_PG
assert self.payments[0].fsp_auth_code == "1"
- assert self.payments[0].delivered_quantity == self.payments[0].entitlement_quantity
+ assert self.payments[0].delivered_quantity is None
assert self.payments[1].status == Payment.STATUS_DISTRIBUTION_PARTIAL
assert self.payments[1].fsp_auth_code == "2"
assert self.payments[1].delivered_quantity == self.payments[1].entitlement_quantity - Decimal(10.00)
+ assert self.pp.is_reconciled is False
+
+ get_records_for_payment_instruction_mock.return_value = [
+ PaymentRecordData(
+ id=1,
+ remote_id=str(self.payments[0].id),
+ created="2023-10-10",
+ modified="2023-10-11",
+ record_code="1",
+ parent="1",
+ status="TRANSFERRED_TO_BENEFICIARY",
+ auth_code="1",
+ payout_amount=float(self.payments[0].entitlement_quantity),
+ fsp_code="1",
+ ),
+ PaymentRecordData(
+ id=2,
+ remote_id=str(self.payments[1].id),
+ created="2023-10-10",
+ modified="2023-10-11",
+ record_code="2",
+ parent="2",
+ status="TRANSFERRED_TO_BENEFICIARY",
+ auth_code="2",
+ payout_amount=float(self.payments[1].entitlement_quantity) - 10.00,
+ fsp_code="2",
+ ),
+ ]
+
+ get_records_for_payment_instruction_mock.reset_mock()
+ pg_service.sync_records()
+ assert get_records_for_payment_instruction_mock.call_count == 1
+ self.payments[0].refresh_from_db()
+ self.payments[1].refresh_from_db()
+ assert self.payments[0].status == Payment.STATUS_DISTRIBUTION_SUCCESS
+ assert self.payments[0].delivered_quantity == self.payments[0].entitlement_quantity
+ assert self.payments[1].status == Payment.STATUS_DISTRIBUTION_PARTIAL
+ assert self.payments[1].delivered_quantity == self.payments[1].entitlement_quantity - Decimal(10.00)
# pp is reconciled at this point
get_records_for_payment_instruction_mock.reset_mock()
@@ -251,7 +368,6 @@ def test_get_hope_status(self) -> None:
record_code="1",
parent="1",
status="TRANSFERRED_TO_BENEFICIARY",
- hope_status=Payment.STATUS_DISTRIBUTION_SUCCESS,
auth_code="1",
payout_amount=float(self.payments[0].entitlement_quantity),
fsp_code="1",
@@ -259,14 +375,12 @@ def test_get_hope_status(self) -> None:
self.assertEqual(p.get_hope_status(self.payments[0].entitlement_quantity), Payment.STATUS_DISTRIBUTION_SUCCESS)
self.assertEqual(p.get_hope_status(Decimal(1000000.00)), Payment.STATUS_DISTRIBUTION_PARTIAL)
- with self.assertRaisesMessage(PaymentGatewayAPI.PaymentGatewayAPIException, "Invalid delivered_quantity"):
- p.payout_amount = None # type: ignore
- p.get_hope_status(Decimal(1000000.00))
+ p.payout_amount = None
+ self.assertEqual(p.get_hope_status(Decimal(1000000.00)), Payment.STATUS_ERROR)
- with self.assertRaisesMessage(PaymentGatewayAPI.PaymentGatewayAPIException, "Invalid Payment status"):
- p.payout_amount = float(self.payments[0].entitlement_quantity)
- p.status = "NOT EXISTING STATUS"
- p.get_hope_status(Decimal(1000000.00))
+ p.payout_amount = float(self.payments[0].entitlement_quantity)
+ p.status = "NOT EXISTING STATUS"
+ self.assertEqual(p.get_hope_status(Decimal(1000000.00)), Payment.STATUS_ERROR)
@mock.patch(
"hct_mis_api.apps.payment.services.payment_gateway.PaymentGatewayAPI.add_records_to_payment_instruction"
diff --git a/backend/hct_mis_api/apps/payment/utils.py b/backend/hct_mis_api/apps/payment/utils.py
index 8d9b28404a..bf2bda0790 100644
--- a/backend/hct_mis_api/apps/payment/utils.py
+++ b/backend/hct_mis_api/apps/payment/utils.py
@@ -167,7 +167,7 @@ def get_payment_plan_object(cash_or_payment_plan_id: str) -> Union["PaymentPlan"
def get_payment_delivered_quantity_status_and_value(
- delivered_quantity: Union[int, float, str], entitlement_quantity: Decimal
+ delivered_quantity: Optional[Union[int, float, str]], entitlement_quantity: Decimal
) -> typing.Tuple[str, Optional[Decimal]]:
"""
* Fully Delivered (entitled quantity = delivered quantity) [int, float, str]
diff --git a/frontend/src/containers/tables/paymentmodule/PaymentsTable/PaymentsTableRow.tsx b/frontend/src/containers/tables/paymentmodule/PaymentsTable/PaymentsTableRow.tsx
index 8be7227078..c22b0dc410 100644
--- a/frontend/src/containers/tables/paymentmodule/PaymentsTable/PaymentsTableRow.tsx
+++ b/frontend/src/containers/tables/paymentmodule/PaymentsTable/PaymentsTableRow.tsx
@@ -85,6 +85,9 @@ export function PaymentsTableRow({
if (status === PaymentStatus.TransactionErroneous) {
return UNSUCCESSFUL;
}
+ if (status === PaymentStatus.ManuallyCancelled) {
+ return CANCELLED;
+ }
if (deliveredQuantity === null) {
return <>>;
}