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,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Smartbill
|
|
7
|
+
module Sdk
|
|
8
|
+
# Shared transport logic for the SmartBill clients.
|
|
9
|
+
#
|
|
10
|
+
# The SmartBill API uses HTTP Basic Auth with +username:token+ and
|
|
11
|
+
# returns JSON envelopes whose root key depends on the endpoint
|
|
12
|
+
# (+sbcResponse+, +Response+, +sbcInvoicePaymentStatusResponse+,
|
|
13
|
+
# +sbcSeries+, +sbcTaxes+, +stocks+, +Fault+). This module centralises
|
|
14
|
+
# request building, auth, envelope unwrapping and error mapping so the
|
|
15
|
+
# clients stay DRY.
|
|
16
|
+
module Transport
|
|
17
|
+
DEFAULT_BASE_URL = "https://ws.smartbill.ro/SBORO/api/"
|
|
18
|
+
DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
# Envelope root keys used by SmartBill responses.
|
|
21
|
+
ENVELOPE_KEYS = %w[
|
|
22
|
+
sbcResponse
|
|
23
|
+
Response
|
|
24
|
+
sbcInvoicePaymentStatusResponse
|
|
25
|
+
sbcSeries
|
|
26
|
+
sbcTaxes
|
|
27
|
+
stocks
|
|
28
|
+
Fault
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
# Fields that may carry an error message inside an envelope.
|
|
32
|
+
ERROR_FIELDS = %w[errorText errorTextError].freeze
|
|
33
|
+
|
|
34
|
+
# Build the +Authorization: Basic ...+ header value for +username:token+.
|
|
35
|
+
def self.build_auth_header(username, token)
|
|
36
|
+
"Basic #{Base64.strict_encode64("#{username}:#{token}")}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Alias kept for parity with the Python SDK.
|
|
40
|
+
def self.build_auth(username, token)
|
|
41
|
+
build_auth_header(username, token)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Construct a {Request} with SmartBill default headers.
|
|
45
|
+
def self.build_request(method:, base_url:, path:, params: nil, json_body: nil,
|
|
46
|
+
accept: "application/json", content_type: "application/json",
|
|
47
|
+
auth_header: nil)
|
|
48
|
+
url = path.start_with?("http") ? path : "#{base_url.chomp("/")}/#{path.delete_prefix("/")}"
|
|
49
|
+
headers = { "Accept" => accept, "Content-Type" => content_type }
|
|
50
|
+
headers["Authorization"] = auth_header if auth_header
|
|
51
|
+
body = json_body ? JSON.generate(json_body) : nil
|
|
52
|
+
Request.new(http_method: method.upcase, url: url, headers: headers, query: params, body: body)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Unwrap the SmartBill response envelope and return its inner object.
|
|
56
|
+
#
|
|
57
|
+
# If the payload is a Hash whose only key is one of the known envelope
|
|
58
|
+
# keys, the value under that key is returned. Otherwise the payload is
|
|
59
|
+
# returned unchanged.
|
|
60
|
+
def self.parse_envelope(payload)
|
|
61
|
+
return payload unless payload.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
if payload.size == 1
|
|
64
|
+
key, value = payload.first
|
|
65
|
+
return value if ENVELOPE_KEYS.include?(key)
|
|
66
|
+
end
|
|
67
|
+
# Some envelopes nest under a known key with sibling fields.
|
|
68
|
+
ENVELOPE_KEYS.each do |key|
|
|
69
|
+
next unless payload.key?(key)
|
|
70
|
+
|
|
71
|
+
inner = payload[key]
|
|
72
|
+
return inner if inner.is_a?(Hash)
|
|
73
|
+
end
|
|
74
|
+
payload
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Extract +(error_text, message)+ from an envelope.
|
|
78
|
+
def self.extract_error(envelope)
|
|
79
|
+
return ["", nil] unless envelope.is_a?(Hash)
|
|
80
|
+
|
|
81
|
+
error_text = ""
|
|
82
|
+
ERROR_FIELDS.each do |field|
|
|
83
|
+
value = envelope[field]
|
|
84
|
+
next unless value.is_a?(String) && !value.strip.empty?
|
|
85
|
+
|
|
86
|
+
error_text = value
|
|
87
|
+
break
|
|
88
|
+
end
|
|
89
|
+
message = envelope["message"]
|
|
90
|
+
message = nil unless message.is_a?(String)
|
|
91
|
+
[error_text, message]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Validate a {Response} and return its parsed payload.
|
|
95
|
+
#
|
|
96
|
+
# Raises the appropriate {Error} subclass for auth / rate-limit / API
|
|
97
|
+
# errors. When +binary+ is true, the raw body String is returned
|
|
98
|
+
# (used for PDF endpoints).
|
|
99
|
+
def self.handle_response(response, binary: false)
|
|
100
|
+
status = response.status
|
|
101
|
+
raise auth_error(response) if status == 401
|
|
102
|
+
raise rate_limit_error if status == 403
|
|
103
|
+
|
|
104
|
+
return response.body if binary && (200..299).cover?(status)
|
|
105
|
+
return handle_success(response, status, binary: binary) if (200..299).cover?(status)
|
|
106
|
+
|
|
107
|
+
raise api_error_from(response, status)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.auth_error(response)
|
|
111
|
+
AuthError.new(
|
|
112
|
+
"Authentication failed (401). Check username/token and company CIF. " \
|
|
113
|
+
"Body: #{(response.body || "")[0, 200]}"
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.rate_limit_error
|
|
118
|
+
RateLimitError.new(
|
|
119
|
+
"Access blocked (403): rate limit exceeded. " \
|
|
120
|
+
"SmartBill blocks access for 10 minutes after >30 calls/10s."
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def self.handle_success(response, status, binary:)
|
|
125
|
+
content_type = response.content_type.to_s
|
|
126
|
+
if content_type.include?("application/octet-stream") || response.body.nil? || response.body.empty?
|
|
127
|
+
return binary ? response.body : nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
payload = begin
|
|
131
|
+
JSON.parse(response.body)
|
|
132
|
+
rescue JSON::ParserError => e
|
|
133
|
+
raise TransportError, "Failed to decode JSON response: #{e.message}"
|
|
134
|
+
end
|
|
135
|
+
envelope = parse_envelope(payload)
|
|
136
|
+
error_text, message = extract_error(envelope)
|
|
137
|
+
raise APIError.new(error_text: error_text, message: message, status_code: status) unless error_text.empty?
|
|
138
|
+
|
|
139
|
+
envelope
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.api_error_from(response, status)
|
|
143
|
+
error_text = ""
|
|
144
|
+
message = nil
|
|
145
|
+
payload = begin
|
|
146
|
+
JSON.parse(response.body || "")
|
|
147
|
+
rescue JSON::ParserError
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
if payload
|
|
151
|
+
envelope = parse_envelope(payload)
|
|
152
|
+
error_text, message = extract_error(envelope)
|
|
153
|
+
else
|
|
154
|
+
error_text = (response.body || "")[0, 300]
|
|
155
|
+
end
|
|
156
|
+
APIError.new(error_text: error_text, message: message, status_code: status)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private_class_method :auth_error, :rate_limit_error, :handle_success, :api_error_from
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry-types"
|
|
4
|
+
|
|
5
|
+
module Smartbill
|
|
6
|
+
module Sdk
|
|
7
|
+
# Shared {Dry::Types} module used by the model structs and the
|
|
8
|
+
# validation contracts.
|
|
9
|
+
#
|
|
10
|
+
# `Strict::` types raise on wrong types (no silent coercion); `Coercible::`
|
|
11
|
+
# types coerce strings/numbers (e.g. `"10"` → `10.0`). Optional fields use
|
|
12
|
+
# `.optional.default(nil)`; collections use `Types::Array.of(...)`.
|
|
13
|
+
module Types
|
|
14
|
+
include Dry::Types()
|
|
15
|
+
|
|
16
|
+
# A value that may be either a strict String or a strict Integer — the
|
|
17
|
+
# SmartBill API returns some fields (e.g. +id+, +nextNumber+, +code+)
|
|
18
|
+
# as either, depending on the endpoint.
|
|
19
|
+
StrOrInt = Strict::String | Strict::Integer
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# smartbill-sdk — Ruby SDK for the SmartBill Cloud REST API.
|
|
4
|
+
#
|
|
5
|
+
# Provides a synchronous {Client} with typed request/response models for
|
|
6
|
+
# every endpoint in the SmartBill OpenAPI specification.
|
|
7
|
+
#
|
|
8
|
+
# Constants under `Smartbill::Sdk` are autoloaded by Zeitwerk from
|
|
9
|
+
# `lib/smartbill/sdk/` (one file per constant). `require "smartbill/sdk"`
|
|
10
|
+
# sets up the loader; constants are then resolved on first reference.
|
|
11
|
+
|
|
12
|
+
require "zeitwerk"
|
|
13
|
+
|
|
14
|
+
module Smartbill
|
|
15
|
+
# Namespace for the SmartBill SDK.
|
|
16
|
+
module Sdk
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
loader = Zeitwerk::Loader.new
|
|
21
|
+
loader.push_dir(File.expand_path("sdk", __dir__), namespace: Smartbill::Sdk)
|
|
22
|
+
# `version.rb` defines the `VERSION` constant (not `Version`), and
|
|
23
|
+
# `api_error.rb` defines `APIError` (not `ApiError`).
|
|
24
|
+
loader.inflector.inflect(
|
|
25
|
+
"version" => "VERSION",
|
|
26
|
+
"api_error" => "APIError"
|
|
27
|
+
)
|
|
28
|
+
loader.setup
|
|
29
|
+
|
|
30
|
+
# Public alias for the default SmartBill Cloud REST API base URL.
|
|
31
|
+
Smartbill::Sdk::DEFAULT_BASE_URL = Smartbill::Sdk::Transport::DEFAULT_BASE_URL
|