phacility/phabricator · error · Exception

Charge has no transaction ID!

Error message

Charge has no transaction ID!

What it means

Exception thrown by PhortunePayPalPaymentProvider::executeRefund() when the charge being refunded has no 'paypal.transactionID' metadata value. That metadata is only written after DoExpressCheckoutPayment succeeds (the controller stores PAYMENTINFO_0_TRANSACTIONID on the charge), so its absence means Phortune has no PayPal transaction reference to issue a RefundTransaction API call against. Refunding without it is impossible, hence the hard stop before any API call is made.

Source

Thrown at src/applications/phortune/provider/PhortunePayPalPaymentProvider.php:170

  }

  public function getPaymentMethodProviderDescription() {
    return 'PayPal';
  }

  protected function executeCharge(
    PhortunePaymentMethod $payment_method,
    PhortuneCharge $charge) {
    throw new Exception('!');
  }

  protected function executeRefund(
    PhortuneCharge $charge,
    PhortuneCharge $refund) {

    $transaction_id = $charge->getMetadataValue('paypal.transactionID');
    if (!$transaction_id) {
      throw new Exception(pht('Charge has no transaction ID!'));
    }

    $refund_amount = $refund->getAmountAsCurrency()->negate();
    $refund_currency = $refund_amount->getCurrency();
    $refund_value = $refund_amount->formatBareValue();

    $params = array(
      'TRANSACTIONID' => $transaction_id,
      'REFUNDTYPE' => 'Partial',
      'AMT' => $refund_value,
      'CURRENCYCODE' => $refund_currency,
    );

    $result = $this
      ->newPaypalAPICall()
      ->setRawPayPalQuery('RefundTransaction', $params)
      ->resolve();

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Only refund charges in CHARGED status that were paid through this PayPal provider — check status before initiating the refund.
  2. Inspect the charge metadata (paypal.transactionID, paypal.token) to confirm the charge actually completed the Express Checkout flow.
  3. If a real PayPal payment exists but metadata is missing, recover the transaction ID from the PayPal account's transaction history and restore the metadata before retrying.
  4. For charges that never completed, fail/void the charge instead of refunding — there is nothing at PayPal to return.

Example fix

// before
$provider->executeRefund($charge, $refund);

// after
$txn = $charge->getMetadataValue('paypal.transactionID');
if ($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED || !strlen((string)$txn)) {
  throw new Exception(
    pht('Charge %s never completed via PayPal; void it instead of refunding.',
      $charge->getPHID()));
}
$provider->executeRefund($charge, $refund);
Defensive patterns

Strategy: validation

Validate before calling

// Preconditions for a PayPal refund.
function paypal_charge_is_refundable(PhortuneCharge $charge) {
  if ($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) {
    return false;
  }
  return strlen((string) $charge->getMetadataValue('paypal.transactionID')) > 0;
}

if (!paypal_charge_is_refundable($charge)) {
  // skip or surface 'nothing at PayPal to refund' instead of calling executeRefund
}

Type guard

function charge_has_paypal_transaction_id(PhortuneCharge $charge) {
  $txn = $charge->getMetadataValue('paypal.transactionID');
  return is_string($txn) && strlen($txn) > 0;
}

Try / catch

try {
  $provider->executeRefund($charge, $refund);
} catch (Exception $ex) {
  // Leave the refund charge unapplied and log enough context to reconcile:
  // the charge PHID tells you which PayPal record to inspect.
  phlog(pht('Refund failed for charge %s: %s', $charge->getPHID(), $ex->getMessage()));
  throw $ex;
}

Prevention

When it happens

Trigger: Calling refund on a PhortuneCharge whose status is HOLD or FAIL (payment never completed, transaction ID never stored); a charge that was created by a different provider (e.g. Stripe) but refunded through the PayPal provider; or metadata lost by manual DB edits / an interrupted save during the charge flow.

Common situations: Issuing refunds on carts stuck in 'purchasing'/'hold' after a sandbox PayPal outage; admin scripts that iterate all charges including uncharged ones; partial data after a crash between DoExpressCheckoutPayment and charge->save().

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/c5b6fc985da268df. Report an issue: GitHub.