diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1116b6 --- /dev/null +++ b/README.md @@ -0,0 +1,188 @@ +# Fiken + +A resource-oriented Ruby client for the [Fiken API v2](https://api.fiken.no/api/v2/docs) — +the Norwegian accounting and invoicing platform. + +- Personal API token **and** OAuth2 (authorization-code) authentication +- Resource-oriented surface that mirrors the API (`client.invoices(slug).drafts.create(...)`) +- Automatic pagination with `auto_paging_each` +- HTTP errors mapped to typed exceptions, with retry on rate limits (429) and server errors + +## Installation + +```ruby +# Gemfile +gem "fiken" +``` + +```sh +bundle install +``` + +## Authentication + +### Personal API token + +Generate a token in Fiken (under your account settings) and pass it in: + +```ruby +client = Fiken::Client.new(token: ENV["FIKEN_TOKEN"]) +client.user # => # +client.companies.list # => # +``` + +### OAuth2 (acting on behalf of other Fiken users) + +```ruby +oauth = Fiken::OAuth.new( + client_id: ENV["FIKEN_CLIENT_ID"], + client_secret: ENV["FIKEN_CLIENT_SECRET"], + redirect_uri: "https://example.com/oauth/callback" +) + +# 1. Send the user to authorize +redirect_to oauth.authorize_url(state: "csrf-token") + +# 2. In your callback, exchange the code for tokens +token = oauth.exchange_code(params[:code]) +client = Fiken::Client.new(access_token: token.access_token) + +# 3. Later, refresh +token = oauth.refresh(token.refresh_token) +``` + +## Usage + +Almost every resource is scoped to a company, identified by its **slug**: + +```ruby +slug = client.companies.list.first.slug +``` + +### Reading + +```ruby +client.contacts(slug).list(page: 0, pageSize: 50) +client.contacts(slug).find(123) +client.invoices(slug).find(456) +client.accounts(slug).find("1500:10001") +``` + +### Pagination + +A list returns a `Fiken::Collection` that exposes the page metadata and can walk +every page lazily: + +```ruby +contacts = client.contacts(slug).list +contacts.result_count # total across all pages +contacts.auto_paging_each do |contact| + puts contact.name +end +``` + +### Creating and updating + +```ruby +# Create returns the new resource's id (parsed from the Location header) +created = client.contacts(slug).create(name: "Acme AS", organizationNumber: "123456789") +created.id + +# Contacts/drafts update via PUT; finalized invoices/projects/etc. via PATCH +client.contacts(slug).update(123, email: "post@acme.no") +client.invoices(slug).update(456, sentManually: true) +client.contacts(slug).delete(123) +``` + +### Invoices, credit notes, offers, order confirmations + +These share a draft → finalize lifecycle, plus counters and (where supported) sending: + +```ruby +invoices = client.invoices(slug) + +draft = invoices.drafts.create( + type: "invoice", + customerId: 123, + lines: [{ description: "Consulting", unitPrice: 100_000, vatType: "HIGH", quantity: 1 }] +) +invoice = invoices.drafts.create_document(draft.id) # finalize the draft + +invoices.dispatch(invoiceId: invoice.id, method: ["email"], includeDocumentAttachments: true) +invoices.counter # current invoice number +invoices.create_counter(value: 1000) # initialize it + +client.credit_notes(slug).create_full(invoiceId: invoice.id) +``` + +### Sales, purchases and payments + +```ruby +sales = client.sales(slug) +sales.create(saleNumber: "1", date: "2026-01-01", kind: "external_invoice", lines: [...]) +sales.payments(5).create(date: "2026-01-05", account: "1920:10001", amount: 125_000) +sales.settle(5, settledDate: "2026-01-10") +sales.write_off(5) +sales.delete(5, description: "duplicate") +``` + +### Attachments and the inbox (file uploads) + +```ruby +client.invoices(slug).attachments(456).add(path: "invoice.pdf") +client.invoices(slug).attachments(456).add(io: pdf_io, filename: "invoice.pdf", + content_type: "application/pdf") +client.inbox(slug).create(path: "receipt.pdf", name: "Office supplies") +``` + +## Error handling + +Non-success responses raise a typed subclass of `Fiken::Error`, each carrying +`#status` and `#body`: + +```ruby +begin + client.invoices(slug).find(999_999) +rescue Fiken::NotFound => e + e.status # => 404 +rescue Fiken::RateLimited + # retried automatically first; raised only if retries are exhausted +rescue Fiken::Error => e + warn e.message +end +``` + +`BadRequest` (400), `Unauthorized` (401), `Forbidden` (403), `NotFound` (404), +`UnprocessableEntity` (422), `RateLimited` (429) and `ServerError` (5xx) are all +provided. + +## Available resources + +`user`, `companies`, `accounts`, `account_balances`, `bank_accounts`, `bank_balances`, +`contacts` (+ `contact_persons`), `groups`, `products`, `journal_entries`, `transactions`, +`invoices`, `credit_notes`, `offers`, `order_confirmations`, `sales`, `purchases`, `inbox`, +`projects`, `activities`, `time_entries`, `time_users`. + +## Configuration + +```ruby +Fiken::Client.new( + token: ENV["FIKEN_TOKEN"], + base_url: "https://api.fiken.no/api/v2", # default + user_agent: "my-app/1.0", + retry_options: { max: 3 }, # passed to faraday-retry + logger: Logger.new($stdout) +) +``` + +## Development + +```sh +bin/setup # or: bundle install +bundle exec rspec +bundle exec rubocop +``` + +## License + +The gem is available as open source under the terms of the [MIT License](LICENSE.txt).