From dff9dcf74316a3e7b63e5ebbcfceceb387a50828 Mon Sep 17 00:00:00 2001 From: Dino Constantinou Date: Sun, 4 Oct 2026 09:52:33 +0100 Subject: [PATCH 1/2] Use Currency And UserRole Enums In Responses Co-Authored-By: Claude Opus 5.5 --- .../Commands/Bill/BillShowCommand.swift | 2 +- .../Commands/Contact/ContactListCommand.swift | 2 +- .../Commands/Contact/ContactShowCommand.swift | 2 +- .../CreditNote/CreditNoteShowCommand.swift | 2 +- .../Estimate/EstimateShowCommand.swift | 2 +- .../Commands/Invoice/InvoiceShowCommand.swift | 2 +- .../PriceListItemListCommand.swift | 4 +-- .../PriceListItemShowCommand.swift | 4 +-- .../Project/ProjectCreateCommand.swift | 34 +------------------ .../Commands/Project/ProjectListCommand.swift | 2 +- .../Commands/Project/ProjectShowCommand.swift | 6 ++-- .../Commands/Task/TaskListCommand.swift | 4 +-- .../Commands/Task/TaskShowCommand.swift | 4 +-- .../Commands/User/UserListCommand.swift | 2 +- .../Commands/User/UserShowCommand.swift | 2 +- Sources/FreeAgentCLI/Fields/FieldValue.swift | 16 +++++++++ .../BankAccountIntegrationTests.swift | 2 +- .../CompanyIntegrationTests.swift | 2 +- .../UserIntegrationTests.swift | 4 +-- .../Project/ProjectCreateCommandTests.swift | 18 ++-------- .../Fields/FieldValueTests.swift | 19 +++++++++++ openapi/components/schemas/BankAccount.yaml | 3 +- openapi/components/schemas/Bill.yaml | 3 +- openapi/components/schemas/Company.yaml | 3 +- openapi/components/schemas/CreditNote.yaml | 3 +- .../schemas/CreditNoteCreatePayload.yaml | 1 + openapi/components/schemas/Currency.yaml | 13 ++++++- openapi/components/schemas/Estimate.yaml | 3 +- .../schemas/EstimateCreatePayload.yaml | 1 + openapi/components/schemas/Expense.yaml | 3 +- .../schemas/ExpenseCreatePayload.yaml | 1 + .../schemas/ExpenseUpdatePayload.yaml | 1 + openapi/components/schemas/Invoice.yaml | 3 +- .../schemas/InvoiceCreatePayload.yaml | 1 + openapi/components/schemas/Project.yaml | 3 +- .../components/schemas/RecurringInvoice.yaml | 3 +- openapi/components/schemas/Task.yaml | 3 +- openapi/components/schemas/User.yaml | 4 +-- 38 files changed, 90 insertions(+), 97 deletions(-) diff --git a/Sources/FreeAgentCLI/Commands/Bill/BillShowCommand.swift b/Sources/FreeAgentCLI/Commands/Bill/BillShowCommand.swift index fba121f..d66a8bf 100644 --- a/Sources/FreeAgentCLI/Commands/Bill/BillShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Bill/BillShowCommand.swift @@ -3,7 +3,7 @@ import Foundation import FreeAgentAPI struct BillShowCommand: ShowCommand { - typealias PricedItem = (item: Components.Schemas.BillItem, currency: String?) + typealias PricedItem = (item: Components.Schemas.BillItem, currency: Components.Schemas.Currency?) static let configuration = CommandConfiguration( commandName: "show", diff --git a/Sources/FreeAgentCLI/Commands/Contact/ContactListCommand.swift b/Sources/FreeAgentCLI/Commands/Contact/ContactListCommand.swift index 283d3e5..d7fb337 100644 --- a/Sources/FreeAgentCLI/Commands/Contact/ContactListCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Contact/ContactListCommand.swift @@ -15,7 +15,7 @@ struct ContactListCommand: AsyncPaginatedListCommand { Field("Organisation") { .text($0.organisationName) } Field("Name") { .text([$0.firstName, $0.lastName].compactMap(\.self).joined(separator: " ")) } Field("Email") { .text($0.email) } - Field("Status") { .status($0.status.rawValue) } + Field("Status") { .status($0.status) } } @Option(name: .long, help: "Filter by view - active by default, which leaves out hidden contacts") diff --git a/Sources/FreeAgentCLI/Commands/Contact/ContactShowCommand.swift b/Sources/FreeAgentCLI/Commands/Contact/ContactShowCommand.swift index 6c2d6d3..b38659c 100644 --- a/Sources/FreeAgentCLI/Commands/Contact/ContactShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Contact/ContactShowCommand.swift @@ -14,7 +14,7 @@ struct ContactShowCommand: ShowCommand { FieldSection("Details") { Field("Organisation") { .text($0.organisationName) } Field("Name") { .text([$0.firstName, $0.lastName].compactMap(\.self).joined(separator: " ")) } - Field("Status") { .status($0.status.rawValue) } + Field("Status") { .status($0.status) } Field("Email") { .text($0.email) } Field("Billing Email") { .text($0.billingEmail) } Field("Phone") { .text($0.phoneNumber) } diff --git a/Sources/FreeAgentCLI/Commands/CreditNote/CreditNoteShowCommand.swift b/Sources/FreeAgentCLI/Commands/CreditNote/CreditNoteShowCommand.swift index a08c7d9..41983d9 100644 --- a/Sources/FreeAgentCLI/Commands/CreditNote/CreditNoteShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/CreditNote/CreditNoteShowCommand.swift @@ -3,7 +3,7 @@ import Foundation import FreeAgentAPI struct CreditNoteShowCommand: ShowCommand { - typealias PricedItem = (item: Components.Schemas.InvoiceItem, currency: String?) + typealias PricedItem = (item: Components.Schemas.InvoiceItem, currency: Components.Schemas.Currency?) static let configuration = CommandConfiguration( commandName: "show", diff --git a/Sources/FreeAgentCLI/Commands/Estimate/EstimateShowCommand.swift b/Sources/FreeAgentCLI/Commands/Estimate/EstimateShowCommand.swift index 0619856..3e49477 100644 --- a/Sources/FreeAgentCLI/Commands/Estimate/EstimateShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Estimate/EstimateShowCommand.swift @@ -3,7 +3,7 @@ import Foundation import FreeAgentAPI struct EstimateShowCommand: ShowCommand { - typealias PricedItem = (item: Components.Schemas.EstimateItem, currency: String?) + typealias PricedItem = (item: Components.Schemas.EstimateItem, currency: Components.Schemas.Currency?) static let configuration = CommandConfiguration( commandName: "show", diff --git a/Sources/FreeAgentCLI/Commands/Invoice/InvoiceShowCommand.swift b/Sources/FreeAgentCLI/Commands/Invoice/InvoiceShowCommand.swift index f55472f..b579aae 100644 --- a/Sources/FreeAgentCLI/Commands/Invoice/InvoiceShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Invoice/InvoiceShowCommand.swift @@ -3,7 +3,7 @@ import Foundation import FreeAgentAPI struct InvoiceShowCommand: ShowCommand { - typealias PricedItem = (item: Components.Schemas.InvoiceItem, currency: String?) + typealias PricedItem = (item: Components.Schemas.InvoiceItem, currency: Components.Schemas.Currency?) static let configuration = CommandConfiguration( commandName: "show", diff --git a/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemListCommand.swift b/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemListCommand.swift index 4e9f97f..6ddcf96 100644 --- a/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemListCommand.swift +++ b/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemListCommand.swift @@ -14,10 +14,10 @@ struct PriceListItemListCommand: AsyncPaginatedListCommand { Field("ID") { .id(url: $0.url) } Field("Code") { .text($0.code) } Field("Description") { .text($0.description) } - Field("Item Type") { .text($0.itemType?.rawValue) } + Field("Item Type") { .text($0.itemType) } Field("Quantity") { .text($0.quantity) } Field("Price") { .currency($0.price, code: nil) } - Field("VAT Status") { .text($0.vatStatus?.rawValue) } + Field("VAT Status") { .text($0.vatStatus) } } @Option(name: .long, parsing: .unconditional, help: "Sort order") diff --git a/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemShowCommand.swift b/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemShowCommand.swift index cf5c2ce..25f2011 100644 --- a/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/PriceListItem/PriceListItemShowCommand.swift @@ -14,12 +14,12 @@ struct PriceListItemShowCommand: ShowCommand { FieldSection("Details") { Field("Code") { .text($0.code) } Field("Description") { .text($0.description) } - Field("Item Type") { .text($0.itemType?.rawValue) } + Field("Item Type") { .text($0.itemType) } Field("Quantity") { .text($0.quantity) } Field("Price") { .currency($0.price, code: nil) } } FieldSection("Tax") { - Field("VAT Status") { .text($0.vatStatus?.rawValue) } + Field("VAT Status") { .text($0.vatStatus) } Field("Sales Tax Rate") { .percent($0.salesTaxRate) } Field("Second Sales Tax Rate") { .percent($0.secondSalesTaxRate) } } diff --git a/Sources/FreeAgentCLI/Commands/Project/ProjectCreateCommand.swift b/Sources/FreeAgentCLI/Commands/Project/ProjectCreateCommand.swift index 18877ab..9db4840 100644 --- a/Sources/FreeAgentCLI/Commands/Project/ProjectCreateCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Project/ProjectCreateCommand.swift @@ -1,9 +1,6 @@ import ArgumentParser import Foundation import FreeAgentAPI -import Noora - -// MARK: - ProjectCreateCommand struct ProjectCreateCommand: MutatingCommand { @@ -65,35 +62,6 @@ struct ProjectCreateCommand: MutatingCommand { return currency } - let code = try await client.companyDetails().ok.body.json.company.currency - - guard let currency = Components.Schemas.Currency(rawValue: code) else { - throw ProjectCreateCommandError.unsupportedCompanyCurrency(code) - } - - return currency - } -} - -// MARK: - ProjectCreateCommandError - -enum ProjectCreateCommandError: CommandError { - case unsupportedCompanyCurrency(String) - - // MARK: Internal - - var errorDescription: String? { - switch self { - case .unsupportedCompanyCurrency(let code): - "The company's currency, \(code), is not one a project can use" - } - } - - var exitCode: ExitCode { - .validationFailure - } - - var takeaways: [TerminalText] { - ["Pass \(.command("--currency"))"] + return try await client.companyDetails().ok.body.json.company.currency } } diff --git a/Sources/FreeAgentCLI/Commands/Project/ProjectListCommand.swift b/Sources/FreeAgentCLI/Commands/Project/ProjectListCommand.swift index 294421b..c7c8106 100644 --- a/Sources/FreeAgentCLI/Commands/Project/ProjectListCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Project/ProjectListCommand.swift @@ -14,7 +14,7 @@ struct ProjectListCommand: AsyncPaginatedListCommand { Field("ID") { .id(url: $0.url) } Field("Name") { .text($0.name) } Field("Contact") { .text($0.contactName) } - Field("Status") { .status($0.status?.rawValue) } + Field("Status") { .status($0.status) } Field("Starts On") { .date($0.startsOn) } Field("Ends On") { .date($0.endsOn) } } diff --git a/Sources/FreeAgentCLI/Commands/Project/ProjectShowCommand.swift b/Sources/FreeAgentCLI/Commands/Project/ProjectShowCommand.swift index f91413a..caf4645 100644 --- a/Sources/FreeAgentCLI/Commands/Project/ProjectShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Project/ProjectShowCommand.swift @@ -17,7 +17,7 @@ struct ProjectShowCommand: ShowCommand { FieldSection("Details") { Field("Name") { .text($0.name) } Field("Contact") { .text($0.contactName) } - Field("Status") { .status($0.status?.rawValue) } + Field("Status") { .status($0.status) } Field("PO Reference") { .text($0.contractPoReference) } Field("IR35") { .flag($0.isIr35) } Field("Deletable") { .flag($0.isDeletable) } @@ -25,13 +25,13 @@ struct ProjectShowCommand: ShowCommand { FieldSection("Billing") { Field("Currency") { .text($0.currency) } Field("Normal Billing Rate") { .currency($0.normalBillingRate, code: $0.currency) } - Field("Billing Period") { .text($0.billingPeriod?.rawValue) } + Field("Billing Period") { .text($0.billingPeriod) } Field("Hours Per Day") { .hours($0.hoursPerDay) } Field("Own Invoice Sequence") { .flag($0.usesProjectInvoiceSequence) } } FieldSection("Budget") { Field("Budget") { budget(of: $0) } - Field("Budget Units") { .text($0.budgetUnits?.rawValue) } + Field("Budget Units") { .text($0.budgetUnits) } Field("Unbilled Time In Profitability") { .flag($0.includeUnbilledTimeInProfitability) } } FieldSection("Dates") { diff --git a/Sources/FreeAgentCLI/Commands/Task/TaskListCommand.swift b/Sources/FreeAgentCLI/Commands/Task/TaskListCommand.swift index 6943728..7e3e7c9 100644 --- a/Sources/FreeAgentCLI/Commands/Task/TaskListCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Task/TaskListCommand.swift @@ -14,10 +14,10 @@ struct TaskListCommand: AsyncPaginatedListCommand { Field("ID") { .id(url: $0.url) } Field("Name") { .text($0.name) } Field("Project") { .id(url: $0.project) } - Field("Status") { .status($0.status?.rawValue) } + Field("Status") { .status($0.status) } Field("Billable") { .flag($0.isBillable) } Field("Billing Rate") { .currency($0.billingRate, code: $0.currency) } - Field("Billing Period") { .text($0.billingPeriod?.rawValue) } + Field("Billing Period") { .text($0.billingPeriod) } } @Option(name: .long, help: "Filter by status - every status is listed by default") diff --git a/Sources/FreeAgentCLI/Commands/Task/TaskShowCommand.swift b/Sources/FreeAgentCLI/Commands/Task/TaskShowCommand.swift index 54574b9..a9a11eb 100644 --- a/Sources/FreeAgentCLI/Commands/Task/TaskShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/Task/TaskShowCommand.swift @@ -13,14 +13,14 @@ struct TaskShowCommand: ShowCommand { static var sections: [FieldSection] { FieldSection("Details") { Field("Name") { .text($0.name) } - Field("Status") { .status($0.status?.rawValue) } + Field("Status") { .status($0.status) } Field("Deletable") { .flag($0.isDeletable) } } FieldSection("Billing") { Field("Billable") { .flag($0.isBillable) } Field("Currency") { .text($0.currency) } Field("Billing Rate") { .currency($0.billingRate, code: $0.currency) } - Field("Billing Period") { .text($0.billingPeriod?.rawValue) } + Field("Billing Period") { .text($0.billingPeriod) } } FieldSection("Dates") { Field("Created") { .timestamp($0.createdAt) } diff --git a/Sources/FreeAgentCLI/Commands/User/UserListCommand.swift b/Sources/FreeAgentCLI/Commands/User/UserListCommand.swift index 51dc3a3..e8afc8d 100644 --- a/Sources/FreeAgentCLI/Commands/User/UserListCommand.swift +++ b/Sources/FreeAgentCLI/Commands/User/UserListCommand.swift @@ -16,7 +16,7 @@ struct UserListCommand: AsyncPaginatedListCommand { Field("Last Name") { .text($0.lastName) } Field("Email") { .text($0.email) } Field("Role") { .text($0.role) } - Field("Permission") { .text($0.permissionLevel?.description) } + Field("Permission") { .text($0.permissionLevel) } Field("Hidden") { .flag($0.hidden) } } diff --git a/Sources/FreeAgentCLI/Commands/User/UserShowCommand.swift b/Sources/FreeAgentCLI/Commands/User/UserShowCommand.swift index 1afccba..dc39d9e 100644 --- a/Sources/FreeAgentCLI/Commands/User/UserShowCommand.swift +++ b/Sources/FreeAgentCLI/Commands/User/UserShowCommand.swift @@ -16,7 +16,7 @@ struct UserShowCommand: ShowCommand { Field("Last Name") { .text($0.lastName) } Field("Email") { .text($0.email) } Field("Role") { .text($0.role) } - Field("Permission") { .text($0.permissionLevel?.description) } + Field("Permission") { .text($0.permissionLevel) } Field("Hidden") { .flag($0.hidden) } } FieldSection("Tax") { diff --git a/Sources/FreeAgentCLI/Fields/FieldValue.swift b/Sources/FreeAgentCLI/Fields/FieldValue.swift index 07514d0..f27ca31 100644 --- a/Sources/FreeAgentCLI/Fields/FieldValue.swift +++ b/Sources/FreeAgentCLI/Fields/FieldValue.swift @@ -15,6 +15,22 @@ enum FieldValue: Sendable { // MARK: Internal + static func text(_ value: (some RawRepresentable)?) -> FieldValue { + .text(value?.rawValue) + } + + static func text(_ value: (some RawRepresentable & CustomStringConvertible)?) -> FieldValue { + .text(value?.description) + } + + static func status(_ value: (some RawRepresentable)?) -> FieldValue { + .status(value?.rawValue) + } + + static func currency(_ amount: String?, code: (some RawRepresentable)?) -> FieldValue { + .currency(amount, code: code?.rawValue) + } + func formatted(locale: Locale = .current, timeZone: TimeZone = .current) -> String? { let text = switch self { diff --git a/Tests/FreeAgentAPIIntegrationTests/BankAccountIntegrationTests.swift b/Tests/FreeAgentAPIIntegrationTests/BankAccountIntegrationTests.swift index 44a5829..b2a3bd0 100644 --- a/Tests/FreeAgentAPIIntegrationTests/BankAccountIntegrationTests.swift +++ b/Tests/FreeAgentAPIIntegrationTests/BankAccountIntegrationTests.swift @@ -24,7 +24,7 @@ struct BankAccountIntegrationTests { let account = try #require(accounts.first) #expect(account.url.contains("/v2/bank_accounts/")) #expect(account._type == "StandardBankAccount") - #expect(account.currency == "GBP") + #expect(account.currency == .gbp) #expect(try #require(account.createdAt) <= Date()) } diff --git a/Tests/FreeAgentAPIIntegrationTests/CompanyIntegrationTests.swift b/Tests/FreeAgentAPIIntegrationTests/CompanyIntegrationTests.swift index f149680..db2a61f 100644 --- a/Tests/FreeAgentAPIIntegrationTests/CompanyIntegrationTests.swift +++ b/Tests/FreeAgentAPIIntegrationTests/CompanyIntegrationTests.swift @@ -24,7 +24,7 @@ struct CompanyIntegrationTests { name: "Acme Technologies Ltd", subdomain: "acmetechnologiesltd", _type: "UkLimitedCompany", - currency: "GBP", + currency: .gbp, mileageUnits: "miles", companyStartDate: "2025-01-01", freeagentStartDate: "2025-01-01", diff --git a/Tests/FreeAgentAPIIntegrationTests/UserIntegrationTests.swift b/Tests/FreeAgentAPIIntegrationTests/UserIntegrationTests.swift index 26ea56f..e2437ff 100644 --- a/Tests/FreeAgentAPIIntegrationTests/UserIntegrationTests.swift +++ b/Tests/FreeAgentAPIIntegrationTests/UserIntegrationTests.swift @@ -74,7 +74,7 @@ struct UserIntegrationTests { #expect(created.email == email) #expect(created.firstName == "Integration") #expect(created.lastName == "Test") - #expect(created.role == "Employee") + #expect(created.role == .employee) #expect(created.permissionLevel == .time) #expect(created.niNumber == "AB123456C") #expect(created.uniqueTaxReference == "1234567890") @@ -94,7 +94,7 @@ struct UserIntegrationTests { #expect(updated.url == created.url) #expect(updated.lastName == "Updated") - #expect(updated.role == "Accountant") + #expect(updated.role == .accountant) #expect(updated.permissionLevel == .taxAccountingAndUsers) #expect(updated.hidden == true) diff --git a/Tests/FreeAgentCLITests/Commands/Project/ProjectCreateCommandTests.swift b/Tests/FreeAgentCLITests/Commands/Project/ProjectCreateCommandTests.swift index d7ecf68..0c6e026 100644 --- a/Tests/FreeAgentCLITests/Commands/Project/ProjectCreateCommandTests.swift +++ b/Tests/FreeAgentCLITests/Commands/Project/ProjectCreateCommandTests.swift @@ -34,23 +34,9 @@ struct ProjectCreateCommandTests { verify(transport).send(.any, body: .any, baseURL: .any, operationID: .value("createProject")).called(1) } - @Test("asks for --currency when the company's currency is not one a project can use") - func unsupportedCompanyCurrency() async throws { - let transport = transport(companyCurrency: "HRK") - let command = try ProjectCreateCommand.parse(["--contact", "215832", "--name", "Website"]) - - let error = await #expect(throws: ProjectCreateCommandError.self) { - try await command.perform(client: client(transport)) - } - - #expect(error?.errorDescription == "The company's currency, HRK, is not one a project can use") - #expect(error?.exitCode == ExitCode.validationFailure) - verify(transport).send(.any, body: .any, baseURL: .any, operationID: .value("createProject")).called(0) - } - // MARK: Private - private func transport(companyCurrency: String = "GBP") -> MockClientTransportInterface { + private func transport() -> MockClientTransportInterface { let transport = MockClientTransportInterface() given(transport) .send(.any, body: .any, baseURL: .any, operationID: .value("companyDetails")) @@ -58,7 +44,7 @@ struct ProjectCreateCommandTests { HTTPResponse(status: .ok, headerFields: [.contentType: "application/json"]), HTTPBody(Data(#""" {"company":{"url":"https://api.freeagent.com/v2/company","name":"Acme Technologies Ltd",\# - "subdomain":"acmetechnologiesltd","type":"UkLimitedCompany","currency":"\#(companyCurrency)","mileage_units":"miles",\# + "subdomain":"acmetechnologiesltd","type":"UkLimitedCompany","currency":"GBP","mileage_units":"miles",\# "company_start_date":"2025-01-01","freeagent_start_date":"2025-01-01","first_accounting_year_end":"2026-01-31"}} """#.utf8)) )) diff --git a/Tests/FreeAgentCLITests/Fields/FieldValueTests.swift b/Tests/FreeAgentCLITests/Fields/FieldValueTests.swift index 7961adc..53c1624 100644 --- a/Tests/FreeAgentCLITests/Fields/FieldValueTests.swift +++ b/Tests/FreeAgentCLITests/Fields/FieldValueTests.swift @@ -1,4 +1,5 @@ import Foundation +import FreeAgentAPI import Testing @testable import FreeAgentCLI @@ -44,6 +45,24 @@ struct FieldValueTests { #expect(format(.status("Overdue")) == "Overdue") } + @Test("shows an enum as the value FreeAgent sends") + func formatsEnum() { + #expect(format(.text(Components.Schemas.PriceListItemType._hyphen_noUnit)) == "-no unit-") + #expect(format(.status(Components.Schemas.TaskStatus.completed)) == "Completed") + #expect(format(.text(Components.Schemas.VatStatus?.none)) == nil) + } + + @Test("shows a described value by its description") + func formatsDescribed() { + #expect(format(.text(PermissionLevel.full)) == PermissionLevel.full.description) + } + + @Test("formats an amount in its currency enum") + func formatsCurrencyEnum() { + #expect(format(.currency("120.5", code: Components.Schemas.Currency.eur)) == "€120.50") + #expect(format(.currency("-840.0", code: Components.Schemas.Currency?.none)) == "-840.00") + } + @Test( "formats an amount in its currency", arguments: [ diff --git a/openapi/components/schemas/BankAccount.yaml b/openapi/components/schemas/BankAccount.yaml index e0c3d62..ff45db2 100644 --- a/openapi/components/schemas/BankAccount.yaml +++ b/openapi/components/schemas/BankAccount.yaml @@ -18,8 +18,7 @@ properties: type: string example: generic currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' status: type: string example: active diff --git a/openapi/components/schemas/Bill.yaml b/openapi/components/schemas/Bill.yaml index 66e5e30..75af937 100644 --- a/openapi/components/schemas/Bill.yaml +++ b/openapi/components/schemas/Bill.yaml @@ -43,8 +43,7 @@ properties: type: string example: Open currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' exchange_rate: type: string example: '1.0' diff --git a/openapi/components/schemas/Company.yaml b/openapi/components/schemas/Company.yaml index 851fa51..dd992ae 100644 --- a/openapi/components/schemas/Company.yaml +++ b/openapi/components/schemas/Company.yaml @@ -23,8 +23,7 @@ properties: type: string example: UkLimitedCompany currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' mileage_units: type: string example: miles diff --git a/openapi/components/schemas/CreditNote.yaml b/openapi/components/schemas/CreditNote.yaml index 470b281..e889ab6 100644 --- a/openapi/components/schemas/CreditNote.yaml +++ b/openapi/components/schemas/CreditNote.yaml @@ -63,8 +63,7 @@ properties: description: Omitted until set example: Refund for cancelled work currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' exchange_rate: type: string example: '1.0' diff --git a/openapi/components/schemas/CreditNoteCreatePayload.yaml b/openapi/components/schemas/CreditNoteCreatePayload.yaml index f10c8e6..854a742 100644 --- a/openapi/components/schemas/CreditNoteCreatePayload.yaml +++ b/openapi/components/schemas/CreditNoteCreatePayload.yaml @@ -20,6 +20,7 @@ properties: example: CN-001 currency: $ref: '#/components/schemas/Currency' + description: The retired codes are refused, and SSP and VES are a 500 that creates nothing comments: type: string example: Refund for cancelled work diff --git a/openapi/components/schemas/Currency.yaml b/openapi/components/schemas/Currency.yaml index a4dbb99..55324ce 100644 --- a/openapi/components/schemas/Currency.yaml +++ b/openapi/components/schemas/Currency.yaml @@ -1,8 +1,9 @@ type: string -description: ISO 4217 codes every FreeAgent resource accepts - its documented list is out of date, and an unknown or lowercase code is refused +description: ISO 4217 codes FreeAgent accepts on some resource - projects take all of them, other resources refuse the retired ANG, BGN, HRK, LTL, LVL, MRO, STD, VEF and ZMK; its documented list is out of date, and an unknown or lowercase code is refused enum: - AED - AMD +- ANG - AOA - ARS - AUD @@ -11,6 +12,7 @@ enum: - BAM - BBD - BDT +- BGN - BHD - BND - BOB @@ -43,6 +45,7 @@ enum: - GYD - HKD - HNL +- HRK - HUF - IDR - ILS @@ -61,6 +64,8 @@ enum: - LAK - LBP - LKR +- LTL +- LVL - LYD - MAD - MDL @@ -68,6 +73,7 @@ enum: - MKD - MMK - MNT +- MRO - MRU - MUR - MVR @@ -95,6 +101,8 @@ enum: - SCR - SEK - SGD +- SSP +- STD - STN - THB - TND @@ -106,6 +114,8 @@ enum: - UGX - USD - UYU +- VEF +- VES - VND - VUV - XAF @@ -113,5 +123,6 @@ enum: - XOF - XPF - ZAR +- ZMK - ZMW example: GBP diff --git a/openapi/components/schemas/Estimate.yaml b/openapi/components/schemas/Estimate.yaml index 2db1776..1d1472a 100644 --- a/openapi/components/schemas/Estimate.yaml +++ b/openapi/components/schemas/Estimate.yaml @@ -36,8 +36,7 @@ properties: type: string example: '2026-10-02' currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' notes: type: string description: Additional text, omitted until set diff --git a/openapi/components/schemas/EstimateCreatePayload.yaml b/openapi/components/schemas/EstimateCreatePayload.yaml index a327833..9bf2f41 100644 --- a/openapi/components/schemas/EstimateCreatePayload.yaml +++ b/openapi/components/schemas/EstimateCreatePayload.yaml @@ -23,6 +23,7 @@ properties: example: EST-002 currency: $ref: '#/components/schemas/Currency' + description: The retired codes are refused notes: type: string example: Valid for 30 days diff --git a/openapi/components/schemas/Expense.yaml b/openapi/components/schemas/Expense.yaml index bad5829..d630ca8 100644 --- a/openapi/components/schemas/Expense.yaml +++ b/openapi/components/schemas/Expense.yaml @@ -32,8 +32,7 @@ properties: type: string example: Team lunch currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' gross_value: type: string example: '-12.0' diff --git a/openapi/components/schemas/ExpenseCreatePayload.yaml b/openapi/components/schemas/ExpenseCreatePayload.yaml index 94d21cd..d900c69 100644 --- a/openapi/components/schemas/ExpenseCreatePayload.yaml +++ b/openapi/components/schemas/ExpenseCreatePayload.yaml @@ -18,6 +18,7 @@ properties: example: Team lunch currency: $ref: '#/components/schemas/Currency' + description: The retired codes are refused gross_value: type: string description: Negative for a payment to the claimant, positive for a refund from them - ignored on a mileage claim diff --git a/openapi/components/schemas/ExpenseUpdatePayload.yaml b/openapi/components/schemas/ExpenseUpdatePayload.yaml index e3b2fc0..625d8b2 100644 --- a/openapi/components/schemas/ExpenseUpdatePayload.yaml +++ b/openapi/components/schemas/ExpenseUpdatePayload.yaml @@ -12,6 +12,7 @@ properties: example: Team lunch currency: $ref: '#/components/schemas/Currency' + description: The retired codes are refused gross_value: type: string example: '-12.0' diff --git a/openapi/components/schemas/Invoice.yaml b/openapi/components/schemas/Invoice.yaml index f3cfd63..390fa6d 100644 --- a/openapi/components/schemas/Invoice.yaml +++ b/openapi/components/schemas/Invoice.yaml @@ -60,8 +60,7 @@ properties: type: string example: Thanks for your business currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' exchange_rate: type: string example: '1.0' diff --git a/openapi/components/schemas/InvoiceCreatePayload.yaml b/openapi/components/schemas/InvoiceCreatePayload.yaml index 12efa0e..b2291aa 100644 --- a/openapi/components/schemas/InvoiceCreatePayload.yaml +++ b/openapi/components/schemas/InvoiceCreatePayload.yaml @@ -26,6 +26,7 @@ properties: example: billed_grouped_by_expense currency: $ref: '#/components/schemas/Currency' + description: The retired codes are refused, and SSP and VES are a 500 that creates nothing dated_on: type: string example: 2019-08-18 diff --git a/openapi/components/schemas/Project.yaml b/openapi/components/schemas/Project.yaml index 31f9441..1234a45 100644 --- a/openapi/components/schemas/Project.yaml +++ b/openapi/components/schemas/Project.yaml @@ -25,8 +25,7 @@ properties: type: boolean example: false currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' budget: type: integer description: Zero when the project has no budget - FreeAgent drops any fraction sent diff --git a/openapi/components/schemas/RecurringInvoice.yaml b/openapi/components/schemas/RecurringInvoice.yaml index 9300f4b..1082d0d 100644 --- a/openapi/components/schemas/RecurringInvoice.yaml +++ b/openapi/components/schemas/RecurringInvoice.yaml @@ -48,8 +48,7 @@ properties: type: string example: Thanks for your business currency: - type: string - example: GBP + $ref: '#/components/schemas/Currency' exchange_rate: type: string example: '1.0' diff --git a/openapi/components/schemas/Task.yaml b/openapi/components/schemas/Task.yaml index 20df735..3e7de31 100644 --- a/openapi/components/schemas/Task.yaml +++ b/openapi/components/schemas/Task.yaml @@ -12,9 +12,8 @@ properties: type: string example: Development currency: - type: string + $ref: '#/components/schemas/Currency' description: Always the project's currency - example: GBP is_billable: type: boolean example: true diff --git a/openapi/components/schemas/User.yaml b/openapi/components/schemas/User.yaml index fa53332..64fb30d 100644 --- a/openapi/components/schemas/User.yaml +++ b/openapi/components/schemas/User.yaml @@ -15,9 +15,7 @@ properties: type: string example: jane@example.com role: - type: string - description: One of Owner, Director, Partner, Company Secretary, Employee, Shareholder or Accountant - example: Director + $ref: '#/components/schemas/UserRole' permission_level: $ref: '#/components/schemas/UserPermissionLevel' ni_number: From 2e8c2da934c2dff6ea3d54f3abde80ff3b67ede3 Mon Sep 17 00:00:00 2001 From: Dino Constantinou Date: Sun, 4 Oct 2026 10:05:09 +0100 Subject: [PATCH 2/2] Skip Integration Test Projects In The Rebill Test Co-Authored-By: Claude Opus 5.5 --- .../ExpenseIntegrationTests.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Tests/FreeAgentAPIIntegrationTests/ExpenseIntegrationTests.swift b/Tests/FreeAgentAPIIntegrationTests/ExpenseIntegrationTests.swift index 31d5075..596dfb0 100644 --- a/Tests/FreeAgentAPIIntegrationTests/ExpenseIntegrationTests.swift +++ b/Tests/FreeAgentAPIIntegrationTests/ExpenseIntegrationTests.swift @@ -148,7 +148,10 @@ struct ExpenseIntegrationTests { @Test("An expense rebilled through invoice create decodes its lock fields") func rebilledExpense() async throws { - let project = try #require(try await client.listProjects(.init()).ok.body.json.projects.last) + let project = try #require( + try await client.listProjects(.init()).ok.body.json.projects + .last { !($0.name?.hasPrefix("ZZ Integration Test") ?? false) } + ) let contact = try #require(project.contact) let user = try await client.showCurrentUser(.init()).ok.body.json.user