diff --git a/lib/fiken.rb b/lib/fiken.rb new file mode 100644 index 0000000..efc05cc --- /dev/null +++ b/lib/fiken.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative "fiken/version" +require_relative "fiken/error" +require_relative "fiken/object" +require_relative "fiken/connection" +require_relative "fiken/collection" +require_relative "fiken/resource" + +# Shared sub-resources used by the resource definitions below. +require_relative "fiken/resources/concerns/attachments" +require_relative "fiken/resources/concerns/payments" +require_relative "fiken/resources/concerns/drafts" + +# Resource definitions (one file per API tag). +require_relative "fiken/resources/companies" +require_relative "fiken/resources/accounts" +require_relative "fiken/resources/bank_accounts" +require_relative "fiken/resources/contacts" +require_relative "fiken/resources/groups" +require_relative "fiken/resources/products" +require_relative "fiken/resources/journal_entries" +require_relative "fiken/resources/transactions" +require_relative "fiken/resources/invoices" +require_relative "fiken/resources/credit_notes" +require_relative "fiken/resources/offers" +require_relative "fiken/resources/order_confirmations" +require_relative "fiken/resources/sales" +require_relative "fiken/resources/purchases" +require_relative "fiken/resources/inbox" +require_relative "fiken/resources/projects" +require_relative "fiken/resources/activities" +require_relative "fiken/resources/time_entries" +require_relative "fiken/resources/time_users" + +require_relative "fiken/oauth" +require_relative "fiken/client" + +# Ruby client for the Fiken API v2 (https://api.fiken.no/api/v2/docs). +module Fiken + class << self + # Convenience constructor: Fiken.client(token: "...") + def client(**options) + Client.new(**options) + end + end +end diff --git a/lib/fiken/client.rb b/lib/fiken/client.rb new file mode 100644 index 0000000..9672a8f --- /dev/null +++ b/lib/fiken/client.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Fiken + # The entry point. Holds the connection and exposes the resource accessors. + # + # client = Fiken::Client.new(token: ENV["FIKEN_TOKEN"]) + # client.user + # client.companies.list + # client.contacts("my-company-slug").find(123) + # client.invoices("my-company-slug").drafts.create(invoice_attrs) + class Client + attr_reader :connection + + def initialize(token: nil, access_token: nil, **options) + bearer = token || access_token + raise ArgumentError, "provide :token or :access_token" if bearer.nil? || bearer.empty? + + @connection = Connection.new(token: bearer, **options) + end + + # GET /user + def user + Object.new(connection.get("/user").body) + end + + # GET /companies, GET /companies/{slug} + def companies + Resources::Companies.new(self) + end + + # Convenience: fetch a single company by slug. + def company(slug) + companies.find(slug) + end + + # Company-scoped resources. Each takes the company slug. + def accounts(slug) = Resources::Accounts.new(self, slug) + def account_balances(slug) = Resources::AccountBalances.new(self, slug) + def activities(slug) = Resources::Activities.new(self, slug) + def bank_accounts(slug) = Resources::BankAccounts.new(self, slug) + def bank_balances(slug) = Resources::BankBalances.new(self, slug) + def contacts(slug) = Resources::Contacts.new(self, slug) + def credit_notes(slug) = Resources::CreditNotes.new(self, slug) + def groups(slug) = Resources::Groups.new(self, slug) + def inbox(slug) = Resources::Inbox.new(self, slug) + def invoices(slug) = Resources::Invoices.new(self, slug) + def journal_entries(slug) = Resources::JournalEntries.new(self, slug) + def offers(slug) = Resources::Offers.new(self, slug) + def order_confirmations(slug) = Resources::OrderConfirmations.new(self, slug) + def products(slug) = Resources::Products.new(self, slug) + def projects(slug) = Resources::Projects.new(self, slug) + def purchases(slug) = Resources::Purchases.new(self, slug) + def sales(slug) = Resources::Sales.new(self, slug) + def time_entries(slug) = Resources::TimeEntries.new(self, slug) + def time_users(slug) = Resources::TimeUsers.new(self, slug) + def transactions(slug) = Resources::Transactions.new(self, slug) + end +end diff --git a/lib/fiken/resources/accounts.rb b/lib/fiken/resources/accounts.rb new file mode 100644 index 0000000..9ae8409 --- /dev/null +++ b/lib/fiken/resources/accounts.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/accounts — find by account code (e.g. "1500:10001"). + class Accounts < Resource::Base + include Resource::Listable + include Resource::Findable + + def resource_path + "accounts" + end + end + + # /companies/{slug}/accountBalances + class AccountBalances < Resource::Base + include Resource::Listable + include Resource::Findable + + def resource_path + "accountBalances" + end + end + end +end diff --git a/lib/fiken/resources/activities.rb b/lib/fiken/resources/activities.rb new file mode 100644 index 0000000..c028eea --- /dev/null +++ b/lib/fiken/resources/activities.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/activities (time-tracking activities) + class Activities < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::PatchUpdatable + include Resource::Deletable + + def resource_path + "activities" + end + end + end +end diff --git a/lib/fiken/resources/bank_accounts.rb b/lib/fiken/resources/bank_accounts.rb new file mode 100644 index 0000000..9fac46f --- /dev/null +++ b/lib/fiken/resources/bank_accounts.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/bankAccounts + class BankAccounts < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + + def resource_path + "bankAccounts" + end + end + + # /companies/{slug}/bankBalances + class BankBalances < Resource::Base + include Resource::Listable + + def resource_path + "bankBalances" + end + end + end +end diff --git a/lib/fiken/resources/companies.rb b/lib/fiken/resources/companies.rb new file mode 100644 index 0000000..18efcf1 --- /dev/null +++ b/lib/fiken/resources/companies.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # GET /companies and GET /companies/{companySlug} + class Companies < Resource::Base + include Resource::Listable + include Resource::Findable + + def resource_path + "companies" + end + end + end +end diff --git a/lib/fiken/resources/contacts.rb b/lib/fiken/resources/contacts.rb new file mode 100644 index 0000000..d9de274 --- /dev/null +++ b/lib/fiken/resources/contacts.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/contacts and nested contact persons. + class Contacts < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::Updatable # PUT + include Resource::Deletable + include Resource::Attachable + + def resource_path + "contacts" + end + + # Contacts only support uploading attachments, not listing them. + def attachments_listable? + false + end + + def contact_persons(contact_id) + ContactPersons.new(client, company_slug, "#{base_path}/#{contact_id}") + end + end + + # /companies/{slug}/contacts/{id}/contactPerson + class ContactPersons < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::Updatable # PUT + include Resource::Deletable + + def initialize(client, company_slug, parent_path) + super(client, company_slug) + @parent_path = parent_path + end + + def base_path + "#{@parent_path}/contactPerson" + end + end + end +end diff --git a/lib/fiken/resources/credit_notes.rb b/lib/fiken/resources/credit_notes.rb new file mode 100644 index 0000000..2317a3b --- /dev/null +++ b/lib/fiken/resources/credit_notes.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/creditNotes + class CreditNotes < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Sendable + include Resource::HasCounter + include Resource::Draftable + + def resource_path + "creditNotes" + end + + def draft_create_action + "createCreditNote" + end + + # POST /creditNotes/full — credit a whole invoice. + def create_full(attributes) + post_create("#{base_path}/full", attributes) + end + + # POST /creditNotes/partial — credit selected lines/amounts. + def create_partial(attributes) + post_create("#{base_path}/partial", attributes) + end + end + end +end diff --git a/lib/fiken/resources/groups.rb b/lib/fiken/resources/groups.rb new file mode 100644 index 0000000..8c290e7 --- /dev/null +++ b/lib/fiken/resources/groups.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/groups (contact groups) + class Groups < Resource::Base + include Resource::Listable + + def resource_path + "groups" + end + end + end +end diff --git a/lib/fiken/resources/inbox.rb b/lib/fiken/resources/inbox.rb new file mode 100644 index 0000000..6f2da1b --- /dev/null +++ b/lib/fiken/resources/inbox.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/inbox — documents awaiting bookkeeping. + class Inbox < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Deletable + + def resource_path + "inbox" + end + + # POST /inbox — multipart upload of an inbox document. + def create(path: nil, io: nil, filename: nil, content_type: "application/octet-stream", **fields) + parts = { "file" => build_file_part(path, io, filename, content_type) } + fields.each { |key, value| parts[key.to_s] = value.to_s unless value.nil? } + build_created(connection.post_multipart(base_path, parts)) + end + end + end +end diff --git a/lib/fiken/resources/invoices.rb b/lib/fiken/resources/invoices.rb new file mode 100644 index 0000000..198972d --- /dev/null +++ b/lib/fiken/resources/invoices.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/invoices + class Invoices < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::PatchUpdatable # finalized invoices update via PATCH + include Resource::Attachable + include Resource::Sendable + include Resource::HasCounter + include Resource::Draftable + + def resource_path + "invoices" + end + + def draft_create_action + "createInvoice" + end + end + end +end diff --git a/lib/fiken/resources/journal_entries.rb b/lib/fiken/resources/journal_entries.rb new file mode 100644 index 0000000..616019d --- /dev/null +++ b/lib/fiken/resources/journal_entries.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/journalEntries + class JournalEntries < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Attachable + + def resource_path + "journalEntries" + end + + # Creation posts to a sibling path: POST /generalJournalEntries + def create_general(attributes) + post_create("/companies/#{company_slug}/generalJournalEntries", attributes) + end + end + end +end diff --git a/lib/fiken/resources/offers.rb b/lib/fiken/resources/offers.rb new file mode 100644 index 0000000..1d67b27 --- /dev/null +++ b/lib/fiken/resources/offers.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/offers + class Offers < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Sendable + include Resource::HasCounter + include Resource::Draftable + + def resource_path + "offers" + end + + def draft_create_action + "createOffer" + end + end + end +end diff --git a/lib/fiken/resources/order_confirmations.rb b/lib/fiken/resources/order_confirmations.rb new file mode 100644 index 0000000..f8ec49b --- /dev/null +++ b/lib/fiken/resources/order_confirmations.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/orderConfirmations + class OrderConfirmations < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::HasCounter + include Resource::Draftable + + def resource_path + "orderConfirmations" + end + + def draft_create_action + "createOrderConfirmation" + end + + # POST /{id}/createInvoiceDraft — turn a confirmation into an invoice draft. + def create_invoice_draft(confirmation_id) + post_create("#{base_path}/#{confirmation_id}/createInvoiceDraft", nil) + end + end + end +end diff --git a/lib/fiken/resources/products.rb b/lib/fiken/resources/products.rb new file mode 100644 index 0000000..ac15cae --- /dev/null +++ b/lib/fiken/resources/products.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/products + class Products < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::Updatable # PUT + include Resource::Deletable + + def resource_path + "products" + end + + # POST /products/salesReport — returns an array of per-product report rows. + def sales_report(attributes) + body = connection.post("#{base_path}/salesReport", attributes).body + Array(body).map { |row| wrap(row) } + end + end + end +end diff --git a/lib/fiken/resources/projects.rb b/lib/fiken/resources/projects.rb new file mode 100644 index 0000000..adb84a4 --- /dev/null +++ b/lib/fiken/resources/projects.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/projects + class Projects < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::PatchUpdatable + include Resource::Deletable + + def resource_path + "projects" + end + end + end +end diff --git a/lib/fiken/resources/purchases.rb b/lib/fiken/resources/purchases.rb new file mode 100644 index 0000000..2d3ad23 --- /dev/null +++ b/lib/fiken/resources/purchases.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/purchases + class Purchases < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::Attachable + include Resource::Payable + include Resource::Draftable + + def resource_path + "purchases" + end + + def draft_create_action + "createPurchase" + end + + # PATCH /{id}/delete — delete a purchase (with a reason). + def delete(id, attributes = nil) + connection.patch("#{base_path}/#{id}/delete", attributes) + true + end + end + end +end diff --git a/lib/fiken/resources/sales.rb b/lib/fiken/resources/sales.rb new file mode 100644 index 0000000..eb2b662 --- /dev/null +++ b/lib/fiken/resources/sales.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/sales + class Sales < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::Attachable + include Resource::Payable + include Resource::Draftable + + def resource_path + "sales" + end + + def draft_create_action + "createSale" + end + + # PATCH /{id}/settled — mark a sale as settled. + def settle(id, attributes = nil) + patch_one("#{base_path}/#{id}/settled", attributes) + end + + # PATCH /{id}/writeOff — write off a sale as a loss. + def write_off(id, attributes = nil) + patch_one("#{base_path}/#{id}/writeOff", attributes) + end + + # PATCH /{id}/delete — delete a sale (with a reason). + def delete(id, attributes = nil) + connection.patch("#{base_path}/#{id}/delete", attributes) + true + end + end + end +end diff --git a/lib/fiken/resources/time_entries.rb b/lib/fiken/resources/time_entries.rb new file mode 100644 index 0000000..59d0f2e --- /dev/null +++ b/lib/fiken/resources/time_entries.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/timeEntries + class TimeEntries < Resource::Base + include Resource::Listable + include Resource::Findable + include Resource::Creatable + include Resource::PatchUpdatable + include Resource::Deletable + + def resource_path + "timeEntries" + end + + # POST /timeEntries/createInvoiceDraft — invoice selected time entries. + def create_invoice_draft(attributes) + post_create("#{base_path}/createInvoiceDraft", attributes) + end + end + end +end diff --git a/lib/fiken/resources/time_users.rb b/lib/fiken/resources/time_users.rb new file mode 100644 index 0000000..c5cd36f --- /dev/null +++ b/lib/fiken/resources/time_users.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/timeUsers + class TimeUsers < Resource::Base + include Resource::Listable + include Resource::Findable + + def resource_path + "timeUsers" + end + end + end +end diff --git a/lib/fiken/resources/transactions.rb b/lib/fiken/resources/transactions.rb new file mode 100644 index 0000000..3bf9a90 --- /dev/null +++ b/lib/fiken/resources/transactions.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Fiken + module Resources + # /companies/{slug}/transactions + class Transactions < Resource::Base + include Resource::Listable + include Resource::Findable + + def resource_path + "transactions" + end + + # Deletion is a PATCH to /{id}/delete (optionally with a description). + def delete(id, attributes = nil) + connection.patch("#{base_path}/#{id}/delete", attributes) + true + end + end + end +end