{"record":{"id":"c5b6fc985da268df","repo":"phacility/phabricator","slug":"charge-has-no-transaction-id","errorCode":null,"errorMessage":"Charge has no transaction ID!","messagePattern":"Charge has no transaction ID!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/phortune/provider/PhortunePayPalPaymentProvider.php","lineNumber":170,"sourceCode":"  }\n\n  public function getPaymentMethodProviderDescription() {\n    return 'PayPal';\n  }\n\n  protected function executeCharge(\n    PhortunePaymentMethod $payment_method,\n    PhortuneCharge $charge) {\n    throw new Exception('!');\n  }\n\n  protected function executeRefund(\n    PhortuneCharge $charge,\n    PhortuneCharge $refund) {\n\n    $transaction_id = $charge->getMetadataValue('paypal.transactionID');\n    if (!$transaction_id) {\n      throw new Exception(pht('Charge has no transaction ID!'));\n    }\n\n    $refund_amount = $refund->getAmountAsCurrency()->negate();\n    $refund_currency = $refund_amount->getCurrency();\n    $refund_value = $refund_amount->formatBareValue();\n\n    $params = array(\n      'TRANSACTIONID' => $transaction_id,\n      'REFUNDTYPE' => 'Partial',\n      'AMT' => $refund_value,\n      'CURRENCYCODE' => $refund_currency,\n    );\n\n    $result = $this\n      ->newPaypalAPICall()\n      ->setRawPayPalQuery('RefundTransaction', $params)\n      ->resolve();\n","sourceCodeStart":152,"sourceCodeEnd":188,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php#L152-L188","documentation":"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.","triggerScenarios":"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.","commonSituations":"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().","solutions":["Only refund charges in CHARGED status that were paid through this PayPal provider — check status before initiating the refund.","Inspect the charge metadata (paypal.transactionID, paypal.token) to confirm the charge actually completed the Express Checkout flow.","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.","For charges that never completed, fail/void the charge instead of refunding — there is nothing at PayPal to return."],"exampleFix":"// before\n$provider->executeRefund($charge, $refund);\n\n// after\n$txn = $charge->getMetadataValue('paypal.transactionID');\nif ($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED || !strlen((string)$txn)) {\n  throw new Exception(\n    pht('Charge %s never completed via PayPal; void it instead of refunding.',\n      $charge->getPHID()));\n}\n$provider->executeRefund($charge, $refund);","handlingStrategy":"validation","validationCode":"// Preconditions for a PayPal refund.\nfunction paypal_charge_is_refundable(PhortuneCharge $charge) {\n  if ($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) {\n    return false;\n  }\n  return strlen((string) $charge->getMetadataValue('paypal.transactionID')) > 0;\n}\n\nif (!paypal_charge_is_refundable($charge)) {\n  // skip or surface 'nothing at PayPal to refund' instead of calling executeRefund\n}","typeGuard":"function charge_has_paypal_transaction_id(PhortuneCharge $charge) {\n  $txn = $charge->getMetadataValue('paypal.transactionID');\n  return is_string($txn) && strlen($txn) > 0;\n}","tryCatchPattern":"try {\n  $provider->executeRefund($charge, $refund);\n} catch (Exception $ex) {\n  // Leave the refund charge unapplied and log enough context to reconcile:\n  // the charge PHID tells you which PayPal record to inspect.\n  phlog(pht('Refund failed for charge %s: %s', $charge->getPHID(), $ex->getMessage()));\n  throw $ex;\n}","preventionTips":["Only refund CHARGED charges that were paid through the same provider.","Never refund HOLD/FAIL charges — void/fail them instead; no money moved at PayPal.","Build refunds through the charge/cart API so provider routing and status checks are automatic.","If you must repair data, restore 'paypal.transactionID' from PayPal's transaction history before retrying."],"tags":["php","phabricator","phortune","paypal","refund","metadata","payments"],"backgroundTag":"missing-payment-metadata","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}