Is it possible to unmask values returned by the REST API (21R1), specifically CCPID?
I am working on upgrading our Acumatica instance from 2019R1 to 2021R1. A substantial part of my project involves updating a set of service layer applications that enable bidirectional communication between our ecommerce site and Acumatica. Our web site runs on a platform that does not have an available integration for Acumatica. The site interacts with Authorize.net for all credit card transactions.
I’ll spare everyone the long explanation for why I need to unmask the Payment Profile ID (CCPID) in the API. The TLDR; is “our web site’s integration with Authorize.net could be better”. Most of what I need to accomplish involves not creating duplicate payment methods in Acumatica because of web orders.
With our customized v18 API endpoint in 2019 R1 my code could load a customer’s payment method list to access the temporary ID for that payment method. A second request was sent to load a single payment method using the temporary ID with the payment method details expanded. That would provide an unmasked CCPID that contained the Authorize.net Payment Profile ID. Based on my testing, this no longer works.
Before I start in on the task of rewriting my code that expects to get a valid Payment Profile ID from Acumatica, I figured I should check if this was something I could change somehow. These payment methods do not have the “Encrypted” checkbox checked for CCPID in the “Settings use in for AR” tab.”
I expect it would be unwise to uncheck the Payment Profile ID checkbox.
As a final thought, I see in the Rest API guide for the recent releases that we can attach externally created credit card transactions in a request to create a sales order (that is great). In that code example there was a line about validation that I’d like to know more about. What does this do when set to true?
"NeedValidation": {"value": true},
Thank you!
Page 1 / 1
Hi @PorchlightZach
I’ve reviewed the field on Acumatica and checked the behavior with REST API.
As per my understanding, If we set the Need Validation = true , The payment is locked for editing and wait for the external transaction result. If the payment does not get unlocked, click Actions > Validate Card Payment to request the transaction result from the processing center. It will allow to authorize/capture the transaction with new card
NeedValidation =True -> if click the validate Card payment action, automatically check the New card check box as checked and Enabled the Authorize button if the transaction details is wrong.
NeedValidation =false, if click the validate Card payment action, it will not allow to authorize again. (authorize button is disabled). Only Shows the validation message like “Credit card processing error. ProcessingCenter : E00040: The record cannot be found..”
Hi @PorchlightZach
NeedValidation =True -> if click the validate Card payment action, automatically check the New card check box as checked and Enabled the Authorize button if the transaction details is wrong
@jinin Thank you for the response, this helps. Do you know if the new card checkbox behavior only occurs if the customer has no existing payment methods on file? The payment behaved like you suggested with regard to validating the card, however the sales order and payment both defaulted to an existing Card Account Number and the new card checkbox was not visible. I tested the API behavior out with Postman in a development environment with my own account. My account already has other payment methods on file that use the same payment method ID.
I plan on doing some additional testing with this. I still have to update my code so that payment methods are created prior to submitting an SO payment for the ecommerce transaction. This is where the unmasked payment profile comes into play.
Authorize.net has an API endpoint that lets you create a customer payment profile from a transaction ID. If that transaction ID matches an existing customer profile ID and payment profile ID, the API returns the existing record.
As previously noted, before I could query the API for customer payment methods and make sure I was never creating a duplicate by comparing the Auth.net Payment Profile ID to the value the API sent back for the CCPID. With that value being returned as ******* my only option to avoid duplication is to query payment methods based on a filtered list of Customer Payment Methods that use equality checks for Customer ID, Customer Profile ID, Processing Center ID, and endswith(CardAccountNbr, <lastFour>).
While the number of times that query is going to return the wrong payment method are pretty small, it’s a non-zero chance. Our system currently lumps all non-AMEX cards into a single payment method (decision made by accounting before we went live). It isn’t unheard of for a customer to have a Visa card that ends in the same four digits as a Mastercard they also have on file.
Hopefully someone can weigh in on the unmasking issue. My plan B here is to track Payment Profile IDs from web orders in a regular text user field on the Customer Payment Method. I am hoping I can create a business event and import scenario to back fill that field. IIRC generic inquiry results will also mask the CCPID value so my plan B is only going to work if it is possible to copy the value of one form field to another field in an import scenario.
I haven’t tried that before and if it isn’t possible Plan C is going to be to write a console application that pulls customer credit card payment methods in from the API, queries that customer profile from Authorize.net and sets the new field value accordingly. That will certainly have to wait until after my upgrade project is complete and will be a lot of work for something I’d just rather have work in the API directly.
If I don’t get any responses, I’ll just pick your response as the best answer.
Hopefully someone can weigh in on the unmasking issue. My plan B here is to track Payment Profile IDs from web orders in a regular text user field on the Customer Payment Method. I am hoping I can create a business event and import scenario to back fill that field. IIRC generic inquiry results will also mask the CCPID value so my plan B is only going to work if it is possible to copy the value of one form field to another field in an import scenario.
Well, looks like I just wasted about an hour on Plan B. When I try to have an import scenario copy the internal field eDetail.Value] to my custom field, it copies nothing.
As you can see in the highlighted row above, the value should be copied. However when the import scenario is triggered by my business event, the value copied is always blank. On a previous version I attempted to use =Concat(CDetail.DetailID], ‘=’, ,Detail.Value]) in the custom field assignment. When my business event would run, only the equal sign would show up in my user field.
Interestingly enough the Export to excel button in the Detail grid on a customer payment method will export the value.
I have to say the way Acumatica treats this field is incredibly inconvenient. This is NOT a credit card number or a CVV code. It isn’t personally identifiable information either. This is a payment method token that is totally useless by itself. Even with the customer profile ID, a bad actor would STILL need to have obtained our API keys in order to do anything with it.
Even if someone did get that, they’d have to gain access to an admin login account to send money some other location (i.e., by changing our bank account details). In the grand scheme of things, any one payment method profile ID does nothing for a bad actor.
Why is this field being treated like this? I’ve never used Acumatica with the older Authorize.net implementations that stored credit card information locally and encrypted them, but this kind of seems like a holdover from that functionality. I imagine if one were using those older methods this area is where card information would show up. I don’t even see those plugins in the list we can pick from on a processing center editing screen.
No PCI compliance SAQ I’ve had to fill out has ever suggested that encryption of tokenized information was necessary. Perhaps some other sector requires this behavior. If that is the case, why can’t this be a configurable option?
If by chance the answer to my original question regarding unmasking the Detail value with the API is “Not possible”, I’d like an explanation for why this field is being treated like it is. Specifically, I’m interested in knowing which industry standard requires encryption of payment token data.
In specifics to the unmasking question the system encrypts the values by default, the IsEncrypted field just determines whether or not it decrypts them when displaying them in the UI(and the decrypting seems to be handled by per Graph event handlers rather than DAC attributes).
The CustomerPaymentMethodDetail.Value DAC field has an attribute called PXRSACryptStringWithMaskAttribute that does the encrypting, so you could probably(haven’t tested) overwrite that to prevent the encryption. You could also probably(haven't tested) just customize the API and have it decrypt the value using the PXRSACryptStringWithMaskAttribute.SetDecrypted method before returning it.
The third option would be to create your own Attribute and model it off the dynamic attributes already on the DAC field and dynamically decrypt the field based on the IsEncrypted value(probably in the FieldSelecting Event).
In specifics to the unmasking question the system encrypts the values by default, the IsEncrypted field just determines whether or not it decrypts them when displaying them in the UI(and the decrypting seems to be handled by per Graph event handlers rather than DAC attributes).
The CustomerPaymentMethodDetail.Value DAC field has an attribute called PXRSACryptStringWithMaskAttribute that does the encrypting, so you could probably(haven’t tested) overwrite that to prevent the encryption. You could also probably(haven't tested) just customize the API and have it decrypt the value using the PXRSACryptStringWithMaskAttribute.SetDecrypted method before returning it.
The third option would be to create your own Attribute and model it off the dynamic attributes already on the DAC field and dynamically decrypt the field based on the IsEncrypted value(probably in the FieldSelecting Event).
Thank you for these suggestions. I didn’t think about looking for the SetDecrypted method in the base code. I’m going to avoid overwriting base behavior wherever possible (first option) and I’m not entirely sure where to start with getting the API to decrypt the value before returning it (second option). That third option is definitely something I am comfortable with, so I’m going to start there.
Just to follow up, I got this working like I needed to. Interestingly enough I did not have to use the Decrypt method in the FieldSelecting event handler. I reused some code from CCProcessingHelper.cs to load the details for a payment method with the intention of starting off with whatever it returned. After I got that working, I’d work on unmasking the value, but it turns out that wasn’t necessary.
When customer payment methods are queried with the v20 endpoint, my custom field contains the value from the payment method detail in plain text. I wish I had looked into this option even with the v18 endpoint. As the payment detail record is a BQL delegate it cannot be returned with customer payment methods. This approach saves an additional API query to get the Payment Profile ID for each customer payment method. For customers with many credit cards this can save up to 10 additional queries.
It is probably obvious that the API cannot query the custom field directly until a value is actually saved to it. So, when the custom field is empty the API results will return the detail value but until the customer payment method is resaved, a query with $filter=CustomerID eq ‘CUST123’ and UsrYourPaymentProfileID eq ‘1234567890’ does not work.
Thanks again for the ideas @markusray17!
In case anyone else needs something like this, I’ll share a version of my code with my custom user field name replaced with a placeholder variable name. I’ve done very little testing on this aside from making sure it works in the UI and with the API using some basic queries that filter on customer ID.
using PX.Data; using PX.Objects.CA; using PX.Objects.CS; using System.Linq;