pennylane 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da1842f4d3dee5f1bd7c9bccd793eb1c185cb5fece2178e967da0cbec3b752be
4
- data.tar.gz: 1f050f189dae434bdf4febe2c7715858b1f422273a129aa6654677c95ba94921
3
+ metadata.gz: 1b079bfa7d777a0adb18de2cb8b02c4b5b3f7a20ef14ee3be35ccc56c4fc5714
4
+ data.tar.gz: 417d611044b5329eb04040ffaab0c1e78e609b6dc5613deeeac35994aab88012
5
5
  SHA512:
6
- metadata.gz: c245562b9cdcfc4754348d1a43d2029d5b6d025cd61191c0ceeb3e7b814ed81bae299cc7e86b76799a11275fc296a4e5fd8080ab133f262981ccf9091fad665f
7
- data.tar.gz: 0404a595eedc597bc04c2dafa9db4796e1ab9d6a12a0bfc9f28132195b50f0ebe8516552c5de04326ed1d79cfef97a2d07de0e7b30ce6fd188abc991e07a221c
6
+ metadata.gz: 8d1e5ec1c7aacb11921d8d7490c8bfcfd626fb185a74c6199b1ac9b75e4b4cae03dc8f66be2e70effada0cc21a1d563b1aef205beca5d906bc522637d76b429c
7
+ data.tar.gz: fbec4d19969a69390de3903fcb7a1e9e372fcdc4d6bb9b3f735ac09e3a31b7bbea14963e0e35d0c0815d42140385e168e21f21e764caae06d6fdbea08ebec21c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2024-04-29
4
+
5
+ - Support CustomerInvoice:
6
+ - `finalize`
7
+ - `mark_as_paid`
8
+ - `send_by_email`
9
+ - `links`
10
+ - `import`
11
+ - Support per request `api_key` e.g `Pennylane::Customer.retrieve('cus_id', {api_key: 'x0fd....'})`
12
+ - Endpoints returning empty response we are doing +1 GET request at the resource. Not the best solution but at least we are sure we have fresh data.
13
+ - Added resource properties access with `#[]` e.g `cus['name']`
14
+
15
+ ## [0.2.0-alpha] - 2024-04-20
16
+
17
+ - Support resources
18
+ - `CategoryGroup`
19
+ - `Category`
20
+ - `Product`
21
+ - Added missing `#create` to `Supplier`
22
+ - Adding `#update` to `Customer` and `Supplier`
23
+
24
+
3
25
  ## [0.1.0] - 2024-04-04
4
26
 
