pennylane 0.2.0.pre.alpha → 1.0.1

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: 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: