fakturan_nu 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/Gemfile +2 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/Rakefile +8 -0
- data/config/locales/en.yml +2 -0
- data/fakturan_nu.gemspec +26 -0
- data/lib/fakturan_nu/address.rb +5 -0
- data/lib/fakturan_nu/base.rb +88 -0
- data/lib/fakturan_nu/client.rb +19 -0
- data/lib/fakturan_nu/error.rb +69 -0
- data/lib/fakturan_nu/invoice.rb +40 -0
- data/lib/fakturan_nu/middleware/parse_json.rb +23 -0
- data/lib/fakturan_nu/middleware/raise_error.rb +33 -0
- data/lib/fakturan_nu/product.rb +7 -0
- data/lib/fakturan_nu/row.rb +7 -0
- data/lib/fakturan_nu/spyke_extensions.rb +24 -0
- data/lib/fakturan_nu.rb +87 -0
- data/test/basics_test.rb +26 -0
- data/test/exceptions_test.rb +104 -0
- data/test/models_test.rb +142 -0
- data/test/test_helper.rb +40 -0
- data/test/vcr_cassettes/models.yml +480 -0
- metadata +231 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f81bdfd634d37068875813a2c43c877ed859b4fb
|
4
|
+
data.tar.gz: 666f324bd953f0a55e9e6af4e8bbf70ad7405036
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c3591f8e8cb7cc5604029feec9e3a91b9e42a09fe302a97f8fe89aec1086abad8d30dafe9adee1163d330aeb7868210a3f9cc293af5ca9540b666f105e89194a
|
7
|
+
data.tar.gz: 4bd0783fff70fe74a037ee80f5a4e8df43873b6d4bb35e249775255d6cc42f789ec613386bd231d47625cd9be45c2d306d0c3d43f19b77b2c4c8d1c31738df59
|
data/.gitignore
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/test/tmp/
|
9
|
+
/test/version_tmp/
|
10
|
+
/tmp/
|
11
|
+
|
12
|
+
## Specific to RubyMotion:
|
13
|
+
.dat*
|
14
|
+
.repl_history
|
15
|
+
build/
|
16
|
+
|
17
|
+
## Documentation cache and generated files:
|
18
|
+
/.yardoc/
|
19
|
+
/_yardoc/
|
20
|
+
/doc/
|
21
|
+
/rdoc/
|
22
|
+
|
23
|
+
## Environment normalisation:
|
24
|
+
/.bundle/
|
25
|
+
/lib/bundler/man/
|
26
|
+
|
27
|
+
# for a library or gem, you might want to ignore these files since the code is
|
28
|
+
# intended to run in multiple environments; otherwise, check them in:
|
29
|
+
Gemfile.lock
|
30
|
+
.ruby-version
|
31
|
+
.ruby-gemset
|
32
|
+
|
33
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
34
|
+
.rvmrc
|
35
|
+
|
36
|
+
.DS_Store
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Imagineit AB
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
Ruby client for the fakturan.nu API
|
2
|
+
==============================
|
3
|
+
|
4
|
+
API client in ruby for the Swedish web based invoicing software [fakturan.nu](https://www.fakturan.nu). The API is currently in Beta and may be subject to change without notice until final release.
|
5
|
+
|
6
|
+
---
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
If you're using Rails/Bundler, add this to your Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
gem "fakturan_nu"
|
14
|
+
```
|
15
|
+
|
16
|
+
Then create an initializer in app/initializers called ``` fakturan_nu.rb ``` (or whatever name you choose) with the following:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
Fakturan.setup 'your api username here', 'your api key/password here'
|
20
|
+
Fakturan.use_sandbox = true # Should be true during development/testing and false in production.
|
21
|
+
```
|
22
|
+
|
23
|
+
If you're not using Rails, just ``` require 'fakturan_nu'``` and run setup wherever you like.
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### General
|
28
|
+
|
29
|
+
The client attempts to provide an interface similar to ActiveRecord.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
Fakturan::Product.all
|
33
|
+
# GET "https://fakturan.nu/api/v2/products" and return an array of product objects
|
34
|
+
|
35
|
+
Fakturan::Product.find(1)
|
36
|
+
# GET "https://fakturan.nu/api/v2/products/1" and return a product object
|
37
|
+
|
38
|
+
product = Fakturan::Product.create(name: "Shoes")
|
39
|
+
# POST "https://fakturan.nu/api/v2/products" and return the saved product object
|
40
|
+
|
41
|
+
product = Fakturan::Product.new(name: "Shoes")
|
42
|
+
product.tax = 12
|
43
|
+
product.save
|
44
|
+
# POST "https://fakturan.nu/api/v2/products" and return the saved product object
|
45
|
+
|
46
|
+
product = Fakturan::Product.find(1)
|
47
|
+
# GET "https://fakturan.nu/api/v2/products/1" and return a product object
|
48
|
+
product.name = "Blue suede shoes" # Update a property
|
49
|
+
product.save
|
50
|
+
# PUT "https://fakturan.nu/api/v2/products/1" and return the updated product object
|
51
|
+
```
|
52
|
+
|
53
|
+
### Invoices
|
54
|
+
|
55
|
+
For creating invoices, a date and a client is required:
|
56
|
+
```ruby
|
57
|
+
invoice = Fakturan::Invoice.create(date: Date.today, client: { company: "Acme Inc" })
|
58
|
+
# POST "https://fakturan.nu/api/v2/invoices" # Will create a new client + invoice
|
59
|
+
|
60
|
+
invoice = Fakturan::Invoice.create(date: Date.today, client_id: 1)
|
61
|
+
# POST "https://fakturan.nu/api/v2/invoices" # Will create a new invoice for client with id: 1
|
62
|
+
|
63
|
+
```
|
64
|
+
|
65
|
+
### Available resources and properties
|
66
|
+
|
67
|
+
For a full list of resources and the properties of each type of resource, see the [api reference](https://sandbox.fakturan.nu/apidocs).
|
68
|
+
|
69
|
+
## Pagination
|
70
|
+
|
71
|
+
The Fakturan.nu API paginates results. When a collection is fetched, it contains pagination information on the metadata accessor. So in order to get all results, you can do:
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
products = Fakturan::Product.all # This will give you paginated results of 30 items per page by default
|
75
|
+
products.concat(Fakturan::Product.get(products.metadata[:next])) while products.metadata[:next]
|
76
|
+
```
|
77
|
+
|
78
|
+
You can change the default number of items per page (30) by passing in the per_page parameter:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
products = Fakturan::Product.where(per_page: 100) # This will give you paginated results of 100 items per page
|
82
|
+
products.concat(Fakturan::Product.get(products.metadata[:next])) while products.metadata[:next]
|
83
|
+
```
|
84
|
+
|
85
|
+
The maximum number of items per page allowed is 100.
|
86
|
+
|
87
|
+
## Validation
|
88
|
+
|
89
|
+
Errors and validation are handled much like in ActiveRecord:
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
invoice = Fakturan::Invoice.new
|
93
|
+
invoice.save # false
|
94
|
+
invoice.errors.to_a # ["Client can't be blank", "Date can't be blank", "Date is invalid"]
|
95
|
+
```
|
96
|
+
|
97
|
+
As with ActiveRecord, the ``` save ``` and ``` create ``` methods will return ``` true ``` or ``` false ``` depending on validations, while ``` save! ``` and ``` create! ``` will raise an error if validations fail. The error that is raised in such cases is ``` Fakturan::Error::ResourceInvalid ```.
|
98
|
+
|
99
|
+
## Error handling
|
100
|
+
|
101
|
+
If the server responds with an error, or if the server cannot be reached, one of the following errors will be raised:
|
102
|
+
|
103
|
+
```ruby
|
104
|
+
Fakturan::Error::AccessDenied # http status code: 401
|
105
|
+
Fakturan::Error::ResourceNotFound # http status code: 404
|
106
|
+
Fakturan::Error::ConnectionFailed # http status code: 407
|
107
|
+
Fakturan::Error::ResourceInvalid # http status code: 422
|
108
|
+
Fakturan::Error::ClientError # http status code: other in the 400-499 range
|
109
|
+
Fakturan::Error::ServerError # http status code: other in the 500-599 range
|
110
|
+
```
|
111
|
+
|
112
|
+
These errors are all descendents of ```Fakturan::Error``` and should be caught in a begin-rescue block like so (example in a Rails-controller):
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
begin
|
116
|
+
invoice = Fakturan::Invoice.create!
|
117
|
+
rescue Fakturan::Error => error
|
118
|
+
render plain: error.message, status: error.status
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
data/Rakefile
ADDED
data/fakturan_nu.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
Gem::Specification.new do |s|
|
3
|
+
s.name = 'fakturan_nu'
|
4
|
+
s.version = '1.0.0'
|
5
|
+
s.date = '2014-12-17'
|
6
|
+
s.summary = 'A ruby client for the Fakturan.nu - API'
|
7
|
+
s.description = 'A ruby client for the Fakturan.nu - API. Fakturan.nu is a webbapp for billing.'
|
8
|
+
s.authors = ['Jonathan Bourque Olivegren']
|
9
|
+
s.email = 'jonathan@imagineit.se'
|
10
|
+
s.files = `git ls-files`.split("\n") # http://www.codinginthecrease.com/news_article/show/350843?referrer_id=948927
|
11
|
+
s.homepage = 'http://rubygems.org/gems/fakturan_nu'
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.add_dependency 'spyke', '~> 1.8', '>= 1.8.7'
|
15
|
+
s.add_dependency 'faraday', '>= 0.8', '< 1.0'
|
16
|
+
s.add_dependency 'multi_json', '~> 1.11', '>= 1.11.0'
|
17
|
+
# So we can use model.errors.details before Rails 5.
|
18
|
+
s.add_dependency 'active_model-errors_details', '~> 1.1', '>= 1.1.1'
|
19
|
+
|
20
|
+
s.add_development_dependency 'rake'
|
21
|
+
s.add_development_dependency 'webmock'
|
22
|
+
s.add_development_dependency 'minitest'
|
23
|
+
s.add_development_dependency 'minitest-reporters'
|
24
|
+
s.add_development_dependency 'minitest-around'
|
25
|
+
s.add_development_dependency 'vcr'
|
26
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'fakturan_nu/error'
|
2
|
+
|
3
|
+
module Fakturan
|
4
|
+
class Base < Spyke::Base
|
5
|
+
include_root_in_json false
|
6
|
+
|
7
|
+
def self.connection
|
8
|
+
Fakturan.connection
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.create!(attrs = {})
|
12
|
+
instance = self.new(attrs)
|
13
|
+
instance.save!
|
14
|
+
return instance
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.create(attrs = {})
|
18
|
+
instance = self.new(attrs)
|
19
|
+
instance.save
|
20
|
+
return instance
|
21
|
+
end
|
22
|
+
|
23
|
+
def save
|
24
|
+
errors.clear
|
25
|
+
super
|
26
|
+
return !errors.any?
|
27
|
+
end
|
28
|
+
|
29
|
+
def save!
|
30
|
+
raise Fakturan::Error::ResourceInvalid, self unless self.save
|
31
|
+
end
|
32
|
+
|
33
|
+
# This is an override from: https://github.com/balvig/spyke/blob/master/lib/spyke/http.rb
|
34
|
+
# to allow for nested errors on associated objects
|
35
|
+
def add_errors_to_model(errors_hash)
|
36
|
+
errors_hash.each do |field, field_errors|
|
37
|
+
ass_name, field_name = field.split(".").map(&:to_sym)
|
38
|
+
field_name = ass_name unless field_name
|
39
|
+
|
40
|
+
if association = self.class.reflect_on_association(ass_name) # The errors are for an associatied object
|
41
|
+
if association.type == Spyke::Associations::HasMany
|
42
|
+
field_errors.each do |new_error_hash_with_index|
|
43
|
+
new_error_hash_with_index.each do |index, inner_errors_hash|
|
44
|
+
error_attribute = inner_errors_hash.keys.first.split('.').last.to_sym
|
45
|
+
self.send(ass_name)[index.to_i].add_to_errors(error_attribute, inner_errors_hash.values.last)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
else # It's a belongs_to or has_one
|
49
|
+
# We add the error to the associated object
|
50
|
+
self.send(ass_name).add_to_errors(field_name, field_errors)
|
51
|
+
# and then we get the errors (with generated messages) and add them to
|
52
|
+
# the parent (but without details, like nested_attributes works)
|
53
|
+
# This only makes sense on belongs_to and has_one, since it's impossible
|
54
|
+
# to know which object is refered to on has_many
|
55
|
+
self.send(ass_name).errors.each do |attribute, message|
|
56
|
+
attribute = "#{ass_name}.#{attribute}"
|
57
|
+
errors[attribute] << message
|
58
|
+
errors[attribute].uniq!
|
59
|
+
end
|
60
|
+
end
|
61
|
+
else
|
62
|
+
self.add_to_errors(field_name.to_sym, field_errors)
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def add_to_errors field_name, field_errors
|
69
|
+
field_errors.each do |attributes|
|
70
|
+
attributes = attributes.symbolize_keys
|
71
|
+
error_name = attributes.delete(:error).to_sym
|
72
|
+
errors.add(field_name.to_sym, error_name, attributes)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# This is an override from: https://github.com/balvig/spyke/blob/master/lib/spyke/http.rb
|
77
|
+
# In order to re-raise Faraday error as Fakturan error
|
78
|
+
def self.request(method, path, params = {})
|
79
|
+
begin
|
80
|
+
super
|
81
|
+
rescue Faraday::ConnectionFailed => e
|
82
|
+
raise Fakturan::Error::ConnectionFailed, e.message
|
83
|
+
rescue Faraday::TimeoutError => e
|
84
|
+
raise Fakturan::Error::ConnectionFailed, e.message
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Fakturan
|
2
|
+
class Client < Base
|
3
|
+
uri "clients/(:id)"
|
4
|
+
has_many :invoices, uri: "clients/:id/invoices", foreign_key: 'id'
|
5
|
+
has_one :address
|
6
|
+
|
7
|
+
accepts_nested_attributes_for :address
|
8
|
+
|
9
|
+
attributes :number, :first_name, :last_name, :email, :company, :phone, :home_phone, :mobile_phone, :fax, :org_number, :private, :web, :vat_number
|
10
|
+
|
11
|
+
def address=(attrs_or_obj)
|
12
|
+
if attrs_or_obj.respond_to?(:each)
|
13
|
+
send(:address_attributes=, attrs_or_obj)
|
14
|
+
else
|
15
|
+
super
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'active_model/errors_details'
|
2
|
+
|
3
|
+
# This is just to provide a set of errors with consistent scope
|
4
|
+
# https://github.com/lostisland/faraday/blob/master/lib/faraday/error.rb
|
5
|
+
module Fakturan
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
class WithResponse < Error
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
def status
|
12
|
+
response[:status]
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(ex, response = nil)
|
16
|
+
@wrapped_exception = nil
|
17
|
+
@response = response
|
18
|
+
|
19
|
+
if ex.respond_to?(:backtrace) # If ex behaves like an Exception
|
20
|
+
super(ex.message)
|
21
|
+
@wrapped_exception = ex
|
22
|
+
elsif ex.respond_to?(:each_key) # If ex behaves like a Hash
|
23
|
+
super("the server responded with status #{ex[:status]} - #{ex[:body]}")
|
24
|
+
@response = ex
|
25
|
+
else
|
26
|
+
super(ex.to_s)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def backtrace
|
31
|
+
if @wrapped_exception
|
32
|
+
@wrapped_exception.backtrace
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def inspect
|
39
|
+
%(#<#{self.class}>)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
class ClientError < WithResponse; end
|
44
|
+
class ServerError < WithResponse; end
|
45
|
+
class AccessDenied < WithResponse; end
|
46
|
+
class ResourceNotFound < WithResponse; end
|
47
|
+
class ParseError < WithResponse; end
|
48
|
+
|
49
|
+
class ConnectionFailed < Error
|
50
|
+
def status; 407; end
|
51
|
+
end
|
52
|
+
|
53
|
+
class ResourceInvalid < Error
|
54
|
+
attr_reader :model
|
55
|
+
|
56
|
+
def status; 422; end
|
57
|
+
|
58
|
+
def initialize(model)
|
59
|
+
@model = model
|
60
|
+
msg = @model.errors.details #to_a.join(" ")
|
61
|
+
super(msg)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# This is just so that we can have Fakturan::Error be both a class and a "scope": Fakturan::Error::ResourceInvalid
|
66
|
+
[:WithResponse, :ClientError, :ServerError, :AccessDenied, :ResourceNotFound, :ParseError, :ConnectionFailed, :ResourceInvalid].each do |const|
|
67
|
+
Error.const_set(const, Fakturan.const_get(const))
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Fakturan
|
2
|
+
class Invoice < Base
|
3
|
+
uri "invoices/(:id)"
|
4
|
+
|
5
|
+
# The foreign_key decides which attribute will be used to set the id
|
6
|
+
belongs_to :client, uri: "clients/:id", foreign_key: 'client_id'
|
7
|
+
has_one :address, uri: nil
|
8
|
+
|
9
|
+
has_many :rows, uri: nil
|
10
|
+
|
11
|
+
accepts_nested_attributes_for :rows, :address, :client
|
12
|
+
|
13
|
+
# These create getter methods on new instances, mostly for use in forms
|
14
|
+
attributes :number, :date, :client_id, :days, :our_reference, :your_reference, :sent, :paid, :paid_at, :interval_period, :auto_send, :recurring, :last_day_of_month, :start_recurring_from, :locale, :currency
|
15
|
+
|
16
|
+
def rows=(attrs_or_obj_array)
|
17
|
+
if attrs_or_obj_array.first.respond_to?(:each)
|
18
|
+
send(:rows_attributes=, attrs_or_obj_array)
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def address=(attrs_or_obj)
|
25
|
+
if attrs_or_obj.respond_to?(:each)
|
26
|
+
send(:address_attributes=, attrs_or_obj)
|
27
|
+
else
|
28
|
+
super
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def client=(attrs_or_obj)
|
33
|
+
if attrs_or_obj.respond_to?(:each)
|
34
|
+
send(:client_attributes=, attrs_or_obj)
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Fakturan
|
2
|
+
module Response
|
3
|
+
class ParseJSON < Faraday::Response::Middleware
|
4
|
+
def parse(body)
|
5
|
+
json = MultiJson.load(body, symbolize_keys: true)
|
6
|
+
res = {
|
7
|
+
data: json[:data],
|
8
|
+
metadata: json[:paging],
|
9
|
+
errors: json[:errors]
|
10
|
+
}
|
11
|
+
return res
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_complete(env)
|
15
|
+
begin # https://github.com/lostisland/faraday/blob/master/lib/faraday/response.rb
|
16
|
+
env.body = parse(env.body) if env.parse_body?
|
17
|
+
rescue MultiJson::ParseError => exception
|
18
|
+
raise Fakturan::Error::ParseError, {:status => env.status, :headers => env.response_headers, :body => env.body}
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'fakturan_nu/error'
|
2
|
+
|
3
|
+
module Fakturan
|
4
|
+
module Response
|
5
|
+
class RaiseError < Faraday::Response::Middleware
|
6
|
+
ClientErrorStatuses = 400...499
|
7
|
+
ServerErrorStatuses = 500...599
|
8
|
+
|
9
|
+
def on_complete(env)
|
10
|
+
case env[:status]
|
11
|
+
when 401
|
12
|
+
raise Fakturan::Error::AccessDenied, response_values(env)
|
13
|
+
when 404
|
14
|
+
raise Fakturan::Error::ResourceNotFound, response_values(env)
|
15
|
+
when 407
|
16
|
+
# mimic the behavior that we get with proxy requests with HTTPS
|
17
|
+
raise Faraday::ConnectionFailed, %{407 "Proxy Authentication Required "} # We raise this instead of our own ConnectionFailed error, since Faraday::ConnectionFailed will be raise on timeouts or refused connections anyway
|
18
|
+
when 422
|
19
|
+
# We don't do anything except fallback to standard behaviour in Spyke, which is to store errors on .errors and not raise an exception
|
20
|
+
# If an exception is desirable, then use .save! on the model instead of .save.
|
21
|
+
when ClientErrorStatuses
|
22
|
+
raise Fakturan::Error::ClientError, response_values(env)
|
23
|
+
when ServerErrorStatuses
|
24
|
+
raise Fakturan::Error::ServerError, response_values(env)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def response_values(env)
|
29
|
+
{:status => env.status, :headers => env.response_headers, :body => env.body}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Spyke
|
2
|
+
class Collection
|
3
|
+
# For pagination.
|
4
|
+
# Makes it possible to do ps = Fakturan::Product.all and then
|
5
|
+
# ps.concat(Fakturan::Product.get(ps.metadata[:next])) while ps.metadata[:next])
|
6
|
+
def concat ary_or_collection
|
7
|
+
@metadata = ary_or_collection.metadata if ary_or_collection.respond_to?(:metadata)
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Relation
|
13
|
+
# For pagination and more array-like behaviour.
|
14
|
+
# It's very tempting to add << as well, but as I recall, it didn't behave as expected
|
15
|
+
delegate :concat, :+, :-, to: :find_some
|
16
|
+
end
|
17
|
+
|
18
|
+
module Associations
|
19
|
+
class Builder
|
20
|
+
# Allows us to do reflect_on_association(ass_name).type
|
21
|
+
attr_accessor :type
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/fakturan_nu.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'spyke'
|
3
|
+
#require 'active_support/json/decoding' # if we want date-strings to be typecast as DateTimes. Can be done through Faraday-middleware too.
|
4
|
+
require 'faraday_middleware'
|
5
|
+
require 'multi_json'
|
6
|
+
require 'fakturan_nu/spyke_extensions'
|
7
|
+
require 'fakturan_nu/middleware/parse_json'
|
8
|
+
require 'fakturan_nu/middleware/raise_error'
|
9
|
+
|
10
|
+
require 'fakturan_nu/base'
|
11
|
+
require 'fakturan_nu/address'
|
12
|
+
require 'fakturan_nu/product'
|
13
|
+
require 'fakturan_nu/client'
|
14
|
+
require 'fakturan_nu/row'
|
15
|
+
require 'fakturan_nu/invoice'
|
16
|
+
|
17
|
+
I18n.load_path += Dir.glob( File.dirname(__FILE__) + "lib/locales/*.{rb,yml}" )
|
18
|
+
|
19
|
+
module Fakturan
|
20
|
+
@use_sandbox = false
|
21
|
+
@api_version = 2
|
22
|
+
|
23
|
+
mattr_accessor :accept_language
|
24
|
+
|
25
|
+
def self.setup username = nil, pass = nil
|
26
|
+
#self.parse_json_times = true
|
27
|
+
|
28
|
+
@username, @pass = username, pass
|
29
|
+
self.accept_language = 'en-US'
|
30
|
+
|
31
|
+
@connection = Faraday.new(url: build_url) do |connection|
|
32
|
+
# Request
|
33
|
+
connection.request :json
|
34
|
+
connection.use Faraday::Request::BasicAuthentication, @username, @pass
|
35
|
+
|
36
|
+
# Response
|
37
|
+
connection.use Fakturan::Response::ParseJSON
|
38
|
+
connection.use Fakturan::Response::RaiseError
|
39
|
+
|
40
|
+
# Adapter should be last
|
41
|
+
connection.adapter Faraday.default_adapter
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.connection
|
46
|
+
@connection
|
47
|
+
end
|
48
|
+
|
49
|
+
#def self.parse_json_times=(true_or_false)
|
50
|
+
# ActiveSupport.parse_json_times = true_or_false
|
51
|
+
#end
|
52
|
+
|
53
|
+
def self.use_token_auth(token)
|
54
|
+
@connection.token_auth(token)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.use_basic_auth(username = nil, pass = nil)
|
58
|
+
@username, @pass = username, pass if (username && pass)
|
59
|
+
@connection.basic_auth(@username, @pass)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.use_sandbox=(true_or_false)
|
63
|
+
@use_sandbox = true_or_false
|
64
|
+
rebuild_url
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.api_version=(version_no)
|
68
|
+
@api_version = version_no
|
69
|
+
rebuild_url
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.url
|
73
|
+
@connection.url_prefix.to_s
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.url=(new_url)
|
77
|
+
@connection.url_prefix = new_url
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.build_url
|
81
|
+
"https://#{@use_sandbox ? 'sandbox.' : ''}fakturan.nu/api/v#{@api_version}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.rebuild_url
|
85
|
+
self.url = self.build_url
|
86
|
+
end
|
87
|
+
end
|
data/test/basics_test.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
VCR.turn_off!
|
3
|
+
|
4
|
+
module Fakturan
|
5
|
+
class BasicsTest < MiniTest::Test
|
6
|
+
|
7
|
+
# They are not, but I dislike them jumping around in the output. Makes it harder to compare two test runs.
|
8
|
+
i_suck_and_my_tests_are_order_dependent!
|
9
|
+
|
10
|
+
def test_auth_with_token_and_back_to_basic
|
11
|
+
basic_auth_endpoint = stub_api_request(:get, '/clients/1').to_return(body: '{"data":{"id": 1, "name":"DCT"}}', status: 200)
|
12
|
+
|
13
|
+
token_endpoint = stub_request(:get, "http://#{BASE_URL}/clients/1")
|
14
|
+
.with(headers: { authorization: "Token token=\"XYZ\""})
|
15
|
+
.to_return(body: '{"data":{"id": 1, "name":"DCT"}}', status: 200)
|
16
|
+
|
17
|
+
Fakturan::Client.find(1)
|
18
|
+
Fakturan.use_token_auth('XYZ')
|
19
|
+
Fakturan::Client.find(1)
|
20
|
+
Fakturan.use_basic_auth
|
21
|
+
Fakturan::Client.find(1)
|
22
|
+
assert_requested basic_auth_endpoint, times: 2
|
23
|
+
assert_requested token_endpoint, times: 1
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|