{"record":{"id":"b78a94a86cfed032","repo":"phacility/phabricator","slug":"stripe-refund-call-did-not-return-an-id","errorCode":null,"errorMessage":"Stripe refund call did not return an ID!","messagePattern":"Stripe refund call did not return an ID!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"critical","filePath":"src/applications/phortune/provider/PhortuneStripePaymentProvider.php","lineNumber":182,"sourceCode":"        pht('Unable to refund charge; no Stripe chargeID!'));\n    }\n\n    $refund_cents = $refund\n      ->getAmountAsCurrency()\n      ->negate()\n      ->getValueInUSDCents();\n\n    $secret_key = $this->getSecretKey();\n    $params = array(\n      'amount' => $refund_cents,\n    );\n\n    $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);\n    $stripe_refund = $stripe_charge->refunds->create($params);\n\n    $id = $stripe_refund->id;\n    if (!$id) {\n      throw new Exception(pht('Stripe refund call did not return an ID!'));\n    }\n\n    $charge->setMetadataValue('stripe.refundID', $id);\n    $charge->save();\n  }\n\n  public function updateCharge(PhortuneCharge $charge) {\n    $this->loadStripeAPILibraries();\n\n    $charge_id = $charge->getMetadataValue('stripe.chargeID');\n    if (!$charge_id) {\n      throw new Exception(\n        pht('Unable to update charge; no Stripe chargeID!'));\n    }\n\n    $secret_key = $this->getSecretKey();\n    $stripe_charge = Stripe_Charge::retrieve($charge_id, $secret_key);\n","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/provider/PhortuneStripePaymentProvider.php#L164-L200","documentation":"Exception thrown by PhortuneStripePaymentProvider::executeRefund() when the refund object returned by $stripe_charge->refunds->create($params) has a falsy ->id. The refund ID is what Phortune stores as 'stripe.refundID' metadata; without it a possibly-executed refund at Stripe has no local trace, leaving the books unbalanced (customer money returned, Phortune unaware). Mirrors the charge-side check: the legacy SDK path can yield objects without ids instead of throwing.","triggerScenarios":"Stripe_Charge::retrieve() succeeds, refunds->create() returns a malformed object (old stripe-php versions, API shape drift, mocks) — ->id empty. The Stripe-side refund may or may not actually have been issued, so the state is unknown and must be reconciled, not retried blindly.","commonSituations":"Vendored Stripe library out of sync with the live API version; sandbox test doubles returning stdClass; network middleboxes mangling the JSON response; partial refunds already issued making subsequent responses unexpected to old SDK parsers.","solutions":["Reconcile first: retrieve the Stripe charge (you have stripe.chargeID) and inspect its refunds collection to see whether the refund executed.","If it executed, record the real refund ID into 'stripe.refundID' metadata and complete the local refund bookkeeping.","If it did not execute, retry the refund once reconciled. Never retry before checking — a double refund loses money.","Upgrade the vendored stripe-php library to the API version in use and re-test refunds in Stripe test mode."],"exampleFix":"// before\n$stripe_refund = $stripe_charge->refunds->create($params);\n$id = $stripe_refund->id;\nif (!$id) {\n  throw new Exception(pht('Stripe refund call did not return an ID!'));\n}\n\n// after: reconcile the unknown refund state\n$stripe_refund = $stripe_charge->refunds->create($params);\n$id = $stripe_refund->id;\nif (!$id) {\n  $fresh = Stripe_Charge::retrieve($charge_id, $secret_key);\n  phlog(pht('Refund for %s returned no ID; %d refunds exist at Stripe. Reconcile before retrying.', $charge->getPHID(), count($fresh->refunds->data)));\n  throw new Exception(\n    pht('Stripe refund state unknown for %s; reconcile in Stripe dashboard.',\n      $charge->getPHID()));\n}\n$charge->setMetadataValue('stripe.refundID', $id);\n$charge->save();","handlingStrategy":"try-catch","validationCode":"// Shape-check the refund result before recording it.\nfunction stripe_refund_has_id($stripe_refund) {\n  return is_object($stripe_refund) && !empty($stripe_refund->id);\n}\n\n$stripe_refund = $stripe_charge->refunds->create($params);\nif (!stripe_refund_has_id($stripe_refund)) {\n  // Unknown refund state: retrieve the charge and inspect ->refunds->data\n  // to see whether money actually moved before doing anything else.\n}","typeGuard":"function stripe_refund_has_id($stripe_refund) {\n  return is_object($stripe_refund) && !empty($stripe_refund->id);\n}","tryCatchPattern":"try {\n  $provider->executeRefund($charge, $refund);\n} catch (Exception $ex) {\n  if (strpos($ex->getMessage(), 'refund call did not return an ID') !== false) {\n    // Retrieve the charge (we hold stripe.chargeID) and reconcile:\n    $fresh = Stripe_Charge::retrieve($charge_id, $secret_key);\n    phlog(pht(\n      'UNRESOLVED STRIPE REFUND for %s: %d refunds at Stripe. Reconcile before retry.',\n      $charge->getPHID(),\n      count($fresh->refunds->data)));\n  }\n  throw $ex;\n}","preventionTips":["Never blind-retry a refund whose result was malformed — verify the charge's refunds list at Stripe first (a duplicate refund is lost money).","Keep the stripe-php library current with the API version you call; malformed-object errors are typically version drift or mocks.","Alert on this exception class: an untracked refund unbalances the books silently."],"tags":["php","phabricator","phortune","stripe","refund","payment-integrity","api-response"],"backgroundTag":"payment-gateway-invalid-response","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}