Skip to content

feat(count): add Count accounting integration with 43 actions#329

Open
Shubhank-Jonnada wants to merge 5 commits into
masterfrom
feat/count-integration
Open

feat(count): add Count accounting integration with 43 actions#329
Shubhank-Jonnada wants to merge 5 commits into
masterfrom
feat/count-integration

Conversation

@Shubhank-Jonnada
Copy link
Copy Markdown
Contributor

Summary

Count is a modern accounting platform built for startups and SMBs. This integration provides 43 actions covering the full accounting lifecycle — chart of accounts, customers, vendors, products, transactions, invoices, bills, journal entries, tags, and financial reports. Useful for automating bookkeeping, syncing financial data, and generating reports programmatically.

Actions

Action Description
list_accounts List all chart of accounts
create_account Add a new account to the chart of accounts
update_account Update an existing account
delete_account Delete an account from the chart of accounts
list_customers List all customers
get_customer Get a customer by UUID
find_customer_by_email Find a customer by email address
create_customer Add a new customer
update_customer Update an existing customer
delete_customer Delete a customer
list_vendors List all vendors
create_vendor Add a new vendor
update_vendor Update an existing vendor
delete_vendor Delete a vendor
list_products List all products and services
get_product Get a product by UUID
find_product_by_name Find a product by name
create_product Add a new product or service
update_product Update an existing product
delete_product Delete a product
list_transactions List all transactions with optional date range filtering
create_transaction Create a new transaction
update_transaction Update an existing transaction
delete_transaction Delete a transaction
get_invoice Get an invoice by UUID
create_invoice Create a new invoice
update_invoice Update an existing invoice
delete_invoice Delete an invoice
create_bill Create a new bill
update_bill Update an existing bill
delete_bill Delete a bill
approve_bill Approve a bill
list_journal_entries List all journal entries
create_journal_entry Create a new journal entry
update_journal_entry Update an existing journal entry
delete_journal_entry Delete a journal entry
list_tags List all tags
create_tag Create a new tag
update_tag Update an existing tag
delete_tag Delete a tag
get_trial_balance Get trial balance report for a date range
get_balance_sheet Get balance sheet report for a specific date
get_profit_and_loss Get profit and loss report for a date range

Authentication

  • Type: Platform OAuth2
  • Provider: count
  • Scopes: none required (workspace-scoped access token)

Files Added

  • count/config.json — Integration schema and action definitions
  • count/count.py — Action handlers
  • count/requirements.txt — Dependencies
  • count/README.md — Setup and usage documentation
  • count/icon.png — 512×512 integration icon
  • count/tests/conftest.py — Shared test fixtures
  • count/tests/test_count_unit.py — Unit tests (mocked)
  • count/tests/test_count_integration.py — Integration tests (live API)

Validation

  • validate_integration.py — passed
  • check_code.py — passed
  • Linting (ruff) — clean
  • Main README updated

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 381c58270e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread count/count.py
async def execute(self, inputs: Dict[str, Any], context: Any):
try:
params = {k: v for k, v in inputs.items() if v is not None}
resp = await _fetch(context, f"{BASE_URL}/reports/trial-balance", params=params)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use POST for report-generation endpoints

The report actions call context.fetch without a method, so they default to GET, but Count’s report-generation APIs (Add Trial Balance, Create Balance Sheet Report, Create PnL) are create/generate endpoints that require POST. In production this causes these actions to fail with method/path errors (or return list endpoints instead of generated reports), so users cannot generate fresh financial statements from get_trial_balance, get_balance_sheet, or get_profit_and_loss.

Useful? React with 👍 / 👎.

Comment thread count/count.py

count = Integration.load()

BASE_URL = "https://api.getcount.com"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use the documented partner API base path

All actions build URLs from https://api.getcount.com with resource paths like /customers, but Count’s partner API documentation defines partner endpoints under /partners/... (for example, the customer endpoint is documented as .../partners/customers). With the current base URL, every action risks calling non-existent or wrong routes, which would break the integration for normal authenticated requests.

Useful? React with 👍 / 👎.

@Shubhank-Jonnada
Copy link
Copy Markdown
Contributor Author

Autohive Code Review
Last reviewed commit: 381c58270ea7c81d0da6076bf3a8801b1d6b2b44 | 2026-05-18 00:00 UTC
P1: 0 | P2: 2 | P3: 0
Intent: matched

@Shubhank-Jonnada
Copy link
Copy Markdown
Contributor Author

Autohive Code Review

P1: 0 | P2: 2 | P3: 0
Security: 0 | Performance: 0 | Memory: 0 | Quality: 2 | Intent: 0

Full review of all changes. Intent verification: Implementation matches stated PR intent (43 actions, all domains covered, OAuth2 auth, all files present).

7 files read beyond the diff to verify findings: conftest.py (root), clickup/clickup.py, asana/, coda/, zoho/zoho.py, count/tests/conftest.py, count/tests/test_count_unit.py.


2 finding(s) — count/count.py

[P2] Quality — inputs.pop() mutates the caller-provided dict in all update handlers

Every update handler (update_account, update_customer, update_vendor, update_product, update_transaction, update_invoice, update_bill, update_journal_entry, update_tag) calls inputs.pop("..._uuid") to extract the resource UUID before passing the remaining dict as the JSON body.

This pattern mutates the inputs dict that was passed in by the framework. Every other integration in this repository uses inputs.get() or constructs a separate data = {} dict — count.py is the only file across the entire repo that uses inputs.pop(). If the SDK or platform ever inspects, logs, or retries with the original inputs dict after execute() returns, the UUID key will be gone.

Additionally, this mutates a dict that the action handler did not allocate, which is unexpected for any caller that holds a reference to the original inputs object.

count/count.py, line 74 (and repeated at lines 144, 190, 256, 305, 350, 385, 447, 501):

# Current — mutates the input dict
uuid = inputs.pop("account_uuid")
resp = await _fetch(context, f"{BASE_URL}/accounts/{uuid}", method="PATCH", json=inputs)

# Fix — extract without mutation, build the body explicitly
uuid = inputs["account_uuid"]
body = {k: v for k, v in inputs.items() if k != "account_uuid" and v is not None}
resp = await _fetch(context, f"{BASE_URL}/accounts/{uuid}", method="PATCH", json=body)

Apply the same pattern to all 9 update handlers.


[P2] Quality — Update handlers send None-valued fields in request body

The list/query handlers correctly filter out None values with {k: v for k, v in inputs.items() if v is not None} before building params. However, all update handlers (update_account, update_customer, etc.) pass inputs directly as the JSON body after popping the UUID — meaning optional fields that the caller did not provide (which the SDK will pass as None) are sent to the Count API as {"name": null, "type": null, ...}.

For a PATCH endpoint this is a correctness issue: the API may interpret an explicit null as an intent to clear the field, rather than leave it unchanged.

count/count.py, line 75 (and all other update handlers):

# Current — sends None fields to a PATCH endpoint
resp = await _fetch(context, f"{BASE_URL}/accounts/{uuid}", method="PATCH", json=inputs)

# Fix — strip None values before sending
body = {k: v for k, v in inputs.items() if k != "account_uuid" and v is not None}
resp = await _fetch(context, f"{BASE_URL}/accounts/{uuid}", method="PATCH", json=body)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant