phacility/phabricator · error · Exception
Trying to refund an uncharged charge!
Error message
Trying to refund an uncharged charge!
What it means
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.
Source
Thrown at src/applications/phortune/storage/PhortuneCart.php:331
if (!$amount->isPositive()) {
throw new Exception(
pht('Trying to refund non-positive amount of money!'));
}
if ($amount->isGreaterThan($charge->getAmountRefundableAsCurrency())) {
throw new Exception(
pht('Trying to refund more money than remaining on charge!'));
}
if ($charge->getRefundedChargePHID()) {
throw new Exception(
pht('Trying to refund a refund!'));
}
if (($charge->getStatus() !== PhortuneCharge::STATUS_CHARGED) &&
($charge->getStatus() !== PhortuneCharge::STATUS_HOLD)) {
throw new Exception(
pht('Trying to refund an uncharged charge!'));
}
$refund_charge = PhortuneCharge::initializeNewCharge()
->setAccountPHID($this->getAccount()->getPHID())
->setCartPHID($this->getPHID())
->setAuthorPHID($actor->getPHID())
->setMerchantPHID($this->getMerchant()->getPHID())
->setProviderPHID($provider->getProviderConfig()->getPHID())
->setPaymentMethodPHID($charge->getPaymentMethodPHID())
->setRefundedChargePHID($charge->getPHID())
->setAmountAsCurrency($amount->negate());
$charge->openTransaction();
$charge->beginReadLocking();
$copy = clone $charge;
$copy->reload();View on GitHub (pinned to 5720a38cfe)
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.
Example fix
// before
$refund = $cart->willRefundCharge($actor, $provider, $charge, $amount); // throws unless CHARGED/HOLD
// after
$refundable = in_array(
$charge->getStatus(),
array(PhortuneCharge::STATUS_CHARGED, PhortuneCharge::STATUS_HOLD));
if ($refundable) {
$refund = $cart->willRefundCharge($actor, $provider, $charge, $amount);
} else {
$errors[] = pht('This charge has not been captured yet and cannot be refunded.');
} Defensive patterns
Strategy: validation
Validate before calling
$ok = in_array(
$charge->getStatus(),
array(PhortuneCharge::STATUS_CHARGED, PhortuneCharge::STATUS_HOLD));
if (!$ok) {
// charge has not been captured; do not offer or perform a refund
} Type guard
function isRefundableCharge(PhortuneCharge $charge) {
return in_array(
$charge->getStatus(),
array(PhortuneCharge::STATUS_CHARGED, PhortuneCharge::STATUS_HOLD))
&& $charge->getRefundedChargePHID() === null;
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Charge has no transaction ID!
- Unable to refund charge; no Stripe chargeID!
- Unable to update charge; no Stripe chargeID!
- Trying to refund non-positive amount of money!
- Trying to refund more money than remaining on charge!
AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21).
Data as JSON: /api/errors/c1a6709249ad1f89.
Report an issue: GitHub.