{"record":{"id":"94bf7cb3535b3b17","repo":"phacility/phabricator","slug":"cart-is-not-charging-yet","errorCode":null,"errorMessage":"Cart is not charging yet!","messagePattern":"Cart is not charging yet!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/phortune/provider/PhortunePayPalPaymentProvider.php","lineNumber":308,"sourceCode":"\n    $viewer = $request->getUser();\n\n    $cart = $controller->loadCart($request->getInt('cartID'));\n    if (!$cart) {\n      return new Aphront404Response();\n    }\n\n    $charge = $controller->loadActiveCharge($cart);\n    switch ($controller->getAction()) {\n      case 'checkout':\n        if ($charge) {\n          throw new Exception(pht('Cart is already charging!'));\n        }\n        break;\n      case 'charge':\n      case 'cancel':\n        if (!$charge) {\n          throw new Exception(pht('Cart is not charging yet!'));\n        }\n        break;\n    }\n\n    switch ($controller->getAction()) {\n      case 'checkout':\n        $return_uri = $this->getControllerURI(\n          'charge',\n          array(\n            'cartID' => $cart->getID(),\n          ));\n\n        $cancel_uri = $this->getControllerURI(\n          'cancel',\n          array(\n            'cartID' => $cart->getID(),\n          ));\n","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php#L290-L326","documentation":"Exception thrown by PhortunePayPalPaymentProvider::processControllerRequest() when the 'charge' or 'cancel' action arrives but loadActiveCharge($cart) finds no active charge. These actions are PayPal's return/cancel callbacks: they only make sense in the middle of an initiated checkout that created a charge. Arriving without one means the callback is being replayed or hit directly after the charge already resolved (applied, held, or failed) and was consumed.","triggerScenarios":"PayPal redirecting the buyer back to the return URL twice (refresh of the 'charge' page); a user bookmarking or re-opening the charge/cancel URL after completion; the cart status no longer being PURCHASING with the prior charge already resolved via didApplyCharge/didFailCharge; direct scraping/manual requests to those endpoints.","commonSituations":"Buyer refreshing the PayPal return landing page; email/IM sharing of the return URL; crawlers following redirect chains; double-fired redirects from PayPal sandbox flakiness.","solutions":["Send the user to the cart's checkout URI (cart->getCheckoutURI()) — the flow there already handles resolved carts gracefully with redirects instead of exceptions.","In custom controllers wrapping these actions, pre-check for an active charge and issue a redirect response for the no-charge case rather than letting the provider throw.","Accept idempotent callbacks: treat a second 'charge' hit on a purchased cart as a no-op redirect."],"exampleFix":"// before\n// GET /phortune/paypal/charge/?cartID=42&token=... a second time\n// -> Exception('Cart is not charging yet!')\n\n// after: guard the callback before dispatching to the provider\n$charge = $controller->loadActiveCharge($cart);\nif (!$charge) {\n  return id(new AphrontRedirectResponse())\n    ->setURI($cart->getCheckoutURI());\n}\n$response = $provider->processControllerRequest($controller, $request);","handlingStrategy":"validation","validationCode":"// Guard the PayPal return/cancel callbacks before dispatch.\n$charge = $controller->loadActiveCharge($cart);\n$action = $controller->getAction();\nif ($action === 'charge' || $action === 'cancel') {\n  if (!$charge) {\n    // Replay/direct hit after the charge already resolved: redirect gracefully.\n    return id(new AphrontRedirectResponse())\n      ->setURI($cart->getCheckoutURI());\n  }\n}\n$response = $provider->processControllerRequest($controller, $request);","typeGuard":null,"tryCatchPattern":"try {\n  $response = $provider->processControllerRequest($controller, $request);\n} catch (Exception $ex) {\n  if (strpos($ex->getMessage(), 'Cart is not charging') !== false) {\n    // Idempotent replay of the return URL: show the cart, not an error.\n    return id(new AphrontRedirectResponse())\n      ->setURI($cart->getCheckoutURI());\n  }\n  throw $ex;\n}","preventionTips":["Accept that buyers refresh return URLs; design wrappers to redirect resolved carts instead of throwing.","Never link or bookmark provider charge/cancel endpoints — link the cart checkout URI.","After didApplyCharge/didFailCharge, the active charge is consumed: any further callback hits this path."],"tags":["php","phabricator","phortune","paypal","cart","state-machine","callback"],"backgroundTag":"invalid-state-transition","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}