{"record":{"id":"a7cfa8f267a9e0f1","repo":"phacility/phabricator","slug":"trying-to-refund-a-refund","errorCode":null,"errorMessage":"Trying to refund a refund!","messagePattern":"Trying to refund a refund!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/phortune/storage/PhortuneCart.php","lineNumber":325,"sourceCode":"\n  public function willRefundCharge(\n    PhabricatorUser $actor,\n    PhortunePaymentProvider $provider,\n    PhortuneCharge $charge,\n    PhortuneCurrency $amount) {\n\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());","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/storage/PhortuneCart.php#L307-L343","documentation":"willRefundCharge() refuses its target because $charge->getRefundedChargePHID() is set: the object passed is itself a refund record, not an original charge. Refunds of refunds are forbidden because currency signs and refunded totals only stay consistent when a refund attaches to the original charge. Always resolve back to the original charge.","triggerScenarios":"Passing the negative-amount PhortuneCharge created by an earlier willRefundCharge() back in as the refund target; a refund UI that lists all charges (including refunds) and uses the selected row without filtering.","commonSituations":"Charge tables mixing charges and their refund rows; double-refund attempts that grab the wrong row; generic 'refund any charge' tooling that ignores record type.","solutions":["Follow $charge->getRefundedChargePHID() (up the chain) to the original charge and refund that instead.","Filter refund records out of any pick-list of refundable charges.","Validate getRefundedChargePHID() === null on the target before calling willRefundCharge()."],"exampleFix":"// before\n$refund = $cart->willRefundCharge($actor, $provider, $target, $amount); // $target may itself be a refund row\n\n// after\nwhile ($target->getRefundedChargePHID()) {\n  $target = id(new PhortuneChargeQuery())\n    ->setViewer($viewer)\n    ->withPHIDs(array($target->getRefundedChargePHID()))\n    ->executeOne();\n}\n$refund = $cart->willRefundCharge($actor, $provider, $target, $amount);","handlingStrategy":"validation","validationCode":"if ($charge->getRefundedChargePHID()) {\n  // this row is itself a refund; resolve the original charge first\n  $charge = id(new PhortuneChargeQuery())\n    ->setViewer($viewer)\n    ->withPHIDs(array($charge->getRefundedChargePHID()))\n    ->executeOne();\n}","typeGuard":"function isRefundCharge(PhortuneCharge $charge) {\n  return $charge->getRefundedChargePHID() !== null;\n}","tryCatchPattern":null,"preventionTips":["Filter refund rows out of refundable-charge lists.","Resolve refundedChargePHID to the original charge before refunding.","Label refund rows distinctly in charge UIs so operators never pick them."],"tags":["phortune","phabricator","php","payments","refund","double-refund"],"backgroundTag":"refund-of-refund","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}