Maybe the component should be User Interface for this one, and maybe it should in Commerce project, not sure.

As a user with appropriate permissions, I can click "Refund" at admin/commerce/orders/{order_id}/payments for payments even if they have not been settled. This results in an error from Authorize.net because un-settled transactions can only be voided, not refunded.

Possible solutions?
- Use the API to get the settlement status & implement a voidPayment method
- Hide the refund button for 24 hours (yuck)
- SupportsVoidInterface?

We could also consider providing helpful messaging instead of a code fix, but the error from Authorize.net is less than helpful: E000027 The transaction was unsuccessful. We'd be making an educated guess as to why.

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

chrisrockwell created an issue. See original summary.

mglaman’s picture

I wonder if there is some logic governing the amount of time before a refund is valid. Apparently I'm testing it:

https://github.com/commerceguys/authnet/blob/master/tests/CreateTransact...

        $request = $this->xmlRequestFactory->createTransactionRequest();
        $request->setTransactionRequest(new TransactionRequest([
            'transactionType' => TransactionRequest::REFUND,
            'refTransId' => $response->transactionResponse->transId,
            'amount' => 5.00,
        ]));
        $transactionRequest->addPayment(new CreditCard([
            'cardNumber' => 'XXXX1111',
            'expirationDate' => 'XXXX',
        ]));
        sleep(4);
        $this->assertTrue(isset($response->transactionResponse));
        $this->assertEquals('I00001', $response->getMessages()[0]->getCode());
        $this->assertEquals('Successful.', $response->getMessages()[0]->getText());
        $this->assertEquals('Ok', $response->getResultCode());
mglaman’s picture

I wonder if on E000027 we should try a void on the transaction. If that fails then we relay the original error?

mglaman’s picture

Abandoning this for now.

If we try to void, there is no "partial" void. See https://developer.authorize.net/api/reference/#payment-transactions-void...

The best bet is to get a detailed log of the response that Authorize.net sends when the transaction is not yet settled. We'll want to show a message that says it is not yet settled, probably.

mglaman’s picture

Status: Active » Postponed

Postponing on needing a response where the transaction has no yet settled. Once we have that, let's plan an attack.

freality’s picture

I ran into the issue on the system I'm currently building.

You can resolve it by fetching the transaction details from Auth.net. Then using the transaction status to override PaymentGatewayBase::buildPaymentOperations.

The attached patch adds this logic.

Please review as I'm not all too sure I understand the semantics of Authorize.net's transactionStatus.

More info on the API call: https://developer.authorize.net/api/reference/index.html#transaction-rep...

freality’s picture

Sorry. Forgot patch.

freality’s picture

I forgot to mention that the Transaction Details API has to be enabled in your Authorize.net console settings. That being true, I think it would be necessary to inform the user to this in the Payment Method configuration form. An opt-in checkbox maybe?

mglaman’s picture

Status: Postponed » Needs review

I almost wonder if we should always show the link and just perform the status check on the form. That's a lot of individual API calls if someone made a transaction view.

freality’s picture

I think presenting an option to users which is not available would be a source of confusion. A user would select the Refund link then be presented with a message stating that While we thought we could preform a refund at this time, apparently we can't. But you can try this option again sometime in the future, and maybe will be able to then ;). Or at worse the bewildering Transaction unsuccessful; as is now.

You could reduce the number of calls to the API by saving transactionStatus to $payment->setRemoteState(). Therefore, you'd only be calling the api for a subset of statuses; like if lastKnownRemoteState() is a pending_state, refreshRemoteStatus. Otherwise, useLocalRemoteStatus.

mglaman’s picture

That makes sense. And I do agree - having a valid action is better than not. And the edge case is if someone made a view. Otherwise there'd only be one call.

freality’s picture

The attached patch implements the discussed caching of remote statuses. Including adding a configuration option for enabling the extended transaction details API.

Please review and comment.

P.S. The previous patch #7 does not work IRL. It's missing use statements which breaks namespacing.

Status: Needs review » Needs work

The last submitted patch, 12: error_on_refund-2911837-12.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

mglaman’s picture

+++ b/src/Plugin/Commerce/PaymentGateway/AcceptJs.php
@@ -143,6 +143,88 @@ class AcceptJs extends OnsiteBase implements SupportsRefundsInterface, SupportsU
+      if ($remote_state = $this->getRemoteTransactionState($payment)) {
...
+    if ($reset && $this->configuration['use_transaction_details_api']) {

+++ b/src/Plugin/Commerce/PaymentGateway/OnsiteBase.php
@@ -159,6 +161,13 @@ abstract class OnsiteBase extends OnsitePaymentGatewayBase implements  OnsitePay
+    $form['use_transaction_details_api'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Use Transaction Details API'),
+      '#description' => $this->t('The Transaction Details API helps present transaction actions more accurately; but must be enabled within the Authorize.net console settings. Its option can be found under Account >> Transaction Details API.'),
+      '#default_value' => $this->configuration['use_transaction_details_api']
+    ];

So, I really wish we didn't need this flag. And unfortunately the merchant details request doesn't give us that information either: https://developer.authorize.net/api/reference/index.html#transaction-rep...

In #2995922: Echeck payment states we're looking into batches. I wonder if we could do that somehow.

https://developer.authorize.net/api/reference/index.html#transaction-rep...

bojanz’s picture

It definitely feels wrong to an API call every time a payment is listed.

I'd go with the validation in the form + code that updates the status on cron (do we need a CronInterface for payment gateways?)

freality’s picture

@mglaman I agree that "just works" solutions are better than configuration. But since authorize.net's Transaction Details API in an opt-in feature, I think you have to disclose it as a requirement for proper use of the module; or allow user's the option to opt-out. Otherwise, you must have a strategy to auto-detect whether the feature is enabled. The API returns Error responses when that API is not enabled.

I'm not aware of the development going on with Echecks, but I'm willing to code this issue to resolution in any solution you feel best.

@bojanz The strategy to cache the remote status means you'd only be calling the API on pending-type transactions. And while the need to pester the API with update requests is funky, it's certainly better than having users attempt operations which fail with nondescript error messages.

I think a more robust solution would be to utilize Authorize.net's webhook API (https://developer.authorize.net/api/reference/features/webhooks.html). Although, there doesn't appear to be a hook for transaction settled; which is disappointing.

Nathaniel’s picture

Status: Needs work » Needs review
FileSize
6.66 KB
6.48 KB

Re-rolled. Some minor comment/code cleanup. I had to add 'completed' to OnsiteBase voidPayment allowed states.

Seems to be working as expected. @freality thanks for the patch!

Status: Needs review » Needs work

The last submitted patch, 17: error_on_refund-2911837-17.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Nathaniel’s picture

Status: Needs work » Needs review
FileSize
3.67 KB
7.82 KB

Updated schema. Added default value. Work on coding standards.

Status: Needs review » Needs work

The last submitted patch, 19: error_on_refund-2911837-19.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Nathaniel’s picture

Status: Needs work » Needs review
FileSize
376 bytes
8.28 KB

Work on existing tests. Added 'use_transaction_details_api' => TRUE, to testPreAcceptJsUpgrade.

Status: Needs review » Needs work

The last submitted patch, 21: error_on_refund-2911837-21.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

Nathaniel’s picture

Status: Needs work » Needs review
shawngo’s picture

I just ran into this issue and was searching for a way to find out if the transaction has been settled. I'm working on an integration with an ERP system which sends shipment information back to the Drupal site which then performs the transaction capture/refund/void/etc.
I couldn't find the Transaction Details API setting so I emailed Authorize.net support. I was informed that "[the] Transaction Details API is automatically enabled on all accounts now, there is no longer a setting to turn it on."

rhovland’s picture

Re-rolled patch so it applies cleanly to dev. Removed the configuration and checks since the API is always enabled on all accounts now.

I tested it on our account which is 10+ years old and it worked perfectly without needing to mess with account settings.

andyg5000’s picture

Thanks for the re-roll Ryan. This is working well, but commerce core doesn't support the balance recalculation properly after a void (see #3356936: Order balance calculation discrepancy for voided and expired payments.).

I've updated the patch to include a forced refresh of the balance. This won't break anything with related to a fix for 3356936, but should be removed once that's resolved.

rhovland’s picture

So there's an issue if you're using authorize and capture. The void button is shown on settled transactions if you previously viewed it when it was unsettled. It's because of the section of code where it checks if it should refresh the status or not is missing capturedPendingSettlement as one of the status codes.

Attached a patch that fixes this problem.

rhovland’s picture

Here is an updated patch to account for changes in the latest release.
Switched commerce_payment.order_updater to use dependency injection
Fixed a couple minor errors in the comments

rhovland’s picture

rhovland’s picture

I screwed up dependency injection in the patch. Here's the correct version