5
- - Initial release
27
+ - Initial release
data/Gemfile.lock CHANGED
@@ -1,15 +1,33 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pennylane (0.1.0)
4
+ pennylane (1.0.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ activesupport (7.1.3.2)
10
+ base64
11
+ bigdecimal
12
+ concurrent-ruby (~> 1.0, >= 1.0.2)
13
+ connection_pool (>= 2.2.5)
14
+ drb
15
+ i18n (>= 1.6, < 2)
16
+ minitest (>= 5.1)
17
+ mutex_m
18
+ tzinfo (~> 2.0)
9
19
  ast (2.4.2)
20
+ base64 (0.2.0)
21
+ bigdecimal (3.1.7)
22
+ concurrent-ruby (1.2.3)
23
+ connection_pool (2.4.1)
24
+ drb (2.2.1)
25
+ i18n (1.14.4)
26
+ concurrent-ruby (~> 1.0)
10
27
  json (2.7.2)
11
28
  language_server-protocol (3.17.0.3)
12
29
  minitest (5.22.3)
30
+ mutex_m (0.2.0)
13
31
  parallel (1.24.0)
14
32
  parser (3.3.0.5)
15
33
  ast (~> 2.4.1)
@@ -36,6 +54,8 @@ GEM
36
54
  ruby-progressbar (1.13.0)
37
55
  test-unit (3.6.2)
38
56
  power_assert
57
+ tzinfo (2.0.6)
58
+ concurrent-ruby (~> 1.0)
39
59
  unicode-display_width (2.5.0)
40
60
  vcr (6.2.0)
41
61
 
@@ -43,6 +63,7 @@ PLATFORMS
43
63
  arm64-darwin-22
44
64
 
45
65
  DEPENDENCIES
66
+ activesupport (~> 7.1)
46
67
  minitest (~> 5.22)
47
68
  pennylane!
48
69
  rake (~> 13.0)
data/README.md CHANGED
@@ -32,12 +32,177 @@ Pennylane.api_key = 'x0fd....'
32
32
 
33
33
  # list customers
34
34
  Pennylane::Customer.list
35
+ ```
36
+
37
+ ### Customers ([API Doc](https://pennylane.readme.io/reference/customers-post-1))
35
38
 
39
+ ```ruby
36
40
  # filter and paginate customers
37
41
  Pennylane::Customer.list(filter: [{field: 'name', operator: 'eq', value: 'Apple'}], page: 2)
38
42
 
39
43
  # Retrieve single customer
40
44
  Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
45
+
46
+ # Update a customer
47
+ cus = Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
48
+ cus.update(name: 'Apple Inc')
49
+
50
+ # Create a customer
51
+ Pennylane::Customer.create customer_type: 'company', name: 'Tesla', address: '4 rue du faubourg', postal_code: '75008', city: 'Paris', country_alpha2: 'FR'
52
+ ```
53
+
54
+ ### CustomerInvoices ([API Doc](https://pennylane.readme.io/reference/customer_invoices-import-1))
55
+
56
+ ```ruby
57
+ # Create a customer invoice
58
+ Pennylane::CustomerInvoice.create(
59
+ create_customer: true,
60
+ create_products: true,
61
+ invoice: {
62
+ date: '2021-01-01',
63
+ deadline: '2021-01-31',
64
+ customer: {
65
+ name: 'Tesla',
66
+ customer_type: 'company',
67
+ address: '4 rue du faubourg',
68
+ postal_code: '75001',
69
+ city: 'Paris',
70
+ country_alpha2: 'FR',
71
+ emails: ['stephane@tesla.com'] },
72
+ line_items: [
73
+ {
74
+ description: 'Consulting',
75
+ quantity: 1,
76
+ unit_price: 1000
77
+ }
78
+ ]
79
+ }
80
+ )
81
+
82
+ # List customer invoices
83
+ Pennylane::CustomerInvoice.list
84
+
85
+ # Retrieve a customer invoice
86
+ invoice = Pennylane::CustomerInvoice.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
87
+
88
+ # Finalize a customer invoice
89
+ invoice.finalize
90
+
91
+ # Send a customer invoice
92
+ invoice.send_by_email
93
+
94
+ # Mark a customer invoice as paid
95
+ invoice.mark_as_paid
96
+
97
+ # Link an invoice and a credit note
98
+ credit_note = Pennylane::CustomerInvoice.retrieve('some-credit-note-id')
99
+ Pennylane::CustomerInvoice.links(invoice.quote_group_uuid, credit_note.quote_group_uuid)
100
+
101
+ # Import a customer invoice
102
+ Pennylane::CustomerInvoice.import(file: Util.file(File.expand_path('../fixtures/files/invoice.pdf', __FILE__)),
103
+ create_customer: true,
104
+ invoice: { date: Date.today, deadline: Date.today >> 1,
105
+ customer: {
106
+ name: 'Tesla',
107
+ customer_type: 'company',
108
+ address: '4 rue du faubourg',
109
+ postal_code: '75001',
110
+ city: 'Paris',
111
+ country_alpha2: 'FR',
112
+ emails: ['stephane@tesla.com'] },
113
+ line_items: [
114
+ {
115
+ description: 'Consulting',
116
+ quantity: 1,
117
+ unit_price: 1000
118
+ }
119
+ ]
120
+ }
121
+ )
122
+ ```
123
+ ### Suppliers ([API Doc](https://pennylane.readme.io/reference/suppliers-post))
124
+
125
+ ```ruby
126
+ # Create a supplier
127
+ Pennylane::Supplier.create(name: 'Apple Inc', address: '4 rue du faubourg', postal_code: '75008', city: 'Paris', country_alpha2: 'FR')
128
+
129
+ # Retrieve a supplier
130
+ Pennylane::Supplier.retrieve('supplier_id')
131
+
132
+ # List all suppliers
133
+ Pennylane::Supplier.list
134
+ ```
135
+
136
+ ### Products ([API Doc](https://pennylane.readme.io/reference/products-post-1))
137
+
138
+ ```ruby
139
+ # Create a product
140
+ Pennylane::Product.create(label: 'Macbook Pro', unit: 'piece', price: 2_999, vat_rate: 'FR_200', currency: 'EUR')
141
+
142
+ # List all products
143
+ Pennylane::Product.list
144
+
145
+ # Retrieve a product
146
+ product = Pennylane::Product.retrieve('product_id')
147
+
148
+ # Update a product
149
+ product.update(label: 'Macbook Pro 2021')
150
+ ```
151
+
152
+ ### Categories ([API Doc](https://pennylane.readme.io/reference/tags-get))
153
+
154
+ ```ruby
155
+ # Create a category
156
+ Pennylane::Category.create(name: 'IT')
157
+
158
+ # Retrieve a category
159
+ Pennylane::Category.retrieve('category_id')
160
+
161
+ # List all categories
162
+ Pennylane::Category.list
163
+
164
+ # Update a category
165
+ category = Pennylane::Category.retrieve('category_id')
166
+ category.update(name: 'IT Services')
167
+ ```
168
+ ### CategoryGroups ([API Doc](https://pennylane.readme.io/reference/tag-groups-get))
169
+ ```ruby
170
+ # List all category groups
171
+ Pennylane::CategoryGroup.list
172
+ ```
173
+
174
+ ### Per-request api key
175
+ For apps that need to use multiple keys during the lifetime of a process. it's also possible to set a per-request key:
176
+ ```ruby
177
+ require "pennylane"
178
+
179
+ Pennylane::Customer.list(
180
+ {},
181
+ {
182
+ api_key: 'x1fa....'
183
+ }
184
+ )
185
+
186
+ Pennylane::Customer.retrieve(
187
+ '38a1f19a-256d-4692-a8fe-0a16403f59ff',
188
+ {
189
+ api_key: 'x1fa....'
190
+ }
191
+ )
192
+
193
+ ```
194
+ ### Accessing resource properties
195
+
196
+ Both indexer and accessors can be used to retrieve values of resource properties.
197
+
198
+ ```ruby
199
+ customer = Pennylane::Customer.retrieve('customer_id')
200
+ puts customer['name']
201
+ puts customer.name
202
+
203
+ # NOTE: To do this the gem will try to guess the key of the resource.
204
+ # Otherwise we will have to do Pennylane::Customer.retrieve('customer_id').customer.name
205
+ # We rely on `method_missing` to do Pennylane::Customer.retrieve('customer_id').name
41
206
  ```
42
207
 
43
208
  ## Test mode
@@ -46,9 +211,30 @@ Pennylane provide a [test environment](https://help.pennylane.com/fr/articles/18
46
211
 
47
212
  ## Development
48
213
 
49
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test-unit` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
214
+ ```bash
215
+ bundle install
216
+ bundle exec rake test
217
+ ```
218
+
219
+ Resources implemented so far :
220
+ ### CUSTOMER INVOICING
221
+
222
+ - Customer Invoices ✅
223
+ - Estimates 🚧
224
+ - Billing Subscriptions 🚧
225
+
226
+ ### REFERENTIALS
227
+
228
+ - Customers ✅
229
+ - Suppliers ✅
230
+ - Categories ✅
231
+ - CategoryGroups ✅
232
+ - Products ✅
233
+ - Plan Items 🚧
234
+ - Enums 🚧
50
235
 
51
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
236
+ ### SUPPLIER INVOICING
237
+ - Supplier Invoices 🚧
52
238
 
53
239
  ## Contributing
54
240
 
@@ -3,7 +3,7 @@ module Pennylane
3
3
  BASE_URI = 'app.pennylane.com/api/external'.freeze
4
4
  VERSION = 'v1'.freeze
5
5
 
6
- attr_accessor :key, :version
6
+ attr_accessor :version
7
7
 
8
8
  def initialize(key, version: 'v1')
9
9
  @key = key
@@ -21,7 +21,7 @@ module Pennylane
21
21
  end
22
22
 
23
23
 
24
- def authorization
24
+ def authorization(key)
25
25
  "Bearer #{key}"
26
26
  end
27
27
  def http
@@ -34,10 +34,9 @@ module Pennylane
34
34
  Net::HTTP.const_get(method.to_s.capitalize)
35
35
  end
36
36
 
37
- def request method, path, params:, opts: {}
38
- req = initialize_request(method, path, params[:query]).tap do |req|
39
- req.body = params[:body].to_json if params[:body] && method != :get
40
- # req.query = params if !params.empty? && method == :get
37
+ def request method, path, params: {}, opts: {}
38
+ req = initialize_request(method, path, params[:query], opts).tap do |req|
39
+ req.body = params[:body].to_json if params[:body]
41
40
  end
42
41
 
43
42
  http.request(req).tap do |resp|
@@ -62,10 +61,11 @@ module Pennylane
62
61
  def should_handle_as_error?(code)
63
62
  code.to_i >= 400
64
63
  end
65
- def initialize_request method=nil, path=nil, params={}
64
+
65
+ def initialize_request method=nil, path=nil, params={}, opts={}
66
66
  klass(method).new(url(path, params)).tap do |request|
67
67
  request["content-type"] = 'application/json'
68
- request["authorization"] = authorization
68
+ request["authorization"] = authorization(opts.fetch(:api_key, @key))
69
69
  end
70
70
  end
71
71
 
@@ -1,10 +1,7 @@
1
1
  module Pennylane
2
2
  class Object
3
3
  include Enumerable
4
-
5
- def initialize(id = nil)
6
- @id = id
7
- end
4
+ attr_reader :id
8
5
 
9
6
  def initialize_from_response(response, params = {}, opts = {})
10
7
  values = Util.symbolize_names(response)
@@ -19,7 +16,7 @@ module Pennylane
19
16
  end
20
17
 
21
18
  def self.build_from(response, params = {}, opts = {})
22
- new(response['id']).initialize_from_response(response, params, opts)
19
+ new.initialize_from_response(response, params, opts)
23
20
  end
24
21
 
25
22
  def self.objects
@@ -89,9 +86,7 @@ module Pennylane
89
86
  when Pennylane::Object
90
87
  obj.class.build_from(
91
88
  deep_copy(obj.instance_variable_get(:@values)),
92
- obj.instance_variable_get(:@opts).select do |k, _v|
93
- Util::OPTS_COPYABLE.include?(k)
94
- end
89
+ obj.instance_variable_get(:@opts)
95
90
  )
96
91
  else
97
92
  obj
@@ -1,6 +1,7 @@
1
1
  module Pennylane
2
2
  module Resources
3
3
  class Base < Pennylane::Object
4
+
4
5
  class << self
5
6
 
6
7
  def object_name
@@ -11,40 +12,74 @@ module Pennylane
11
12
  "#{object_name}s"
12
13
  end
13
14
 
14
- def request_pennylane_object(method:, path:, params: {}, opts: {}, usage: [])
15
+ def request_pennylane_object(method:, path:, params: {}, opts: {}, usage: [], with: {})
15
16
  resp, opts = execute_resource_request(method, path, params, opts, usage)
16
- Util.convert_to_pennylane_object(Util.normalize_response(resp), params, opts)
17
+ if resp.empty?
18
+ {}
19
+ else
20
+ Util.convert_to_pennylane_object(Util.normalize_response(resp, with), params, opts)
21
+ end
17
22
  end
18
23
 
19
24
  def execute_resource_request(method, path, params = {}, opts = {}, usage = [])
20
- api_key = opts.delete(:api_key) || Pennylane.api_key
21
-
22
-
23
25
  resp = client.request(
24
26
  method,
25
27
  path,
26
28
  params: params,
27
29
  opts: opts
28
30
  )
29
-
30
- [JSON.parse(resp.read_body), opts]
31
+ [JSON.parse(resp.read_body || "{}"), opts] # in case body is nil ew return an empty hash
31
32
  end
32
33
 
33
34
  def client
34
- @client ||= Pennylane::Client.new(Pennylane.api_key)
35
+ @client ||= {}
36
+ @client[Pennylane.api_key] ||= Pennylane::Client.new(Pennylane.api_key)
35
37
  end
38
+
39
+ def normalize_filters(filters)
40
+ filters[:filter] = filters[:filter].to_json if filters[:filter]
41
+ filters
42
+ end
43
+ end
44
+
45
+ # object happens to be nil when the object is in a list
46
+ def id
47
+ object.source_id
48
+ rescue
49
+ super
50
+ end
51
+
52
+ # object happens to be nil when the object is the nested object
53
+ def [](k)
54
+ (object && object[k.to_sym]) || @values[k.to_sym]
55
+ end
56
+
57
+ #
58
+ def object
59
+ @values[self.class.object_name.to_sym]
60
+ end
61
+
62
+ # def inspect
63
+ # id_string = respond_to?(:id) && !id.nil? ? " id=#{id}" : ""
64
+ # "#<#{self.class}:0x#{object_id.to_s(16)}#{id_string}> JSON: " +
65
+ # JSON.pretty_generate(object.instance_variable_get(:@values) || @values)
66
+ # end
67
+
68
+ def update(attributes)
69
+ resp, opts = self.class.request_pennylane_object(method: :put, path: "/#{self.class.object_name_plural}/#{id}", params: { body: { self.class.object_name => attributes } })
70
+ @values = resp.instance_variable_get :@values
71
+ self
36
72
  end
37
73
 
38
74
  # So we can call directly method on the object rather than going through his key
39
75
  # Pennylane::Customer.retrieve('any-id').name == Pennylane::Customer.retrieve('any-id').customer.name
40
76
  def method_missing(method_name, *args, &block)
41
- obj = @values[self.class.object_name.to_sym]
42
- raise NoMethodError, "undefined method `#{method_name}` for #{self.class}" unless obj
43
- obj.send(method_name, *args, &block)
77
+ raise NoMethodError, "undefined method `#{method_name}` for #{self.class}" unless object
78
+ object.send(method_name, *args, &block)
44
79
  end
45
80
 
46
81
  def respond_to_missing?(method_name, include_private = false)
47
- @values[self.class.object_name.to_sym].respond_to?(method_name) || super
82
+ object.respond_to?(method_name) || super
48
83
  end
49
84
 
50
85
  end
@@ -0,0 +1,27 @@
1
+ module Pennylane
2
+ class Category < Resources::Base
3
+
4
+ class << self
5
+
6
+ # override the object_name_plural method otherwise it will return 'categorys'
7
+ def object_name_plural
8
+ 'categories'
9
+ end
10
+
11
+ def list filters = {}, opts = {}
12
+ normalize_filters(filters)
13
+ request_pennylane_object(method: :get, path: "/categories", params: { query: filters }, opts: opts)
14
+ end
15
+
16
+ def retrieve id, opts = {}
17
+ request_pennylane_object(method: :get, path: "/categories/#{id}", params: {}, opts: opts)
18
+ end
19
+
20
+ def create params, opts = {}
21
+ request_pennylane_object(method: :post, path: "/categories", params: { body: { object_name => params } }, opts: opts)
22
+ end
23
+
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Pennylane
2
+ class CategoryGroup < Resources::Base
3
+
4
+ class << self
5
+
6
+ def object_name
7
+ 'category_group'
8
+ end
9
+
10
+ def list filters = {}, opts = {}
11
+ request_pennylane_object(method: :get, path: "/category_groups", params: { query: filters }, opts: opts)
12
+ end
13
+ end
14
+
15
+ end
16
+ end
@@ -4,11 +4,12 @@ module Pennylane
4
4
  class << self
5
5
 
6
6
  def list filters = {}, opts = {}
7
+ normalize_filters(filters)
7
8
  request_pennylane_object(method: :get, path: "/customers", params: { query: filters }, opts: opts)
8
9
  end
9
10
 
10
- def retrieve customer_id, opts = {}
11
- request_pennylane_object(method: :get, path: "/customers/#{customer_id}", params: {}, opts: opts)
11
+ def retrieve id, opts = {}
12
+ request_pennylane_object(method: :get, path: "/customers/#{id}", params: {}, opts: opts)
12
13
  end
13
14
 
14
15
  def create params, opts = {}
@@ -0,0 +1,90 @@
1
+ module Pennylane
2
+ class CustomerInvoice < Resources::Base
3
+
4
+ class << self
5
+
6
+ def object_name
7
+ 'customer_invoice'
8
+ end
9
+
10
+ def list filters = {}, opts = {}
11
+ normalize_filters(filters)
12
+ request_pennylane_object(method: :get, path: "/customer_invoices", params: { query: filters }, opts: opts, with: { invoice: 'customer_invoice' })
13
+ end
14
+
15
+ def retrieve id, opts = {}
16
+ request_pennylane_object(method: :get, path: "/customer_invoices/#{id}", params: {}, opts: opts, with: { invoice: 'customer_invoice' })
17
+ end
18
+
19
+ def create params, opts = {}
20
+ request_pennylane_object(method: :post, path: "/customer_invoices", params: { body: params }, opts: opts, with: { invoice: 'customer_invoice' })
21
+ end
22
+
23
+ def import params, opts={}
24
+ request_pennylane_object(method: :post, path: "/customer_invoices/import", params: { body: params }, opts: opts, with: { invoice: 'customer_invoice' })
25
+ end
26
+
27
+ def links(*quote_group_uuids, opts: {})
28
+ request_pennylane_object(method: :post, path: "/customer_invoices/links", params: { body: { quote_group_uuids: quote_group_uuids } },
29
+ opts: {})
30
+ end
31
+ end
32
+
33
+ # since object name is different from the class name, we need to override the object_name method
34
+ def object
35
+ @values[:invoice]
36
+ end
37
+
38
+ # doesnt have a `source_id` so we override it
39
+ def id
40
+ object.id
41
+ end
42
+
43
+ # API CALLS
44
+
45
+ # since object name is different from the class name, we need to override the method
46
+ def update(attributes)
47
+ resp, opts = self.class.request_pennylane_object(method: :put,
48
+ path: "/customer_invoices/#{id}",
49
+ params: { body: { 'invoice' => attributes } },
50
+ opts: {}, with: { invoice: 'customer_invoice' })
51
+ @values = resp.instance_variable_get :@values
52
+ self
53
+ end
54
+
55
+ def finalize
56
+ request_and_retrieve(method: :put, path: "/customer_invoices/#{id}", action: 'finalize')
57
+ end
58
+
59
+ def mark_as_paid
60
+ resp, opts = self.class.request_pennylane_object(method: :put,
61
+ path: "/customer_invoices/#{id}/mark_as_paid",
62
+ params: {},
63
+ opts: {}, with: { invoice: 'customer_invoice' })
64
+ @values = resp.instance_variable_get :@values
65
+ self
66
+ end
67
+
68
+ def send_by_email
69
+ request_and_retrieve(method: :post, path: "/customer_invoices/#{id}", action: 'send_by_email')
70
+ end
71
+
72
+ private
73
+
74
+ # When API returns an empty body
75
+ # so we need to skip values assignment from the response
76
+ # GET /customer_invoices/:id again to get the updated values
77
+ def request_and_retrieve(method:, path:, action:)
78
+ self.class.request_pennylane_object(method: method,
79
+ path: "#{path}/#{action}",
80
+ params: {},
81
+ opts: {}, with: { invoice: 'customer_invoice' })
82
+ resp, opts = self.class.request_pennylane_object(method: :get,
83
+ path: path,
84
+ params: {},
85
+ opts: {}, with: { invoice: 'customer_invoice' })
86
+ @values = resp.instance_variable_get :@values
87
+ self
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,22 @@
1
+ module Pennylane
2
+ class Product < Resources::Base
3
+
4
+ class << self
5
+
6
+ def list filters = {}, opts = {}
7
+ normalize_filters(filters)
8
+ request_pennylane_object(method: :get, path: "/products", params: { query: filters }, opts: opts)
9
+ end
10
+
11
+ def retrieve id, opts = {}
12
+ request_pennylane_object(method: :get, path: "/products/#{id}", params: {}, opts: opts)
13
+ end
14
+
15
+ def create params, opts = {}
16
+ request_pennylane_object(method: :post, path: "/products", params: { body: { product: params } }, opts: opts)
17
+ end
18
+
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ module Pennylane
2
+ class Supplier < Resources::Base
3
+ class << self
4
+
5
+ def list filters = {}, opts = {}
6
+ normalize_filters(filters)
7
+ request_pennylane_object(method: :get, path: "/suppliers", params: { query: filters }, opts: opts)
8
+ end
9
+
10
+ def retrieve supplier_id, opts = {}
11
+ request_pennylane_object(method: :get, path: "/suppliers/#{supplier_id}", params: {}, opts: opts)
12
+ end
13
+
14
+ def create params, opts = {}
15
+ request_pennylane_object(method: :post, path: "/suppliers", params: { body: { object_name => params } }, opts: opts)
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -20,34 +20,49 @@ module Pennylane
20
20
  # It will add an _object key to each hash in the response
21
21
  # This key will contain the name of the object
22
22
  # It will also add an _object key to the root of the response
23
- def normalize_response(object)
23
+ # with: is used to map the object name to a different class
24
+ # Example : GET /customer_invoices will return a list of invoices
25
+ # {
26
+ # "total_pages": 5,
27
+ # "current_page": 1,
28
+ # "total_invoices": 12,
29
+ # "invoices": []
30
+ # }
31
+ # `invoices` should be `customer_invoice` so we can cast it to the right class CustomerInvoice
32
+ # Since we don't have the ability to change the API response.
33
+ # We can achieve this by calling normalize_response(response, with: {invoice: 'customer_invoice'})
34
+ def normalize_response(object, with={})
24
35
  # puts object.inspect
25
36
  case object
26
37
  when Hash
27
38
  new_hash = {}
28
- new_hash['_object'] = object.has_key?('total_pages') ? 'list' : singularize(object.keys.first)
29
-
39
+ new_hash['_object'] = object.has_key?('total_pages') ? 'list' : (with[singularize(object.keys.first).to_sym] || singularize(object.keys.first))
30
40
  object.each do |key, value|
31
41
  if value.is_a? Array
32
42
  new_hash[key] = value.map do |h|
33
- h['_object'] = singularize(key) if h.is_a? Hash
34
- normalize_response(h)
43
+ h['_object'] = with[singularize(key).to_sym] || singularize(key) if h.is_a? Hash
44
+ normalize_response(h, with)
35
45
  end
36
46
  elsif value.is_a? Hash
37
- value['_object'] = singularize(key)
47
+ value['_object'] = with[singularize(key).to_sym] || singularize(key)
38
48
  end
39
- new_hash[key] = normalize_response(value)
49
+ new_hash[key] = normalize_response(value, with)
40
50
  end
41
51
  new_hash
42
52
  when Array
43
53
  object.map do |value|
44
- normalize_response(value)
54
+ normalize_response(value, with)
45
55
  end
46
56
  else
47
57
  object
48
58
  end
49
59
  end
50
60
 
61
+ def file(file_path)
62
+ file_data = File.open(file_path).read
63
+ Base64.strict_encode64(file_data)
64
+ end
65
+
51
66
  # This method is used to convert the keys of a hash from strings to symbols
52
67
  def symbolize_names(object)
53
68
  case object
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pennylane
4
- VERSION = "0.1.0"
5
- end
4
+ VERSION = "1.0.0"
5
+ end
data/lib/pennylane.rb CHANGED
@@ -22,7 +22,12 @@ module Pennylane
22
22
 
23
23
  API_RESOURCES = {
24
24
  ListObject.object_name => ListObject,
25
- Customer.object_name => Customer
25
+ Category.object_name => Category,
26
+ CategoryGroup.object_name => CategoryGroup,
27
+ Customer.object_name => Customer,
28
+ CustomerInvoice.object_name => CustomerInvoice,
29
+ Product.object_name => Product,
30
+ Supplier.object_name => Supplier
26
31
  }.freeze
27
32
 
28
33
  @config = Pennylane::Configuration.new
data/pennylane.gemspec CHANGED
@@ -34,5 +34,6 @@ Gem::Specification.new do |spec|
34
34
  spec.add_development_dependency "vcr", "~> 6.2"
35
35
  spec.add_development_dependency "test-unit", "~> 3.6"
36
36
  spec.add_development_dependency "minitest", "~> 5.22"
37
+ spec.add_development_dependency "activesupport", "~> 7.1"
37
38
  # guide at: https://bundler.io/guides/creating_gem.html
38
39
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pennylane
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephane Bounmy
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-14 00:00:00.000000000 Z
11
+ date: 2024-04-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: vcr
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '5.22'
55
+ - !ruby/object:Gem::Dependency
56
+ name: activesupport
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '7.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '7.1'
55
69
  description: Pennylane offers integrated financial management and accounting tools
56
70
  for businesses. See https://pennylane.com for details.
57
71
  email:
@@ -74,7 +88,12 @@ files:
74
88
  - lib/pennylane/list_object.rb
75
89
  - lib/pennylane/object.rb
76
90
  - lib/pennylane/resources/base.rb
91
+ - lib/pennylane/resources/category.rb
92
+ - lib/pennylane/resources/category_group.rb
77
93
  - lib/pennylane/resources/customer.rb
94
+ - lib/pennylane/resources/customer_invoice.rb
95
+ - lib/pennylane/resources/product.rb
96
+ - lib/pennylane/resources/supplier.rb
78
97
  - lib/pennylane/util.rb
79
98
  - lib/pennylane/version.rb
80
99
  - pennylane.gemspec