{"record":{"id":"e2f808556657ad4b","repo":"actualbudget/actual","slug":"subtransaction-amount-is-invalid-must-be-an-integ","errorCode":null,"errorMessage":"Subtransaction amount is invalid, must be an integer: ${sub.amount}","messagePattern":"Subtransaction amount is invalid, must be an integer: (.+?)","errorType":"validation","errorClass":"TransactionError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/accounts/sync.ts","lineNumber":468,"sourceCode":"    // layer does better validation, but this will give nicer errors\n    if (trans.date == null) {\n      throw new Error('`date` is required when adding a transaction');\n    }\n\n    // Strip off the irregular properties\n    const { payee_name: originalPayeeName, subtransactions, ...rest } = trans;\n    trans = rest;\n\n    if (trans.amount != null && !Number.isInteger(trans.amount)) {\n      throw new TransactionError(\n        `Amount is invalid, must be an integer: ${trans.amount}`,\n      );\n    }\n\n    if (subtransactions) {\n      for (const sub of subtransactions) {\n        if (sub.amount != null && !Number.isInteger(sub.amount)) {\n          throw new TransactionError(\n            `Subtransaction amount is invalid, must be an integer: ${sub.amount}`,\n          );\n        }\n      }\n    }\n\n    let payee_name = originalPayeeName;\n    if (payee_name) {\n      const trimmed = payee_name.trim();\n      if (trimmed === '') {\n        payee_name = null;\n      } else {\n        payee_name = normalizePayeeName(trimmed, payeeNameNormalization);\n      }\n    }\n\n    trans.imported_payee = trans.imported_payee || payee_name;\n    if (trans.imported_payee) {","sourceCodeStart":450,"sourceCodeEnd":486,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/accounts/sync.ts#L450-L486","documentation":"Actual Budget stores all money amounts as integer cents. During transaction normalization in the account sync pipeline, every subtransaction (split) amount is checked with Number.isInteger before being persisted. If a subtransaction has a non-null amount that is fractional or non-numeric (e.g. 10.5, \"10.50\", NaN), sync.ts throws this TransactionError to prevent corrupted ledger data.","triggerScenarios":"normalizeTransactions is called (directly or via bank sync / file import) with a transaction whose subtransactions array contains an entry where amount is a float (10.5), a numeric string (\"10.50\"), NaN, or another non-integer value while also being non-null.","commonSituations":"Bank importers or bank-sync adapters that compute subtransaction amounts from decimal currency values without multiplying by 100 and rounding; custom API scripts (actual-js) passing dollar floats as sub.amount; CSV import rules that split a transaction into percentage-based pieces producing fractional cents; floating-point arithmetic (0.1+0.2) producing amounts like 30.000000000000004.","solutions":["Convert amounts to integer cents before calling the sync/import APIs: Math.round(amount * 100).","If amounts arrive as strings (\"10.50\"), parse and convert: Math.round(parseFloat(s) * 100).","Audit the bank-sync/import adapter producing the subtransactions and ensure it emits cents, not dollars.","Catch TransactionError in the caller and surface which subtransaction failed so the source data can be corrected."],"exampleFix":"// before\nsubtransactions: [\n  { amount: 12.5, category: 'groceries' },\n]\n// after\nsubtransactions: [\n  { amount: Math.round(12.5 * 100), category: 'groceries' }, // 1250 cents\n]","handlingStrategy":"validation","validationCode":"function hasValidSubamounts(t) {\n  return (t.subtransactions ?? []).every(\n    sub => sub.amount == null || (typeof sub.amount === 'number' && Number.isInteger(sub.amount)),\n  );\n}\nif (!hasValidSubamounts(txn)) throw new Error('subtransaction amounts must be integer cents');","typeGuard":"function isCents(n) {\n  return typeof n === 'number' && Number.isInteger(n);\n}","tryCatchPattern":null,"preventionTips":["Always convert dollars to cents with Math.round(x * 100) at the boundary of your integration.","Parse string amounts explicitly (parseFloat) before converting; never pass strings through.","Beware floating-point drift — round, don't truncate, when computing cents."],"tags":["validation","transactions","data-integrity","integer-cents"],"backgroundTag":"non-integer-currency-amount","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}