Skip to content

Add Users - #63

Merged
dinoconstantinou87 merged 2 commits into
mainfrom
feat/users
Oct 3, 2026
Merged

dinoconstantinou87 merged 2 commits into
mainfrom
feat/users

Conversation

@dinoconstantinou87

@dinoconstantinou87 dinoconstantinou87 commented Oct 3, 2026 •

Copy link
Copy Markdown
Owner

Summary

Users could only be read through GET /v2/users/me, which --user defaults and self assessment use. This adds full coverage.

$ freeagent user update --last-name Smith --dry-run
  -> PUT https://api.sandbox.freeagent.com/v2/users/me

$ freeagent user update 43114 --role Accountant
✖ Error
  FreeAgent rejected the request (HTTP 422): permission_level must be Tax & Accounting or higher for Accountants

$ freeagent user delete 43114 --yes
✖ Error
  FreeAgent rejected the request (HTTP 422): Only users with no dependencies can be deleted
  • user list takes --view (all, staff, active_staff, advisors, active_advisors). With no view, every user is listed.
  • user show [<id>] shows the logged-in user without an ID, through GET /v2/users/me, or the given user. It groups details, tax, previous employment pay, dates and IDs.
  • user create needs --email, --first-name, --last-name and --role. --send-invitation defaults to false, shown in help as (default: false), and the value is always sent, so no one is emailed unless asked.
  • user update [<id>] changes only the fields passed. Without an ID it updates the logged-in user through PUT /v2/users/me, which FreeAgent documents as needing only Time access.
  • user delete <id> needs an ID, as there is no documented DELETE /v2/users/me. FreeAgent's 422 for a user with timeslips, expenses or other records passes straight through (exit 4).
  • --permission-level takes 0 to 8. Help lists each level's name, list and show print the name, and anything else is refused before sending.

What The Sandbox Showed

Probed under the pinned X-Api-Version: 2026-09-01, no header and 2024-10-01; no shape differs between them. Probe users used @example.com addresses and send_invitation: false, and all of them were deleted afterwards. The logged-in user's name was round-tripped through PUT /v2/users/me. Its email, role and permission level were never changed, and deleting it was never tried.

  • The list pages properly. X-Total-Count is sent and page is respected. An unknown view is a 400. sort and updated_since are ignored.
  • Create needs an email, first name, last name and role. Opening mileage is documented as required, but defaults to 0 and accepts decimals and negatives. The permission level defaults to 0 (No Access). FreeAgent turns 9 into 8 silently, and -1 is a 422.
  • Roles are strict and case-sensitive. All seven documented roles work on this limited company. An Accountant needs permission level 7 or more, on create and on update.
  • NI numbers are validated. The docs' own example, QQ123456C, is a 422, while AB123456C works. A UTR must be 10 digits. Both are omitted from responses until set, and hidden is undocumented and writable.
  • Updates are clean partial updates. Every field was changed alone with the whole record diffed: nothing else moved. An empty NI number or UTR removes it. Resending current values is a 200. Only an empty user is refused: a 422 returning the unchanged user on /users/:id, and a 400 on /users/me.
  • Delete is a 200 with an empty body, for hidden and Accountant users too. A user with a timeslip or an expense is a 422 saying it has dependencies.
  • send_invitation: the docs give no default, only "true to send the user an invitation to set their password, false otherwise". Sending true was not probed, as it sends mail.

Code

  • Spec: paths/users.yaml adds listUsers, createUser, showUser, updateUser, deleteUser and updateCurrentUser. This takes the total from 111 to 117 operations.
  • Schemas: new UserCreatePayload, UserUpdatePayload, UserListResponse, UserRole and UserPermissionLevel. The role is an enum in requests only. Responses keep it as a string, since other company types may return roles the sandbox can't show.
  • permission_level is an enum. UserPermissionLevel is an integer enum of 0 to 8, used by User and both payloads. The generator would name its cases _0 to _8, so openapi-generator-config.yaml maps it to a hand-written PermissionLevel in TypeOverrides/, with cases from noAccess to full, as InvoiceView maps to ViewFilter. The CLI names each level through CustomStringConvertible, and ArgumentParser lists the names in help.
  • UserProfileOptions in Options/ holds the fields create and update share. UserOptions already names the --user option group.

Tests

  • User integration tests: a typed, counted list with a view. A lifecycle test creates a user with every field, then updates it to an Accountant, lists, shows and deletes it. PUT /v2/users/me is tested with the current first name, so nothing changes.
  • The user suite passes against the sandbox, as does self assessment, which reads the user. changed-suites selects users.
  • CLI unit tests cover only CLI logic:
    • show and update call the me operations without an ID, and the ID operations with one;
    • each permission level's name, and that help lists the names while refusing 9.
  • API unit tests: PermissionLevel decodes every documented level, encodes as an integer and refuses 9.
  • 140 CLI, 90 API and 11 tools unit tests pass, and format and lint are clean.
  • The debug CLI was run end to end against the sandbox. That covered:
    • list, a view, show with and without an ID, and a dry-run update without an ID;
    • a dry-run create, a real create, an Accountant refused below level 7 (exit 4), then the full update and a show;
    • an unchanged update of the logged-in user, and a lowercase role refused with exit 64;
    • a delete refused for a timeslip (exit 4), the non-interactive refusal without --yes (exit 64), then the delete itself and a repeat 404 (exit 3).

🤖 Generated with Claude Code

dinoconstantinou87 and others added 2 commits October 3, 2026 09:19
Users could only be read through /v2/users/me. This adds user list,
create, show, update and delete. Show and update target the logged-in
user when no ID is given, through GET and PUT /v2/users/me, and
--send-invitation defaults to false, shown in help.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
permission_level is a UserPermissionLevel integer enum in the spec,
mapped through typeOverrides to a PermissionLevel enum with named
cases, since the generator would name them _0 to _8. The CLI names
each level in help and output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dinoconstantinou87
dinoconstantinou87 merged commit 5635900 into main Oct 3, 2026
4 checks passed
@dinoconstantinou87
dinoconstantinou87 deleted the feat/users branch October 3, 2026 08:30
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