smartbill-sdk 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 +7 -0
- data/AGENTS.md +211 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +21 -0
- data/README.md +265 -0
- data/Rakefile +22 -0
- data/Steepfile +6 -0
- data/docs/openapi.json +9741 -0
- data/examples/create_estimate_sync.rb +53 -0
- data/examples/create_invoice_eur_product_in_ron.rb +100 -0
- data/examples/create_invoice_sync.rb +57 -0
- data/examples/create_payment_sync.rb +36 -0
- data/examples/fiscal_receipt_sync.rb +50 -0
- data/examples/invoice_lifecycle_sync.rb +43 -0
- data/examples/list_series_sync.rb +27 -0
- data/examples/send_email_sync.rb +40 -0
- data/examples/taxes_and_stocks_sync.rb +48 -0
- data/lib/smartbill/sdk/api_error.rb +27 -0
- data/lib/smartbill/sdk/auth_error.rb +8 -0
- data/lib/smartbill/sdk/client.rb +68 -0
- data/lib/smartbill/sdk/contracts/base.rb +33 -0
- data/lib/smartbill/sdk/contracts/email_contract.rb +29 -0
- data/lib/smartbill/sdk/contracts/estimate_contract.rb +18 -0
- data/lib/smartbill/sdk/contracts/invoice_contract.rb +29 -0
- data/lib/smartbill/sdk/contracts/invoice_payment_contract.rb +18 -0
- data/lib/smartbill/sdk/contracts/invoice_ref_contract.rb +19 -0
- data/lib/smartbill/sdk/contracts/payment_contract.rb +37 -0
- data/lib/smartbill/sdk/contracts/storno_contract.rb +17 -0
- data/lib/smartbill/sdk/contracts.rb +19 -0
- data/lib/smartbill/sdk/error.rb +8 -0
- data/lib/smartbill/sdk/models/base_response.rb +16 -0
- data/lib/smartbill/sdk/models/client.rb +26 -0
- data/lib/smartbill/sdk/models/discount_type.rb +13 -0
- data/lib/smartbill/sdk/models/document_type.rb +13 -0
- data/lib/smartbill/sdk/models/email_document.rb +23 -0
- data/lib/smartbill/sdk/models/email_response.rb +12 -0
- data/lib/smartbill/sdk/models/email_status.rb +13 -0
- data/lib/smartbill/sdk/models/estimate.rb +32 -0
- data/lib/smartbill/sdk/models/fiscal_receipt_response.rb +16 -0
- data/lib/smartbill/sdk/models/invoice.rb +44 -0
- data/lib/smartbill/sdk/models/invoice_create_response.rb +10 -0
- data/lib/smartbill/sdk/models/invoice_payment.rb +15 -0
- data/lib/smartbill/sdk/models/invoice_ref.rb +24 -0
- data/lib/smartbill/sdk/models/payment.rb +49 -0
- data/lib/smartbill/sdk/models/payment_status_response.rb +19 -0
- data/lib/smartbill/sdk/models/payment_type.rb +22 -0
- data/lib/smartbill/sdk/models/product.rb +38 -0
- data/lib/smartbill/sdk/models/proforma_invoices_response.rb +13 -0
- data/lib/smartbill/sdk/models/series.rb +14 -0
- data/lib/smartbill/sdk/models/series_list_response.rb +14 -0
- data/lib/smartbill/sdk/models/stock_list.rb +13 -0
- data/lib/smartbill/sdk/models/stock_product.rb +15 -0
- data/lib/smartbill/sdk/models/stock_warehouse.rb +13 -0
- data/lib/smartbill/sdk/models/stocks_response.rb +14 -0
- data/lib/smartbill/sdk/models/storno_request.rb +17 -0
- data/lib/smartbill/sdk/models/storno_response.rb +14 -0
- data/lib/smartbill/sdk/models/struct.rb +102 -0
- data/lib/smartbill/sdk/models/tax.rb +14 -0
- data/lib/smartbill/sdk/models/taxes_response.rb +14 -0
- data/lib/smartbill/sdk/models.rb +17 -0
- data/lib/smartbill/sdk/net_http_adapter.rb +62 -0
- data/lib/smartbill/sdk/rate_limit_error.rb +9 -0
- data/lib/smartbill/sdk/response.rb +12 -0
- data/lib/smartbill/sdk/services/base_service.rb +39 -0
- data/lib/smartbill/sdk/services/configuration_service.rb +29 -0
- data/lib/smartbill/sdk/services/email_service.rb +18 -0
- data/lib/smartbill/sdk/services/estimates_service.rb +60 -0
- data/lib/smartbill/sdk/services/invoices_service.rb +69 -0
- data/lib/smartbill/sdk/services/payments_service.rb +50 -0
- data/lib/smartbill/sdk/services/stocks_service.rb +21 -0
- data/lib/smartbill/sdk/services.rb +30 -0
- data/lib/smartbill/sdk/transport/rate_limiter.rb +49 -0
- data/lib/smartbill/sdk/transport/request.rb +17 -0
- data/lib/smartbill/sdk/transport.rb +162 -0
- data/lib/smartbill/sdk/transport_error.rb +8 -0
- data/lib/smartbill/sdk/types.rb +22 -0
- data/lib/smartbill/sdk/validation_error.rb +8 -0
- data/lib/smartbill/sdk/version.rb +7 -0
- data/lib/smartbill/sdk.rb +31 -0
- data/sig/smartbill/sdk.rbs +661 -0
- data/skills/README.md +35 -0
- data/skills/smartbill-email/SKILL.md +120 -0
- data/skills/smartbill-invoices/SKILL.md +178 -0
- data/skills/smartbill-payments/SKILL.md +194 -0
- metadata +214 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Models
|
|
6
|
+
# Response for +POST /invoice/reverse+.
|
|
7
|
+
class StornoResponse < BaseResponse
|
|
8
|
+
attribute :document_url, Types::Strict::String.optional.default(nil)
|
|
9
|
+
attribute :document_id, Types::StrOrInt.optional.default(nil)
|
|
10
|
+
attribute :document_view_url, Types::Strict::String.optional.default(nil)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-struct"
|
|
4
|
+
|
|
5
|
+
module Smartbill
|
|
6
|
+
module Sdk
|
|
7
|
+
module Models
|
|
8
|
+
# Base class for every SmartBill request/response model.
|
|
9
|
+
#
|
|
10
|
+
# A thin adapter over `Dry::Struct` that gives the SDK:
|
|
11
|
+
#
|
|
12
|
+
# * snake_case Ruby attributes aliased to camelCase JSON keys — both
|
|
13
|
+
# `company_vat_code` and `"companyVatCode"` are accepted on input,
|
|
14
|
+
# and {#to_h} emits camelCase keys (matching the SmartBill API);
|
|
15
|
+
# * type coercion of scalars and nested structs / arrays of structs;
|
|
16
|
+
# * required-attribute presence (a missing required attribute raises
|
|
17
|
+
# {ValidationError}, translated from `Dry::Struct::Error`);
|
|
18
|
+
# * permissive parsing — unknown input keys are ignored so new API
|
|
19
|
+
# fields don't break parsing;
|
|
20
|
+
# * {#to_attributes} returning the snake_case hash (with nils) used by
|
|
21
|
+
# the dry-validation contracts.
|
|
22
|
+
#
|
|
23
|
+
# Subclasses declare attributes with the dry-struct `attribute` DSL:
|
|
24
|
+
#
|
|
25
|
+
# class MyThing < Struct
|
|
26
|
+
# attribute :company_vat_code, Types::Strict::String
|
|
27
|
+
# attribute :client, Client.optional.default(nil)
|
|
28
|
+
# attribute :products, Types::Array.of(Product).default([].freeze)
|
|
29
|
+
# end
|
|
30
|
+
class Struct < Dry::Struct
|
|
31
|
+
# Accept snake_case and camelCase input keys (String or Symbol),
|
|
32
|
+
# normalising to the snake_case Symbol keys dry-struct expects.
|
|
33
|
+
transform_keys { |key| INFLECTOR.underscore(key.to_s).to_sym }
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
# Construct a struct, translating dry-struct type errors (e.g. a
|
|
37
|
+
# missing required attribute) into the SDK's {ValidationError}.
|
|
38
|
+
def new(attrs = Dry::Core::Constants::EMPTY_HASH, &)
|
|
39
|
+
super
|
|
40
|
+
rescue Dry::Struct::Error => e
|
|
41
|
+
raise ValidationError, e.message
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Serialize to a camelCase-keyed Hash matching the SmartBill API,
|
|
46
|
+
# omitting nil values by default. Nested structs and arrays of
|
|
47
|
+
# structs are serialized recursively.
|
|
48
|
+
#
|
|
49
|
+
# @param exclude_none [Boolean] skip nil values (default true).
|
|
50
|
+
def to_h(exclude_none: true)
|
|
51
|
+
self.class.schema.keys.each_with_object({}) do |key, hash|
|
|
52
|
+
value = public_send(key.name)
|
|
53
|
+
next if exclude_none && value.nil?
|
|
54
|
+
|
|
55
|
+
hash[camelize_key(key.name)] = serialize_value(value, exclude_none)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_json(*)
|
|
60
|
+
to_h.to_json(*)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Return a snake_case Symbol-keyed Hash (including nils) reflecting
|
|
64
|
+
# the Ruby attributes, with nested structs and arrays of structs
|
|
65
|
+
# recursively converted to hashes. Used as input to the validation
|
|
66
|
+
# contracts (which operate on hashes, not struct instances).
|
|
67
|
+
def to_attributes
|
|
68
|
+
self.class.schema.keys.to_h do |key|
|
|
69
|
+
[key.name, attribute_value(public_send(key.name))]
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def attribute_value(value)
|
|
76
|
+
if value.is_a?(Struct)
|
|
77
|
+
value.to_attributes
|
|
78
|
+
elsif value.is_a?(Array)
|
|
79
|
+
value.map { |element| attribute_value(element) }
|
|
80
|
+
else
|
|
81
|
+
value
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def camelize_key(name)
|
|
86
|
+
camelized = INFLECTOR.camelize(name.to_s)
|
|
87
|
+
"#{camelized[0].downcase}#{camelized[1..]}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def serialize_value(value, exclude_none)
|
|
91
|
+
if value.is_a?(Struct)
|
|
92
|
+
value.to_h(exclude_none: exclude_none)
|
|
93
|
+
elsif value.is_a?(Array)
|
|
94
|
+
value.map { |element| serialize_value(element, exclude_none) }
|
|
95
|
+
else
|
|
96
|
+
value
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Models
|
|
6
|
+
# A VAT rate entry from +GET /tax+.
|
|
7
|
+
class Tax < Struct
|
|
8
|
+
attribute :name, Types::Strict::String.optional.default(nil)
|
|
9
|
+
attribute :percentage, Types::Coercible::Float.optional.default(nil)
|
|
10
|
+
attribute :id, Types::StrOrInt.optional.default(nil)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Models
|
|
6
|
+
# Parsed response of +GET /tax+.
|
|
7
|
+
class TaxesResponse < Struct
|
|
8
|
+
attribute :error_text, Types::Strict::String.optional.default(nil)
|
|
9
|
+
attribute :message, Types::Strict::String.optional.default(nil)
|
|
10
|
+
attribute :taxes, Types::Array.of(Tax).default([].freeze)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-inflector"
|
|
4
|
+
|
|
5
|
+
module Smartbill
|
|
6
|
+
module Sdk
|
|
7
|
+
# Typed request/response models for the SmartBill Cloud REST API.
|
|
8
|
+
#
|
|
9
|
+
# Each model is a {Models::Struct} (a `Dry::Struct` subclass) and lives
|
|
10
|
+
# in its own file (e.g. `models/invoice.rb` defines `Invoice`),
|
|
11
|
+
# autoloaded by Zeitwerk.
|
|
12
|
+
module Models
|
|
13
|
+
# Shared inflector for snake_case ⇄ camelCase key mapping.
|
|
14
|
+
INFLECTOR = Dry::Inflector.new
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
module Smartbill
|
|
7
|
+
module Sdk
|
|
8
|
+
# Default HTTP adapter backed by stdlib +Net::HTTP+.
|
|
9
|
+
#
|
|
10
|
+
# The SDK talks to the adapter through a single method, {#call}, which
|
|
11
|
+
# receives a {Transport::Request} and returns a {Response}. This keeps
|
|
12
|
+
# the transport logic decoupled from the HTTP library and makes it
|
|
13
|
+
# trivial to swap in another adapter (or stub one in tests).
|
|
14
|
+
class NetHttpAdapter
|
|
15
|
+
METHOD_CLASSES = {
|
|
16
|
+
"GET" => Net::HTTP::Get,
|
|
17
|
+
"POST" => Net::HTTP::Post,
|
|
18
|
+
"PUT" => Net::HTTP::Put,
|
|
19
|
+
"DELETE" => Net::HTTP::Delete,
|
|
20
|
+
"PATCH" => Net::HTTP::Patch
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
def initialize(timeout: 30)
|
|
24
|
+
@timeout = timeout
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(req)
|
|
28
|
+
uri = build_uri(req)
|
|
29
|
+
request = build_request(req, uri)
|
|
30
|
+
response = perform(uri, request)
|
|
31
|
+
Response.new(status: response.code.to_i, body: response.body,
|
|
32
|
+
content_type: response["content-type"])
|
|
33
|
+
rescue ArgumentError, Net::HTTPError => e
|
|
34
|
+
raise TransportError, "Transport error: #{e.message}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_uri(req)
|
|
40
|
+
uri = URI(req.url)
|
|
41
|
+
uri.query = URI.encode_www_form(req.query) if req.query && !req.query.empty?
|
|
42
|
+
uri
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def build_request(req, uri)
|
|
46
|
+
klass = METHOD_CLASSES[req.http_method] ||
|
|
47
|
+
raise(TransportError, "Unsupported HTTP method: #{req.http_method}")
|
|
48
|
+
request = klass.new(uri.request_uri)
|
|
49
|
+
req.headers.each { |key, value| request[key] = value }
|
|
50
|
+
request.body = req.body if req.body
|
|
51
|
+
request
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def perform(uri, request)
|
|
55
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
56
|
+
read_timeout: @timeout, open_timeout: @timeout) do |http|
|
|
57
|
+
http.request(request)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
# A simple HTTP response value object returned by adapters.
|
|
6
|
+
#
|
|
7
|
+
# @!attribute [r] status HTTP status code (Integer).
|
|
8
|
+
# @!attribute [r] body Raw response body (String, possibly binary).
|
|
9
|
+
# @!attribute [r] content_type Value of the +Content-Type+ header.
|
|
10
|
+
Response = Struct.new(:status, :body, :content_type, keyword_init: true)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Services
|
|
6
|
+
# Base class for all services.
|
|
7
|
+
class BaseService
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# Run +contract+.validate! on +struct+, raising {ValidationError}
|
|
15
|
+
# on failure. No-op contract-wise if +contract+ is nil.
|
|
16
|
+
def validate(struct, contract)
|
|
17
|
+
contract.validate!(struct)
|
|
18
|
+
struct
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def build_request(...)
|
|
22
|
+
Transport.build_request(...)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute(request, binary: false)
|
|
26
|
+
@client.execute(request, binary: binary)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def dump(model, envelope_key: nil)
|
|
30
|
+
Services.dump_model(model, envelope_key: envelope_key)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse(payload, model_class)
|
|
34
|
+
Services.parse(payload, model_class)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Services
|
|
6
|
+
# +/tax+ and +/series+ endpoints.
|
|
7
|
+
#
|
|
8
|
+
# Note: on a {Client}, both +client.taxes+ and +client.series+ are the
|
|
9
|
+
# same +ConfigurationService+ instance.
|
|
10
|
+
class ConfigurationService < BaseService
|
|
11
|
+
def taxes(cif)
|
|
12
|
+
parse(execute(build_request(
|
|
13
|
+
method: "GET", base_url: @client.base_url, path: "tax",
|
|
14
|
+
params: { "cif" => cif }, auth_header: @client.auth_header
|
|
15
|
+
)), Models::TaxesResponse)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def series(cif, type: nil)
|
|
19
|
+
params = { "cif" => cif }
|
|
20
|
+
params["type"] = type unless type.nil?
|
|
21
|
+
parse(execute(build_request(
|
|
22
|
+
method: "GET", base_url: @client.base_url, path: "series",
|
|
23
|
+
params: params, auth_header: @client.auth_header
|
|
24
|
+
)), Models::SeriesListResponse)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Services
|
|
6
|
+
# +/document/send+ endpoint.
|
|
7
|
+
class EmailService < BaseService
|
|
8
|
+
def send(email)
|
|
9
|
+
validate(email, Contracts::EmailContract)
|
|
10
|
+
parse(execute(build_request(
|
|
11
|
+
method: "POST", base_url: @client.base_url, path: "document/send",
|
|
12
|
+
json_body: dump(email), auth_header: @client.auth_header
|
|
13
|
+
)), Models::EmailResponse)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Services
|
|
6
|
+
# +/estimate+ endpoints.
|
|
7
|
+
class EstimatesService < BaseService
|
|
8
|
+
def create(estimate)
|
|
9
|
+
validate(estimate, Contracts::EstimateContract)
|
|
10
|
+
parse(execute(build_request(
|
|
11
|
+
method: "POST", base_url: @client.base_url, path: "estimate",
|
|
12
|
+
json_body: dump(estimate), auth_header: @client.auth_header
|
|
13
|
+
)), Models::InvoiceCreateResponse)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete(cif, series_name, number)
|
|
17
|
+
parse(execute(build_request(
|
|
18
|
+
method: "DELETE", base_url: @client.base_url, path: "estimate",
|
|
19
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
20
|
+
auth_header: @client.auth_header
|
|
21
|
+
)), Models::BaseResponse)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def cancel(cif, series_name, number)
|
|
25
|
+
cancel_restore("cancel", cif, series_name, number)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def restore(cif, series_name, number)
|
|
29
|
+
cancel_restore("restore", cif, series_name, number)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def pdf(cif, series_name, number)
|
|
33
|
+
execute(build_request(
|
|
34
|
+
method: "GET", base_url: @client.base_url, path: "estimate/pdf",
|
|
35
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
36
|
+
accept: "application/octet-stream", auth_header: @client.auth_header
|
|
37
|
+
), binary: true)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def invoices_status(cif, series_name, number)
|
|
41
|
+
parse(execute(build_request(
|
|
42
|
+
method: "GET", base_url: @client.base_url, path: "estimate/invoices",
|
|
43
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
44
|
+
auth_header: @client.auth_header
|
|
45
|
+
)), Models::ProformaInvoicesResponse)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def cancel_restore(op, cif, series_name, number)
|
|
51
|
+
parse(execute(build_request(
|
|
52
|
+
method: "PUT", base_url: @client.base_url, path: "estimate/#{op}",
|
|
53
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
54
|
+
auth_header: @client.auth_header
|
|
55
|
+
)), Models::BaseResponse)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Services
|
|
6
|
+
# +/invoice+ endpoints.
|
|
7
|
+
class InvoicesService < BaseService
|
|
8
|
+
def create(invoice)
|
|
9
|
+
validate(invoice, Contracts::InvoiceContract)
|
|
10
|
+
parse(execute(build_request(
|
|
11
|
+
method: "POST", base_url: @client.base_url, path: "invoice",
|
|
12
|
+
json_body: dump(invoice), auth_header: @client.auth_header
|
|
13
|
+
)), Models::InvoiceCreateResponse)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def delete(cif, series_name, number)
|
|
17
|
+
parse(execute(build_request(
|
|
18
|
+
method: "DELETE", base_url: @client.base_url, path: "invoice",
|
|
19
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
20
|
+
auth_header: @client.auth_header
|
|
21
|
+
)), Models::BaseResponse)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def reverse(storno)
|
|
25
|
+
validate(storno, Contracts::StornoContract)
|
|
26
|
+
parse(execute(build_request(
|
|
27
|
+
method: "POST", base_url: @client.base_url, path: "invoice/reverse",
|
|
28
|
+
json_body: dump(storno), auth_header: @client.auth_header
|
|
29
|
+
)), Models::StornoResponse)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cancel(cif, series_name, number)
|
|
33
|
+
cancel_restore("cancel", cif, series_name, number)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def restore(cif, series_name, number)
|
|
37
|
+
cancel_restore("restore", cif, series_name, number)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def payment_status(cif, series_name, number)
|
|
41
|
+
parse(execute(build_request(
|
|
42
|
+
method: "GET", base_url: @client.base_url, path: "invoice/paymentstatus",
|
|
43
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
44
|
+
auth_header: @client.auth_header
|
|
45
|
+
)), Models::PaymentStatusResponse)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the raw PDF body as a binary String.
|
|
49
|
+
def pdf(cif, series_name, number)
|
|
50
|
+
execute(build_request(
|
|
51
|
+
method: "GET", base_url: @client.base_url, path: "invoice/pdf",
|
|
52
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
53
|
+
accept: "application/octet-stream", auth_header: @client.auth_header
|
|
54
|
+
), binary: true)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def cancel_restore(op, cif, series_name, number)
|
|
60
|
+
parse(execute(build_request(
|
|
61
|
+
method: "PUT", base_url: @client.base_url, path: "invoice/#{op}",
|
|
62
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
63
|
+
auth_header: @client.auth_header
|
|
64
|
+
)), Models::BaseResponse)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Services
|
|
6
|
+
# +/payment+ endpoints.
|
|
7
|
+
class PaymentsService < BaseService
|
|
8
|
+
def create(payment)
|
|
9
|
+
validate(payment, Contracts::PaymentContract)
|
|
10
|
+
parse(execute(build_request(
|
|
11
|
+
method: "POST", base_url: @client.base_url, path: "payment",
|
|
12
|
+
json_body: dump(payment), auth_header: @client.auth_header
|
|
13
|
+
)), Models::FiscalReceiptResponse)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Delete a non-chitanta payment via +DELETE /payment/v2+.
|
|
17
|
+
def delete_other(cif, payment_type:, payment_date: nil, payment_value: nil,
|
|
18
|
+
client_name: nil, client_cif: nil, invoice_series: nil,
|
|
19
|
+
invoice_number: nil)
|
|
20
|
+
params = { "cif" => cif, "paymentType" => payment_type }
|
|
21
|
+
params["paymentDate"] = payment_date unless payment_date.nil?
|
|
22
|
+
params["paymentValue"] = payment_value unless payment_value.nil?
|
|
23
|
+
params["clientName"] = client_name unless client_name.nil?
|
|
24
|
+
params["clientCif"] = client_cif unless client_cif.nil?
|
|
25
|
+
params["invoiceSeries"] = invoice_series unless invoice_series.nil?
|
|
26
|
+
params["invoiceNumber"] = invoice_number unless invoice_number.nil?
|
|
27
|
+
parse(execute(build_request(
|
|
28
|
+
method: "DELETE", base_url: @client.base_url, path: "payment/v2",
|
|
29
|
+
params: params, auth_header: @client.auth_header
|
|
30
|
+
)), Models::BaseResponse)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def delete_chitanta(cif, series_name, number)
|
|
34
|
+
parse(execute(build_request(
|
|
35
|
+
method: "DELETE", base_url: @client.base_url, path: "payment/chitanta",
|
|
36
|
+
params: { "cif" => cif, "seriesName" => series_name, "number" => number },
|
|
37
|
+
auth_header: @client.auth_header
|
|
38
|
+
)), Models::BaseResponse)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def fiscal_receipt_text(cif, id)
|
|
42
|
+
parse(execute(build_request(
|
|
43
|
+
method: "GET", base_url: @client.base_url, path: "payment/text",
|
|
44
|
+
params: { "cif" => cif, "id" => id }, auth_header: @client.auth_header
|
|
45
|
+
)), Models::FiscalReceiptResponse)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Services
|
|
6
|
+
# +/stocks+ endpoint.
|
|
7
|
+
class StocksService < BaseService
|
|
8
|
+
def get(cif, date, warehouse_name: nil, product_name: nil, product_code: nil)
|
|
9
|
+
params = { "cif" => cif, "date" => date }
|
|
10
|
+
params["warehouseName"] = warehouse_name unless warehouse_name.nil?
|
|
11
|
+
params["productName"] = product_name unless product_name.nil?
|
|
12
|
+
params["productCode"] = product_code unless product_code.nil?
|
|
13
|
+
parse(execute(build_request(
|
|
14
|
+
method: "GET", base_url: @client.base_url, path: "stocks",
|
|
15
|
+
params: params, auth_header: @client.auth_header
|
|
16
|
+
)), Models::StocksResponse)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
# Per-resource endpoint logic.
|
|
6
|
+
#
|
|
7
|
+
# Each service builds a {Transport::Request} for an endpoint and parses
|
|
8
|
+
# the response into a model. Services are instantiated by {Client} with a
|
|
9
|
+
# reference to the client (the "executor") which provides +base_url+,
|
|
10
|
+
# +auth_header+ and +#execute+.
|
|
11
|
+
#
|
|
12
|
+
# Each service lives in its own file (e.g. `services/invoices_service.rb`
|
|
13
|
+
# defines `InvoicesService`) and is autoloaded by Zeitwerk.
|
|
14
|
+
module Services
|
|
15
|
+
# Serialize a model, optionally wrapping it in an envelope.
|
|
16
|
+
def self.dump_model(model, envelope_key: nil)
|
|
17
|
+
data = model.to_h
|
|
18
|
+
envelope_key ? { envelope_key => data } : data
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Parse a payload into a model instance.
|
|
22
|
+
def self.parse(payload, model_class)
|
|
23
|
+
return model_class.new if payload.nil?
|
|
24
|
+
return model_class.new(payload) if payload.is_a?(Hash)
|
|
25
|
+
|
|
26
|
+
model_class.new(message: payload.to_s)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Transport
|
|
6
|
+
# Simple token-bucket limiter: +max_calls+ per +window_seconds+.
|
|
7
|
+
#
|
|
8
|
+
# Optionally enabled by clients to preempt the server's 403.
|
|
9
|
+
class RateLimiter
|
|
10
|
+
attr_reader :max_calls, :window_seconds
|
|
11
|
+
|
|
12
|
+
def initialize(max_calls: 30, window_seconds: 10.0)
|
|
13
|
+
@max_calls = max_calls
|
|
14
|
+
@window_seconds = window_seconds
|
|
15
|
+
@timestamps = []
|
|
16
|
+
@blocked_until = 0.0
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Raise {RateLimitError} if calling now would exceed the limit.
|
|
20
|
+
def acquire
|
|
21
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
22
|
+
if now < @blocked_until
|
|
23
|
+
wait = @blocked_until - now
|
|
24
|
+
raise RateLimitError, format("Client-side rate limit: would block for %.1fs.", wait)
|
|
25
|
+
end
|
|
26
|
+
prune(now)
|
|
27
|
+
if @timestamps.size >= @max_calls
|
|
28
|
+
@blocked_until = @timestamps.first + @window_seconds
|
|
29
|
+
wait = @blocked_until - now
|
|
30
|
+
raise RateLimitError, format("Client-side rate limit exceeded: would block for %.1fs.", wait)
|
|
31
|
+
end
|
|
32
|
+
@timestamps << now
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Record a server-side 403 so the limiter backs off for 10 minutes.
|
|
36
|
+
def notify_403
|
|
37
|
+
@blocked_until = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 600.0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def prune(now)
|
|
43
|
+
cutoff = now - @window_seconds
|
|
44
|
+
@timestamps.select! { |t| t >= cutoff }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Smartbill
|
|
4
|
+
module Sdk
|
|
5
|
+
module Transport
|
|
6
|
+
# A request value object built by {Transport.build_request} and sent
|
|
7
|
+
# by an adapter.
|
|
8
|
+
#
|
|
9
|
+
# @!attribute [r] http_method HTTP method ("GET", "POST", ...).
|
|
10
|
+
# @!attribute [r] url Full URL (without query string).
|
|
11
|
+
# @!attribute [r] headers Hash of HTTP headers.
|
|
12
|
+
# @!attribute [r] query Hash of query parameters (may be nil).
|
|
13
|
+
# @!attribute [r] body Serialized request body (may be nil).
|
|
14
|
+
Request = Struct.new(:http_method, :url, :headers, :query, :body, keyword_init: true)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|