{"record":{"id":"0cb3327c435995ed","repo":"phacility/phabricator","slug":"charge-is-in-the-wrong-refunding-state","errorCode":null,"errorMessage":"Charge is in the wrong refunding state!","messagePattern":"Charge is in the wrong refunding state!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"critical","filePath":"src/applications/phortune/storage/PhortuneCart.php","lineNumber":379,"sourceCode":"    $charge->saveTransaction();\n\n    return $refund_charge;\n  }\n\n  public function didRefundCharge(\n    PhortuneCharge $charge,\n    PhortuneCharge $refund) {\n\n    $refund->setStatus(PhortuneCharge::STATUS_CHARGED);\n\n    $this->openTransaction();\n      $this->beginReadLocking();\n\n        $copy = clone $charge;\n        $copy->reload();\n\n        if ($charge->getRefundingPHID() !== $refund->getPHID()) {\n          throw new Exception(\n            pht('Charge is in the wrong refunding state!'));\n        }\n\n        $charge->setRefundingPHID(null);\n\n        // NOTE: There's some trickiness here to get the signs right. Both\n        // these values are positive but the refund has a negative value.\n        $total_refunded = $charge\n          ->getAmountRefundedAsCurrency()\n          ->add($refund->getAmountAsCurrency()->negate());\n\n        $charge->setAmountRefundedAsCurrency($total_refunded);\n        $charge->save();\n        $refund->save();\n\n      $this->endReadLocking();\n    $this->saveTransaction();\n","sourceCodeStart":361,"sourceCodeEnd":397,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/storage/PhortuneCart.php#L361-L397","documentation":"didRefundCharge() is the completion half of Phortune's two-phase refund: it requires the charge to still point at this refund (refundingPHID === refund PHID), a pairing that willRefundCharge() establishes. A mismatch means didRefundCharge() was called without a matching willRefundCharge(), with different objects, twice, or after the state was already cleared - an integration bug in the refund flow that must be fixed, not retried.","triggerScenarios":"Calling didRefundCharge($charge, $refund) for a refund never opened by willRefundCharge(); calling it a second time after a first completion; passing charge/refund objects from different operations; a provider implementation that retries the completion step after an error.","commonSituations":"Custom payment providers hand-writing the refund sequence instead of pairing will/did calls; queue retries that re-run the whole sequence; state left half-finished by an earlier crash being finished with mismatched objects.","solutions":["Implement the refund as a strict sequence: willRefundCharge() -> provider refund -> didRefundCharge() or didFailRefund(), reusing the exact objects willRefundCharge() returned.","Never re-run did* steps on retry; only the provider call may be retried.","When recovering half-finished state, inspect the charge's refundingPHID to decide whether to complete or clean up before calling did*."],"exampleFix":"// before\n$cart->didRefundCharge($charge, $refund); // called directly, may not match refundingPHID\n\n// after\n$fresh = clone $charge;\n$fresh->reload();\nif ($fresh->getRefundingPHID() === $refund->getPHID()) {\n  $cart->didRefundCharge($charge, $refund);\n} else {\n  // state already completed or belongs to another refund; do not call did* again\n}","handlingStrategy":"validation","validationCode":"$fresh = clone $charge;\n$fresh->reload();\nif ($fresh->getRefundingPHID() !== $refund->getPHID()) {\n  // wrong pairing or already completed; do not call didRefundCharge()\n}","typeGuard":"function refundMatchesCharge(PhortuneCharge $charge, PhortuneCharge $refund) {\n  return $charge->getRefundingPHID() === $refund->getPHID();\n}","tryCatchPattern":null,"preventionTips":["Always pair willRefundCharge() with exactly one didRefundCharge() or didFailRefund() using the same objects.","Never re-run completion steps on retry; retry only the provider call.","Recover crashed refunds by inspecting refundingPHID before finishing them."],"tags":["phortune","phabricator","php","payments","refund","state-machine","two-phase"],"backgroundTag":"state-machine-violation","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}