pennylane 0.2.0.pre.alpha → 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: 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: