AutoCount Cloud Integration

AutoCount Cloud Integration #

Classification: Internal-Only

AutoCount Cloud is a Malaysian cloud accounting platform. tofu authenticates with an API key (API-Key + Key-ID headers, minted per account book in AutoCount’s Settings → API Keys). There is no OAuth flow.

The connector reads reference data and documents, and publishes AP bills, AR invoices and direct expenses. Bank statements are not published — see Bank statements. Everything below is what the code does; where AutoCount’s own documentation disagrees, the code follows what the live API was observed to do.

Connector module: libs/rust/bonsai-integration/src/accounting/autocount/.

Support matrix #

Type Create Republish Delete / void Attachments
AP bill POST /{book}/purchaseInvoice delete, then create DELETE /{book}/purchaseinvoice?docNo= — refused when knocked off none
AR invoice POST /{book}/invoice delete, then create DELETE /{book}/invoice?docNo= — refused when knocked off none
Direct expense POST /{book}/payment (PV out, OR in) delete, then create DELETE /{book}/payment?docNo= none
AP credit note POST /{book}/purchaseReturn delete, then create DELETE /{book}/purchasereturn?docNo= — refused when knocked off none
AR credit note POST /{book}/creditNote delete, then create DELETE /{book}/creditnote?docNo= — refused when knocked off none
Bank statement not published — AutoCount has no statement API — — —

update_invoice and update_direct_expense are deliberately unimplemented. AutoCount’s PUT is a positional patch: a shorter details array silently deletes the trailing rows, and {} means “leave this row alone”. AutoCount is therefore in EntityIntegration::should_delete_before_publish, and every republish is DELETE + POST. If you ever reach one of those methods, the policy list is wrong, not the method.

Void is not wired. delete_invoice only issues a DELETE; AutoCount’s void endpoint (which requires a voidReason) is not called, and voiding is refused on a knocked-off document anyway.

Attachments are not supported at all #

AutoCount’s Integration API has no attachment endpoint. The web app has File Attachment; it is not exposed over the API.

The connector therefore has no attachment capability flag, and the trait methods are inert rather than implemented: the downloads return an empty Vec, delete_direct_expense_attachment returns Ok(()), and the uploads return unimplemented. Do not add a capability flag to “enable” them — there is nothing behind it.

Authentication: no scopes, no reconnect #

AutoCount has no scope list and no incremental consent, so adding publish required no new scopes and no customer reconnect. Connections made before publish shipped keep working unchanged. There is no reconnect migration for this provider and no need to write one.

Re-entering the API key for an existing integration is this provider’s reconnect. That matters for the next point.

Auto-publish is not reset on connect #

AutoCount is not in should_disable_auto_publish_on_first_connect, and the connect path leaves the entity’s flags alone: per-extraction-type auto-publish is being reworked in the extraction overhaul, and a provider-specific reset here would only have to be unpicked again. An entity that already had auto-publish on keeps it.

Credit notes #

AP credit notes publish as Purchase Returns (PR), AR credit notes as Credit Notes (CN). Despite its name a Purchase Return carries a dedicated supplierCNNo beside supplierInvoiceNo, and with goodsReturn left off it books a supplier credit without touching stock — which is what tofu has to publish.

Both store their amounts positive and post to the contact ledger with the opposite sign to the invoice they reverse, so credit_note::normalized already produces the right shape and nothing flips a sign.

A credit note publishes standalone. Applying it to an invoice is a separate Knock Off Entry, and ourInvoiceNo on a Credit Note is free text that AutoCount neither validates nor links. Once knocked off, AutoCount refuses both DELETE and /void, so void is not a fallback.

Status comes from AutoCount’s own allocation labels, which are not the invoice payment labels: Unapplied → Authorized, Partially Applied → PartiallyPaid, Fully Applied → Paid, Void → Voided.

A document AutoCount considers settled cannot be removed, and a republish is a delete, so a paid document cannot be republished either.

