{"record":{"id":"6764a65cd0dd0568","repo":"phacility/phabricator","slug":"invalid-currency-format-s","errorCode":null,"errorMessage":"Invalid currency format ('%s').","messagePattern":"Invalid currency format \\('(.+?)'\\)\\.","errorType":"validation","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/phortune/currency/PhortuneCurrency.php","lineNumber":124,"sourceCode":"  }\n\n  public function getValue() {\n    return $this->value;\n  }\n\n  public function getCurrency() {\n    return $this->currency;\n  }\n\n  public function getValueInUSDCents() {\n    if ($this->currency !== 'USD') {\n      throw new Exception(pht('Unexpected currency!'));\n    }\n    return $this->value;\n  }\n\n  private static function throwFormatException($string) {\n    throw new Exception(pht(\"Invalid currency format ('%s').\", $string));\n  }\n\n  private function throwUnlikeCurrenciesException(PhortuneCurrency $other) {\n    throw new Exception(\n      pht(\n        'Trying to operate on unlike currencies (\"%s\" and \"%s\")!',\n        $this->currency,\n        $other->currency));\n  }\n\n  public function add(PhortuneCurrency $other) {\n    if ($this->currency !== $other->currency) {\n      $this->throwUnlikeCurrenciesException($other);\n    }\n\n    $currency = new PhortuneCurrency();\n\n    // TODO: This should check for integer overflows, etc.","sourceCodeStart":106,"sourceCodeEnd":142,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/phortune/currency/PhortuneCurrency.php#L106-L142","documentation":"Thrown by PhortuneCurrency::throwFormatException(), reached from PhortuneCurrency::newFromString() when the input string does not match Phortune's strict currency grammar: an optional '-' and/or '$' prefix (each at most once), digits with at most two decimal places, and an optional uppercase currency code separated by whitespace (e.g. '12.34 USD'). Phortune stores money as integer cents, so it refuses any string it cannot unambiguously convert to cents. The failing string is echoed verbatim in the message, which is the fastest way to see which grammar rule was violated.","triggerScenarios":"Calling PhortuneCurrency::newFromString() or PhortuneCurrency::newFromUserInput() with input such as: '1.234' or '1.234 USD' (the regex allows only [.]\\d{0,2}, i.e. at most two decimals); '1,23' (comma decimal separator); '$5$' or '--5' (substr_count checks reject a repeated '-' or '$'); '12 usd' (currency code must be [A-Z]+); '12USD' (missing whitespace before the code); 'USD 12' (code before the amount); or any string with embedded text. Note the string is trim()ed first, so surrounding whitespace alone is fine.","commonSituations":"Payment or product-price forms in Phabricator/Phortune where users type amounts freehand; locales that use comma decimal separators or thousands separators ('1,299.99'); currency codes pasted in lowercase from ISO tables; amounts imported from spreadsheets with more than two decimal places; test fixtures that assume a lenient float parser.","solutions":["Compare the echoed string in the message against the grammar: at most 2 decimal places, '-' and '$' only once each and only at the front, optional single uppercase currency code after a space.","Normalize user input before parsing: trim(), strip thousands separators/commas, round to 2 decimals, uppercase the currency code, and append ' USD' if no code was given.","Pre-validate with a regex equivalent to ^[-$]*(?:\\d+)?(?:[.]\\d{0,2})?(?:\\s+([A-Z]+))?$ before calling newFromString(), and reject early with a friendly form error.","If the input comes from your own code rather than a user, stop serializing floats and pass strings produced by formatBareValue().' '.getCurrency() (what serializeForStorage() emits), which always round-trips."],"exampleFix":"// before\n$currency = PhortuneCurrency::newFromString($user_typed);\n\n// after\n$raw = strtoupper(trim($user_typed));\n$raw = str_replace(',', '.', $raw);                 // comma locales\nif (!preg_match('/^[-$]*(\\d+)?(?:[.]\\d{0,2})?(?:\\s+[A-Z]{3})?$/', $raw)) {\n  throw new Exception(pht('Enter an amount like \"12.34 USD\".'));\n}\n$currency = PhortuneCurrency::newFromString($raw, 'USD');","handlingStrategy":"validation","validationCode":"// Validate against Phortune's grammar BEFORE parsing:\n// ^[-$]* digits [. up-to-2 decimals] [ whitespace UPPERCASE code ]\nfunction phortune_currency_string_is_valid($raw) {\n  $s = trim($raw);\n  if ($s === '') {\n    return false;\n  }\n  $ok = preg_match('/^([-$]*)(\\d+)?(?:[.](\\d{0,2}))?(?:\\s+([A-Z]+))?$/', $s, $m);\n  if (!$ok) {\n    return false;\n  }\n  // newFromString also rejects repeated '-' or repeated '$'.\n  if (substr_count($m[1], '-') > 1 || substr_count($m[1], '$') > 1) {\n    return false;\n  }\n  // '.' alone, '$' alone, or '' with no digits would cast to 0; treat as invalid.\n  if (!isset($m[2]) && !isset($m[3])) {\n    return false;\n  }\n  return true;\n}\n\nif (!phortune_currency_string_is_valid($input)) {\n  $error = pht('Enter an amount like \"12.34 USD\" (two decimal places max).');\n} else {\n  $currency = PhortuneCurrency::newFromString($input, 'USD');\n}","typeGuard":null,"tryCatchPattern":"try {\n  $currency = PhortuneCurrency::newFromString($input, 'USD');\n} catch (Exception $ex) {\n  // Message embeds the offending string; surface as a form field error,\n  // never let it escape as a 500.\n  $e_price = pht('Invalid');\n  $errors[] = $ex->getMessage();\n}","preventionTips":["Normalize user input first: trim, uppercase, strip thousands separators, round to 2 decimals.","Always pass the default currency ('USD') so code-only inputs don't rely on the optional suffix.","Round-trip stored values via serializeForStorage()/formatBareValue(), never via float serialization.","In forms, use a numeric input pattern (\\d+(\\.\\d{1,2})?) client-side to make invalid strings unreachable."],"tags":["php","phabricator","phortune","currency","input-validation","payments"],"backgroundTag":"currency-format-validation","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}