Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Sources/FreeAgentAPI/TypeOverrides/PermissionLevel.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
public enum PermissionLevel: Int, Codable, Hashable, Sendable, CaseIterable {
case noAccess = 0
case time = 1
case myMoney = 2
case contactsAndProjects = 3
case invoicesEstimatesAndFiles = 4
case bills = 5
case banking = 6
case taxAccountingAndUsers = 7
case full = 8
}
55 changes: 55 additions & 0 deletions Sources/FreeAgentCLI/Commands/User/UserCreateCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct UserCreateCommand: MutatingCommand {
static let configuration = CommandConfiguration(
commandName: "create",
abstract: "Create a user",
discussion: "The permission level defaults to No Access, and an Accountant needs Tax, Accounting & Users or Full."
)

@Option(name: .long, help: "Login email address")
var email: String

@Option(name: .long, help: "First name")
var firstName: String

@Option(name: .long, help: "Last name")
var lastName: String

@Option(name: .long, help: "Role in the company")
var role: Components.Schemas.UserRole

@OptionGroup var profile: UserProfileOptions

@Option(name: .long, help: "Email the user an invitation to set their password")
var sendInvitation = false

@Flag(help: "Print the request instead of sending it")
var dryRun = false

@Flag(name: .long, help: "Output JSON")
var json = false

func perform(client: Client) async throws -> Components.Schemas.UserResponse {
let userPayload = profile.createPayload(
email: email,
firstName: firstName,
lastName: lastName,
role: role,
sendInvitation: sendInvitation
)

let input = Operations.CreateUser.Input(
body: .json(.init(user: userPayload))
)

return try await client.createUser(input)
.created.body.json
}

func success(for response: Components.Schemas.UserResponse) -> String {
"Created user \(response.user.url)"
}
}
38 changes: 38 additions & 0 deletions Sources/FreeAgentCLI/Commands/User/UserDeleteCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct UserDeleteCommand: DestructiveCommand {
static let configuration = CommandConfiguration(
commandName: "delete",
abstract: "Delete a user",
discussion: "A user with timeslips, expenses or other records cannot be deleted, but can be hidden instead with 'freeagent user update <id> --hidden true'."
)

@Argument(help: "User ID or URL")
var id: ResourceID

@Flag(help: "Print the request instead of sending it")
var dryRun = false

@Flag(help: "Skip the confirmation prompt")
var yes = false

var confirmation: String {
"Delete user \(id)?"
}

func perform(client: Client) async throws -> EmptyResponse {
let input = Operations.DeleteUser.Input(
path: .init(id: id.value)
)

_ = try await client.deleteUser(input).ok

return EmptyResponse()
}

func success(for _: EmptyResponse) -> String {
"Deleted user \(id)"
}
}
49 changes: 49 additions & 0 deletions Sources/FreeAgentCLI/Commands/User/UserListCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct UserListCommand: AsyncPaginatedListCommand {
static let configuration = CommandConfiguration(
commandName: "list",
abstract: "List users"
)

static let noun = "users"

static var columns: [Field<Components.Schemas.User>] {
Field("ID") { .id(url: $0.url) }
Field("First Name") { .text($0.firstName) }
Field("Last Name") { .text($0.lastName) }
Field("Email") { .text($0.email) }
Field("Role") { .text($0.role) }
Field("Permission") { .text($0.permissionLevel?.description) }
Field("Hidden") { .flag($0.hidden) }
}

@Option(name: .long, help: "Filter by role and visibility - every user is listed by default")
var view: Operations.ListUsers.Input.Query.ViewPayload?

@OptionGroup var pagination: PaginationOptions

@Flag(name: .long, help: "Output JSON")
var json = false

func fetch(
client: Client,
page: Int
) async throws -> (response: Components.Schemas.UserListResponse, totalCount: Int?) {
let ok = try await client.listUsers(
.init(query: .init(
view: view,
page: page,
perPage: pagination.size
))
).ok

return (try ok.body.json, ok.headers.xTotalCount)
}

func items(in response: Components.Schemas.UserListResponse) -> [Components.Schemas.User] {
response.users
}
}
57 changes: 57 additions & 0 deletions Sources/FreeAgentCLI/Commands/User/UserShowCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct UserShowCommand: ShowCommand {
static let configuration = CommandConfiguration(
commandName: "show",
abstract: "Show user details"
)

static let title = "User"

static var sections: [FieldSection<Components.Schemas.User>] {
FieldSection("Details") {
Field("First Name") { .text($0.firstName) }
Field("Last Name") { .text($0.lastName) }
Field("Email") { .text($0.email) }
Field("Role") { .text($0.role) }
Field("Permission") { .text($0.permissionLevel?.description) }
Field("Hidden") { .flag($0.hidden) }
}
FieldSection("Tax") {
Field("NI Number") { .text($0.niNumber) }
Field("Unique Tax Reference") { .text($0.uniqueTaxReference) }
Field("Opening Mileage") { .text($0.openingMileage) }
}
FieldSection("Previous Employment") {
Field("Total Pay") { .currency($0.currentPayrollProfile?.totalPayInPreviousEmployment, code: nil) }
Field("Total Tax") { .currency($0.currentPayrollProfile?.totalTaxInPreviousEmployment, code: nil) }
}
FieldSection("Dates") {
Field("Created") { .timestamp($0.createdAt) }
Field("Updated") { .timestamp($0.updatedAt) }
}
FieldSection("IDs") {
Field("User") { .id(url: $0.url) }
}
}

@Argument(help: "User ID or URL - defaults to the logged-in user")
var id: ResourceID?

@Flag(name: .long, help: "Output JSON")
var json = false

func fetch(client: Client) async throws -> Components.Schemas.UserResponse {
guard let id else {
return try await client.showCurrentUser(.init()).ok.body.json
}

return try await client.showUser(.init(path: .init(id: id.value))).ok.body.json
}

func record(in response: Components.Schemas.UserResponse) -> Components.Schemas.User {
response.user
}
}
60 changes: 60 additions & 0 deletions Sources/FreeAgentCLI/Commands/User/UserUpdateCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import ArgumentParser
import Foundation
import FreeAgentAPI

