phacility/phabricator · error · Exception

Trying to refund more money than remaining on charge!

Error message

Trying to refund more money than remaining on charge!

What it means

willRefundCharge() throws when the requested amount exceeds $charge->getAmountRefundableAsCurrency(), the original charge amount minus all completed refunds. This prevents refunding more money than was ever captured on that charge. It is the main over-refund guard for partial refunds.

Source

Thrown at src/applications/phortune/storage/PhortuneCart.php:320

    $this->saveTransaction();

    return $this;
  }


  public function willRefundCharge(
    PhabricatorUser $actor,
    PhortunePaymentProvider $provider,
    PhortuneCharge $charge,
    PhortuneCurrency $amount) {

    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())

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Cap the requested amount at $charge->getAmountRefundableAsCurrency() and prefill refund forms with that value.
  2. Reload the charge immediately before refunding so prior refunds are reflected in the refundable total.
  3. Serialize refunds per charge (single refund button, lockstep flow) so the refundable balance cannot be double-spent.

Example fix

// before
$amount = $charge->getAmountAsCurrency(); // ignores earlier partial refund
$cart->willRefundCharge($actor, $provider, $charge, $amount); // throws: exceeds refundable

// after
$amount = $charge->getAmountRefundableAsCurrency();
if ($requested->isGreaterThan($amount)) {
  $requested = $amount; // clamp to remaining refundable
}
if ($requested->isPositive()) {
  $cart->willRefundCharge($actor, $provider, $charge, $requested);
}
Defensive patterns

Strategy: validation

Validate before calling

$refundable = $charge->getAmountRefundableAsCurrency();
if (!$amount->isPositive()) {
  $errors[] = pht('Refund amount must be greater than zero.');
} else if ($amount->isGreaterThan($refundable)) {
  $amount = $refundable; // or reject with an error
}

Prevention

When it happens

Trigger: Refunding the full charge amount again after an earlier partial or full refund; computing the refund from getAmountAsCurrency() instead of getAmountRefundableAsCurrency(); two concurrent refund flows each reading the same refundable balance.

Common situations: Support staff issuing repeated partial refunds; refund forms prefilled with the original price rather than remaining refundable; race between two admins refunding the same charge.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/c4d9d7a6a7862e3a. Report an issue: GitHub.