pennylane 0.2.0.pre.alpha → 1.0.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a60c1035dbad731707f86c3e97610c740d286cb8e6f70b7ac30709bbe29a0f0
4
- data.tar.gz: b3fd35e53857c0d8df547bb361d4ae69bd1b73a482b7552232ab4cfcce5e8926
3
+ metadata.gz: d39d39c57d464e5c95acf6535e0e80508e9836fcbd8dd3afe49373283d965438
4
+ data.tar.gz: f4339e0fda690f67625aefb006f3964fdeefd8f125d9ff68ef26408cbb30caef
5
5
  SHA512:
6
- metadata.gz: 903f576a32e59f954c68c255ce0aeacf6a820315f233f9825b2d35036cff3ab0928c6ac9fb0e84daa059da40dfde4f1829c5dff61ad58a57a406a3483e69b9e8
7
- data.tar.gz: 72ed99e03c1d31ff1a871ebf0d335110c9a0a38a9acc9d8e14ecaf27a50731afef55ce6d1c77d89cbb0f95558eba121938ba8da7b87827e6c2416be10def3c5d
6
+ metadata.gz: 745d583286f8eeb20da9acb324ae2b7b0e2ca9611537c4aa340d674ad8c380428ee337e03c70c823940e61748520c7b5a267fa09a34493af4a6234a7ebded501
7
+ data.tar.gz: 4e671e89e28b95717eb5a800c4463896349eadf5756a1ebb11a69a645391881bb51f247960d9f6bd6e118a3386542796cf6646defaf7baafaa36a4d6152a8fc3
data/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
- ## [Unreleased]
1
+ ## [1.0.1] - 2024-04-29
2
+ - Fixed gem initialization issue (resources/*.rb) from a Rails app.
3
+
4
+ ## [1.0.0] - 2024-04-29
5
+
6
+ - Support CustomerInvoice:
7
+ - `finalize`
8
+ - `mark_as_paid`
9
+ - `send_by_email`
10
+ - `links`
11
+ - `import`
12
+ - Support per request `api_key` e.g `Pennylane::Customer.retrieve('cus_id', {api_key: 'x0fd....'})`
13
+ - 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.
14
+ - Added resource properties access with `#[]` e.g `cus['name']`
2
15
 
3
16
  ## [0.2.0-alpha] - 2024-04-20
4
17
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- pennylane (0.2.0.pre.alpha)
4
+ pennylane (1.0.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -16,9 +16,22 @@ Install the gem and add to the application's Gemfile by executing:
16
16
  $ bundle add pennylane
17
17
 
18
18
  If bundler is not being used to manage dependencies, install the gem by executing:
19
+ ```ruby
20
+ # Add to gemfile
21
+ gem 'pennylane'
22
+ ```
23
+
24
+ For Rails app :
25
+ ```ruby
26
+ # Create initializers/pennylane.rb
27
+ Pennylane.api_key = Rails.application.credentials.dig(:pennylane, :api_key)
19
28
 
20
- $ gem install pennylane
29
+ # Add credentials to config/credentials.yml.enc
30
+ $ EDITOR=vim bin/rails credentials:edit
21
31
 
32
+ pennylane:
33
+ api_key: 'x0fd....'
34
+ ```
22
35
  ## Requirements
23
36
  Ruby 2.3+.
24
37
 
@@ -32,7 +45,11 @@ Pennylane.api_key = 'x0fd....'
32
45
 
33
46
  # list customers
34
47
  Pennylane::Customer.list
48
+ ```
35
49
 
50
+ ### Customers ([API Doc](https://pennylane.readme.io/reference/customers-post-1))
51
+
52
+ ```ruby
36
53
  # filter and paginate customers
37
54
  Pennylane::Customer.list(filter: [{field: 'name', operator: 'eq', value: 'Apple'}], page: 2)
38
55
 
@@ -43,9 +60,131 @@ Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
43
60
  cus = Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
44
61
  cus.update(name: 'Apple Inc')
45
62
 
63
+ # Create a customer
64
+ Pennylane::Customer.create customer_type: 'company', name: 'Tesla', address: '4 rue du faubourg', postal_code: '75008', city: 'Paris', country_alpha2: 'FR'
65
+ ```
66
+
67
+ ### CustomerInvoices ([API Doc](https://pennylane.readme.io/reference/customer_invoices-import-1))
68
+
69
+ ```ruby
70
+ # Create a customer invoice
71
+ Pennylane::CustomerInvoice.create(
72
+ create_customer: true,
73
+ create_products: true,
74
+ invoice: {
75
+ date: '2021-01-01',
76
+ deadline: '2021-01-31',
77
+ customer: {
78
+ name: 'Tesla',
79
+ customer_type: 'company',
80
+ address: '4 rue du faubourg',
81
+ postal_code: '75001',
82
+ city: 'Paris',
83
+ country_alpha2: 'FR',
84
+ emails: ['stephane@tesla.com'] },
85
+ line_items: [
86
+ {
87
+ description: 'Consulting',
88
+ quantity: 1,
89
+ unit_price: 1000
90
+ }
91
+ ]
92
+ }
93
+ )
94
+
95
+ # List customer invoices
96
+ Pennylane::CustomerInvoice.list
97
+
98
+ # Retrieve a customer invoice
99
+ invoice = Pennylane::CustomerInvoice.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
100
+
101
+ # Finalize a customer invoice
102
+ invoice.finalize
103
+
104
+ # Send a customer invoice
105
+ invoice.send_by_email
106
+
107
+ # Mark a customer invoice as paid
108
+ invoice.mark_as_paid
109
+
110
+ # Link an invoice and a credit note
111
+ credit_note = Pennylane::CustomerInvoice.retrieve('some-credit-note-id')
112
+ Pennylane::CustomerInvoice.links(invoice.quote_group_uuid, credit_note.quote_group_uuid)
113
+
114
+ # Import a customer invoice
115
+ Pennylane::CustomerInvoice.import(file: Util.file(File.expand_path('../fixtures/files/invoice.pdf', __FILE__)),
116
+ create_customer: true,
117
+ invoice: { date: Date.today, deadline: Date.today >> 1,
118
+ customer: {
119
+ name: 'Tesla',
120
+ customer_type: 'company',
121
+ address: '4 rue du faubourg',
122
+ postal_code: '75001',
123
+ city: 'Paris',
124
+ country_alpha2: 'FR',
125
+ emails: ['stephane@tesla.com'] },
126
+ line_items: [
127
+ {
128
+ description: 'Consulting',
129
+ quantity: 1,
130
+ unit_price: 1000
131
+ }
132
+ ]
133
+ }
134
+ )
135
+ ```
136
+ ### Suppliers ([API Doc](https://pennylane.readme.io/reference/suppliers-post))
137
+
138
+ ```ruby
139
+ # Create a supplier
140
+ Pennylane::Supplier.create(name: 'Apple Inc', address: '4 rue du faubourg', postal_code: '75008', city: 'Paris', country_alpha2: 'FR')
141
+
142
+ # Retrieve a supplier
143
+ Pennylane::Supplier.retrieve('supplier_id')
144
+
145
+ # List all suppliers
146
+ Pennylane::Supplier.list
147
+ ```
148
+
149
+ ### Products ([API Doc](https://pennylane.readme.io/reference/products-post-1))
150
+
151
+ ```ruby
152
+ # Create a product
153
+ Pennylane::Product.create(label: 'Macbook Pro', unit: 'piece', price: 2_999, vat_rate: 'FR_200', currency: 'EUR')
154
+
155
+ # List all products
156
+ Pennylane::Product.list
157
+
158
+ # Retrieve a product
159
+ product = Pennylane::Product.retrieve('product_id')
160
+
161
+ # Update a product
162
+ product.update(label: 'Macbook Pro 2021')
163
+ ```
164
+
165
+ ### Categories ([API Doc](https://pennylane.readme.io/reference/tags-get))
166
+
167
+ ```ruby
168
+ # Create a category
169
+ Pennylane::Category.create(name: 'IT')
170
+
171
+ # Retrieve a category
172
+ Pennylane::Category.retrieve('category_id')
173
+
174
+ # List all categories
175
+ Pennylane::Category.list
176
+
177
+ # Update a category
178
+ category = Pennylane::Category.retrieve('category_id')
179
+ category.update(name: 'IT Services')
180
+ ```
181
+ ### CategoryGroups ([API Doc](https://pennylane.readme.io/reference/tag-groups-get))
182
+ ```ruby
183
+ # List all category groups
184
+ Pennylane::CategoryGroup.list
46
185
  ```
47
186
 
48
- ### Per-request api key [TODO]
187
+ ### Per-request api key
49
188
  For apps that need to use multiple keys during the lifetime of a process. it's also possible to set a per-request key:
50
189
  ```ruby
51
190
  require "pennylane"
@@ -64,6 +203,19 @@ Pennylane::Customer.retrieve(
64
203
  }
65
204
  )
66
205
 
206
+ ```
207
+ ### Accessing resource properties
208
+
209
+ Both indexer and accessors can be used to retrieve values of resource properties.
210
+
211
+ ```ruby
212
+ customer = Pennylane::Customer.retrieve('customer_id')
213
+ puts customer['name']
214
+ puts customer.name
215
+
216
+ # NOTE: To do this the gem will try to guess the key of the resource.
217
+ # Otherwise we will have to do Pennylane::Customer.retrieve('customer_id').customer.name
218
+ # We rely on `method_missing` to do Pennylane::Customer.retrieve('customer_id').name
67
219
  ```
68
220
 
69
221
  ## Test mode
@@ -72,14 +224,15 @@ Pennylane provide a [test environment](https://help.pennylane.com/fr/articles/18
72
224
 
73
225
  ## Development
74
226
 
75
- 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.
76
-
77
- 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).
227
+ ```bash
228
+ bundle install
229
+ bundle exec rake test
230
+ ```
78
231
 
79
232
  Resources implemented so far :
80
233
  ### CUSTOMER INVOICING
81
234
 
82
- - Customer Invoices 🚧
235
+ - Customer Invoices
83
236
  - Estimates 🚧
84
237
  - Billing Subscriptions 🚧
85
238
 
@@ -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,8 +34,8 @@ 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|
37
+ def request method, path, params: {}, opts: {}
38
+ req = initialize_request(method, path, params[:query], opts).tap do |req|
39
39
  req.body = params[:body].to_json if params[:body]
40
40
  end
41
41
 
@@ -61,10 +61,11 @@ module Pennylane
61
61
  def should_handle_as_error?(code)
62
62
  code.to_i >= 400
63
63
  end
64
- def initialize_request method=nil, path=nil, params={}
64
+
65
+ def initialize_request method=nil, path=nil, params={}, opts={}
65
66
  klass(method).new(url(path, params)).tap do |request|
66
67
  request["content-type"] = 'application/json'
67
- request["authorization"] = authorization
68
+ request["authorization"] = authorization(opts.fetch(:api_key, @key))
68
69
  end
69
70
  end
70
71
 
@@ -19,7 +19,7 @@ module Pennylane
19
19
  end
20
20
 
21
21
  def key_for(resp)
22
- resp.keys.find { |k| Pennylane::API_RESOURCES.keys.include?(Util.singularize(k.to_s)) } || resp.keys.find { |k| resp[k].is_a? Array }
22
+ resp.keys.find { |k| Pennylane::ObjectTypes.object_names_to_classes.keys.include?(Util.singularize(k.to_s)) } || resp.keys.find { |k| resp[k].is_a? Array }
23
23
  end
24
24
 
25
25
  end
@@ -86,9 +86,7 @@ module Pennylane
86
86
  when Pennylane::Object
87
87
  obj.class.build_from(
88
88
  deep_copy(obj.instance_variable_get(:@values)),
89
- obj.instance_variable_get(:@opts).select do |k, _v|
90
- Util::OPTS_COPYABLE.include?(k)
91
- end
89
+ obj.instance_variable_get(:@opts)
92
90
  )
93
91
  else
94
92
  obj
@@ -0,0 +1,15 @@
1
+ module Pennylane
2
+ module ObjectTypes
3
+ def self.object_names_to_classes
4
+ {
5
+ ListObject.object_name => ListObject,
6
+ Category.object_name => Category,
7
+ CategoryGroup.object_name => CategoryGroup,
8
+ Customer.object_name => Customer,
9
+ CustomerInvoice.object_name => CustomerInvoice,
10
+ Product.object_name => Product,
11
+ Supplier.object_name => Supplier
12
+ }.freeze
13
+ end
14
+ end
15
+ end
@@ -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
@@ -13,25 +14,26 @@ module Pennylane
13
14
 
14
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, with), 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
36
38
 
37
39
  def normalize_filters(filters)
@@ -47,6 +49,11 @@ module Pennylane
47
49
  super
48
50
  end
49
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
+
50
57
  #
51
58
  def object
52
59
  @values[self.class.object_name.to_sym]
@@ -20,26 +20,71 @@ module Pennylane
20
20
  request_pennylane_object(method: :post, path: "/customer_invoices", params: { body: params }, opts: opts, with: { invoice: 'customer_invoice' })
21
21
  end
22
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]
23
36
  end
24
37
 
38
+ # doesnt have a `source_id` so we override it
39
+ def id
40
+ object.id
41
+ end
42
+
43
+ # API CALLS
44
+
25
45
  # since object name is different from the class name, we need to override the method
26
46
  def update(attributes)
27
- resp, opts = self.class.request_pennylane_object(method: :put, path: "/#{self.class.object_name_plural}/#{id}",
47
+ resp, opts = self.class.request_pennylane_object(method: :put,
48
+ path: "/customer_invoices/#{id}",
28
49
  params: { body: { 'invoice' => attributes } },
29
50
  opts: {}, with: { invoice: 'customer_invoice' })
30
51
  @values = resp.instance_variable_get :@values
31
52
  self
32
53
  end
33
54
 
34
- # since object name is different from the class name, we need to override the object_name method
35
- def object
36
- @values[:invoice]
55
+ def finalize
56
+ request_and_retrieve(method: :put, path: "/customer_invoices/#{id}", action: 'finalize')
37
57
  end
38
58
 
39
- # doesnt have a `source_id` so we override it
40
- def id
41
- object.id
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
42
66
  end
43
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
44
89
  end
45
90
  end
@@ -58,6 +58,11 @@ module Pennylane
58
58
  end
59
59
  end
60
60
 
61
+ def file(file_path)
62
+ file_data = File.open(file_path).read
63
+ Base64.strict_encode64(file_data)
64
+ end
65
+
61
66
  # This method is used to convert the keys of a hash from strings to symbols
62
67
  def symbolize_names(object)
63
68
  case object
@@ -80,7 +85,7 @@ module Pennylane
80
85
  end
81
86
 
82
87
  def klass_for(object)
83
- Pennylane::API_RESOURCES[singularize(object)] || Pennylane::Object
88
+ Pennylane::ObjectTypes.object_names_to_classes[singularize(object)] || Pennylane::Object
84
89
  rescue
85
90
  Pennylane::Object
86
91
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pennylane
4
- VERSION = "0.2.0-alpha"
4
+ VERSION = "1.0.1"
5
5
  end
data/lib/pennylane.rb CHANGED
@@ -11,8 +11,9 @@ require 'forwardable'
11
11
  require 'uri'
12
12
  require 'net/http'
13
13
 
14
- Dir["./lib/pennylane/resources/*.rb"].each {|file| require file }
14
+ Dir[File.join(__dir__, 'pennylane/resources/*.rb')].each {|file| require file }
15
15
 
16
+ require 'pennylane/object_types'
16
17
 
17
18
  module Pennylane
18
19
  class Error < StandardError; end
@@ -20,16 +21,6 @@ module Pennylane
20
21
  class ConfigurationError < Error; end
21
22
  class NotFoundError < Error; end
22
23
 
23
- API_RESOURCES = {
24
- ListObject.object_name => ListObject,
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
31
- }.freeze
32
-
33
24
  @config = Pennylane::Configuration.new
34
25
  # So we can have a module Pennylane that can be a class as well Pennylane.api_key = '1234'
35
26
  class << self
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.2.0.pre.alpha
4
+ version: 1.0.1
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-20 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
@@ -87,6 +87,7 @@ files:
87
87
  - lib/pennylane/configuration.rb
88
88
  - lib/pennylane/list_object.rb
89
89
  - lib/pennylane/object.rb
90
+ - lib/pennylane/object_types.rb
90
91
  - lib/pennylane/resources/base.rb
91
92
  - lib/pennylane/resources/category.rb
92
93
  - lib/pennylane/resources/category_group.rb
@@ -116,9 +117,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
117
  version: 2.6.0
117
118
  required_rubygems_version: !ruby/object:Gem::Requirement
118
119
  requirements:
119
- - - ">"
120
+ - - ">="
120
121
  - !ruby/object:Gem::Version
121
- version: 1.3.1
122
+ version: '0'
122
123
  requirements: []
123
124
  rubygems_version: 3.4.10
124
125
  signing_key: