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.
Files changed (85) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +211 -0
  3. data/CHANGELOG.md +18 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +265 -0
  6. data/Rakefile +22 -0
  7. data/Steepfile +6 -0
  8. data/docs/openapi.json +9741 -0
  9. data/examples/create_estimate_sync.rb +53 -0
  10. data/examples/create_invoice_eur_product_in_ron.rb +100 -0
  11. data/examples/create_invoice_sync.rb +57 -0
  12. data/examples/create_payment_sync.rb +36 -0
  13. data/examples/fiscal_receipt_sync.rb +50 -0
  14. data/examples/invoice_lifecycle_sync.rb +43 -0
  15. data/examples/list_series_sync.rb +27 -0
  16. data/examples/send_email_sync.rb +40 -0
  17. data/examples/taxes_and_stocks_sync.rb +48 -0
  18. data/lib/smartbill/sdk/api_error.rb +27 -0
  19. data/lib/smartbill/sdk/auth_error.rb +8 -0
  20. data/lib/smartbill/sdk/client.rb +68 -0
  21. data/lib/smartbill/sdk/contracts/base.rb +33 -0
  22. data/lib/smartbill/sdk/contracts/email_contract.rb +29 -0
  23. data/lib/smartbill/sdk/contracts/estimate_contract.rb +18 -0
  24. data/lib/smartbill/sdk/contracts/invoice_contract.rb +29 -0
  25. data/lib/smartbill/sdk/contracts/invoice_payment_contract.rb +18 -0
  26. data/lib/smartbill/sdk/contracts/invoice_ref_contract.rb +19 -0
  27. data/lib/smartbill/sdk/contracts/payment_contract.rb +37 -0
  28. data/lib/smartbill/sdk/contracts/storno_contract.rb +17 -0
  29. data/lib/smartbill/sdk/contracts.rb +19 -0
  30. data/lib/smartbill/sdk/error.rb +8 -0
  31. data/lib/smartbill/sdk/models/base_response.rb +16 -0
  32. data/lib/smartbill/sdk/models/client.rb +26 -0
  33. data/lib/smartbill/sdk/models/discount_type.rb +13 -0
  34. data/lib/smartbill/sdk/models/document_type.rb +13 -0
  35. data/lib/smartbill/sdk/models/email_document.rb +23 -0
  36. data/lib/smartbill/sdk/models/email_response.rb +12 -0
  37. data/lib/smartbill/sdk/models/email_status.rb +13 -0
  38. data/lib/smartbill/sdk/models/estimate.rb +32 -0
  39. data/lib/smartbill/sdk/models/fiscal_receipt_response.rb +16 -0
  40. data/lib/smartbill/sdk/models/invoice.rb +44 -0
  41. data/lib/smartbill/sdk/models/invoice_create_response.rb +10 -0
  42. data/lib/smartbill/sdk/models/invoice_payment.rb +15 -0
  43. data/lib/smartbill/sdk/models/invoice_ref.rb +24 -0
  44. data/lib/smartbill/sdk/models/payment.rb +49 -0
  45. data/lib/smartbill/sdk/models/payment_status_response.rb +19 -0
  46. data/lib/smartbill/sdk/models/payment_type.rb +22 -0
  47. data/lib/smartbill/sdk/models/product.rb +38 -0
  48. data/lib/smartbill/sdk/models/proforma_invoices_response.rb +13 -0
  49. data/lib/smartbill/sdk/models/series.rb +14 -0
  50. data/lib/smartbill/sdk/models/series_list_response.rb +14 -0
  51. data/lib/smartbill/sdk/models/stock_list.rb +13 -0
  52. data/lib/smartbill/sdk/models/stock_product.rb +15 -0
  53. data/lib/smartbill/sdk/models/stock_warehouse.rb +13 -0
  54. data/lib/smartbill/sdk/models/stocks_response.rb +14 -0
  55. data/lib/smartbill/sdk/models/storno_request.rb +17 -0
  56. data/lib/smartbill/sdk/models/storno_response.rb +14 -0
  57. data/lib/smartbill/sdk/models/struct.rb +102 -0
  58. data/lib/smartbill/sdk/models/tax.rb +14 -0
  59. data/lib/smartbill/sdk/models/taxes_response.rb +14 -0
  60. data/lib/smartbill/sdk/models.rb +17 -0
  61. data/lib/smartbill/sdk/net_http_adapter.rb +62 -0
  62. data/lib/smartbill/sdk/rate_limit_error.rb +9 -0
  63. data/lib/smartbill/sdk/response.rb +12 -0
  64. data/lib/smartbill/sdk/services/base_service.rb +39 -0
  65. data/lib/smartbill/sdk/services/configuration_service.rb +29 -0
  66. data/lib/smartbill/sdk/services/email_service.rb +18 -0
  67. data/lib/smartbill/sdk/services/estimates_service.rb +60 -0
  68. data/lib/smartbill/sdk/services/invoices_service.rb +69 -0
  69. data/lib/smartbill/sdk/services/payments_service.rb +50 -0
  70. data/lib/smartbill/sdk/services/stocks_service.rb +21 -0
  71. data/lib/smartbill/sdk/services.rb +30 -0
  72. data/lib/smartbill/sdk/transport/rate_limiter.rb +49 -0
  73. data/lib/smartbill/sdk/transport/request.rb +17 -0
  74. data/lib/smartbill/sdk/transport.rb +162 -0
  75. data/lib/smartbill/sdk/transport_error.rb +8 -0
  76. data/lib/smartbill/sdk/types.rb +22 -0
  77. data/lib/smartbill/sdk/validation_error.rb +8 -0
  78. data/lib/smartbill/sdk/version.rb +7 -0
  79. data/lib/smartbill/sdk.rb +31 -0
  80. data/sig/smartbill/sdk.rbs +661 -0
  81. data/skills/README.md +35 -0
  82. data/skills/smartbill-email/SKILL.md +120 -0
  83. data/skills/smartbill-invoices/SKILL.md +178 -0
  84. data/skills/smartbill-payments/SKILL.md +194 -0
  85. 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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smartbill
4
+ module Sdk
5
+ # Raised on HTTP 403 — SmartBill blocks access for 10 minutes after
6
+ # more than 30 calls / 10 seconds.
7
+ class RateLimitError < Error; end
8
+ end
9
+ 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