{"record":{"id":"1dfc4d73020a85c6","repo":"phacility/phabricator","slug":"cart-is-already-charging","errorCode":null,"errorMessage":"Cart is already charging!","messagePattern":"Cart is already charging!","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/phortune/provider/PhortunePayPalPaymentProvider.php","lineNumber":302,"sourceCode":"    return parent::canRespondToControllerAction();\n  }\n\n  public function processControllerRequest(\n    PhortuneProviderActionController $controller,\n    AphrontRequest $request) {\n\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","sourceCodeStart":284,"sourceCodeEnd":320,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php#L284-L320","documentation":"Exception thrown by PhortunePayPalPaymentProvider::processControllerRequest() when the 'checkout' action arrives but loadActiveCharge($cart) already returns a charge. Phortune's state machine requires exactly one in-flight charge per cart: starting a new PayPal Express Checkout while an existing charge is active would create a second concurrent payment attempt for the same cart. This guard is an invariant check that stops double-checkout before SetExpressCheckout is ever called.","triggerScenarios":"A user clicks 'Pay with PayPal', then re-visits/reloads the provider checkout initiation URL while the first charge is still active (cart in STATUS_PURCHASING); double-click on the checkout submit; a stale browser tab replaying the checkout GET; automated hitting of the checkout route for an in-flight cart.","commonSituations":"Impatient buyers clicking pay twice; back-button navigation into the checkout start point after PayPal already redirected; QA scripts re-running checkout steps against the same cart; browser prefetch touching the checkout URL.","solutions":["Navigate to the cart's checkout URI and resume the existing in-flight charge instead of initiating a new checkout.","Fail or cancel the existing active charge (e.g. via the 'cancel' provider action) before starting a new checkout attempt.","In UI code, disable the pay button once the cart transitions to STATUS_PURCHASING so the second initiation never happens."],"exampleFix":"// before\n// user re-clicks \"Checkout with PayPal\" while a charge is active\n// -> Exception('Cart is already charging!')\n\n// after: in the checkout controller, short-circuit in-flight carts\nif ($cart->getStatus() === PhortuneCart::STATUS_PURCHASING) {\n  return id(new AphrontRedirectResponse())\n    ->setURI($cart->getCheckoutURI()); // resume existing charge\n}","handlingStrategy":"validation","validationCode":"// Before initiating a provider checkout, ensure no charge is in flight.\nfunction cart_can_start_checkout(PhortuneCart $cart) {\n  return $cart->getStatus() === PhortuneCart::STATUS_READY;\n}\n\nif (!cart_can_start_checkout($cart)) {\n  // PURCHASING cart: resume the existing charge instead of starting a new one\n  return id(new AphrontRedirectResponse())\n    ->setURI($cart->getCheckoutURI());\n}\n$provider->processControllerRequest($controller, $request);","typeGuard":null,"tryCatchPattern":"try {\n  $response = $provider->processControllerRequest($controller, $request);\n} catch (Exception $ex) {\n  // 'Cart is already charging!' is an invariant breach: send the user back\n  // to the cart rather than showing a raw error; log for double-click analysis.\n  phlog($ex->getMessage());\n  return id(new AphrontRedirectResponse())\n    ->setURI($cart->getCheckoutURI());\n}","preventionTips":["Disable the pay button as soon as the cart leaves STATUS_READY.","Treat the provider 'checkout' action as idempotent in wrappers: in-flight cart -> redirect to checkout URI.","Cancel the active charge before allowing a deliberate retry of payment."],"tags":["php","phabricator","phortune","paypal","cart","state-machine","double-payment"],"backgroundTag":"invalid-state-transition","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}