struct UserUpdateCommand: MutatingCommand {
static let configuration = CommandConfiguration(
commandName: "update",
abstract: "Update a user",
discussion: "Fields left out are kept, and an empty --ni-number or --unique-tax-reference removes it."
)

@Argument(help: "User ID or URL - defaults to the logged-in user")
var id: ResourceID?

@Option(name: .long, help: "Login email address")
var email: String?

@Option(name: .long, help: "First name")
var firstName: String?

@Option(name: .long, help: "Last name")
var lastName: String?

@Option(name: .long, help: "Role in the company")
var role: Components.Schemas.UserRole?

@OptionGroup var profile: UserProfileOptions

@Flag(help: "Print the request instead of sending it")
var dryRun = false

@Flag(name: .long, help: "Output JSON")
var json = false

func perform(client: Client) async throws -> Components.Schemas.UserResponse {
let userPayload = profile.updatePayload(
email: email,
firstName: firstName,
lastName: lastName,
role: role
)

guard let id else {
return try await client.updateCurrentUser(.init(body: .json(.init(user: userPayload))))
.ok.body.json
}

let input = Operations.UpdateUser.Input(
path: .init(id: id.value),
body: .json(.init(user: userPayload))
)

return try await client.updateUser(input)
.ok.body.json
}

func success(for response: Components.Schemas.UserResponse) -> String {
"Updated user \(ResourceID(response.user.url))"
}
}
15 changes: 15 additions & 0 deletions Sources/FreeAgentCLI/Commands/UserCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import ArgumentParser

struct UserCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "user",
abstract: "Manage users",
subcommands: [
UserListCommand.self,
UserCreateCommand.self,
UserShowCommand.self,
UserUpdateCommand.self,
UserDeleteCommand.self,
]
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import FreeAgentAPI

extension PermissionLevel: CustomStringConvertible {
public var description: String {
switch self {
case .noAccess: "No Access"
case .time: "Time"
case .myMoney: "My Money"
case .contactsAndProjects: "Contacts & Projects"
case .invoicesEstimatesAndFiles: "Invoices, Estimates & Files"
case .bills: "Bills"
case .banking: "Banking"
case .taxAccountingAndUsers: "Tax, Accounting & Users"
case .full: "Full"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import ArgumentParser
import FreeAgentAPI

extension PermissionLevel: ExpressibleByArgument {
public var defaultValueDescription: String {
description
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ArgumentParser
import FreeAgentAPI

extension Components.Schemas.UserRole: ExpressibleByArgument { }
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import ArgumentParser
import FreeAgentAPI

extension Operations.ListUsers.Input.Query.ViewPayload: ExpressibleByArgument { }
1 change: 1 addition & 0 deletions Sources/FreeAgentCLI/FreeAgentCLI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct FreeAgentCLI: AsyncParsableCommand {
CreditNoteCommand.self,
ProjectCommand.self,
TaskCommand.self,
UserCommand.self,
TimeslipCommand.self,
BillCommand.self,
ContactCommand.self,
Expand Down
59 changes: 59 additions & 0 deletions Sources/FreeAgentCLI/Options/UserProfileOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import ArgumentParser
import FreeAgentAPI

struct UserProfileOptions: ParsableArguments {
@Option(name: .long, help: "What the user can see and do in FreeAgent")
var permissionLevel: PermissionLevel?

@Option(name: .long, help: "UK National Insurance number, e.g. AB123456C")
var niNumber: String?

@Option(name: .long, help: "10-digit UK Unique Taxpayer Reference")
var uniqueTaxReference: String?

@Option(name: .long, help: "Opening mileage as of the company start date")
var openingMileage: Double?

@Option(name: .long, help: "Whether the user is hidden")
var hidden: Bool?

func createPayload(
email: String,
firstName: String,
lastName: String,
role: Components.Schemas.UserRole,
sendInvitation: Bool
) -> Components.Schemas.UserCreatePayload {
.init(
email: email,
firstName: firstName,
lastName: lastName,
role: role,
permissionLevel: permissionLevel,
niNumber: niNumber,
uniqueTaxReference: uniqueTaxReference,
openingMileage: openingMileage,
hidden: hidden,
sendInvitation: sendInvitation
)
}

func updatePayload(
email: String?,
firstName: String?,
lastName: String?,
role: Components.Schemas.UserRole?
) -> Components.Schemas.UserUpdatePayload {
.init(
email: email,
firstName: firstName,
lastName: lastName,
role: role,
permissionLevel: permissionLevel,
niNumber: niNumber,
uniqueTaxReference: uniqueTaxReference,
openingMileage: openingMileage,
hidden: hidden
)
}
}
Loading
Loading