intacct-rest 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/CHANGELOG.md +32 -0
- data/LICENSE.txt +22 -0
- data/README.md +523 -0
- data/lib/intacct_rest/authenticated_request.rb +75 -0
- data/lib/intacct_rest/client.rb +65 -0
- data/lib/intacct_rest/configuration.rb +169 -0
- data/lib/intacct_rest/custom_field.rb +19 -0
- data/lib/intacct_rest/endpoints/create_bill.rb +28 -0
- data/lib/intacct_rest/endpoints/create_bill_line.rb +31 -0
- data/lib/intacct_rest/endpoints/create_customer.rb +35 -0
- data/lib/intacct_rest/endpoints/create_invoice.rb +35 -0
- data/lib/intacct_rest/endpoints/create_invoice_line.rb +31 -0
- data/lib/intacct_rest/endpoints/create_term.rb +28 -0
- data/lib/intacct_rest/endpoints/create_vendor.rb +35 -0
- data/lib/intacct_rest/endpoints/update_vendor.rb +38 -0
- data/lib/intacct_rest/error_reporting.rb +28 -0
- data/lib/intacct_rest/errors.rb +48 -0
- data/lib/intacct_rest/filter.rb +34 -0
- data/lib/intacct_rest/model/base.rb +48 -0
- data/lib/intacct_rest/model/bill.rb +120 -0
- data/lib/intacct_rest/model/bill_line.rb +103 -0
- data/lib/intacct_rest/model/contact.rb +52 -0
- data/lib/intacct_rest/model/currency.rb +50 -0
- data/lib/intacct_rest/model/customer.rb +116 -0
- data/lib/intacct_rest/model/invoice.rb +121 -0
- data/lib/intacct_rest/model/invoice_line.rb +101 -0
- data/lib/intacct_rest/model/term.rb +88 -0
- data/lib/intacct_rest/model/vendor.rb +163 -0
- data/lib/intacct_rest/oauth_client.rb +85 -0
- data/lib/intacct_rest/objects.rb +30 -0
- data/lib/intacct_rest/page.rb +5 -0
- data/lib/intacct_rest/patch.rb +46 -0
- data/lib/intacct_rest/post.rb +43 -0
- data/lib/intacct_rest/query.rb +88 -0
- data/lib/intacct_rest/result/base.rb +17 -0
- data/lib/intacct_rest/result/error.rb +14 -0
- data/lib/intacct_rest/result/success.rb +27 -0
- data/lib/intacct_rest/schema_generator.rb +69 -0
- data/lib/intacct_rest/schema_source.rb +49 -0
- data/lib/intacct_rest/token_store/memory.rb +36 -0
- data/lib/intacct_rest/validators/custom.rb +17 -0
- data/lib/intacct_rest/validators/inclusion.rb +16 -0
- data/lib/intacct_rest/validators/kind_of.rb +24 -0
- data/lib/intacct_rest/validators/presence.rb +13 -0
- data/lib/intacct_rest/validators.rb +33 -0
- data/lib/intacct_rest/version.rb +3 -0
- data/lib/intacct_rest.rb +75 -0
- metadata +152 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
class Client
|
|
5
|
+
HTTP_POST = 'Post'.freeze
|
|
6
|
+
HTTP_GET = 'Get'.freeze
|
|
7
|
+
HTTP_PUT = 'Put'.freeze
|
|
8
|
+
HTTP_DELETE = 'Delete'.freeze
|
|
9
|
+
HTTP_PATCH = 'Patch'.freeze
|
|
10
|
+
METHODS = {
|
|
11
|
+
post: HTTP_POST,
|
|
12
|
+
get: HTTP_GET,
|
|
13
|
+
put: HTTP_PUT,
|
|
14
|
+
patch: HTTP_PATCH,
|
|
15
|
+
delete: HTTP_DELETE
|
|
16
|
+
}
|
|
17
|
+
attr_reader :uri, :http, :net_request
|
|
18
|
+
|
|
19
|
+
def initialize(uri, method = METHODS.fetch(:post))
|
|
20
|
+
@method = METHODS.fetch(method.to_sym, HTTP_POST)
|
|
21
|
+
@uri = URI(uri)
|
|
22
|
+
@http = Net::HTTP.new(@uri.host, @uri.port)
|
|
23
|
+
@http.use_ssl = @uri.scheme == 'https'
|
|
24
|
+
@net_request = Object.const_get("Net::HTTP::#{@method}").new(@uri)
|
|
25
|
+
@body = {}
|
|
26
|
+
@headers = default_headers
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def body
|
|
30
|
+
@body
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def body=(value = {})
|
|
34
|
+
@body.merge!(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def add_header(name, value)
|
|
38
|
+
@headers[name] = value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def headers
|
|
42
|
+
@headers
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def request
|
|
46
|
+
net_request.body = body.to_json
|
|
47
|
+
headers.each_pair do |header, value|
|
|
48
|
+
net_request.add_field(header,value)
|
|
49
|
+
end
|
|
50
|
+
http.request(net_request)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def request_method
|
|
54
|
+
@method
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def default_headers
|
|
60
|
+
{
|
|
61
|
+
'CONTENT-TYPE' => 'application/json'
|
|
62
|
+
}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
module IntacctRest
|
|
2
|
+
class Configuration
|
|
3
|
+
DEFAULT_BASE_URL = "#{IntacctRest::API_URI}/#{IntacctRest::API_VERSION}".freeze
|
|
4
|
+
DEFAULT_TOKEN_PATH = '/oauth2/token'.freeze
|
|
5
|
+
DEFAULT_QUERY_PATH = '/services/core/query'.freeze
|
|
6
|
+
DEFAULT_PAGE_SIZE = 200
|
|
7
|
+
DEFAULT_MAX_PAGES = 50
|
|
8
|
+
DEFAULT_TOKEN_KEY_PREFIX = 'intacct_rest:oauth'.freeze
|
|
9
|
+
|
|
10
|
+
# Maps IntacctRest::Model::Vendor's Ruby (snake_case) accessor names to the
|
|
11
|
+
# exact camelCase JSON field names Intacct's vendor object uses. Kept
|
|
12
|
+
# as explicit pairs rather than derived by a camelCase<->snake_case
|
|
13
|
+
# regex, since a couple of fields have acronym runs a naive regex
|
|
14
|
+
# would mangle (overrideOffsetGLAccount -> override_offset_g_l_account
|
|
15
|
+
# instead of override_offset_gl_account). Excludes the deprecated
|
|
16
|
+
# `accountlabel` alias in favor of `accountLabel`.
|
|
17
|
+
DEFAULT_VENDOR_WRITABLE_ATTRIBUTES = {
|
|
18
|
+
id: 'id', name: 'name', is_one_time_use: 'isOneTimeUse', status: 'status', state: 'state',
|
|
19
|
+
vendor_type: 'vendorType', parent: 'parent', account_group: 'accountGroup',
|
|
20
|
+
account_label: 'accountLabel', default_lead_time: 'defaultLeadTime',
|
|
21
|
+
file_payment_service: 'filePaymentService', bank_files: 'bankFiles', tax_id: 'taxId',
|
|
22
|
+
tpar: 'tpar', t5018: 't5018', form1099: 'form1099', attachment: 'attachment',
|
|
23
|
+
is_on_hold: 'isOnHold', do_not_pay: 'doNotPay', always_create_bill: 'alwaysCreateBill',
|
|
24
|
+
credit_limit: 'creditLimit', retainage_percentage: 'retainagePercentage',
|
|
25
|
+
is_individual_person: 'isIndividualPerson', notes: 'notes', price_list: 'priceList',
|
|
26
|
+
price_schedule: 'priceSchedule', override_offset_gl_account: 'overrideOffsetGLAccount',
|
|
27
|
+
default_expense_gl_account: 'defaultExpenseGLAccount', discount_percent: 'discountPercent',
|
|
28
|
+
contacts: 'contacts', contact_list: 'contactList',
|
|
29
|
+
vendor_email_templates: 'vendorEmailTemplates', vendor_payment_providers: 'vendorPaymentProviders',
|
|
30
|
+
vendor_bank_file_setup: 'vendorBankFileSetup', vendor_account_number_list: 'vendorAccountNumberList',
|
|
31
|
+
preferred_payment_method: 'preferredPaymentMethod', merge_payment_requests: 'mergePaymentRequests',
|
|
32
|
+
send_payment_notification: 'sendPaymentNotification', billing_type: 'billingType',
|
|
33
|
+
payment_priority: 'paymentPriority', currency: 'currency',
|
|
34
|
+
display_term_discount_on_check_stub: 'displayTermDiscountOnCheckStub', term: 'term',
|
|
35
|
+
bill_payment: 'billPayment', vendor_account_number: 'vendorAccountNumber',
|
|
36
|
+
display_vendor_account_on_check_stub: 'displayVendorAccountOnCheckStub', ach: 'ach',
|
|
37
|
+
vendor_restriction: 'vendorRestriction', restricted_locations: 'restrictedLocations',
|
|
38
|
+
restricted_departments: 'restrictedDepartments'
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
# Maps IntacctRest::Model::Invoice's Ruby (snake_case) accessor names
|
|
42
|
+
# to the exact camelCase JSON field names Intacct's AR invoice object
|
|
43
|
+
# uses. Nested objects (customer, lines, currency, ...) stay raw
|
|
44
|
+
# Hashes — not individually modeled, same as Vendor's nested fields.
|
|
45
|
+
DEFAULT_INVOICE_WRITABLE_ATTRIBUTES = {
|
|
46
|
+
invoice_number: 'invoiceNumber', state: 'state', reference_number: 'referenceNumber',
|
|
47
|
+
description: 'description', invoice_date: 'invoiceDate', due_date: 'dueDate',
|
|
48
|
+
currency: 'currency', customer_message: 'customerMessage', contacts: 'contacts',
|
|
49
|
+
billback_template: 'billbackTemplate', attachment: 'attachment', customer: 'customer',
|
|
50
|
+
term: 'term', tax_solution: 'taxSolution', invoice_type: 'invoiceType',
|
|
51
|
+
invoice_mode: 'invoiceMode', invoice_summary: 'invoiceSummary', lines: 'lines'
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Maps IntacctRest::Model::Term's Ruby (snake_case) accessor names to
|
|
55
|
+
# the exact camelCase JSON field names Intacct's AR term object uses.
|
|
56
|
+
# due/discount/penalty stay raw Hashes — not individually modeled.
|
|
57
|
+
DEFAULT_TERM_WRITABLE_ATTRIBUTES = {
|
|
58
|
+
id: 'id', status: 'status', description: 'description', due: 'due',
|
|
59
|
+
discount: 'discount', penalty: 'penalty'
|
|
60
|
+
}.freeze
|
|
61
|
+
|
|
62
|
+
# Maps IntacctRest::Model::InvoiceLine's Ruby (snake_case) accessor
|
|
63
|
+
# names to the exact camelCase JSON field names Intacct's AR invoice
|
|
64
|
+
# line object uses. Nested objects (glAccount, dimensions, ...) stay
|
|
65
|
+
# raw Hashes — not individually modeled.
|
|
66
|
+
DEFAULT_INVOICE_LINE_WRITABLE_ATTRIBUTES = {
|
|
67
|
+
gl_account: 'glAccount', override_offset_gl_account: 'overrideOffsetGLAccount',
|
|
68
|
+
txn_amount: 'txnAmount', memo: 'memo', allocation: 'allocation',
|
|
69
|
+
account_label: 'accountLabel', tax_entries: 'taxEntries', dimensions: 'dimensions',
|
|
70
|
+
invoice: 'invoice'
|
|
71
|
+
}.freeze
|
|
72
|
+
|
|
73
|
+
# Maps IntacctRest::Model::Currency's Ruby (snake_case) accessor names
|
|
74
|
+
# to the exact camelCase JSON field names used wherever a currency
|
|
75
|
+
# object is nested (invoice header, invoice line, ...). Currency has
|
|
76
|
+
# no standalone create endpoint of its own in Intacct's REST API — this
|
|
77
|
+
# model exists purely to build/validate that nested Hash.
|
|
78
|
+
DEFAULT_CURRENCY_ATTRIBUTES = {
|
|
79
|
+
base_currency: 'baseCurrency', txn_currency: 'txnCurrency', exchange_rate: 'exchangeRate'
|
|
80
|
+
}.freeze
|
|
81
|
+
|
|
82
|
+
# Maps IntacctRest::Model::Customer's Ruby (snake_case) accessor names
|
|
83
|
+
# to the exact camelCase JSON field names Intacct's customer object
|
|
84
|
+
# uses. Excludes deprecated fields (electronicAddress, resaleNumber,
|
|
85
|
+
# enableOnlineACHPayment, enableOnlineCardPayment, activationDate) in
|
|
86
|
+
# favor of their replacements or because they're pure legacy no-ops —
|
|
87
|
+
# same treatment as Vendor's deprecated `accountlabel`.
|
|
88
|
+
DEFAULT_CUSTOMER_WRITABLE_ATTRIBUTES = {
|
|
89
|
+
id: 'id', name: 'name', status: 'status', customer_type: 'customerType', parent: 'parent',
|
|
90
|
+
sales_representative: 'salesRepresentative', tax_id: 'taxId',
|
|
91
|
+
default_revenue_gl_account: 'defaultRevenueGLAccount', shipping_method: 'shippingMethod',
|
|
92
|
+
credit_limit: 'creditLimit', is_on_hold: 'isOnHold', contacts: 'contacts',
|
|
93
|
+
contact_list: 'contactList', restrictions: 'restrictions',
|
|
94
|
+
customer_email_templates: 'customerEmailTemplates', discount_percent: 'discountPercent',
|
|
95
|
+
term: 'term', advance_bill_by: 'advanceBillBy', advance_bill_by_type: 'advanceBillByType',
|
|
96
|
+
customer_resale_number: 'customerResaleNumber', delivery_options: 'deliveryOptions',
|
|
97
|
+
override_price_list: 'overridePriceList', customer_message: 'customerMessage',
|
|
98
|
+
currency: 'currency', email_opt_in: 'emailOptIn', territory: 'territory',
|
|
99
|
+
is_one_time_use: 'isOneTimeUse', disable_refund: 'disableRefund', vendor: 'vendor',
|
|
100
|
+
account_group: 'accountGroup', account_label: 'accountLabel', attachment: 'attachment',
|
|
101
|
+
retainage_percentage: 'retainagePercentage', notes: 'notes', price_list: 'priceList',
|
|
102
|
+
price_schedule: 'priceSchedule', override_offset_gl_account: 'overrideOffsetGLAccount',
|
|
103
|
+
customer_restriction: 'customerRestriction', restricted_locations: 'restrictedLocations',
|
|
104
|
+
restricted_departments: 'restrictedDepartments', customer_account_health: 'customerAccountHealth'
|
|
105
|
+
}.freeze
|
|
106
|
+
|
|
107
|
+
# Maps IntacctRest::Model::Contact's Ruby (snake_case) accessor names
|
|
108
|
+
# to the exact camelCase JSON field names Intacct's contact object
|
|
109
|
+
# uses. Deliberately a small subset of the "contacts.default" nested
|
|
110
|
+
# shape (which appears identically inside both the vendor and customer
|
|
111
|
+
# specs) — every other field there (firstName, lastName, email1/2,
|
|
112
|
+
# phone1/2, mobile, pager, fax, url1/2, companyName, mailingAddress,
|
|
113
|
+
# ...) is individually marked deprecated: true in Intacct's schema, in
|
|
114
|
+
# favor of referencing an existing contact by id. Contact has no
|
|
115
|
+
# standalone create endpoint documented.
|
|
116
|
+
DEFAULT_CONTACT_ATTRIBUTES = {
|
|
117
|
+
id: 'id', show_in_contact_list: 'showInContactList', tax: 'tax',
|
|
118
|
+
electronic_invoice_details: 'electronicInvoiceDetails',
|
|
119
|
+
international_tax_id: 'internationalTaxId', electronic_address: 'electronicAddress'
|
|
120
|
+
}.freeze
|
|
121
|
+
|
|
122
|
+
# Maps IntacctRest::Model::Bill's Ruby (snake_case) accessor names to
|
|
123
|
+
# the exact camelCase JSON field names Intacct's AP bill object uses.
|
|
124
|
+
# Nested objects (vendor, term, currency, lines, ...) stay raw Hashes
|
|
125
|
+
# — not individually modeled, same as Invoice's nested fields.
|
|
126
|
+
# Excludes deprecated fields (`purchasing`, `location` — both marked
|
|
127
|
+
# deprecated: true in the schema in favor of `purchasingDocument` and
|
|
128
|
+
# nothing, respectively).
|
|
129
|
+
DEFAULT_BILL_WRITABLE_ATTRIBUTES = {
|
|
130
|
+
bill_number: 'billNumber', state: 'state', vendor: 'vendor', term: 'term',
|
|
131
|
+
reference_number: 'referenceNumber', description: 'description', posting_date: 'postingDate',
|
|
132
|
+
discount_cut_off_date: 'discountCutOffDate', due_date: 'dueDate',
|
|
133
|
+
recommended_payment_date: 'recommendedPaymentDate', created_date: 'createdDate',
|
|
134
|
+
is_on_hold: 'isOnHold', is_tax_inclusive: 'isTaxInclusive', payment_priority: 'paymentPriority',
|
|
135
|
+
contacts: 'contacts', currency: 'currency', tax_solution: 'taxSolution',
|
|
136
|
+
invoice_type: 'invoiceType', invoice_mode: 'invoiceMode', bill_summary: 'billSummary',
|
|
137
|
+
bill_back_template: 'billBackTemplate', attachment: 'attachment', lines: 'lines',
|
|
138
|
+
dispute: 'dispute', refuse: 'refuse'
|
|
139
|
+
}.freeze
|
|
140
|
+
|
|
141
|
+
# Maps IntacctRest::Model::BillLine's Ruby (snake_case) accessor names
|
|
142
|
+
# to the exact camelCase JSON field names Intacct's AP bill line
|
|
143
|
+
# object uses. Nested objects (glAccount, dimensions, ...) stay raw
|
|
144
|
+
# Hashes — not individually modeled.
|
|
145
|
+
DEFAULT_BILL_LINE_WRITABLE_ATTRIBUTES = {
|
|
146
|
+
vendor: 'vendor', gl_account: 'glAccount', override_offset_gl_account: 'overrideOffsetGLAccount',
|
|
147
|
+
account_label: 'accountLabel', txn_amount: 'txnAmount', total_txn_amount: 'totalTxnAmount',
|
|
148
|
+
memo: 'memo', allocation: 'allocation', has_form1099: 'hasForm1099', form1099: 'form1099',
|
|
149
|
+
release_to_pay: 'releaseToPay', project: 'project', fixed_asset: 'fixedAsset',
|
|
150
|
+
amortization_template: 'amortizationTemplate', amortization_start_date: 'amortizationStartDate',
|
|
151
|
+
amortization_end_date: 'amortizationEndDate', purchasing: 'purchasing',
|
|
152
|
+
tax_entries: 'taxEntries', dimensions: 'dimensions', bill: 'bill'
|
|
153
|
+
}.freeze
|
|
154
|
+
|
|
155
|
+
attr_accessor :client_id, :client_secret, :username, :base_url, :token_path,
|
|
156
|
+
:query_path, :token_store, :page_size, :max_pages, :token_key_prefix,
|
|
157
|
+
:on_error, :schema
|
|
158
|
+
|
|
159
|
+
def initialize
|
|
160
|
+
@base_url = DEFAULT_BASE_URL
|
|
161
|
+
@token_path = DEFAULT_TOKEN_PATH
|
|
162
|
+
@query_path = DEFAULT_QUERY_PATH
|
|
163
|
+
@page_size = DEFAULT_PAGE_SIZE
|
|
164
|
+
@max_pages = DEFAULT_MAX_PAGES
|
|
165
|
+
@token_key_prefix = DEFAULT_TOKEN_KEY_PREFIX
|
|
166
|
+
@token_store = IntacctRest::TokenStore::Memory.new
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
class CustomField
|
|
5
|
+
DEFAULT_NAMESPACE = 'nsp'
|
|
6
|
+
|
|
7
|
+
attr_accessor :namespace, :name, :value
|
|
8
|
+
|
|
9
|
+
def initialize(name:, value: nil, namespace: DEFAULT_NAMESPACE)
|
|
10
|
+
@namespace = namespace
|
|
11
|
+
@name = name
|
|
12
|
+
@value = value
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def key
|
|
16
|
+
"#{namespace}::#{name}"
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
class CreateBill
|
|
6
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(bill:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration, token_provider: nil)
|
|
9
|
+
raise ArgumentError, 'bill must be an IntacctRest::Model::Bill' unless bill.is_a?(IntacctRest::Model::Bill)
|
|
10
|
+
|
|
11
|
+
result = IntacctRest::Post.call(bill, config: config, token_provider: token_provider)
|
|
12
|
+
verify_expected_fields!(result, results) if result.success?
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
17
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
18
|
+
return if missing.empty?
|
|
19
|
+
|
|
20
|
+
raise IntacctRest::ApiError.new(
|
|
21
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
22
|
+
http_status: result.code, body: result.body
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
private_class_method :verify_expected_fields!
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
class CreateBillLine
|
|
6
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(bill_line:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration,
|
|
9
|
+
token_provider: nil)
|
|
10
|
+
unless bill_line.is_a?(IntacctRest::Model::BillLine)
|
|
11
|
+
raise ArgumentError, 'bill_line must be an IntacctRest::Model::BillLine'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
result = IntacctRest::Post.call(bill_line, config: config, token_provider: token_provider)
|
|
15
|
+
verify_expected_fields!(result, results) if result.success?
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
20
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
21
|
+
return if missing.empty?
|
|
22
|
+
|
|
23
|
+
raise IntacctRest::ApiError.new(
|
|
24
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
25
|
+
http_status: result.code, body: result.body
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
private_class_method :verify_expected_fields!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
class CreateCustomer
|
|
6
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
7
|
+
|
|
8
|
+
# results: fields this endpoint expects a successful response to
|
|
9
|
+
# contain — checked after the fact (raises ApiError if Intacct's
|
|
10
|
+
# response is missing one), not a filter on what gets written back.
|
|
11
|
+
# model.apply_result already decides what it absorbs from any
|
|
12
|
+
# successful result.
|
|
13
|
+
def self.call(customer:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration, token_provider: nil)
|
|
14
|
+
unless customer.is_a?(IntacctRest::Model::Customer)
|
|
15
|
+
raise ArgumentError, 'customer must be an IntacctRest::Model::Customer'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
result = IntacctRest::Post.call(customer, config: config, token_provider: token_provider)
|
|
19
|
+
verify_expected_fields!(result, results) if result.success?
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
24
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
25
|
+
return if missing.empty?
|
|
26
|
+
|
|
27
|
+
raise IntacctRest::ApiError.new(
|
|
28
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
29
|
+
http_status: result.code, body: result.body
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
private_class_method :verify_expected_fields!
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
class CreateInvoice
|
|
6
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
7
|
+
|
|
8
|
+
# results: fields this endpoint expects a successful response to
|
|
9
|
+
# contain — checked after the fact (raises ApiError if Intacct's
|
|
10
|
+
# response is missing one), not a filter on what gets written back.
|
|
11
|
+
# model.apply_result already decides what it absorbs from any
|
|
12
|
+
# successful result.
|
|
13
|
+
def self.call(invoice:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration, token_provider: nil)
|
|
14
|
+
unless invoice.is_a?(IntacctRest::Model::Invoice)
|
|
15
|
+
raise ArgumentError, 'invoice must be an IntacctRest::Model::Invoice'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
result = IntacctRest::Post.call(invoice, config: config, token_provider: token_provider)
|
|
19
|
+
verify_expected_fields!(result, results) if result.success?
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
24
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
25
|
+
return if missing.empty?
|
|
26
|
+
|
|
27
|
+
raise IntacctRest::ApiError.new(
|
|
28
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
29
|
+
http_status: result.code, body: result.body
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
private_class_method :verify_expected_fields!
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
class CreateInvoiceLine
|
|
6
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(invoice_line:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration,
|
|
9
|
+
token_provider: nil)
|
|
10
|
+
unless invoice_line.is_a?(IntacctRest::Model::InvoiceLine)
|
|
11
|
+
raise ArgumentError, 'invoice_line must be an IntacctRest::Model::InvoiceLine'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
result = IntacctRest::Post.call(invoice_line, config: config, token_provider: token_provider)
|
|
15
|
+
verify_expected_fields!(result, results) if result.success?
|
|
16
|
+
result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
20
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
21
|
+
return if missing.empty?
|
|
22
|
+
|
|
23
|
+
raise IntacctRest::ApiError.new(
|
|
24
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
25
|
+
http_status: result.code, body: result.body
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
private_class_method :verify_expected_fields!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
class CreateTerm
|
|
6
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
7
|
+
|
|
8
|
+
def self.call(term:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration, token_provider: nil)
|
|
9
|
+
raise ArgumentError, 'term must be an IntacctRest::Model::Term' unless term.is_a?(IntacctRest::Model::Term)
|
|
10
|
+
|
|
11
|
+
result = IntacctRest::Post.call(term, config: config, token_provider: token_provider)
|
|
12
|
+
verify_expected_fields!(result, results) if result.success?
|
|
13
|
+
result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
17
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
18
|
+
return if missing.empty?
|
|
19
|
+
|
|
20
|
+
raise IntacctRest::ApiError.new(
|
|
21
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
22
|
+
http_status: result.code, body: result.body
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
private_class_method :verify_expected_fields!
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
class CreateVendor
|
|
6
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
7
|
+
|
|
8
|
+
# results: fields this endpoint expects a successful response to
|
|
9
|
+
# contain — checked after the fact (raises ApiError if Intacct's
|
|
10
|
+
# response is missing one), not a filter on what gets written back.
|
|
11
|
+
# model.apply_result already decides what it absorbs from any
|
|
12
|
+
# successful result.
|
|
13
|
+
def self.call(vendor:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration, token_provider: nil)
|
|
14
|
+
unless vendor.is_a?(IntacctRest::Model::Vendor)
|
|
15
|
+
raise ArgumentError, 'vendor must be an IntacctRest::Model::Vendor'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
result = IntacctRest::Post.call(vendor, config: config, token_provider: token_provider)
|
|
19
|
+
verify_expected_fields!(result, results) if result.success?
|
|
20
|
+
result
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
24
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
25
|
+
return if missing.empty?
|
|
26
|
+
|
|
27
|
+
raise IntacctRest::ApiError.new(
|
|
28
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
29
|
+
http_status: result.code, body: result.body
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
private_class_method :verify_expected_fields!
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Endpoints
|
|
5
|
+
# PATCH /objects/accounts-payable/vendor/{key}. vendor.key picks the
|
|
6
|
+
# record; only the fields set on the model are sent, so anything left
|
|
7
|
+
# nil stays unchanged in Intacct.
|
|
8
|
+
class UpdateVendor
|
|
9
|
+
DEFAULT_RESULT_FIELDS = %i[id key href].freeze
|
|
10
|
+
|
|
11
|
+
# results: fields this endpoint expects a successful response to
|
|
12
|
+
# contain — checked after the fact (raises ApiError if Intacct's
|
|
13
|
+
# response is missing one), not a filter on what gets written back.
|
|
14
|
+
# model.apply_result already decides what it absorbs from any
|
|
15
|
+
# successful result.
|
|
16
|
+
def self.call(vendor:, results: DEFAULT_RESULT_FIELDS, config: IntacctRest.configuration, token_provider: nil)
|
|
17
|
+
unless vendor.is_a?(IntacctRest::Model::Vendor)
|
|
18
|
+
raise ArgumentError, 'vendor must be an IntacctRest::Model::Vendor'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
result = IntacctRest::Patch.call(vendor, config: config, token_provider: token_provider)
|
|
22
|
+
verify_expected_fields!(result, results) if result.success?
|
|
23
|
+
result
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.verify_expected_fields!(result, expected_fields)
|
|
27
|
+
missing = expected_fields.reject { |field| result.respond_to?(field) }
|
|
28
|
+
return if missing.empty?
|
|
29
|
+
|
|
30
|
+
raise IntacctRest::ApiError.new(
|
|
31
|
+
"Expected result fields missing from response: #{missing.join(', ')}",
|
|
32
|
+
http_status: result.code, body: result.body
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
private_class_method :verify_expected_fields!
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
# Lets Configuration#on_error see an error right before the gem raises it.
|
|
5
|
+
# Used only where an error originates (a failed request, an unparseable
|
|
6
|
+
# response, an unreadable schema file), so each error reaches the hook once.
|
|
7
|
+
#
|
|
8
|
+
# Including classes must expose a private `config` reader (same implicit
|
|
9
|
+
# contract as AuthenticatedRequest).
|
|
10
|
+
module ErrorReporting
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
# Raises error exactly as `raise error` would at the call site (same
|
|
14
|
+
# class, message, backtrace and cause), after passing it to on_error
|
|
15
|
+
# with context: — a Hash of safe scalars, never credentials or bodies.
|
|
16
|
+
def raise_reported(error, **context)
|
|
17
|
+
error.set_backtrace(caller) unless error.backtrace
|
|
18
|
+
raise error
|
|
19
|
+
rescue IntacctRest::Error
|
|
20
|
+
begin
|
|
21
|
+
config.on_error&.call(error, context: context)
|
|
22
|
+
rescue StandardError
|
|
23
|
+
nil # a failing hook must never replace the error the caller gets
|
|
24
|
+
end
|
|
25
|
+
raise
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class AuthenticationError < Error; end
|
|
7
|
+
|
|
8
|
+
class ResponseParseError < Error
|
|
9
|
+
attr_reader :raw_body
|
|
10
|
+
|
|
11
|
+
def initialize(message, raw_body: nil)
|
|
12
|
+
super(message)
|
|
13
|
+
@raw_body = raw_body
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class ApiError < Error
|
|
18
|
+
attr_reader :http_status, :body
|
|
19
|
+
|
|
20
|
+
def initialize(message, http_status: nil, body: nil)
|
|
21
|
+
super(message)
|
|
22
|
+
@http_status = http_status
|
|
23
|
+
@body = body
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class ValidationError < Error
|
|
28
|
+
attr_reader :attributes
|
|
29
|
+
|
|
30
|
+
def initialize(message, attributes: [])
|
|
31
|
+
super(message)
|
|
32
|
+
@attributes = attributes
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class TooManyPagesError < Error
|
|
37
|
+
attr_reader :pages_fetched
|
|
38
|
+
|
|
39
|
+
def initialize(message, pages_fetched:)
|
|
40
|
+
super(message)
|
|
41
|
+
@pages_fetched = pages_fetched
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class SchemaGenerationError < Error; end
|
|
46
|
+
|
|
47
|
+
class SchemaLoadError < Error; end
|
|
48
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
# Builds Intacct's REST query filter fragments. Intacct expects `filters`
|
|
5
|
+
# as an array of single-key hashes that are implicitly AND'd together.
|
|
6
|
+
module Filter
|
|
7
|
+
OPERATORS = {
|
|
8
|
+
equalto: '$eq',
|
|
9
|
+
notequalto: '$ne',
|
|
10
|
+
greaterthan: '$gt',
|
|
11
|
+
greaterthanorequalto: '$gte',
|
|
12
|
+
lessthan: '$lt',
|
|
13
|
+
lessthanorequalto: '$lte'
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
def eq(field, value) = operator(:equalto, field, value)
|
|
18
|
+
def not_eq(field, value) = operator(:notequalto, field, value)
|
|
19
|
+
def gt(field, value) = operator(:greaterthan, field, value)
|
|
20
|
+
def gte(field, value) = operator(:greaterthanorequalto, field, value)
|
|
21
|
+
def lt(field, value) = operator(:lessthan, field, value)
|
|
22
|
+
def lte(field, value) = operator(:lessthanorequalto, field, value)
|
|
23
|
+
|
|
24
|
+
# Filters are implicitly AND'd by Intacct — this just documents intent at call sites.
|
|
25
|
+
def and(*fragments) = fragments.flatten
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def operator(name, field, value)
|
|
30
|
+
{ OPERATORS.fetch(name) => { field.to_s => value } }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|