Two sources answer two different questions, and they diverge:

  • Paid is master.status == "Full Payment" (or "Partial Payment"), which the detail read already carries for free.
  • Undeletable is a non-empty GET /{book}/{purchaseinvoice|invoice}/knockoffdetails?docNo=, and nothing else. An AR invoice settled through the master’s inline paymentAmt reads "Full Payment" while knockoffdetails returns [], and is still deletable.

delete_invoice reads the knock-offs first and refuses with errorMessage.autoCount.DELETE_REFUSED_KNOCKED_OFF before calling AutoCount, because AutoCount’s own refusal names the contact’s control account rather than the document (Can't remove account "400-B001" because it was knocked off in Knock Off Entry.) and would be unreadable in the UI. The knock-off read only costs a call on documents that have payments.

delete_direct_expense does not pre-check. It posts the DELETE and maps AutoCount’s refusal through map_cash_book_write_error onto the same key. The outcome matches; the cost is one failed call.

There is no “paid, refuse edits” rule on AutoCount’s side. A PUT changing only a description on a settled bill returns 204. The protection is entirely tofu’s.

Voided documents read back 200, not 404, with cancelled: true, status: "Void" and all amounts retained, so map_autocount_invoice_status maps them to Voided — miss that and a voided bill re-ingests at full value.

Quirks worth knowing before you debug #

Documents are auto-numbered. docNo is never sent. It is optional on every write resource — AutoCount’s docs are wrong about this on Create Payment — and AutoCount answers 201 with an empty body and the number in the location header (.../purchaseinvoice?docNo=PI-000001). Sending one invites a duplicate-number rejection for nothing. The supplier’s own number goes in supplierInvoiceNo on AP; on AR it falls back into ref.

The receipt voucher doc type is OR, not RV. RV is rejected outright with 400 "DocType is invalid." and matches no record in any book. The connector shipped with RV during read-only work, which also broke reading real OR records.

Every autoFillOption flag is false, and nothing can set one. With taxCode: true, AutoCount silently discards the line’s explicit tax code: the same P-5 on a 100.00 line stores taxCode: "P-5" / tax: 5.00 / finalTotal: 105.00 with autofill off, and taxCode: null / tax: 0.00 / finalTotal: 100.00 with it on — and still answers 201. A verified tax rate vanishes and the document under-posts with no error. accNo autofill is inert in both directions and does not supply a missing account, which is a hard 400. A test asserts all three flags.

finalTotal is the grand total; total is not. total is the sum of the line amounts as entered — net on a tax-exclusive document, gross on an inclusive one. Mapping tofu’s total to total under-reports every exclusive bill by exactly the tax while looking correct on inclusive ones. netTotal equals finalTotal when there is no rounding adjustment.

AP/AR currency follows the contact and is never sent. AutoCount ignores a currency on the document and books in the creditor’s or debtor’s. Only currencyRate goes on the wire, and it is always explicit: omitted on a foreign-currency contact, AutoCount applies whatever rate the book happens to hold. tofu carries no exchange rate for AutoCount, so a contact whose currency differs from the account book’s base currency is rejected with errorMessage.autoCount.EXCHANGE_RATE_REQUIRED rather than published at a guessed rate. Direct expenses do send currencyCode, and reject a non-base currency the same way.

Tax-rate ids carry an entity suffix. AutoCount exposes no tax-rate master endpoint, so tofu seeds its own SST catalogue (uses_internal_accounting_tax_rates), and those ids are shaped "{code}|{entity_id}" — which is what the review form stores on a line. AutoCount knows only the bare code. autocount_tax_code (model/tax_rate.rs) strips the suffix in both write builders and leaves a bare code alone, because the connector’s own fetch_tax_rates returns bare codes. Without it every publish carrying a tax code fails.

creditTerm comes from the contact, read back from GET /{book}/creditor|debtor?code=. It is required on the master and there is no safe constant.

purchaseLocation / salesLocation are required on AP/AR. The document’s location is used when it has one, otherwise the account book’s default from fetch_location, otherwise LOCATION_REQUIRED.

journalType is required on every cash book entry and has no catalog endpoint. An unknown value is rejected (JournalType (JournalTypeKey = NOTATYPE) not exists.), and it does not have to match the payment method’s, so it cannot be derived from the posting. The connector takes the payment method’s own journalType, falling back to BANK or CASH from specialAccType, because that value is guaranteed to exist in the book while a hardcoded constant is not.

A payment method is matched by free-text name, not a code. paymentDetails[].paymentMethod takes the name, spaces included, so the name is also the id tofu stores. Resolving one from a GL account can be ambiguous — nothing stops two methods pointing at the same account — so the tie-break is fixed: active first, then one whose account really is bank or cash, then alphabetical, so an account always resolves to the same method.

sum(paymentDetails[].paymentAmt) must equal the line total, tax-inclusive, or AutoCount answers Total Payment not equal to Total Amount.

Only leaf departments are selectable; a parent gives DeptNo "..." is parent department. Areas are a debtor-level dimension with no line field, so an area_-prefixed tag id is rejected rather than dropped.

Listing conventions differ per endpoint, not per family. Document listings (invoice, purchaseinvoice, payment) are POST and answer camelCase. Master-data listings are mostly GET with query params (a POST returns 405), require page, and answer PascalCase. POST /{book}/account/listing takes page as a query param and ignores the body; POST /{book}/product/listing wants it in the body. A casing miss leaves a catalog silently empty, which is why the payment-method model carries camelCase aliases.

Errors are one string, never a list, in {"statusCode", "message"} with no field name. The messages are far better than the envelope suggests: they name the offending field, value and line number, and the fiscal-year rejection even names the valid range. AutoCount rejects over-length fields rather than truncating them.

Bank statements are not published #

AutoCount has no bank-feed API and no statement-import API — a full sitemap sweep returns zero matches. The only way to post statement data would be one Cash Book Entry per transaction, the same object a direct expense uses, which puts two different things in one drawer and risks booking the same movement twice when a receipt and a statement line both describe it.

AutoCount’s own Bank Transactions screen is the right home for this, and it is web-app only. The connector carries no bank statement publish code.

Known gaps #

  • MyInvois (LHDN e-invoicing) is unprobed. Every e-invoice field is omitted, which is the confirmed safe default (submitEInvoice: false), and an AR request-shape test asserts an exact match so one appearing would fail. Whether a book can be configured to auto-submit, and whether that blocks delete, needs a throwaway account book — never a real one, because deleting a document does not undo a tax filing.
  • autocount_compatibility (model/account.rs) is too permissive. It marks cash and debtor/creditor control accounts valid on AP/AR lines; AutoCount rejects them (AccNo "310-1000" is not valid account for posting.). They are legal on a cash-book line, so the rule is line-type-specific. The tofu validator catches it; the read-side flag still lies.
  • The shared is_publish_account excludes Bank, so a bank account on a cash-book line reports ACCOUNT_NOT_FOUND although AutoCount accepts it. Pre-existing and shared across providers, so it is pinned by a test rather than changed.
  • Several error keys have no caller. ATTACHMENTS_NOT_SUPPORTED, VOID_REASON_REQUIRED, CREDIT_TERM_UNKNOWN, TAX_DATE_BEFORE_DOC_DATE, CURRENCY_NOT_IN_BOOK, ITEM_VARIANT_REQUIRED, TAX_CODE_UNKNOWN and DOC_DATE_OUTSIDE_FISCAL_YEAR are in the catalog and translated, but nothing returns them: the parity test only checks that the Rust list and error.json agree. The tofu validator catches most of these first under its own invoiceValidation / directExpenseValidation keys, and anything it misses surfaces as AutoCount’s own message. Do not assume a key here means a mapped path.
  • Deep-link URLs to AutoCount’s web UI are unverified against the live product.
  • Line descriptions are not length-capped. AutoCount caps a detail description at 100 and rejects rather than truncating, so a long line fails the publish with its raw message. The master description is capped at 80 and is truncated, with the full text kept in note.
  • Eight combination probes remain open — foreign-currency AR, product variants, inactive master data, 5-cent rounding, an approval workflow, a closed period, and a scoped API key for 403-versus-401 — each needing an account book the test book cannot provide.