cin7_core_api 0.2.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.
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Cin7CoreAPI
6
+ # Only sanitized copies leave the transport. Never mutate a caller's payload.
7
+ class Redactor
8
+ FILTERED = "[FILTERED]"
9
+ SENSITIVE_KEY = /authorization|authentication|password|passwd|secret|token|cookie|apikey|applicationkey|apiauthaccountid|externalusername/i
10
+ GUID = /\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i
11
+ HTTP_URL = %r{\A(https?://)([^/?#]*)([^?#]*)(?:\?([^#]*))?(?:\#(.*))?\z}i
12
+
13
+ def initialize(secrets = [])
14
+ @secrets = secrets.grep(String).reject(&:empty?).uniq.sort_by { |value| -value.length }.map { |value| value.dup.freeze }.freeze
15
+ end
16
+
17
+ def with_sensitive_values(value)
18
+ self.class.new(@secrets + sensitive_values(value))
19
+ end
20
+
21
+ def filter(value)
22
+ case value
23
+ when Hash
24
+ value.each_with_object({}) do |(key, entry), result|
25
+ # Field names are schema, even when a credential happens to match one.
26
+ result[key.to_s] = if sensitive_key?(key) || external_headers?(key)
27
+ FILTERED
28
+ elsif callback_url?(key) && entry.is_a?(String)
29
+ filter_url(entry)
30
+ elsif structured_identifier?(key, entry)
31
+ # Short header values must not corrupt identifiers or schema enums.
32
+ @secrets.include?(entry) ? FILTERED : entry.dup
33
+ else
34
+ filter(entry)
35
+ end
36
+ end
37
+ when Array
38
+ value.map { |entry| filter(entry) }
39
+ when String
40
+ filter_string(value)
41
+ else
42
+ value
43
+ end
44
+ end
45
+
46
+ def inspect
47
+ "#<#{self.class}>"
48
+ end
49
+
50
+ private
51
+
52
+ def sensitive_key?(key)
53
+ normalized = normalized_key(key)
54
+ # AccountID and BankAccountId identify business records, not API access.
55
+ # The authentication header is api-auth-accountid; known credential values
56
+ # are still filtered wherever they occur, including business ID fields.
57
+ normalized != "externalauthorizationtype" && normalized.match?(SENSITIVE_KEY)
58
+ end
59
+
60
+ def normalized_key(key)
61
+ key.to_s.gsub(/[^a-z0-9]/i, "").downcase
62
+ end
63
+
64
+ def external_headers?(key)
65
+ normalized_key(key) == "externalheaders"
66
+ end
67
+
68
+ def callback_url?(key)
69
+ normalized_key(key) == "externalurl"
70
+ end
71
+
72
+ def structured_identifier?(key, value)
73
+ value.is_a?(String) && ((normalized_key(key).end_with?("id") && GUID.match?(value)) ||
74
+ %w[type externalauthorizationtype].include?(normalized_key(key)))
75
+ end
76
+
77
+ def sensitive_values(value)
78
+ case value
79
+ when Hash
80
+ value.flat_map do |key, entry|
81
+ if external_headers?(key)
82
+ Array(entry).flat_map { |header| header.is_a?(Hash) ? strings_in(header["Value"] || header[:Value] || header["value"]) : [] }
83
+ elsif sensitive_key?(key)
84
+ strings_in(entry)
85
+ elsif callback_url?(key) && entry.is_a?(String)
86
+ url_credentials(entry)
87
+ else
88
+ sensitive_values(entry)
89
+ end
90
+ end
91
+ when Array
92
+ value.flat_map { |entry| sensitive_values(entry) }
93
+ else
94
+ []
95
+ end
96
+ end
97
+
98
+ def strings_in(value)
99
+ case value
100
+ when Hash then value.values.flat_map { |entry| strings_in(entry) }
101
+ when Array then value.flat_map { |entry| strings_in(entry) }
102
+ when String then [value, value.sub(/\A(?:Bearer|Basic)\s+/i, "")]
103
+ else []
104
+ end
105
+ end
106
+
107
+ def sensitive_query_key?(key)
108
+ sensitive_key?(key) || %w[accountid username user auth key signature sig].include?(normalized_key(key))
109
+ end
110
+
111
+ def url_credentials(value)
112
+ url = URI.parse(value)
113
+ return [] unless url.is_a?(URI::HTTP) && url.host
114
+
115
+ credentials = url.userinfo.to_s.split(":", 2).flat_map { |part| [part, URI::DEFAULT_PARSER.unescape(part)] }
116
+ [url.query, url.fragment].compact.each do |query|
117
+ query.split(/[&;]/).each do |part|
118
+ key, separator, entry = part.partition("=")
119
+ if !separator.empty? && sensitive_query_key?(URI.decode_www_form_component(key))
120
+ credentials.concat([entry, URI.decode_www_form_component(entry)])
121
+ end
122
+ end
123
+ end
124
+ strings_in(credentials)
125
+ rescue URI::InvalidURIError, ArgumentError
126
+ []
127
+ end
128
+
129
+ def filter_url(value)
130
+ return FILTERED if @secrets.include?(value)
131
+
132
+ url = URI.parse(value)
133
+ return FILTERED unless url.is_a?(URI::HTTP) && url.host
134
+
135
+ # Keep spelling, escaping, parameter order and delimiters intact. Rebuilding
136
+ # a URI or substring-filtering it can change callback ownership comparisons.
137
+ scheme, authority, path, query, fragment = HTTP_URL.match(value).captures
138
+ authority = filter_authority(authority, url.host)
139
+ path = path.split("/", -1).map { |part| filter_url_component(part) }.join("/")
140
+ result = "#{scheme}#{authority}#{path}"
141
+ result += "?#{filter_query(query)}" unless query.nil?
142
+ result += "##{filter_query(fragment)}" unless fragment.nil?
143
+ result
144
+ rescue URI::InvalidURIError, ArgumentError
145
+ FILTERED
146
+ end
147
+
148
+ def filter_authority(authority, host)
149
+ _, separator, host_port = authority.rpartition("@")
150
+ filtered_host = filter_url_component(host)
151
+ if filtered_host != FILTERED
152
+ filtered_host = host.split(".", -1).map { |part| filter_url_component(part) }.join(".")
153
+ end
154
+ filtered = host_port.sub(host) { filtered_host }
155
+ separator.empty? ? filtered : "#{FILTERED}@#{filtered}"
156
+ end
157
+
158
+ def filter_query(query)
159
+ query.split(/([&;])/, -1).map do |part|
160
+ key, separator, entry = part.partition("=")
161
+ if separator.empty?
162
+ filter_url_component(key, form: true)
163
+ else
164
+ filtered = sensitive_query_key?(URI.decode_www_form_component(key)) ? FILTERED : filter_url_component(entry, form: true)
165
+ "#{filter_url_component(key, form: true)}=#{filtered}"
166
+ end
167
+ end.join
168
+ end
169
+
170
+ def filter_url_component(value, form: false)
171
+ decoded = form ? URI.decode_www_form_component(value) : URI::DEFAULT_PARSER.unescape(value)
172
+ (@secrets.include?(value) || @secrets.include?(decoded)) ? FILTERED : value
173
+ end
174
+
175
+ def filter_string(value)
176
+ filtered = @secrets.reduce(value.dup) { |text, secret| text.gsub(secret, FILTERED) }
177
+ filtered = filtered.gsub(%r{(https?://)[^\s/@]+:[^\s/@]+@}i, "\\1#{FILTERED}@")
178
+ # Non-JSON error bodies may echo credentials as header or form text.
179
+ filtered = filtered.gsub(/\b(Bearer|Basic)\s+[^\s,"'<>]+/i, "\\1 #{FILTERED}")
180
+ filtered.gsub(/([\w-]*(?:password|passwd|secret|token|authorization|api[-_]?key|application[-_]?key|account[-_]?id)[\w-]*["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;&<>]+)/i, "\\1#{FILTERED}")
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Accounts < Base
6
+ PARAMETERS = {
7
+ page: "Page",
8
+ limit: "Limit",
9
+ code: "Code",
10
+ name: "Name",
11
+ type: "Type",
12
+ status: "Status"
13
+ }.freeze
14
+
15
+ def list(**filters)
16
+ get("ref/account", params: filters, parameter_map: PARAMETERS)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class BankAccounts < Base
6
+ PARAMETERS = {
7
+ page: "Page",
8
+ limit: "Limit",
9
+ id: "ID",
10
+ name: "Name",
11
+ bank: "Bank"
12
+ }.freeze
13
+
14
+ def list(**filters)
15
+ get("ref/account/bank", params: filters, parameter_map: PARAMETERS)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Base
6
+ attr_reader :connection
7
+
8
+ def initialize(connection)
9
+ @connection = connection
10
+ end
11
+
12
+ def inspect
13
+ "#<#{self.class}>"
14
+ end
15
+
16
+ private
17
+
18
+ def get(path, params: {}, parameter_map: {})
19
+ connection.get(path, params: map_parameters(params, parameter_map))
20
+ end
21
+
22
+ def validate_payload!(payload, *identifiers)
23
+ unless payload.is_a?(Hash) && payload.keys.all? { |key| key.is_a?(String) }
24
+ raise ArgumentError, "payload must be a Hash with Cin7 JSON field names as String keys"
25
+ end
26
+
27
+ identifiers.each { |name| require_parameter!(name, payload[name]) }
28
+ end
29
+
30
+ def map_parameters(params, parameter_map)
31
+ unknown = params.keys - parameter_map.keys
32
+ unless unknown.empty?
33
+ raise ArgumentError, "Unknown parameters: #{unknown.sort.join(", ")}"
34
+ end
35
+
36
+ params.each_with_object({}) do |(key, value), mapped|
37
+ mapped[parameter_map.fetch(key)] = value unless value.nil?
38
+ end
39
+ end
40
+
41
+ def require_parameter!(name, value)
42
+ missing = value.nil? || (value.is_a?(String) ? value.strip.empty? : value.respond_to?(:empty?) && value.empty?)
43
+ return unless missing
44
+
45
+ raise ArgumentError, "#{name} is required"
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class CreditNotes < Base
6
+ LIST_PARAMETERS = {
7
+ page: "Page",
8
+ limit: "Limit",
9
+ search: "Search",
10
+ created_since: "CreatedSince",
11
+ updated_since: "UpdatedSince",
12
+ updated_until: "UpdatedUntil",
13
+ credit_note_status: "CreditNoteStatus",
14
+ status: "Status"
15
+ }.freeze
16
+
17
+ FOR_SALE_PARAMETERS = {
18
+ sale_id: "SaleID",
19
+ combine_additional_charges: "CombineAdditionalCharges",
20
+ include_product_info: "IncludeProductInfo",
21
+ include_payment_info: "IncludePaymentInfo"
22
+ }.freeze
23
+
24
+ def list(**filters)
25
+ get("saleCreditNoteList", params: filters, parameter_map: LIST_PARAMETERS)
26
+ end
27
+
28
+ def for_sale(sale_id:, **options)
29
+ require_parameter!(:sale_id, sale_id)
30
+ get("sale/creditnote", params: options.merge(sale_id: sale_id), parameter_map: FOR_SALE_PARAMETERS)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class CustomerCredits < Base
6
+ PARAMETERS = {
7
+ page: "Page",
8
+ limit: "Limit",
9
+ customer_id: "CustomerID",
10
+ show_used_credits: "ShowUsedCredits"
11
+ }.freeze
12
+
13
+ def list(**filters)
14
+ get("ref/customer/credits", params: filters, parameter_map: PARAMETERS)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Customers < Base
6
+ PARAMETERS = {
7
+ page: "Page",
8
+ limit: "Limit",
9
+ id: "ID",
10
+ name: "Name",
11
+ modified_since: "ModifiedSince",
12
+ include_deprecated: "IncludeDeprecated",
13
+ include_product_prices: "IncludeProductPrices",
14
+ contact_filter: "ContactFilter"
15
+ }.freeze
16
+
17
+ def list(**filters)
18
+ get("customer", params: filters, parameter_map: PARAMETERS)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Invoices < Base
6
+ PARAMETERS = {
7
+ sale_id: "SaleID",
8
+ combine_additional_charges: "CombineAdditionalCharges",
9
+ include_product_info: "IncludeProductInfo"
10
+ }.freeze
11
+
12
+ def for_sale(sale_id:, **options)
13
+ require_parameter!(:sale_id, sale_id)
14
+ get("sale/invoice", params: options.merge(sale_id: sale_id), parameter_map: PARAMETERS)
15
+ end
16
+
17
+ def create(payload:)
18
+ validate_payload!(payload, "SaleID", "TaskID")
19
+ connection.post("sale/invoice", payload: payload)
20
+ end
21
+
22
+ def update(payload:)
23
+ validate_payload!(payload, "SaleID", "TaskID")
24
+ connection.put("sale/invoice", payload: payload)
25
+ end
26
+
27
+ def undo(task_id:)
28
+ require_parameter!(:task_id, task_id)
29
+ connection.delete("sale/invoice", params: {"TaskID" => task_id, "Void" => false})
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Journals < Base
6
+ PARAMETERS = {
7
+ page: "Page", limit: "Limit", task_id: "TaskID", status: "Status", search: "Search"
8
+ }.freeze
9
+
10
+ def list(**filters)
11
+ get("journal", params: filters, parameter_map: PARAMETERS)
12
+ end
13
+
14
+ def retrieve(task_id:)
15
+ require_parameter!(:task_id, task_id)
16
+ list(task_id: task_id)
17
+ end
18
+
19
+ def create(payload:)
20
+ validate_payload!(payload)
21
+ connection.post("journal", payload: payload)
22
+ end
23
+
24
+ def update(payload:)
25
+ validate_payload!(payload, "TaskID")
26
+ connection.put("journal", payload: payload)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Locations < Base
6
+ PARAMETERS = {
7
+ page: "Page",
8
+ limit: "Limit",
9
+ id: "ID",
10
+ deprecated: "Deprecated",
11
+ name: "Name"
12
+ }.freeze
13
+
14
+ def list(**filters)
15
+ get("ref/location", params: filters, parameter_map: PARAMETERS)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class ManualJournals < Base
6
+ def for_sale(sale_id:)
7
+ require_parameter!(:sale_id, sale_id)
8
+ get("sale/manualJournal", params: {sale_id: sale_id}, parameter_map: {sale_id: "SaleID"})
9
+ end
10
+
11
+ def create(payload:)
12
+ validate_payload!(payload, "SaleID")
13
+ connection.post("sale/manualJournal", payload: payload)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Me < Base
6
+ def retrieve
7
+ get("me")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Orders < Base
6
+ PARAMETERS = {
7
+ sale_id: "SaleID",
8
+ combine_additional_charges: "CombineAdditionalCharges",
9
+ include_product_info: "IncludeProductInfo"
10
+ }.freeze
11
+
12
+ def for_sale(sale_id:, **options)
13
+ require_parameter!(:sale_id, sale_id)
14
+ get("sale/order", params: options.merge(sale_id: sale_id), parameter_map: PARAMETERS)
15
+ end
16
+
17
+ def create(payload:)
18
+ validate_payload!(payload, "SaleID")
19
+ connection.post("sale/order", payload: payload)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Payments < Base
6
+ PARAMETERS = {sale_id: "SaleID"}.freeze
7
+
8
+ def for_sale(sale_id:)
9
+ require_parameter!(:sale_id, sale_id)
10
+ get("sale/payment", params: {sale_id: sale_id}, parameter_map: PARAMETERS)
11
+ end
12
+
13
+ def create(payload:)
14
+ validate_payload!(payload, "TaskID")
15
+ connection.post("sale/payment", payload: payload)
16
+ end
17
+
18
+ def update(payload:)
19
+ validate_payload!(payload, "ID")
20
+ connection.put("sale/payment", payload: payload)
21
+ end
22
+
23
+ def delete(id:)
24
+ require_parameter!(:id, id)
25
+ connection.delete("sale/payment", params: {"ID" => id})
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Sales < Base
6
+ LIST_PARAMETERS = {
7
+ page: "Page",
8
+ limit: "Limit",
9
+ search: "Search",
10
+ created_since: "CreatedSince",
11
+ updated_since: "UpdatedSince",
12
+ updated_until: "UpdatedUntil",
13
+ ship_by: "ShipBy",
14
+ quote_status: "QuoteStatus",
15
+ order_status: "OrderStatus",
16
+ combined_pick_status: "CombinedPickStatus",
17
+ combined_pack_status: "CombinedPackStatus",
18
+ combined_shipping_status: "CombinedShippingStatus",
19
+ combined_invoice_status: "CombinedInvoiceStatus",
20
+ credit_note_status: "CreditNoteStatus",
21
+ external_id: "ExternalID",
22
+ status: "Status",
23
+ ready_for_shipping: "ReadyForShipping",
24
+ order_location_id: "OrderLocationID"
25
+ }.freeze
26
+
27
+ RETRIEVE_PARAMETERS = {
28
+ id: "ID",
29
+ combine_additional_charges: "CombineAdditionalCharges",
30
+ hide_inventory_movements: "HideInventoryMovements",
31
+ include_transactions: "IncludeTransactions",
32
+ country_format: "CountryFormat"
33
+ }.freeze
34
+
35
+ def list(**filters)
36
+ get("saleList", params: filters, parameter_map: LIST_PARAMETERS)
37
+ end
38
+
39
+ def retrieve(id:, **options)
40
+ require_parameter!(:id, id)
41
+ get("sale", params: options.merge(id: id), parameter_map: RETRIEVE_PARAMETERS)
42
+ end
43
+
44
+ def update(payload:)
45
+ validate_payload!(payload, "ID")
46
+ connection.put("sale", payload: payload)
47
+ end
48
+
49
+ def undo(id:)
50
+ require_parameter!(:id, id)
51
+ connection.delete("sale", params: {"ID" => id, "Void" => false})
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class TaxRules < Base
6
+ PARAMETERS = {
7
+ page: "Page", limit: "Limit", id: "ID", name: "Name", is_active: "IsActive",
8
+ is_tax_for_sale: "IsTaxForSale", is_tax_for_purchase: "IsTaxForPurchase", account: "Account"
9
+ }.freeze
10
+
11
+ def list(**filters)
12
+ get("ref/tax", params: filters, parameter_map: PARAMETERS)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ module Resources
5
+ class Webhooks < Base
6
+ def list
7
+ get("webhooks")
8
+ end
9
+
10
+ def create(payload:)
11
+ validate_payload!(payload)
12
+ connection.post("webhooks", payload: payload)
13
+ end
14
+
15
+ def update(payload:)
16
+ validate_payload!(payload, "ID")
17
+ connection.put("webhooks", payload: payload)
18
+ end
19
+
20
+ def delete(id:)
21
+ require_parameter!(:id, id)
22
+ connection.delete("webhooks", params: {"ID" => id})
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ class Response
5
+ attr_reader :status, :headers, :body
6
+
7
+ def initialize(status:, headers:, body:)
8
+ @status = status
9
+ @headers = headers.freeze
10
+ @body = body
11
+ freeze
12
+ end
13
+
14
+ def success?
15
+ (200..299).cover?(status)
16
+ end
17
+
18
+ def no_content?
19
+ status == 204 || body.nil?
20
+ end
21
+
22
+ def inspect
23
+ "#<#{self.class} status=#{status}>"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cin7CoreAPI
4
+ VERSION = "0.2.0"
5
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cin7_core_api/version"
4
+ require_relative "cin7_core_api/errors"
5
+ require_relative "cin7_core_api/response"
6
+ require_relative "cin7_core_api/redactor"
7
+ require_relative "cin7_core_api/connection"
8
+ require_relative "cin7_core_api/resources/base"
9
+ require_relative "cin7_core_api/resources/me"
10
+ require_relative "cin7_core_api/resources/sales"
11
+ require_relative "cin7_core_api/resources/credit_notes"
12
+ require_relative "cin7_core_api/resources/payments"
13
+ require_relative "cin7_core_api/resources/locations"
14
+ require_relative "cin7_core_api/resources/accounts"
15
+ require_relative "cin7_core_api/resources/bank_accounts"
16
+ require_relative "cin7_core_api/resources/customers"
17
+ require_relative "cin7_core_api/resources/customer_credits"
18
+ require_relative "cin7_core_api/resources/orders"
19
+ require_relative "cin7_core_api/resources/invoices"
20
+ require_relative "cin7_core_api/resources/manual_journals"
21
+ require_relative "cin7_core_api/resources/journals"
22
+ require_relative "cin7_core_api/resources/tax_rules"
23
+ require_relative "cin7_core_api/resources/webhooks"
24
+ require_relative "cin7_core_api/client"
25
+
26
+ module Cin7CoreAPI
27
+ end