conexa 0.1.1 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +265 -1
- data/README.md +36 -4
- data/README_pt-BR.md +37 -3
- data/REFERENCE.md +53 -10
- data/lib/conexa/configuration.rb +48 -2
- data/lib/conexa/deprecation.rb +44 -0
- data/lib/conexa/errors.rb +79 -7
- data/lib/conexa/model.rb +80 -14
- data/lib/conexa/object.rb +24 -3
- data/lib/conexa/request.rb +90 -11
- data/lib/conexa/resources/charge.rb +61 -8
- data/lib/conexa/resources/company.rb +20 -1
- data/lib/conexa/resources/contract.rb +147 -30
- data/lib/conexa/resources/credit_card.rb +46 -0
- data/lib/conexa/resources/recurring_sale.rb +16 -5
- data/lib/conexa/resources/result.rb +23 -5
- data/lib/conexa/util.rb +49 -9
- data/lib/conexa/version.rb +1 -1
- data/lib/conexa.rb +40 -3
- metadata +28 -70
- data/.editorconfig +0 -30
- data/.rspec +0 -3
- data/.rubocop.yml +0 -13
- data/.solargraph.yml +0 -2
- data/.vscode/launch.json +0 -30
- data/Gemfile +0 -18
- data/Gemfile.lock +0 -159
- data/Rakefile +0 -12
- data/bin/console +0 -61
- data/bin/setup +0 -8
- data/docs/postman-collection.json +0 -30785
- data/lib/conexa/authenticator.rb +0 -116
- data/lib/conexa/order_common.rb +0 -44
- data/lib/conexa/token_manager.rb +0 -136
- data/scripts/extract_fixtures.rb +0 -35
|
@@ -3,16 +3,71 @@
|
|
|
3
3
|
module Conexa
|
|
4
4
|
# Contract resource for recurring billing contracts
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
6
|
+
# == Creating a contract
|
|
7
|
+
#
|
|
8
|
+
# Required: +plan_id+, +customer_id+, +payment_frequency+ (+monthly+,
|
|
9
|
+
# +bimonthly+, +quarterly+, +semester+ or +yearly+) and +start_date+.
|
|
10
|
+
#
|
|
11
|
+
# Conexa::Contract.create(
|
|
12
|
+
# plan_id: 5, customer_id: 127,
|
|
13
|
+
# payment_frequency: 'monthly', start_date: '2026-01-01'
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# === +due_day+ is conditionally required *and* conditionally forbidden
|
|
17
|
+
#
|
|
18
|
+
# Required on a customer's **first** contract (or when they use automatic
|
|
19
|
+
# invoicing); **rejected** on every later one, which inherits the customer's
|
|
20
|
+
# +defaultDueDay+:
|
|
21
|
+
#
|
|
22
|
+
# 422 CONTRACT_RECURRING_SALE_10
|
|
23
|
+
# "The due day can not be informed for customers who already have a contract"
|
|
24
|
+
#
|
|
25
|
+
# Code that always sends +due_day+ works at onboarding and fails forever after.
|
|
26
|
+
# Read the code off the exception rather than the message:
|
|
27
|
+
#
|
|
28
|
+
# rescue Conexa::ResponseError => e
|
|
29
|
+
# retry_without_due_day if e.api_error_codes.include?('CONTRACT_RECURRING_SALE_10')
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# === Creating, charging and settling in one call
|
|
33
|
+
#
|
|
34
|
+
# +generate_sales: 'firstOccurrenceSettleRetroactive'+ also generates *and
|
|
35
|
+
# settles* retroactive charges, and then requires +expense_settlement+. It
|
|
36
|
+
# replaces a three-call sequence in which each step can fail on its own.
|
|
37
|
+
#
|
|
38
|
+
# Conexa::Contract.create(
|
|
39
|
+
# plan_id: 5, customer_id: 127,
|
|
40
|
+
# payment_frequency: 'monthly', start_date: '2026-01-01',
|
|
41
|
+
# generate_sales: 'firstOccurrenceSettleRetroactive',
|
|
42
|
+
# expense_settlement: { receiving_method_id: 53, account_id: 1 }
|
|
12
43
|
# )
|
|
13
44
|
#
|
|
45
|
+
# Other documented values: +firstOccurrence+ (default), +currentOccurrence+,
|
|
46
|
+
# +nextOccurrence+.
|
|
47
|
+
#
|
|
48
|
+
# === Other documented fields
|
|
49
|
+
#
|
|
50
|
+
# +end_date+, +first_due_date+ (required when the customer uses automatic
|
|
51
|
+
# invoicing), +fidelity_date+, +amount+, +discount_value+, +seller_id+,
|
|
52
|
+
# +contract_summary+, +notes+, +membership_fee+, +nfse_description+,
|
|
53
|
+
# +prorata_type+ (+startOfMonth+ / +notCalculate+ / +perDueDate+), +refund+
|
|
54
|
+
# (an explicit +nil+ opts out even when the plan configures one),
|
|
55
|
+
# +complementary_services+ (array), +extra_fields+ (array).
|
|
56
|
+
#
|
|
57
|
+
# +cost_center_id+ is *not* accepted on create — it 400s — even though it is
|
|
58
|
+
# present when the contract is read back.
|
|
59
|
+
#
|
|
14
60
|
# @example End a contract
|
|
15
|
-
# Conexa::Contract.
|
|
61
|
+
# Conexa::Contract.set_end_date(456, date: '2026-12-31')
|
|
62
|
+
#
|
|
63
|
+
# == Attributes
|
|
64
|
+
#
|
|
65
|
+
# Checked against a live response, not only the collection — which omits
|
|
66
|
+
# +isActive+, +extraFields+ and +firstDueDate+ from its `GET /contract/:id`
|
|
67
|
+
# examples even though the API returns them.
|
|
68
|
+
#
|
|
69
|
+
# **There is no +status+ field on a contract.** +is_active+ is how you tell an
|
|
70
|
+
# open contract from a closed one.
|
|
16
71
|
#
|
|
17
72
|
# @!attribute [r] contract_id
|
|
18
73
|
# @return [Integer] Contract ID (also accessible as #id)
|
|
@@ -20,49 +75,111 @@ module Conexa
|
|
|
20
75
|
# @return [Integer] Customer ID
|
|
21
76
|
# @!attribute [r] plan_id
|
|
22
77
|
# @return [Integer, nil] Plan ID
|
|
23
|
-
# @!attribute [r]
|
|
24
|
-
# @return [
|
|
78
|
+
# @!attribute [r] is_active
|
|
79
|
+
# @return [Boolean] whether the contract is open
|
|
25
80
|
# @!attribute [r] start_date
|
|
26
81
|
# @return [String] Start date
|
|
27
82
|
# @!attribute [r] end_date
|
|
28
|
-
# @return [String, nil]
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
83
|
+
# @return [String, nil] closing date. May be in the future on an **active**
|
|
84
|
+
# contract — a scheduled close is not a closed contract.
|
|
85
|
+
# @!attribute [r] end_reason_id
|
|
86
|
+
# @return [Integer, nil] closing-reason id
|
|
87
|
+
# @!attribute [r] due_day
|
|
88
|
+
# @return [Integer] day of the month the contract falls due
|
|
89
|
+
# @!attribute [r] first_due_date
|
|
90
|
+
# @return [String, nil] due date of the first instalment
|
|
91
|
+
# @!attribute [r] amount
|
|
92
|
+
# @return [Float] contract value
|
|
93
|
+
# @!attribute [r] payment_frequency
|
|
94
|
+
# @return [String] monthly, bimonthly, quarterly, semester or yearly
|
|
95
|
+
# @!attribute [r] date_sales_generation
|
|
96
|
+
# @return [String, nil] when sales are generated from the contract
|
|
97
|
+
# @!attribute [r] cost_center_id
|
|
98
|
+
# @return [Integer, nil] cost centre — present on read, rejected on create
|
|
99
|
+
# @!attribute [r] seller_id
|
|
100
|
+
# @return [Integer, nil] seller (user) id
|
|
101
|
+
# @!attribute [r] contract_summary
|
|
102
|
+
# @return [String, nil] short description
|
|
103
|
+
# @!attribute [r] fidelity_date
|
|
104
|
+
# @return [String, nil] loyalty date
|
|
105
|
+
# @!attribute [r] had_prorata
|
|
106
|
+
# @return [Boolean] whether pro rata was applied
|
|
35
107
|
#
|
|
36
108
|
class Contract < Model
|
|
37
109
|
primary_key_attribute :contract_id
|
|
38
110
|
|
|
39
|
-
#
|
|
40
|
-
#
|
|
111
|
+
# Is this contract open?
|
|
112
|
+
#
|
|
113
|
+
# Reads +is_active+, which is what the API sends. It used to compare a
|
|
114
|
+
# +status+ field that contracts have never had, so it answered +false+ for an
|
|
115
|
+
# active contract — the answer that makes a caller create a second one.
|
|
116
|
+
#
|
|
117
|
+
# Deliberately not derived from +end_date+: an active contract can carry a
|
|
118
|
+
# future closing date, so a present +end_date+ does not mean closed.
|
|
119
|
+
#
|
|
120
|
+
# **Prefer {#ended?} over `!active?`.** Ruby cannot tell +nil+ from +false+
|
|
121
|
+
# through `!`, so `!active?` reads an *unknown* contract as closed — the
|
|
122
|
+
# same "treat unknown as inactive" that this fix exists to remove. `ended?`
|
|
123
|
+
# preserves the nil.
|
|
124
|
+
#
|
|
125
|
+
# @return [Boolean, nil] nil when the response did not carry +is_active+,
|
|
126
|
+
# rather than a guess
|
|
41
127
|
def active?
|
|
42
|
-
|
|
128
|
+
value = is_active
|
|
129
|
+
value.nil? ? nil : !!value
|
|
43
130
|
end
|
|
44
131
|
|
|
45
|
-
#
|
|
46
|
-
# @
|
|
132
|
+
# Is this contract closed?
|
|
133
|
+
# @see #active?
|
|
134
|
+
# @return [Boolean, nil] nil when the response did not carry +is_active+
|
|
47
135
|
def ended?
|
|
48
|
-
|
|
136
|
+
value = active?
|
|
137
|
+
value.nil? ? nil : !value
|
|
49
138
|
end
|
|
50
139
|
|
|
51
|
-
#
|
|
52
|
-
#
|
|
140
|
+
# Set this contract's end date — closing it, or amending an existing closure.
|
|
141
|
+
#
|
|
142
|
+
# The endpoint is documented as "encerra um contrato ativo **ou atualiza a
|
|
143
|
+
# data de encerramento**": it both closes and amends, and a future date on an
|
|
144
|
+
# already-closed contract **reopens** it. `end_contract` is kept as an alias,
|
|
145
|
+
# but the name understates what the call does.
|
|
146
|
+
#
|
|
147
|
+
# A contract cannot be closed retroactively past a day that already has
|
|
148
|
+
# invoiced sales (422 CONTRACT_RECURRING_SALE_23).
|
|
149
|
+
#
|
|
150
|
+
# The API may answer with an empty body on success.
|
|
151
|
+
#
|
|
152
|
+
# @param params [Hash]
|
|
153
|
+
# @option params [String] :date required, yyyy-MM-dd — the closing date
|
|
154
|
+
# @option params [Integer] :reason_id closing-reason id, from
|
|
155
|
+
# Listagem de Contratos > Outros Cadastros > Motivo de Encerramento de Contrato
|
|
156
|
+
# @option params [Boolean] :unlink_customer unlinks DDRs, mailboxes, extensions
|
|
157
|
+
# and recurring sales. Requires date <= today and no other active contracts;
|
|
158
|
+
# ends *all* the customer's recurring sales and cancels their uninvoiced sales.
|
|
159
|
+
# @option params [String] :end_date deprecated alias for +:date+
|
|
53
160
|
# @return [self]
|
|
54
|
-
def
|
|
55
|
-
|
|
161
|
+
def set_end_date(params = {})
|
|
162
|
+
params = self.class.normalize_end_params(params)
|
|
163
|
+
Conexa::Request.patch(self.class.show_url("end", primary_key), params: params).call(class_name)
|
|
56
164
|
self
|
|
57
165
|
end
|
|
166
|
+
alias_method :end_contract, :set_end_date
|
|
58
167
|
|
|
59
168
|
class << self
|
|
60
|
-
#
|
|
169
|
+
# Set a contract's end date by ID
|
|
170
|
+
# @see #set_end_date
|
|
61
171
|
# @param id [Integer, String] contract ID
|
|
62
|
-
# @param params [Hash] options including :end_date, :reason
|
|
63
172
|
# @return [Contract]
|
|
64
|
-
def
|
|
65
|
-
find(id).
|
|
173
|
+
def set_end_date(id, params = {})
|
|
174
|
+
find(id).set_end_date(params)
|
|
175
|
+
end
|
|
176
|
+
alias_method :end_contract, :set_end_date
|
|
177
|
+
|
|
178
|
+
# @deprecated Use {Conexa::Util.normalize_end_date_param}, which both "end"
|
|
179
|
+
# endpoints share.
|
|
180
|
+
# @api private
|
|
181
|
+
def normalize_end_params(params)
|
|
182
|
+
Util.normalize_end_date_param(params)
|
|
66
183
|
end
|
|
67
184
|
|
|
68
185
|
# Create contract with custom product items
|
|
@@ -1,9 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Conexa
|
|
4
|
+
# Credit card, registered against the customer through Cielo.
|
|
5
|
+
#
|
|
6
|
+
# **Write-only.** API v2 exposes `POST /creditCard` and nothing else — the card
|
|
7
|
+
# number and CVC live encrypted at Cielo, not in Conexa, so there is nothing to
|
|
8
|
+
# read back. Verified against a live tenant on 2026-08-12:
|
|
9
|
+
#
|
|
10
|
+
# GET /creditCard => 404 "Unable to resolve the request"
|
|
11
|
+
# GET /creditCard/:id => 404 "unable to find the requested action \"view\""
|
|
12
|
+
#
|
|
13
|
+
# Neither is the "does not exist or you have no permission to access it"
|
|
14
|
+
# wording the API uses for a resource an account cannot see, and the collection
|
|
15
|
+
# documents a 403 for authorization — so this is the shape of the API, not a
|
|
16
|
+
# feature switched off for one tenant.
|
|
17
|
+
#
|
|
18
|
+
# `#save` and `#destroy` are left in place but are **undocumented and
|
|
19
|
+
# unverified**: the collection describes only `POST`, and the documented
|
|
20
|
+
# behaviour of `default:` and `enable_recurring:` — re-registering a card
|
|
21
|
+
# changes which one is default, or turns recurrence off — reads as if there is
|
|
22
|
+
# no update path at all. They were not probed, because probing them means
|
|
23
|
+
# writing. Treat a 404 from either as expected rather than as a bug.
|
|
24
|
+
#
|
|
25
|
+
# @example Register a card
|
|
26
|
+
# Conexa::CreditCard.create(
|
|
27
|
+
# customer_id: 127, number: '4111111111111111',
|
|
28
|
+
# expiration_date: '12/26', cvc: '123', name: 'JOAO DA SILVA',
|
|
29
|
+
# default: true, enable_recurring: true
|
|
30
|
+
# )
|
|
4
31
|
class CreditCard < Model
|
|
5
32
|
primary_key_attribute :credit_card_id
|
|
6
33
|
|
|
34
|
+
NO_READ = "a API v2 não expõe leitura de cartão de crédito: só " \
|
|
35
|
+
"POST /creditCard é documentado, e GET /creditCard[/:id] responde 404. " \
|
|
36
|
+
"Os dados do cartão ficam na Cielo, não no Conexa."
|
|
37
|
+
|
|
38
|
+
# Model#create re-fetches the created record to pick up server-side defaults.
|
|
39
|
+
# There is no read here, so the id from the response is all there is.
|
|
40
|
+
# @return [self]
|
|
41
|
+
def create
|
|
42
|
+
created = Conexa::Request.post(self.class.show_url, params: to_hash).call(class_name)
|
|
43
|
+
set_primary_key created.attributes['id'] if created.respond_to?(:attributes)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
7
47
|
class << self
|
|
8
48
|
def url(*params)
|
|
9
49
|
["/creditCard", *params].join '/'
|
|
@@ -12,6 +52,12 @@ module Conexa
|
|
|
12
52
|
def show_url(*params)
|
|
13
53
|
["/creditCard", *params].join '/'
|
|
14
54
|
end
|
|
55
|
+
|
|
56
|
+
# Every read name Model provides, refused with an explanation rather than
|
|
57
|
+
# left to return a bare Conexa::NotFound that reads as "no such card".
|
|
58
|
+
%i[all where find find_by find_by_hash find_by_id].each do |name|
|
|
59
|
+
define_method(name) { |*, **| raise RequestError, NO_READ }
|
|
60
|
+
end
|
|
15
61
|
end
|
|
16
62
|
end
|
|
17
63
|
end
|
|
@@ -4,13 +4,23 @@ module Conexa
|
|
|
4
4
|
class RecurringSale < Model
|
|
5
5
|
primary_key_attribute :recurring_sale_id
|
|
6
6
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
7
|
+
# Set this recurring sale's end date — closing it, or amending an existing
|
|
8
|
+
# closure.
|
|
9
|
+
#
|
|
10
|
+
# Like the contract endpoint, this one is documented as "encerra uma venda
|
|
11
|
+
# recorrente ativa **ou atualiza a data de encerramento**".
|
|
12
|
+
#
|
|
13
|
+
# @param params [Hash]
|
|
14
|
+
# @option params [String] :date required, yyyy-MM-dd — the closing date
|
|
15
|
+
# @option params [String] :end_date deprecated alias for +:date+; the API
|
|
16
|
+
# rejects the +endDate+ it camelizes to
|
|
9
17
|
# @return [self]
|
|
10
18
|
def end_recurring_sale(params = {})
|
|
11
|
-
|
|
19
|
+
params = Util.normalize_end_date_param(params)
|
|
20
|
+
Conexa::Request.patch(self.class.show_url("end", primary_key), params: params).call(class_name)
|
|
12
21
|
self
|
|
13
22
|
end
|
|
23
|
+
alias_method :set_end_date, :end_recurring_sale
|
|
14
24
|
|
|
15
25
|
class << self
|
|
16
26
|
def url(*params)
|
|
@@ -21,13 +31,14 @@ module Conexa
|
|
|
21
31
|
["/recurringSale", *params].join '/'
|
|
22
32
|
end
|
|
23
33
|
|
|
24
|
-
#
|
|
34
|
+
# Set a recurring sale's end date by ID
|
|
35
|
+
# @see #end_recurring_sale
|
|
25
36
|
# @param id [Integer, String] recurring sale ID
|
|
26
|
-
# @param params [Hash] optional parameters (e.g., endDate)
|
|
27
37
|
# @return [RecurringSale]
|
|
28
38
|
def end_recurring_sale(id, params = {})
|
|
29
39
|
find(id).end_recurring_sale(params)
|
|
30
40
|
end
|
|
41
|
+
alias_method :set_end_date, :end_recurring_sale
|
|
31
42
|
end
|
|
32
43
|
end
|
|
33
44
|
end
|
|
@@ -18,19 +18,37 @@ module Conexa
|
|
|
18
18
|
@attributes["pagination"]
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# @return [Boolean] always a boolean — it used to yield nil when there was no
|
|
22
|
+
# pagination at all, which is falsy but not `false`, and leaks out of any
|
|
23
|
+
# caller that serialises or compares the result.
|
|
21
24
|
def has_next?
|
|
22
|
-
pagination && pagination.has_next == true
|
|
25
|
+
pagination.respond_to?(:has_next) && pagination.has_next == true
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
def next_page
|
|
26
29
|
raise StopIteration, "No more pages" unless has_next?
|
|
27
|
-
raise "No query context available for next_page" unless @query_context
|
|
30
|
+
raise Conexa::RequestError, "No query context available for next_page" unless @query_context
|
|
31
|
+
|
|
32
|
+
limit = pagination.limit
|
|
33
|
+
offset = pagination.offset
|
|
34
|
+
unless limit.is_a?(Integer) && offset.is_a?(Integer)
|
|
35
|
+
raise Conexa::ResponseError.new(@query_context[:params], nil,
|
|
36
|
+
"pagination is missing limit/offset " \
|
|
37
|
+
"(limit=#{limit.inspect}, offset=#{offset.inspect}), " \
|
|
38
|
+
"so the next page cannot be computed")
|
|
39
|
+
end
|
|
28
40
|
|
|
29
41
|
resource_class = @query_context[:resource_class]
|
|
30
|
-
next_params = Marshal.load(Marshal.dump(@query_context[:params]))
|
|
31
42
|
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
begin
|
|
44
|
+
next_params = Marshal.load(Marshal.dump(@query_context[:params]))
|
|
45
|
+
rescue TypeError
|
|
46
|
+
# A non-marshallable filter (a Proc, an IO) in the original query.
|
|
47
|
+
next_params = @query_context[:params].dup
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
next_params[:limit] = limit
|
|
51
|
+
next_params[:offset] = offset + limit
|
|
34
52
|
next_params.delete(:page)
|
|
35
53
|
next_params.delete(:size)
|
|
36
54
|
|
data/lib/conexa/util.rb
CHANGED
|
@@ -47,25 +47,65 @@ module Conexa
|
|
|
47
47
|
string.to_s.strip.gsub(/[\s\-]+/, '_').to_sym
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Both "end" endpoints — PATCH /contract/end/:id and
|
|
51
|
+
# PATCH /recurringSale/end/:id — document the closing date as `date`. The
|
|
52
|
+
# gem used to send `end_date`, which camelizes to `endDate` and is rejected:
|
|
53
|
+
# "endDate field does not exist or is not available in the company".
|
|
54
|
+
#
|
|
55
|
+
# Both key spellings are handled, and both are removed. Checking only the
|
|
56
|
+
# symbol `:date` would leave a caller's string `"date"` in place and add a
|
|
57
|
+
# second, colliding key — camelize_hash folds the two into one and the last
|
|
58
|
+
# one wins, so the deprecated value would silently replace the real one.
|
|
59
|
+
#
|
|
60
|
+
# @param params [Hash] caller params, possibly using the old name
|
|
61
|
+
# @return [Hash] a copy with end_date folded into date
|
|
62
|
+
def normalize_end_date_param(params)
|
|
63
|
+
params = params.dup
|
|
64
|
+
|
|
65
|
+
# Collapse both spellings of each key up front. Leaving one behind would
|
|
66
|
+
# let camelize_hash fold two `date` keys into one, last-write-wins — which
|
|
67
|
+
# is how the deprecated value once silently replaced the real one.
|
|
68
|
+
legacy = [params.delete(:end_date), params.delete("end_date")].compact.first
|
|
69
|
+
explicit = [params.delete(:date), params.delete("date")].compact.first
|
|
70
|
+
|
|
71
|
+
warn_end_date_renamed if legacy
|
|
72
|
+
|
|
73
|
+
# An explicit date wins; a key present with a nil value is not one.
|
|
74
|
+
date = explicit || legacy
|
|
75
|
+
params[:date] = date unless date.nil?
|
|
76
|
+
params
|
|
77
|
+
end
|
|
78
|
+
|
|
50
79
|
def to_snake_case str
|
|
51
80
|
str.gsub(/([A-Z])/, '_\1').downcase.sub(/^_/, '')
|
|
52
81
|
end
|
|
53
82
|
|
|
54
83
|
|
|
84
|
+
def warn_end_date_renamed
|
|
85
|
+
Deprecation.warn_once(:end_date_param,
|
|
86
|
+
"`end_date:` foi renomeado para `date:` em conexa 0.2.0 " \
|
|
87
|
+
"(a API v2 rejeita `endDate`). O alias será removido em 0.3.0.")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Convert a payload's keys to the camelCase the API expects, all the way
|
|
91
|
+
# down. Arrays of objects matter as much as nested hashes: ten documented
|
|
92
|
+
# endpoints take them (complementaryServices, productQuotas, devices,
|
|
93
|
+
# extraFields, bookingModels, visitors, costCenters, ...), and a snake_case
|
|
94
|
+
# key inside one is rejected outright.
|
|
55
95
|
def camelize_hash(hash)
|
|
56
96
|
return {} if hash.nil?
|
|
57
97
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
hash.each do |key, value|
|
|
61
|
-
if value.is_a?(Hash)
|
|
62
|
-
new_hash[camel_case_lower(key).to_sym] = camelize_hash(value)
|
|
63
|
-
else
|
|
64
|
-
new_hash[camel_case_lower(key).to_sym] = value
|
|
65
|
-
end
|
|
98
|
+
hash.each_with_object({}) do |(key, value), new_hash|
|
|
99
|
+
new_hash[camel_case_lower(key).to_sym] = camelize_value(value)
|
|
66
100
|
end
|
|
101
|
+
end
|
|
67
102
|
|
|
68
|
-
|
|
103
|
+
def camelize_value(value)
|
|
104
|
+
case value
|
|
105
|
+
when Hash then camelize_hash(value)
|
|
106
|
+
when Array then value.map { |element| camelize_value(element) }
|
|
107
|
+
else value
|
|
108
|
+
end
|
|
69
109
|
end
|
|
70
110
|
|
|
71
111
|
def camelize_str(str)
|
data/lib/conexa/version.rb
CHANGED
data/lib/conexa.rb
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "conexa/version"
|
|
4
|
-
|
|
4
|
+
# No dependencies of its own, and Model extends it at load time.
|
|
5
|
+
require_relative "conexa/deprecation"
|
|
5
6
|
require_relative "conexa/request"
|
|
6
7
|
require_relative "conexa/object"
|
|
7
8
|
require_relative "conexa/model"
|
|
@@ -9,8 +10,6 @@ require_relative "conexa/core_ext"
|
|
|
9
10
|
require_relative "conexa/errors"
|
|
10
11
|
require_relative "conexa/util"
|
|
11
12
|
require_relative "conexa/configuration"
|
|
12
|
-
require_relative "conexa/order_common"
|
|
13
|
-
require_relative "conexa/token_manager"
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
Dir[File.expand_path('../conexa/resources/*.rb', __FILE__)].map do |path|
|
|
@@ -32,4 +31,42 @@ module Conexa
|
|
|
32
31
|
def self.api_endpoint
|
|
33
32
|
configuration.api_host + "/index.php/api/v2"
|
|
34
33
|
end
|
|
34
|
+
|
|
35
|
+
# Key for the block-scoped read-only override. Thread-local so a guarded block
|
|
36
|
+
# in one thread cannot relax or tighten another.
|
|
37
|
+
READ_ONLY_KEY = :conexa_read_only
|
|
38
|
+
|
|
39
|
+
# Is writing currently forbidden?
|
|
40
|
+
#
|
|
41
|
+
# True inside a {read_only} block, or whenever `configuration.read_only` is set
|
|
42
|
+
# (which itself defaults from CONEXA_READ_ONLY).
|
|
43
|
+
#
|
|
44
|
+
# @return [Boolean]
|
|
45
|
+
def self.read_only?
|
|
46
|
+
scoped = Thread.current[READ_ONLY_KEY]
|
|
47
|
+
return scoped unless scoped.nil?
|
|
48
|
+
|
|
49
|
+
!!configuration&.read_only
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Run a block with writes forbidden, then restore the previous state.
|
|
53
|
+
#
|
|
54
|
+
# Useful for auditing or reporting code that should never mutate a tenant,
|
|
55
|
+
# without having to reconfigure the client globally.
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# Conexa.read_only do
|
|
59
|
+
# Conexa::Charge.all(status: "pending") # fine
|
|
60
|
+
# Conexa::Charge.settle(555) # raises Conexa::ReadOnlyError
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# @param enabled [Boolean] pass false to explicitly allow writes in the block
|
|
64
|
+
# @return [Object] the block's return value
|
|
65
|
+
def self.read_only(enabled = true)
|
|
66
|
+
previous = Thread.current[READ_ONLY_KEY]
|
|
67
|
+
Thread.current[READ_ONLY_KEY] = enabled
|
|
68
|
+
yield
|
|
69
|
+
ensure
|
|
70
|
+
Thread.current[READ_ONLY_KEY] = previous
|
|
71
|
+
end
|
|
35
72
|
end
|