{"record":{"id":"c1a6709249ad1f89","repo":"phacility/phabricator","slug":"trying-to-refund-an-uncharged-charge","errorCode":null,"errorMessage":"Trying to refund an uncharged charge!","messagePattern":"Trying to refund an uncharged charge!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/phortune/storage/PhortuneCart.php","lineNumber":331,"sourceCode":"\n    if (!$amount->isPositive()) {\n      throw new Exception(\n        pht('Trying to refund non-positive amount of money!'));\n    }\n\n    if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {\n      throw new Exception(\n        pht('Trying to refund more money than remaining on charge!'));\n    }\n\n    if ($charge->getRefundedChargePHID()) {\n      throw new Exception(\n        pht('Trying to refund a refund!'));\n    }\n\n    if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&\n        ($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {\n      throw new Exception(\n        pht('Trying to refund an uncharged charge!'));\n    }\n\n    $refund_charge = PhortuneCharge::initializeNewCharge()\n      ->setAccountPHID($this->getAccount()->getPHID())\n      ->setCartPHID($this->getPHID())\n      ->setAuthorPHID($actor->getPHID())\n      ->setMerchantPHID($this->getMerchant()->getPHID())\n      ->setProviderPHID($provider->getProviderConfig()->getPHID())\n      ->setPaymentMethodPHID($charge->getPaymentMethodPHID())\n      ->setRefundedChargePHID($charge->getPHID())\n      ->setAmountAsCurrency($amount->negate());\n\n    $charge->openTransaction();\n      $charge->beginReadLocking();\n\n        $copy = clone $charge;\n        $copy->reload();","sourceCodeStart":313,"sourceCodeEnd":349,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/storage/PhortuneCart.php#L313-L349","documentation":"willRefundCharge() only accepts charges whose status is STATUS_CHARGED or STATUS_HOLD; any other status throws 'Trying to refund an uncharged charge!'. Money must have been captured (or is being held pending review) before Phortune can refund it. Authorized-but-unpaid, pending, and failed charges are all rejected.","triggerScenarios":"Calling willRefundCharge() on a charge that is still STATUS_AUTHORIZED/pending because the provider flow has not completed, or on a STATUS_FAILED charge, or from stale data where the charge never reached CHARGED.","commonSituations":"Refund buttons rendered too early in an asynchronous payment flow; test charges that never succeeded; retries of a failed charge being mistaken for a completed one.","solutions":["Wait until the charge reaches STATUS_CHARGED (or STATUS_HOLD for held charges) before exposing refund actions.","Gate refund UI on $charge->getStatus() being PhortuneCharge::STATUS_CHARGED or STATUS_HOLD.","For held charges, remember that a refund releases the hold rather than moving money back."],"exampleFix":"// before\n$refund = $cart->willRefundCharge($actor, $provider, $charge, $amount); // throws unless CHARGED/HOLD\n\n// after\n$refundable = in_array(\n  $charge->getStatus(),\n  array(PhortuneCharge::STATUS_CHARGED, PhortuneCharge::STATUS_HOLD));\nif ($refundable) {\n  $refund = $cart->willRefundCharge($actor, $provider, $charge, $amount);\n} else {\n  $errors[] = pht('This charge has not been captured yet and cannot be refunded.');\n}","handlingStrategy":"validation","validationCode":"$ok = in_array(\n  $charge->getStatus(),\n  array(PhortuneCharge::STATUS_CHARGED, PhortuneCharge::STATUS_HOLD));\nif (!$ok) {\n  // charge has not been captured; do not offer or perform a refund\n}","typeGuard":"function isRefundableCharge(PhortuneCharge $charge) {\n  return in_array(\n      $charge->getStatus(),\n      array(PhortuneCharge::STATUS_CHARGED, PhortuneCharge::STATUS_HOLD))\n    && $charge->getRefundedChargePHID() === null;\n}","tryCatchPattern":null,"preventionTips":["Expose refund actions only after the provider confirms the charge (STATUS_CHARGED/HOLD).","Refresh charge status from the database before refunding.","Treat held charges as refundable only to release the hold."],"tags":["phortune","phabricator","php","payments","refund","charge-status"],"backgroundTag":"invalid-charge-status","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}