Repository: cal-itp/benefits

Issue:
Adjust handling of enrollment when customer already exists in group
Our payment processor uses a `500` error response to indicate that a customer is already enrolled in a group.

This is not an actual problem for Benefits, so we have [code that handles the response](https://github.com/cal-itp/benefits/blob/dev/benefits/enrollment/api.py#L82) and returns a success to the user.

The error handling is brittle, relying to some degree on the returned error message. This message has now changed, thus our handling is broken.

## To Reproduce

Steps to reproduce the behavior:

1. Go to test
2. Verify as a senior with SacRT
3. Enroll a card
4. Do 2-3 again with the same card --> BOOM

## Expected behavior

Attempting to re-enroll with the same card should not cause an error for the end user.

## Additional context

New error message from the logs looks like:

```log
Customer xxxx already exists in group yyyy
```

The old one looked more like:

```
Duplicate customer xxxxx...
```




Discussion:
Sentry issue: [BENEFITS-48](https://sentry.calitp.org/organizations/sentry/issues/73154/?referrer=github_integration)

Resolving patch:
diff --git a/benefits/enrollment/api.py b/benefits/enrollment/api.py
--- a/benefits/enrollment/api.py
+++ b/benefits/enrollment/api.py
@@ -65,7 +65,7 @@ def __init__(self, response):
 class GroupResponse:
     """Benefits Enrollment Customer Group API response."""
 
-    def __init__(self, response, requested_id, payload=None):
+    def __init__(self, response, requested_id, group_id, payload=None):
         if payload is None:
             try:
                 payload = response.json()
@@ -74,18 +74,12 @@ def __init__(self, response, requested_id, payload=None):
         else:
             try:
                 # Group API uses an error response (500) to indicate that the customer already exists in the group (!!!)
-                # The error message should contain the customer ID we sent via payload and start with "Duplicate"
+                # The error message should contain the customer ID and group ID we sent via payload
                 error = response.json()["errors"][0]
                 customer_id = payload[0]
                 detail = error["detail"]
 
-                failure = (
-                    customer_id is None
-                    or detail is None
-                    or customer_id not in detail
-                    or customer_id in detail
-                    and not detail.startswith("Duplicate")
-                )
+                failure = customer_id is None or detail is None or not (customer_id in detail and group_id in detail)
 
                 if failure:
                     raise ApiError("Invalid response format")
@@ -269,10 +263,10 @@ def enroll(self, customer_token, group_id):
 
             if r.status_code in (200, 201):
                 logger.info("Customer enrolled in group")
-                return GroupResponse(r, customer.id)
+                return GroupResponse(r, customer.id, group_id)
             elif r.status_code == 500:
                 logger.info("Customer already exists in group")
-                return GroupResponse(r, customer.id, payload=payload)
+                return GroupResponse(r, customer.id, group_id, payload=payload)
             else:
                 r.raise_for_status()
         except requests.ConnectionError:


Test patch:
diff --git a/tests/pytest/enrollment/test_api_GroupResponse.py b/tests/pytest/enrollment/test_api_GroupResponse.py
--- a/tests/pytest/enrollment/test_api_GroupResponse.py
+++ b/tests/pytest/enrollment/test_api_GroupResponse.py
@@ -8,14 +8,14 @@ def test_no_payload_invalid_response(mocker):
     mock_response.json.side_effect = ValueError
 
     with pytest.raises(ApiError, match=r"response"):
-        GroupResponse(mock_response, 0)
+        GroupResponse(mock_response, "customer", "group")
 
 
 def test_no_payload_valid_response_single_matching_id(mocker):
     mock_response = mocker.Mock()
     mock_response.json.return_value = ["0"]
 
-    response = GroupResponse(mock_response, "0")
+    response = GroupResponse(mock_response, "0", "group")
 
     assert response.customer_ids == ["0"]
     assert response.updated_customer_id == "0"
@@ -27,7 +27,7 @@ def test_no_payload_valid_response_single_unmatching_id(mocker):
     mock_response = mocker.Mock()
     mock_response.json.return_value = ["1"]
 
-    response = GroupResponse(mock_response, "0")
+    response = GroupResponse(mock_response, "0", "group")
 
     assert response.customer_ids == ["1"]
     assert response.updated_customer_id == "1"
@@ -39,7 +39,7 @@ def test_no_payload_valid_response_multiple_ids(mocker):
     mock_response = mocker.Mock()
     mock_response.json.return_value = ["0", "1"]
 
-    response = GroupResponse(mock_response, "0")
+    response = GroupResponse(mock_response, "0", "group")
 
     assert response.customer_ids == ["0", "1"]
     assert not response.updated_customer_id
@@ -53,14 +53,14 @@ def test_payload_invalid_response(mocker, exception):
     mock_response.json.side_effect = exception
 
     with pytest.raises(ApiError, match=r"response"):
-        GroupResponse(mock_response, "0", [])
+        GroupResponse(mock_response, "0", "group", [])
 
 
 def test_payload_valid_response(mocker):
     mock_response = mocker.Mock()
-    mock_response.json.return_value = {"errors": [{"detail": "Duplicate id 0"}]}
+    mock_response.json.return_value = {"errors": [{"detail": "0 group"}]}
 
-    response = GroupResponse(mock_response, "0", ["0"])
+    response = GroupResponse(mock_response, "0", "group", ["0"])
 
     assert response.customer_ids == ["0"]
     assert response.updated_customer_id == "0"
@@ -69,13 +69,13 @@ def test_payload_valid_response(mocker):
 
 
 failure_conditions = [
-    # customer_id is None
-    ({"detail": "Duplicate"}, [None]),
     # detail is None
     ({"detail": None}, ["0"]),
+    # customer_id is None
+    ({"detail": "0 group"}, [None]),
     # customer_id not in detail
-    ({"detail": "1"}, ["0"]),
-    # customer_id in detail, detail doesn't start with Duplicate
+    ({"detail": "1 group"}, ["0"]),
+    # group_id not in detail
     ({"detail": "0"}, ["0"]),
 ]
 
@@ -86,4 +86,4 @@ def test_payload_failure_response(mocker, error, payload):
     mock_response.json.return_value = {"errors": [error]}
 
     with pytest.raises(ApiError, match=r"response"):
-        GroupResponse(mock_response, "0", payload)
+        GroupResponse(mock_response, "0", "group", payload)