Page MenuHomePhabricator

Gravy Recurring: Validation error during recurring charge, in field: processor_contact_id. Message: Input should be a valid UUID
Open, MediumPublic

Description

I noticed this one in the logs today, and it stood out. It looks like we're sending over values to Gravy in recurring charges that they expect to be UUIDs, but instead map to ct_ids and other PSP IDs.

This started happening last month, based on the data.

Validation error during recurring charge, in field: processor_contact_id. Message: Input should be a valid UUID, invalid character: expected an optional prefix of `urn:uuid:` followed by [0-9a-fA-F-], found `.` at 10.

Response: {"type":"error","code":"bad_request","status":400,"message":"Request failed validation","details":[{"location":"body","pointer":"/buyer_id","message":"Input should be a valid UUID, invalid character: expected an optional prefix of `urn
 :uuid:` followed by [0-9a-fA-F-], found `.` at 10","type":"uuid_parsing"}]}

The buyer_id mapping is happening here

		if ( !empty( $params['processor_contact_id'] ) ) {
			$request['buyer_id'] = $params['processor_contact_id'];
		}

And these values are not UUIDs according to a quick look at the data:

MariaDB [civicrm]> SELECT 
    ->   cr.id, 
    ->   cr.create_date, 
    ->   pp.name AS processor_name, 
    ->   crs.processor_contact_id 
    -> FROM 
    ->   civicrm_contribution_recur cr 
    ->   JOIN civicrm_contribution_recur_smashpig crs ON crs.entity_id = cr.id 
    ->   JOIN civicrm_payment_processor pp ON pp.id = cr.payment_processor_id 
    -> WHERE 
    ->   cr.payment_processor_id = 19 
    ->   AND crs.processor_contact_id IS NOT NULL 
    -> ORDER BY 
    ->   cr.id DESC 
    -> LIMIT 
    ->   20;
+---------+---------------------+----------------+----------------------+
| id      | create_date         | processor_name | processor_contact_id |
+---------+---------------------+----------------+----------------------+
| 2778114 | 2026-04-20 00:34:40 | gravy          | KYLJRFM3SNKN6        |
| 2777222 | 2026-04-17 18:04:04 | gravy          | JH9FJCSKGXHNW        |
| 2775224 | 2026-04-14 18:55:14 | gravy          | LHR6KVKQMY7GC        |
| 2774761 | 2026-04-14 16:29:17 | gravy          | TFC95WHAHLHW6        |
| 2774760 | 2026-04-14 16:28:54 | gravy          | 549927R9MM3PU        |
| 2773291 | 2026-04-12 10:43:03 | gravy          | ZSZ75Z9Y95AB6        |
| 2773094 | 2026-04-12 05:58:10 | gravy          | EW53MAXV6YZYW        |
| 2772522 | 2026-04-11 08:24:27 | gravy          | SHEK65VA4P49J        |
| 2772326 | 2026-04-10 22:55:07 | gravy          | 834ZVDX2GT54N        |
| 2772273 | 2026-04-10 21:03:29 | gravy          | A34WMZB2L7BNJ        |
| 2769945 | 2026-04-07 20:00:58 | gravy          | 245933565.1          |
| 2768592 | 2026-04-06 20:13:22 | gravy          | 245953635.1          |
| 2768456 | 2026-04-05 20:10:33 | gravy          | 245942611.1          |
| 2768418 | 2026-04-06 16:10:35 | gravy          | XTB72V5JMVG9E        |
| 2768331 | 2026-04-06 11:29:57 | gravy          | RE2THFHU9X4FS        |
| 2768125 | 2026-04-05 14:34:24 | gravy          | 6FPFSGPM3S4MU        |
| 2767503 | 2026-04-03 07:10:03 | gravy          | MFBCDN738D7L6        |
| 2766962 | 2026-04-01 16:38:10 | gravy          | ARNCA2LT8JZ4J        |
| 2766645 | 2026-03-31 20:36:55 | gravy          | QJS89G6QDE58U        |
| 2766594 | 2026-03-30 20:25:27 | gravy          | 245728521.1          |
+---------+---------------------+----------------+----------------------+

Looks like only 33 recurrings are affected here:

MariaDB [civicrm]> SELECT 
    ->   COUNT(*) 
    -> FROM 
    ->   civicrm_contribution_recur cr 
    ->   JOIN civicrm_contribution_recur_smashpig crs ON crs.entity_id = cr.id 
    -> WHERE 
    ->   cr.payment_processor_id = 19 
    ->   AND crs.processor_contact_id IS NOT NULL 
    ->   AND crs.processor_contact_id NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' 
    ->   AND cr.cancel_date IS NULL;
+----------+
| COUNT(*) |
+----------+
|       33 |
+----------+

1049 are UUIDs

MariaDB [civicrm]> SELECT 
    ->   COUNT(*) 
    -> FROM 
    ->   civicrm_contribution_recur cr 
    ->   JOIN civicrm_contribution_recur_smashpig crs ON crs.entity_id = cr.id 
    -> WHERE 
    ->   cr.payment_processor_id = 19 
    ->   AND crs.processor_contact_id IS NOT NULL 
    ->   AND crs.processor_contact_id REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$' 
    ->   AND cr.cancel_date IS NULL;
+----------+
| COUNT(*) |
+----------+
|     1049 |
+----------+
1 row in set (0.330 sec)

MariaDB [civicrm]>

Event Timeline

Ok it looks like this started happening in https://gerrit.wikimedia.org/r/c/wikimedia/fundraising/SmashPig/+/1222300/7/PaymentProviders/Gravy/Mapper/ResponseMapper.php, and it was then partially fixed in https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DonationInterface/+/1260070.

I see we had an older ticket for this T420651: PayPal recurring fail wellness check

However, it looks like we still need to clean up the monthly convert flow, which is still using the processor_contact_id here

			foreach ( [ 'processor_contact_id', 'fiscal_number' ] as $optionalKey ) {
				if ( !empty( $sessionData[$optionalKey] ) ) {
					$message[$optionalKey] = $sessionData[$optionalKey];
				}
			}

We were clean them directly from the db for paypal recurring, I updated the processor_contact_id to NULL, validate with below should see nothing.

MariaDB [civicrm]> SELECT
    -> cr.id,
    -> cr.create_date,
    -> pp.name AS processor_name,
    -> crs.processor_contact_id,
    -> crs.id as delete_id
    ->     FROM
    -> civicrm_contribution_recur cr
    -> JOIN civicrm_contribution_recur_smashpig crs ON crs.entity_id = cr.id
    -> JOIN civicrm_payment_processor pp ON pp.id = cr.payment_processor_id
    ->     WHERE
    -> cr.payment_processor_id = 19
    -> AND crs.processor_contact_id IS NOT NULL
    ->       AND processor_contact_id NOT REGEXP '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
    -> ORDER BY
    -> cr.id DESC;

Change #1276835 had a related patch set uploaded (by Wfan; author: Wfan):

[mediawiki/extensions/DonationInterface@master] Unset processor_contact_id if paypal

https://gerrit.wikimedia.org/r/1276835

@AnnWF While this is the same error we saw with the paypal ones I think something else is going on here. Recurring convert isn't turned on for gravy paypal.

I dug into one of the errors from yesterday:
cid=56200564
Its a adyen credit card recurring but failed with the same reason
2026-04-23 06:56:24,626 ERROR civicrm.wmf.INFO: Charging contribution_recur id: 2764507 with invoice_id: 245656263.2
2026-04-23 06:56:24,834 ERROR civicrm.wmf.ALERT: Validation error during recurring charge, in field: processor_contact_id. Message: Input should be a valid UUID, invalid character: expected an optional prefix of urn:uuid: followed by [0-9a-fA-F-], found . at 10. Payment Method: cc. Payment Submethod: . Request: {"recurring_payment_token":"710f184c-c932-490a-a54e-d3baa7a2920e","amount":5.35,"currency":"EUR","first_name":"*","last_name":"","email":"","country":"AT","order_id":"245656263.2","installment":"recurring","description":"Wikimedia 877 600 9454","recurring":true,"user_ip":"*","processor_contact_id":"245656263.1","fiscal_number":null}. Response: {"type":"error","code":"bad_request","status":400,"message":"Request failed validation","details":[{"location":"body","pointer":"/buyer_id","message":"Input should be a valid UUID, invalid character: expected an optional prefix of urn:uuid: followed by [0-9a-fA-F-], found . at 10","type":"uuid_parsing"}]}
2026-04-23 06:56:24,835 ERROR civicrm.wmf.INFO: Error: 1000014 invoice_id:245656263.2
2026-04-23 06:56:24,835 ERROR civicrm.wmf.INFO: Raw response: {"type":"error","code":"bad_request","status":400,"message":"Request failed validation","details":[{"location":"body","pointer":"\/buyer_id","message":"Input should be a valid UUID, invalid character: expected an optional prefix of urn:uuid: followed by [0-9a-fA-F-], found . at 10","type":"uuid_parsing"}]}

I tried looking in the gravy console for more info but I can't find the attempt in there, I asked on the slack channel if they could find anything

Ah okay so it wont be in the gravy console because of that validation error. I think we maybe just need to stop sending the processor_contact_id for all recurring converts

Added regex check before send over processor_contact_id, this should catch the above issues

Change #1276835 merged by jenkins-bot:

[mediawiki/extensions/DonationInterface@master] Unset processor_contact_id if not uuid

https://gerrit.wikimedia.org/r/1276835

Change #1277613 had a related patch set uploaded (by Ejegg; author: Ejegg):

[wikimedia/fundraising/SmashPig@master] Map paypal payer ID to backendProcessorContactID

https://gerrit.wikimedia.org/r/1277613

Change #1277615 had a related patch set uploaded (by Ejegg; author: Ejegg):

[mediawiki/extensions/DonationInterface@master] Revert "Unset processor_contact_id if not uuid"

https://gerrit.wikimedia.org/r/1277615

Change #1277616 had a related patch set uploaded (by Ejegg; author: Ejegg):

[mediawiki/extensions/DonationInterface@master] More targetted processor_contact_id filter

https://gerrit.wikimedia.org/r/1277616

Change #1277615 merged by jenkins-bot:

[mediawiki/extensions/DonationInterface@master] Revert "Unset processor_contact_id if not uuid"

https://gerrit.wikimedia.org/r/1277615

Change #1277616 merged by jenkins-bot:

[mediawiki/extensions/DonationInterface@master] More targetted processor_contact_id filter

https://gerrit.wikimedia.org/r/1277616

Ok, I found that original batch I showed and pulled out their backend processor. It's a mix of PayPal and Adyen.

MariaDB [civicrm]> SELECT 
    ->   cr.id, 
    ->   crs.processor_contact_id, 
    ->   x.backend_processor, 
    ->   COUNT(c.id) AS contribution_count 
    -> FROM 
    ->   civicrm_contribution_recur cr 
    ->   JOIN civicrm_contribution_recur_smashpig crs ON crs.entity_id = cr.id 
    ->   JOIN civicrm_contribution c ON c.contribution_recur_id = cr.id 
    ->   JOIN wmf_contribution_extra x ON x.entity_id = c.id 
    -> WHERE 
    ->   cr.id IN (
    ->     2778114, 2777222, 2775224, 2774761, 
    ->     2774760, 2773291, 2773094, 2772522, 
    ->     2772326, 2772273, 2769945, 2768592, 
    ->     2768456, 2768418, 2768331, 2768125, 
    ->     2767503, 2766962, 2766645, 2766594
    ->   ) 
    -> GROUP BY 
    ->   cr.id, 
    ->   crs.processor_contact_id, 
    ->   x.backend_processor;
+---------+----------------------+-------------------+--------------------+
| id      | processor_contact_id | backend_processor | contribution_count |
+---------+----------------------+-------------------+--------------------+
| 2766594 | NULL                 | adyen             |                  1 |
| 2766645 | NULL                 | paypal            |                  1 |
| 2766962 | NULL                 | paypal            |                  1 |
| 2767503 | NULL                 | paypal            |                  1 |
| 2768125 | NULL                 | paypal            |                  1 |
| 2768331 | NULL                 | paypal            |                  1 |
| 2768418 | NULL                 | paypal            |                  1 |
| 2768456 | NULL                 | adyen             |                  1 |
| 2768592 | NULL                 | adyen             |                  1 |
| 2769945 | NULL                 | adyen             |                  1 |
| 2772273 | NULL                 | paypal            |                  1 |
| 2772326 | NULL                 | paypal            |                  1 |
| 2772522 | NULL                 | paypal            |                  1 |
| 2773094 | NULL                 | paypal            |                  1 |
| 2773291 | NULL                 | paypal            |                  1 |
| 2774760 | NULL                 | paypal            |                  1 |
| 2774761 | NULL                 | paypal            |                  1 |
| 2775224 | NULL                 | paypal            |                  1 |
| 2777222 | NULL                 | paypal            |                  1 |
| 2778114 | NULL                 | paypal            |                  1 |
+---------+----------------------+-------------------+--------------------+
20 rows in set (0.003 sec)

Yeah the paypal one is only payerID, and adyen one is orderID, thus I used the UUID type check after Christine points out this

but we do have this https://gerrit.wikimedia.org/r/c/mediawiki/extensions/DonationInterface/+/1277616 to catch it I believe this should be gone, we are not limited this check to paypal

Yeah, that should fix them all.

Change #1277613 merged by jenkins-bot:

[wikimedia/fundraising/SmashPig@master] Map paypal payer ID to backendProcessorContactID

https://gerrit.wikimedia.org/r/1277613

XenoRyet set Final Story Points to 4.

It looks like we have a few more of these to fix. We had failmail today, so I reran the queries and found 9 other affected recurring subscriptions:

+---------+----------------------+---------------------+---------------+
| id      | processor_contact_id | cancel_date         | failure_count |
+---------+----------------------+---------------------+---------------+
| 2784217 | 246567951.1          | 2026-06-07 08:21:38 |             4 |
| 2797132 | 247122453.2          | NULL                |             1 |
| 2799694 | 247195403.1          | NULL                |             0 |
| 2801444 | 247201432.1          | NULL                |             0 |
| 2804908 | 247349707.1          | NULL                |             0 |
| 2806007 | 247594866.1          | NULL                |             0 |
| 2807590 | 247784300.1          | NULL                |             0 |
| 2808221 | 247787290.1          | NULL                |             0 |
| 2810940 | 248047690.1          | NULL                |             0 |
+---------+----------------------+---------------------+---------------+
jgleeson triaged this task as Medium priority.

Ok, looks like one got cancelled already, so I've updated the 8 still-active recurrings, setting processor_contact_id to null, which prevents the bug from happening.

MariaDB [civicrm]> SELECT 
    ->   entity_id, 
    ->   processor_contact_id 
    -> FROM 
    ->   civicrm_contribution_recur_smashpig 
    -> WHERE 
    ->   entity_id IN (
    ->     2797132, 2799694, 2801444, 2804908, 
    ->     2806007, 2807590, 2808221, 2810940
    ->   );
+-----------+----------------------+
| entity_id | processor_contact_id |
+-----------+----------------------+
|   2797132 | NULL                 |
|   2799694 | NULL                 |
|   2801444 | NULL                 |
|   2804908 | NULL                 |
|   2806007 | NULL                 |
|   2807590 | NULL                 |
|   2808221 | NULL                 |
|   2810940 | NULL                 |
+-----------+----------------------+
8 rows in set (0.001 sec)