pennylane 0.2.0.pre.alpha → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a60c1035dbad731707f86c3e97610c740d286cb8e6f70b7ac30709bbe29a0f0
4
- data.tar.gz: b3fd35e53857c0d8df547bb361d4ae69bd1b73a482b7552232ab4cfcce5e8926
3
+ metadata.gz: 1b079bfa7d777a0adb18de2cb8b02c4b5b3f7a20ef14ee3be35ccc56c4fc5714
4
+ data.tar.gz: 417d611044b5329eb04040ffaab0c1e78e609b6dc5613deeeac35994aab88012
5
5
  SHA512:
6
- metadata.gz: 903f576a32e59f954c68c255ce0aeacf6a820315f233f9825b2d35036cff3ab0928c6ac9fb0e84daa059da40dfde4f1829c5dff61ad58a57a406a3483e69b9e8
7
- data.tar.gz: 72ed99e03c1d31ff1a871ebf0d335110c9a0a38a9acc9d8e14ecaf27a50731afef55ce6d1c77d89cbb0f95558eba121938ba8da7b87827e6c2416be10def3c5d
6
+ metadata.gz: 8d1e5ec1c7aacb11921d8d7490c8bfcfd626fb185a74c6199b1ac9b75e4b4cae03dc8f66be2e70effada0cc21a1d563b1aef205beca5d906bc522637d76b429c
7
+ data.tar.gz: fbec4d19969a69390de3903fcb7a1e9e372fcdc4d6bb9b3f735ac09e3a31b7bbea14963e0e35d0c0815d42140385e168e21f21e764caae06d6fdbea08ebec21c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
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
+
3
15
  ## [0.2.0-alpha] - 2024-04-20
4
16
 
5
17
  - Support resources
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.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -32,7 +32,11 @@ 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
 
@@ -43,9 +47,131 @@ Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
43
47
  cus = Pennylane::Customer.retrieve('38a1f19a-256d-4692-a8fe-0a16403f59ff')
44
48
  cus.update(name: 'Apple Inc')
45
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
46
134
  ```
47
135
 
48
- ### Per-request api key [TODO]
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
49
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:
50
176
  ```ruby
51
177
  require "pennylane"
@@ -64,6 +190,19 @@ Pennylane::Customer.retrieve(
64
190
  }
65
191
  )
66
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
67
206
  ```
68
207
 
69
208
  ## Test mode
@@ -72,14 +211,15 @@ Pennylane provide a [test environment](https://help.pennylane.com/fr/articles/18
72
211
 
73
212
  ## Development
74
213
 
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).
214
+ ```bash
215
+ bundle install
216
+ bundle exec rake test
217
+ ```
78
218
 
79
219
  Resources implemented so far :
80
220
  ### CUSTOMER INVOICING
81
221
 
82
- - Customer Invoices 🚧
222
+ - Customer Invoices
83
223
  - Estimates 🚧
84
224
  - Billing Subscriptions 🚧
85
225
 
@@ -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
 
@@ -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
@@ -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
@@ -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.0"
5
5
  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.2.0.pre.alpha
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-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
@@ -116,9 +116,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
116
116
  version: 2.6.0
117
117
  required_rubygems_version: !ruby/object:Gem::Requirement
118
118
  requirements:
119
- - - ">"
119
+ - - ">="
120
120
  - !ruby/object:Gem::Version
121
- version: 1.3.1
121
+ version: '0'
122
122
  requirements: []
123
123
  rubygems_version: 3.4.10
124
124
  signing_key: