hcbv4 0.1.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/README.md +579 -0
- data/Rakefile +12 -0
- data/lib/hcbv4/client.rb +618 -0
- data/lib/hcbv4/errors.rb +38 -0
- data/lib/hcbv4/models/ach_transfer.rb +48 -0
- data/lib/hcbv4/models/base_transaction_detail.rb +37 -0
- data/lib/hcbv4/models/card_charge.rb +25 -0
- data/lib/hcbv4/models/card_design.rb +21 -0
- data/lib/hcbv4/models/card_grant.rb +100 -0
- data/lib/hcbv4/models/check.rb +72 -0
- data/lib/hcbv4/models/comment.rb +19 -0
- data/lib/hcbv4/models/donation.rb +11 -0
- data/lib/hcbv4/models/donation_transaction.rb +77 -0
- data/lib/hcbv4/models/expense_payout.rb +25 -0
- data/lib/hcbv4/models/invitation.rb +47 -0
- data/lib/hcbv4/models/invoice.rb +30 -0
- data/lib/hcbv4/models/invoice_transaction.rb +47 -0
- data/lib/hcbv4/models/merchant.rb +16 -0
- data/lib/hcbv4/models/organization.rb +164 -0
- data/lib/hcbv4/models/organization_user.rb +33 -0
- data/lib/hcbv4/models/receipt.rb +30 -0
- data/lib/hcbv4/models/resource.rb +23 -0
- data/lib/hcbv4/models/sponsor.rb +43 -0
- data/lib/hcbv4/models/stripe_card.rb +134 -0
- data/lib/hcbv4/models/tag.rb +12 -0
- data/lib/hcbv4/models/transaction.rb +129 -0
- data/lib/hcbv4/models/transaction_list.rb +100 -0
- data/lib/hcbv4/models/transaction_type.rb +11 -0
- data/lib/hcbv4/models/transfer.rb +67 -0
- data/lib/hcbv4/models/user.rb +76 -0
- data/lib/hcbv4/models.rb +37 -0
- data/lib/hcbv4/version.rb +5 -0
- data/lib/hcbv4.rb +10 -0
- data/sig/hcbv4.rbs +4 -0
- metadata +77 -0
data/lib/hcbv4/client.rb
ADDED
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "faraday"
|
|
4
|
+
require "oauth2"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module HCBV4
|
|
8
|
+
class Client
|
|
9
|
+
DEFAULT_BASE_URL = "https://hcb.hackclub.com"
|
|
10
|
+
API_PATH = "/api/v4"
|
|
11
|
+
|
|
12
|
+
attr_reader :oauth_token, :base_url
|
|
13
|
+
|
|
14
|
+
def initialize(oauth_token:, base_url: DEFAULT_BASE_URL)
|
|
15
|
+
@oauth_token = oauth_token
|
|
16
|
+
@base_url = base_url
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.from_credentials(client_id:, client_secret:, access_token:, refresh_token:, expires_at: nil,
|
|
20
|
+
base_url: DEFAULT_BASE_URL)
|
|
21
|
+
oauth_client = OAuth2::Client.new(
|
|
22
|
+
client_id,
|
|
23
|
+
client_secret,
|
|
24
|
+
site: base_url,
|
|
25
|
+
token_url: "/oauth/token"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
token = OAuth2::AccessToken.new(
|
|
29
|
+
oauth_client,
|
|
30
|
+
access_token,
|
|
31
|
+
refresh_token:,
|
|
32
|
+
expires_at:
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
new(oauth_token: token, base_url:)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
39
|
+
# Users
|
|
40
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
# Returns the currently authenticated user.
|
|
43
|
+
# @return [User]
|
|
44
|
+
def me
|
|
45
|
+
User.from_hash(get("/user"))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns a user by id. Admin only.
|
|
49
|
+
# @param id [String] user id
|
|
50
|
+
# @return [User]
|
|
51
|
+
def user(id)
|
|
52
|
+
User.from_hash(get("/users/#{id}"))
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns a user by email address. Admin only.
|
|
56
|
+
# @param email [String]
|
|
57
|
+
# @return [User]
|
|
58
|
+
def user_by_email(email)
|
|
59
|
+
User.from_hash(get("/users/by_email/#{email}"))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns profile icons the current user can select from.
|
|
63
|
+
# @return [Array<Hash>] icon objects with :id, :url, :name
|
|
64
|
+
def available_icons
|
|
65
|
+
get("/user/available_icons")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns beacon configuration for the current user.
|
|
69
|
+
# @return [Hash]
|
|
70
|
+
def beacon_config
|
|
71
|
+
get("/user/beacon_config")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
75
|
+
# Organizations (Events)
|
|
76
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
# Returns all organizations the current user belongs to.
|
|
79
|
+
# @param expand [Array<Symbol>] fields to expand (:balance_cents, :reporting, :account_number, :users)
|
|
80
|
+
# @return [Array<Organization>]
|
|
81
|
+
def organizations(expand: [])
|
|
82
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
83
|
+
get("/user/organizations", params).map { |h| Organization.from_hash(h, client: self) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns an organization by id or slug (fetches from API).
|
|
87
|
+
# @param id_or_slug [String]
|
|
88
|
+
# @param expand [Array<Symbol>] fields to expand
|
|
89
|
+
# @return [Organization]
|
|
90
|
+
def organization(id_or_slug, expand: [])
|
|
91
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
92
|
+
Organization.from_hash(get("/organizations/#{id_or_slug}", params), client: self)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Stub methods for performing actions without fetching.
|
|
96
|
+
# @!method organization!(id)
|
|
97
|
+
# @!method card_grant!(id)
|
|
98
|
+
# @!method stripe_card!(id)
|
|
99
|
+
# @!method transaction!(id)
|
|
100
|
+
# @!method sponsor!(id)
|
|
101
|
+
# @!method invoice!(id)
|
|
102
|
+
# @!method invitation!(id)
|
|
103
|
+
# @!method receipt!(id)
|
|
104
|
+
%i[Organization CardGrant StripeCard Transaction Sponsor Invoice Invitation Receipt].each do |klass|
|
|
105
|
+
define_method(:"#{klass.to_s.gsub(/([a-z])([A-Z])/, '\1_\2').downcase}!") do |id|
|
|
106
|
+
HCBV4.const_get(klass).of_id(id, client: self)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
111
|
+
# Transactions
|
|
112
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
# Returns paginated transactions for an organization.
|
|
115
|
+
# @param organization_id_or_slug [String]
|
|
116
|
+
# @param limit [Integer, nil] max results per page
|
|
117
|
+
# @param after [String, nil] cursor for pagination (transaction id)
|
|
118
|
+
# @param type [String, nil] filter by transaction type
|
|
119
|
+
# @param filters [Hash] additional filters
|
|
120
|
+
# @param expand [Array<Symbol>] fields to expand (:organization)
|
|
121
|
+
# @return [TransactionList]
|
|
122
|
+
def transactions(organization_id_or_slug, limit: nil, after: nil, type: nil, filters: {}, expand: [])
|
|
123
|
+
params = { limit:, after:, type:, filters:, expand: expand.join(",") }.compact.reject do |_, v|
|
|
124
|
+
v.respond_to?(:empty?) && v.empty?
|
|
125
|
+
end
|
|
126
|
+
pagination_context = {
|
|
127
|
+
type: :organization_transactions,
|
|
128
|
+
organization_id: organization_id_or_slug,
|
|
129
|
+
limit:,
|
|
130
|
+
tx_type: type,
|
|
131
|
+
filters:,
|
|
132
|
+
expand:
|
|
133
|
+
}
|
|
134
|
+
TransactionList.from_hash(get("/organizations/#{organization_id_or_slug}/transactions", params), client: self,
|
|
135
|
+
pagination_context:)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Returns a transaction by id.
|
|
139
|
+
# @param id [String]
|
|
140
|
+
# @param event_id [String, nil] organization id for scoping
|
|
141
|
+
# @param expand [Array<Symbol>] fields to expand
|
|
142
|
+
# @return [Transaction]
|
|
143
|
+
def transaction(id, event_id: nil, expand: [])
|
|
144
|
+
params = { event_id:, expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
145
|
+
Transaction.from_hash(get("/transactions/#{id}", params), client: self)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Returns transactions missing receipts across all orgs.
|
|
149
|
+
# @param limit [Integer, nil] max results per page
|
|
150
|
+
# @param after [String, nil] cursor for pagination
|
|
151
|
+
# @return [TransactionList]
|
|
152
|
+
def missing_receipt_transactions(limit: nil, after: nil)
|
|
153
|
+
params = { limit:, after: }.compact
|
|
154
|
+
pagination_context = { type: :missing_receipt, limit: }
|
|
155
|
+
TransactionList.from_hash(get("/user/transactions/missing_receipt", params), client: self, pagination_context:)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
159
|
+
# Card Grants
|
|
160
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
# Returns card grants for the current user.
|
|
163
|
+
# @param expand [Array<Symbol>] fields to expand (:user, :organization, :balance_cents, :disbursements)
|
|
164
|
+
# @return [Array<CardGrant>]
|
|
165
|
+
def card_grants(expand: [])
|
|
166
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
167
|
+
get("/user/card_grants", params).map { |h| CardGrant.from_hash(h, client: self) }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Returns card grants for a specific organization.
|
|
171
|
+
# @param organization_id [String]
|
|
172
|
+
# @param expand [Array<Symbol>] fields to expand
|
|
173
|
+
# @return [Array<CardGrant>]
|
|
174
|
+
def organization_card_grants(organization_id, expand: [])
|
|
175
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
176
|
+
get("/organizations/#{organization_id}/card_grants", params).map { |h| CardGrant.from_hash(h, client: self) }
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Returns a card grant by id.
|
|
180
|
+
# @param id [String]
|
|
181
|
+
# @param expand [Array<Symbol>] fields to expand
|
|
182
|
+
# @return [CardGrant]
|
|
183
|
+
def card_grant(id, expand: [])
|
|
184
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
185
|
+
CardGrant.from_hash(get("/card_grants/#{id}", params), client: self)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Creates a new card grant (virtual card with spending limits).
|
|
189
|
+
# @param event_id [String] organization id
|
|
190
|
+
# @param amount_cents [Integer] initial funding amount
|
|
191
|
+
# @param email [String] recipient email
|
|
192
|
+
# @param options [Hash] additional options (purpose, merchant_lock, one_time_use, etc.)
|
|
193
|
+
# @return [CardGrant]
|
|
194
|
+
def create_card_grant(event_id:, amount_cents:, email:, **options)
|
|
195
|
+
body = { amount_cents:, email:, **options }
|
|
196
|
+
CardGrant.from_hash(post("/organizations/#{event_id}/card_grants", body), client: self)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
200
|
+
# Stripe Cards
|
|
201
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
# Returns stripe cards for the current user.
|
|
204
|
+
# @param expand [Array<Symbol>] fields to expand (:user, :organization, :total_spent_cents, :balance_available)
|
|
205
|
+
# @return [Array<StripeCard>]
|
|
206
|
+
def stripe_cards(expand: [])
|
|
207
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
208
|
+
get("/user/cards", params).map { |h| StripeCard.from_hash(h, client: self) }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Returns stripe cards for a specific organization.
|
|
212
|
+
# @param organization_id [String]
|
|
213
|
+
# @param expand [Array<Symbol>] fields to expand
|
|
214
|
+
# @return [Array<StripeCard>]
|
|
215
|
+
def organization_stripe_cards(organization_id, expand: [])
|
|
216
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
217
|
+
get("/organizations/#{organization_id}/cards", params).map { |h| StripeCard.from_hash(h, client: self) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Returns a stripe card by id.
|
|
221
|
+
# @param id [String]
|
|
222
|
+
# @param expand [Array<Symbol>] fields to expand
|
|
223
|
+
# @return [StripeCard]
|
|
224
|
+
def stripe_card(id, expand: [])
|
|
225
|
+
params = { expand: expand.join(",") }.compact.reject { |_, v| v.respond_to?(:empty?) && v.empty? }
|
|
226
|
+
StripeCard.from_hash(get("/cards/#{id}", params), client: self)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Creates a new stripe card (virtual or physical).
|
|
230
|
+
# @param organization_id [String]
|
|
231
|
+
# @param card_type [String] "virtual" or "physical"
|
|
232
|
+
# @param options [Hash] additional options (name, design_id, shipping address, etc.)
|
|
233
|
+
# @return [StripeCard]
|
|
234
|
+
def create_stripe_card(organization_id:, card_type:, **options)
|
|
235
|
+
body = { card: { organization_id:, card_type:, **options } }
|
|
236
|
+
StripeCard.from_hash(post("/cards", body), client: self)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Returns available card designs for physical card personalization.
|
|
240
|
+
# @param event_id [String, nil] organization id to filter designs
|
|
241
|
+
# @return [Array<CardDesign>] available designs
|
|
242
|
+
def card_designs(event_id: nil)
|
|
243
|
+
params = event_id ? { event_id: } : {}
|
|
244
|
+
get("/cards/card_designs", params).map { |h| CardDesign.from_hash(h) }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
248
|
+
# Invoices
|
|
249
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
# Returns invoices for an organization.
|
|
252
|
+
# @param event_id [String] organization id
|
|
253
|
+
# @return [Array<Invoice>]
|
|
254
|
+
def invoices(event_id:)
|
|
255
|
+
get("/organizations/#{event_id}/invoices").map { |h| Invoice.from_hash(h, client: self) }
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Returns an invoice by id.
|
|
259
|
+
# @param id [String]
|
|
260
|
+
# @return [Invoice]
|
|
261
|
+
def invoice(id)
|
|
262
|
+
Invoice.from_hash(get("/invoices/#{id}"), client: self)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Creates an invoice for a sponsor.
|
|
266
|
+
# @param event_id [String] organization id
|
|
267
|
+
# @param sponsor_id [String]
|
|
268
|
+
# @param due_date [String] ISO date (YYYY-MM-DD)
|
|
269
|
+
# @param item_description [String]
|
|
270
|
+
# @param item_amount [Integer] amount in cents
|
|
271
|
+
# @return [Invoice]
|
|
272
|
+
def create_invoice(event_id:, sponsor_id:, due_date:, item_description:, item_amount:)
|
|
273
|
+
body = { organization_id: event_id, sponsor_id:, invoice: { due_date:, item_description:, item_amount: } }
|
|
274
|
+
Invoice.from_hash(post("/invoices", body), client: self)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
278
|
+
# Sponsors
|
|
279
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
# Returns sponsors for an organization.
|
|
282
|
+
# @param event_id [String] organization id
|
|
283
|
+
# @return [Array<Sponsor>]
|
|
284
|
+
def sponsors(event_id:)
|
|
285
|
+
get("/organizations/#{event_id}/sponsors").map { |h| Sponsor.from_hash(h, client: self) }
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Returns a sponsor by id.
|
|
289
|
+
# @param id [String]
|
|
290
|
+
# @return [Sponsor]
|
|
291
|
+
def sponsor(id)
|
|
292
|
+
Sponsor.from_hash(get("/sponsors/#{id}"), client: self)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Creates a sponsor for invoicing.
|
|
296
|
+
# @param event_id [String] organization id
|
|
297
|
+
# @param name [String] sponsor/company name
|
|
298
|
+
# @param contact_email [String] billing email
|
|
299
|
+
# @param address [Hash] address fields (address_line1, address_city, address_state, etc.)
|
|
300
|
+
# @return [Sponsor]
|
|
301
|
+
def create_sponsor(event_id:, name:, contact_email:, **address)
|
|
302
|
+
body = { organization_id: event_id, sponsor: { name:, contact_email:, **address } }
|
|
303
|
+
Sponsor.from_hash(post("/sponsors", body), client: self)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
307
|
+
# Disbursements (Transfers)
|
|
308
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
# Transfers funds from one organization to another.
|
|
311
|
+
# @param event_id [String] source organization id
|
|
312
|
+
# @param to_organization_id [String] destination organization id
|
|
313
|
+
# @param amount_cents [Integer]
|
|
314
|
+
# @param name [String] transfer description
|
|
315
|
+
# @return [Transfer]
|
|
316
|
+
def create_disbursement(event_id:, to_organization_id:, amount_cents:, name:)
|
|
317
|
+
body = { to_organization_id:, amount_cents:, name: }
|
|
318
|
+
organization = Organization.of_id(event_id, client: self)
|
|
319
|
+
Transfer.from_hash(post("/organizations/#{event_id}/transfers", body), client: self, organization:)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
323
|
+
# ACH Transfers
|
|
324
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
# Initiates an ACH bank transfer.
|
|
327
|
+
# @param event_id [String] organization id
|
|
328
|
+
# @param routing_number [String] 9-digit ABA routing number
|
|
329
|
+
# @param account_number [String] bank account number
|
|
330
|
+
# @param recipient_name [String] name on the bank account
|
|
331
|
+
# @param amount_money [String] amount as decimal string (e.g., "100.00")
|
|
332
|
+
# @param payment_for [String] payment description/memo
|
|
333
|
+
# @param options [Hash] additional options
|
|
334
|
+
# @return [Hash]
|
|
335
|
+
def create_ach_transfer(event_id:, routing_number:, account_number:, recipient_name:, amount_money:, payment_for:,
|
|
336
|
+
**options)
|
|
337
|
+
body = {
|
|
338
|
+
ach_transfer: {
|
|
339
|
+
routing_number:, account_number:, recipient_name:, amount_money:, payment_for:, **options
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
post("/organizations/#{event_id}/ach_transfers", body)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
346
|
+
# Donations
|
|
347
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
# Records an in-person donation (cash, check, etc.).
|
|
350
|
+
# @param event_id [String] organization id
|
|
351
|
+
# @param amount_cents [Integer]
|
|
352
|
+
# @param options [Hash] donor info (name, email, anonymous, tax_deductible, fee_covered)
|
|
353
|
+
# @return [Donation] returns only the donation id
|
|
354
|
+
def create_donation(event_id:, amount_cents:, **options)
|
|
355
|
+
body = { amount_cents:, **options }
|
|
356
|
+
Donation.from_hash(post("/organizations/#{event_id}/donations", body))
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
360
|
+
# Receipts
|
|
361
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
# Returns receipts, optionally filtered by transaction.
|
|
364
|
+
# @param transaction_id [String, nil] filter to a specific transaction
|
|
365
|
+
# @return [Array<Receipt>]
|
|
366
|
+
def receipts(transaction_id: nil)
|
|
367
|
+
params = transaction_id ? { transaction_id: } : {}
|
|
368
|
+
get("/receipts", params).map { |h| Receipt.from_hash(h, client: self) }
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Uploads a receipt image.
|
|
372
|
+
# @param file [File] receipt file
|
|
373
|
+
# @param transaction_id [String, nil] attach to a transaction, or nil for receipt bin
|
|
374
|
+
# @return [Receipt]
|
|
375
|
+
def create_receipt(file:, transaction_id: nil)
|
|
376
|
+
body = { file:, transaction_id: }.compact
|
|
377
|
+
Receipt.from_hash(post("/receipts", body), client: self)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
381
|
+
# Invitations
|
|
382
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
# Returns pending invitations for the current user.
|
|
385
|
+
# @return [Array<Invitation>]
|
|
386
|
+
def invitations
|
|
387
|
+
get("/user/invitations").map { |h| Invitation.from_hash(h, client: self) }
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
# Returns an invitation by id.
|
|
391
|
+
# @param id [String]
|
|
392
|
+
# @return [Invitation]
|
|
393
|
+
def invitation(id)
|
|
394
|
+
Invitation.from_hash(get("/user/invitations/#{id}"), client: self)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# Invites a user to join an organization.
|
|
398
|
+
# @param event_id [String] organization id
|
|
399
|
+
# @param email [String] invitee email
|
|
400
|
+
# @param role [String, nil] "member" or "manager"
|
|
401
|
+
# @param enable_spending_controls [Boolean, nil] enable spending controls
|
|
402
|
+
# @param initial_control_allowance_amount [Integer, nil] initial allowance in cents
|
|
403
|
+
# @return [Invitation]
|
|
404
|
+
def create_invitation(event_id:, email:, role: nil, enable_spending_controls: nil,
|
|
405
|
+
initial_control_allowance_amount: nil)
|
|
406
|
+
body = { event_id:, email:, role:, enable_spending_controls:, initial_control_allowance_amount: }.compact
|
|
407
|
+
Invitation.from_hash(post("/user/invitations", body), client: self)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
411
|
+
# Stripe Terminal
|
|
412
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
# Returns a connection token for Stripe Terminal hardware.
|
|
415
|
+
# @return [Hash] token data
|
|
416
|
+
def terminal_connection_token
|
|
417
|
+
get("/stripe_terminal_connection_token")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
421
|
+
# Raw HTTP methods
|
|
422
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
424
|
+
def get(path, params = {})
|
|
425
|
+
request(:get, path, params:)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def post(path, body = {})
|
|
429
|
+
request(:post, path, body:)
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
def patch(path, body = {})
|
|
433
|
+
request(:patch, path, body:)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def delete(path)
|
|
437
|
+
request(:delete, path)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
441
|
+
# Resource instance methods (called via resource objects)
|
|
442
|
+
# ─────────────────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
# @!visibility private
|
|
445
|
+
def update_card_grant(id, **attributes)
|
|
446
|
+
CardGrant.from_hash(patch("/card_grants/#{id}", attributes), client: self)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# @!visibility private
|
|
450
|
+
def topup_card_grant(id, amount_cents:)
|
|
451
|
+
CardGrant.from_hash(post("/card_grants/#{id}/topup", { amount_cents: }), client: self)
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
# @!visibility private
|
|
455
|
+
def withdraw_card_grant(id, amount_cents:)
|
|
456
|
+
CardGrant.from_hash(post("/card_grants/#{id}/withdraw", { amount_cents: }), client: self)
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# @!visibility private
|
|
460
|
+
def cancel_card_grant(id)
|
|
461
|
+
CardGrant.from_hash(post("/card_grants/#{id}/cancel"), client: self)
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# @!visibility private
|
|
465
|
+
def activate_card_grant(id)
|
|
466
|
+
CardGrant.from_hash(post("/card_grants/#{id}/activate"), client: self)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# @!visibility private
|
|
470
|
+
def update_stripe_card(id, status:, last4: nil)
|
|
471
|
+
params = { status:, last4: }.compact
|
|
472
|
+
StripeCard.from_hash(patch("/cards/#{id}", params), client: self)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
# @!visibility private
|
|
476
|
+
def cancel_stripe_card(id)
|
|
477
|
+
post("/cards/#{id}/cancel")
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
# @!visibility private
|
|
481
|
+
def stripe_card_ephemeral_keys(id, nonce:, stripe_version: nil)
|
|
482
|
+
params = { nonce:, stripe_version: }.compact
|
|
483
|
+
get("/cards/#{id}/ephemeral_keys", params)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# @!visibility private
|
|
487
|
+
def stripe_card_transactions(id, limit: nil, after: nil, missing_receipts: nil)
|
|
488
|
+
params = { limit:, after:, missing_receipts: }.compact
|
|
489
|
+
pagination_context = { type: :stripe_card_transactions, card_id: id, limit:, missing_receipts: }
|
|
490
|
+
TransactionList.from_hash(get("/cards/#{id}/transactions", params), client: self, pagination_context:)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# @!visibility private
|
|
494
|
+
def accept_invitation(id)
|
|
495
|
+
Invitation.from_hash(post("/user/invitations/#{id}/accept"), client: self)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# @!visibility private
|
|
499
|
+
def reject_invitation(id)
|
|
500
|
+
Invitation.from_hash(post("/user/invitations/#{id}/reject"), client: self)
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# @!visibility private
|
|
504
|
+
def delete_receipt(id)
|
|
505
|
+
delete("/receipts/#{id}")
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# @!visibility private
|
|
509
|
+
def update_transaction(id, event_id:, memo:)
|
|
510
|
+
Transaction.from_hash(patch("/organizations/#{event_id}/transactions/#{id}", { memo: }), client: self)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# @!visibility private
|
|
514
|
+
def memo_suggestions(event_id:, transaction_id:)
|
|
515
|
+
get("/organizations/#{event_id}/transactions/#{transaction_id}/memo_suggestions")
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# @!visibility private
|
|
519
|
+
def organization_followers(id_or_slug)
|
|
520
|
+
data = get("/organizations/#{id_or_slug}/followers")
|
|
521
|
+
data["followers"]&.map { |h| User.from_hash(h) } || []
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
# @!visibility private
|
|
525
|
+
def sub_organizations(id_or_slug)
|
|
526
|
+
get("/organizations/#{id_or_slug}/sub_organizations").map { |h| Organization.from_hash(h, client: self) }
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# @!visibility private
|
|
530
|
+
def create_sub_organization(parent_id_or_slug, name:, email:, cosigner_email: nil, country: nil, scoped_tags: nil)
|
|
531
|
+
body = { name:, email:, cosigner_email:, country:, scoped_tags: }.compact
|
|
532
|
+
Organization.from_hash(post("/organizations/#{parent_id_or_slug}/sub_organizations", body), client: self)
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
# @!visibility private
|
|
536
|
+
def comments(organization_id:, transaction_id:)
|
|
537
|
+
get("/organizations/#{organization_id}/transactions/#{transaction_id}/comments").map { |h| Comment.from_hash(h) }
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
# @!visibility private
|
|
541
|
+
def create_comment(organization_id:, transaction_id:, content:, admin_only: false, file: nil)
|
|
542
|
+
body = { content:, admin_only:, file: }.compact
|
|
543
|
+
Comment.from_hash(post("/organizations/#{organization_id}/transactions/#{transaction_id}/comments", body))
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
private
|
|
547
|
+
|
|
548
|
+
def request(method, path, params: nil, body: nil)
|
|
549
|
+
refresh_token_if_needed!
|
|
550
|
+
|
|
551
|
+
response = connection.public_send(method, "#{API_PATH}#{path}") do |req|
|
|
552
|
+
req.params = params if params
|
|
553
|
+
req.body = body.to_json if body
|
|
554
|
+
req.headers["Authorization"] = "Bearer #{oauth_token.token}"
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
handle_response(response)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def refresh_token_if_needed!
|
|
561
|
+
return unless oauth_token.respond_to?(:expired?)
|
|
562
|
+
return unless oauth_token.expired?
|
|
563
|
+
|
|
564
|
+
@oauth_token = oauth_token.refresh!
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def connection
|
|
568
|
+
@connection ||= Faraday.new(url: base_url) do |f|
|
|
569
|
+
f.request :json
|
|
570
|
+
f.response :json
|
|
571
|
+
f.headers["Content-Type"] = "application/json"
|
|
572
|
+
f.headers["Accept"] = "application/json"
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def handle_response(response)
|
|
577
|
+
case response.status
|
|
578
|
+
when 200..299
|
|
579
|
+
response.body
|
|
580
|
+
when 400
|
|
581
|
+
raise_api_error(response, BadRequestError)
|
|
582
|
+
when 401
|
|
583
|
+
raise_api_error(response, UnauthorizedError)
|
|
584
|
+
when 403
|
|
585
|
+
raise_api_error(response, ForbiddenError)
|
|
586
|
+
when 404
|
|
587
|
+
raise_api_error(response, NotFoundError)
|
|
588
|
+
when 422
|
|
589
|
+
raise_api_error(response, UnprocessableEntityError)
|
|
590
|
+
when 429
|
|
591
|
+
raise_api_error(response, RateLimitError)
|
|
592
|
+
when 500..599
|
|
593
|
+
raise_api_error(response, ServerError)
|
|
594
|
+
else
|
|
595
|
+
raise_api_error(response, APIError)
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def raise_api_error(response, error_class)
|
|
600
|
+
body = response.body || {}
|
|
601
|
+
error_code = body["error"]
|
|
602
|
+
messages = Array(body["messages"])
|
|
603
|
+
|
|
604
|
+
error_class = case error_code
|
|
605
|
+
when "invalid_operation" then InvalidOperationError
|
|
606
|
+
when "invalid_user" then InvalidUserError
|
|
607
|
+
else error_class
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
raise error_class.new(
|
|
611
|
+
messages.first || body["error"] || "API error",
|
|
612
|
+
status: response.status,
|
|
613
|
+
error_code:,
|
|
614
|
+
messages:
|
|
615
|
+
)
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
end
|
data/lib/hcbv4/errors.rb
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HCBV4
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :messages
|
|
6
|
+
|
|
7
|
+
def initialize(message = nil, messages: [])
|
|
8
|
+
@messages = messages
|
|
9
|
+
super(message || messages.first)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class AuthenticationError < Error; end
|
|
14
|
+
class TokenExpiredError < AuthenticationError; end
|
|
15
|
+
class TokenRevokedError < AuthenticationError; end
|
|
16
|
+
class InvalidScopeError < AuthenticationError; end
|
|
17
|
+
|
|
18
|
+
class APIError < Error
|
|
19
|
+
attr_reader :status, :error_code
|
|
20
|
+
|
|
21
|
+
def initialize(message = nil, status: nil, error_code: nil, messages: [])
|
|
22
|
+
@status = status
|
|
23
|
+
@error_code = error_code
|
|
24
|
+
super(message, messages:)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class BadRequestError < APIError; end
|
|
29
|
+
class UnauthorizedError < APIError; end
|
|
30
|
+
class ForbiddenError < APIError; end
|
|
31
|
+
class NotFoundError < APIError; end
|
|
32
|
+
class UnprocessableEntityError < APIError; end
|
|
33
|
+
class RateLimitError < APIError; end
|
|
34
|
+
class ServerError < APIError; end
|
|
35
|
+
|
|
36
|
+
class InvalidOperationError < APIError; end
|
|
37
|
+
class InvalidUserError < APIError; end
|
|
38
|
+
end
|