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,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Model
|
|
5
|
+
# Shared by any IntacctRest::Model class: a small `validate` macro plus
|
|
6
|
+
# #errors/#valid?. Not tied to Vendor specifically — Customer, Bill,
|
|
7
|
+
# etc. can reuse this the same way.
|
|
8
|
+
#
|
|
9
|
+
# class Thing < IntacctRest::Model::Base
|
|
10
|
+
# attr_accessor :name
|
|
11
|
+
# validate :presence, [:name]
|
|
12
|
+
# end
|
|
13
|
+
class Base
|
|
14
|
+
class << self
|
|
15
|
+
# kind: a key registered in IntacctRest::Validators (:presence,
|
|
16
|
+
# :kind_of, :inclusion, ...). Trailing positional args before the
|
|
17
|
+
# final Array of attribute names are passed through to that
|
|
18
|
+
# validator (e.g. the type symbol for :kind_of, the list for
|
|
19
|
+
# :inclusion).
|
|
20
|
+
#
|
|
21
|
+
# on: an optional operation context (:create, :update, ...). A
|
|
22
|
+
# validation declared with on: only runs when #errors/#valid? is
|
|
23
|
+
# called with that same context; without on: it always runs.
|
|
24
|
+
def validate(kind, *args, on: nil)
|
|
25
|
+
attributes = Array(args.pop)
|
|
26
|
+
validators << { kind: kind, attributes: attributes, options: args, on: on }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def validators
|
|
30
|
+
@validators ||= superclass.respond_to?(:validators) ? superclass.validators.dup : []
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def errors(context = nil)
|
|
35
|
+
self.class.validators.flat_map do |spec|
|
|
36
|
+
next [] unless spec[:on].nil? || spec[:on] == context
|
|
37
|
+
|
|
38
|
+
validator = IntacctRest::Validators.fetch(spec[:kind])
|
|
39
|
+
spec[:attributes].flat_map { |attribute| validator.call(self, attribute, *spec[:options]) }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def valid?(context = nil)
|
|
44
|
+
errors(context).empty?
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Model
|
|
5
|
+
# An AP bill's data and validations only — no config, no token
|
|
6
|
+
# provider, no HTTP. Build and inspect one freely; IntacctRest::Post
|
|
7
|
+
# (via IntacctRest::Endpoints::CreateBill) is what actually sends it.
|
|
8
|
+
#
|
|
9
|
+
# Nested objects (vendor, term, currency, lines, ...) are NOT
|
|
10
|
+
# individually modeled — pass them as raw Hashes using Intacct's
|
|
11
|
+
# native (camelCase) nested key names, e.g.:
|
|
12
|
+
#
|
|
13
|
+
# vendor: { "id" => "1099 Int" }
|
|
14
|
+
# lines: [{ "txnAmount" => "5", "glAccount" => { "id" => "6000" } }]
|
|
15
|
+
#
|
|
16
|
+
# Nested required fields (vendor.id, currency.txnCurrency,
|
|
17
|
+
# lines[].txnAmount/glAccount, lines[].dimensions.location, ...) are
|
|
18
|
+
# not validated client-side — only top-level presence/type is.
|
|
19
|
+
# Intacct's own 400 response is the source of truth for those, same
|
|
20
|
+
# boundary already accepted for Invoice/Customer/Vendor's nested
|
|
21
|
+
# objects.
|
|
22
|
+
class Bill < IntacctRest::Model::Base
|
|
23
|
+
INTACCT_OBJECT = '/objects/accounts-payable/bill'.freeze
|
|
24
|
+
|
|
25
|
+
WRITABLE_ATTRIBUTES = IntacctRest::Configuration::DEFAULT_BILL_WRITABLE_ATTRIBUTES
|
|
26
|
+
|
|
27
|
+
# Set by IntacctRest::Post after a successful create — writable here
|
|
28
|
+
# even though Intacct itself treats them as read-only.
|
|
29
|
+
READONLY_ATTRIBUTES = %i[
|
|
30
|
+
key id href module_name record_type due_in_days total_base_amount total_base_amount_due
|
|
31
|
+
total_txn_amount total_txn_amount_due is_system_generated purchasing_document
|
|
32
|
+
recurring_schedule e_invoice_status import_status import_error_message recipient_email
|
|
33
|
+
sender_email anomaly_code document_source source_module payment_information retainage
|
|
34
|
+
customer_refund entity web_url audit
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
attr_accessor(*WRITABLE_ATTRIBUTES.keys)
|
|
38
|
+
attr_accessor(*READONLY_ATTRIBUTES)
|
|
39
|
+
attr_reader :custom_fields
|
|
40
|
+
|
|
41
|
+
validate :presence, %i[due_date created_date]
|
|
42
|
+
|
|
43
|
+
validate :kind_of, :string, %i[
|
|
44
|
+
bill_number state reference_number description posting_date discount_cut_off_date
|
|
45
|
+
due_date recommended_payment_date created_date invoice_type invoice_mode
|
|
46
|
+
]
|
|
47
|
+
validate :kind_of, :boolean, %i[is_on_hold is_tax_inclusive]
|
|
48
|
+
validate :kind_of, :hash, %i[
|
|
49
|
+
vendor term contacts currency tax_solution bill_summary bill_back_template attachment
|
|
50
|
+
dispute refuse
|
|
51
|
+
]
|
|
52
|
+
validate :kind_of, :array, %i[lines]
|
|
53
|
+
|
|
54
|
+
# source: an arbitrary domain object (ActiveRecord record, OpenStruct,
|
|
55
|
+
# another Model::Bill, ...) to pull matching attributes off of via
|
|
56
|
+
# duck-typing — any WRITABLE_ATTRIBUTES/READONLY_ATTRIBUTES key it
|
|
57
|
+
# responds to is copied over. Explicit keyword attributes always win
|
|
58
|
+
# over whatever the source provided.
|
|
59
|
+
def initialize(source = nil, custom_fields: [], **attributes)
|
|
60
|
+
self.custom_fields = custom_fields
|
|
61
|
+
merged = source ? attributes_from(source).merge(attributes) : attributes
|
|
62
|
+
merged.each { |key, value| public_send(:"#{key}=", value) if respond_to?(:"#{key}=") }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def custom_fields=(value)
|
|
66
|
+
@custom_fields = normalize_custom_fields(value)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def intacct_object
|
|
70
|
+
self.class::INTACCT_OBJECT
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Ruby-side attributes (unmapped, no custom fields) — useful for
|
|
74
|
+
# logging/introspection without the camelCase JSON translation.
|
|
75
|
+
def attributes
|
|
76
|
+
WRITABLE_ATTRIBUTES.keys.each_with_object({}) { |attr, hash| hash[attr] = public_send(attr) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def payload
|
|
80
|
+
WRITABLE_ATTRIBUTES.each_with_object({}) do |(attr, json_key), hash|
|
|
81
|
+
value = public_send(attr)
|
|
82
|
+
hash[json_key] = value unless value.nil?
|
|
83
|
+
end.merge(custom_fields_payload)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Called by IntacctRest::Post after a successful create.
|
|
87
|
+
def apply_result(result)
|
|
88
|
+
data = result.result
|
|
89
|
+
self.key = data['key'] if data['key']
|
|
90
|
+
self.href = data['href'] if data['href']
|
|
91
|
+
self.id = data['id'] if data['id'] && id.nil?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def attributes_from(source)
|
|
97
|
+
(WRITABLE_ATTRIBUTES.keys + READONLY_ATTRIBUTES).each_with_object({}) do |attr, hash|
|
|
98
|
+
hash[attr] = source.public_send(attr) if source.respond_to?(attr)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def normalize_custom_fields(value)
|
|
103
|
+
case value
|
|
104
|
+
when Array
|
|
105
|
+
value.map { |v| v.is_a?(IntacctRest::CustomField) ? v : IntacctRest::CustomField.new(**v) }
|
|
106
|
+
when Hash
|
|
107
|
+
value.map { |name, val| IntacctRest::CustomField.new(name: name.to_s, value: val) }
|
|
108
|
+
when nil
|
|
109
|
+
[]
|
|
110
|
+
else
|
|
111
|
+
raise ArgumentError, "Unsupported custom_fields value: #{value.class}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def custom_fields_payload
|
|
116
|
+
custom_fields.each_with_object({}) { |field, hash| hash[field.key] = field.value }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Model
|
|
5
|
+
# An AP bill line's data and validations only — no config, no token
|
|
6
|
+
# provider, no HTTP. Build and inspect one freely; IntacctRest::Post
|
|
7
|
+
# (via IntacctRest::Endpoints::CreateBillLine) is what actually sends
|
|
8
|
+
# it.
|
|
9
|
+
#
|
|
10
|
+
# This is a standalone-creatable object (POST
|
|
11
|
+
# /objects/accounts-payable/bill-line) distinct from the raw line-item
|
|
12
|
+
# Hashes accepted by Model::Bill#lines — Intacct allows both: send
|
|
13
|
+
# lines inline with the bill, or create them separately against an
|
|
14
|
+
# existing bill via `bill: { "key" => "..." }`.
|
|
15
|
+
#
|
|
16
|
+
# Nested objects (glAccount, dimensions, allocation, ...) are NOT
|
|
17
|
+
# individually modeled — pass them as raw Hashes using Intacct's
|
|
18
|
+
# native (camelCase) nested key names.
|
|
19
|
+
class BillLine < IntacctRest::Model::Base
|
|
20
|
+
INTACCT_OBJECT = '/objects/accounts-payable/bill-line'.freeze
|
|
21
|
+
|
|
22
|
+
WRITABLE_ATTRIBUTES = IntacctRest::Configuration::DEFAULT_BILL_LINE_WRITABLE_ATTRIBUTES
|
|
23
|
+
|
|
24
|
+
# Set by IntacctRest::Post after a successful create — writable here
|
|
25
|
+
# even though Intacct itself treats them as read-only.
|
|
26
|
+
READONLY_ATTRIBUTES = %i[
|
|
27
|
+
key id href line_number bill_amount base_amount currency payment_information
|
|
28
|
+
is_sub_total base_location retainage created_date audit
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
attr_accessor(*WRITABLE_ATTRIBUTES.keys)
|
|
32
|
+
attr_accessor(*READONLY_ATTRIBUTES)
|
|
33
|
+
attr_reader :custom_fields
|
|
34
|
+
|
|
35
|
+
validate :presence, %i[bill txn_amount gl_account]
|
|
36
|
+
validate :kind_of, :string, %i[txn_amount total_txn_amount memo has_form1099]
|
|
37
|
+
validate :kind_of, :boolean, %i[release_to_pay]
|
|
38
|
+
validate :kind_of, :hash, %i[
|
|
39
|
+
vendor gl_account override_offset_gl_account account_label allocation form1099 project
|
|
40
|
+
fixed_asset amortization_template purchasing dimensions bill
|
|
41
|
+
]
|
|
42
|
+
validate :kind_of, :array, %i[tax_entries]
|
|
43
|
+
|
|
44
|
+
def initialize(source = nil, custom_fields: [], **attributes)
|
|
45
|
+
self.custom_fields = custom_fields
|
|
46
|
+
merged = source ? attributes_from(source).merge(attributes) : attributes
|
|
47
|
+
merged.each { |key, value| public_send(:"#{key}=", value) if respond_to?(:"#{key}=") }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def custom_fields=(value)
|
|
51
|
+
@custom_fields = normalize_custom_fields(value)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def intacct_object
|
|
55
|
+
self.class::INTACCT_OBJECT
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def attributes
|
|
59
|
+
WRITABLE_ATTRIBUTES.keys.each_with_object({}) { |attr, hash| hash[attr] = public_send(attr) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def payload
|
|
63
|
+
WRITABLE_ATTRIBUTES.each_with_object({}) do |(attr, json_key), hash|
|
|
64
|
+
value = public_send(attr)
|
|
65
|
+
hash[json_key] = value unless value.nil?
|
|
66
|
+
end.merge(custom_fields_payload)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Called by IntacctRest::Post after a successful create.
|
|
70
|
+
def apply_result(result)
|
|
71
|
+
data = result.result
|
|
72
|
+
self.key = data['key'] if data['key']
|
|
73
|
+
self.href = data['href'] if data['href']
|
|
74
|
+
self.id = data['id'] if data['id'] && id.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def attributes_from(source)
|
|
80
|
+
(WRITABLE_ATTRIBUTES.keys + READONLY_ATTRIBUTES).each_with_object({}) do |attr, hash|
|
|
81
|
+
hash[attr] = source.public_send(attr) if source.respond_to?(attr)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def normalize_custom_fields(value)
|
|
86
|
+
case value
|
|
87
|
+
when Array
|
|
88
|
+
value.map { |v| v.is_a?(IntacctRest::CustomField) ? v : IntacctRest::CustomField.new(**v) }
|
|
89
|
+
when Hash
|
|
90
|
+
value.map { |name, val| IntacctRest::CustomField.new(name: name.to_s, value: val) }
|
|
91
|
+
when nil
|
|
92
|
+
[]
|
|
93
|
+
else
|
|
94
|
+
raise ArgumentError, "Unsupported custom_fields value: #{value.class}"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def custom_fields_payload
|
|
99
|
+
custom_fields.each_with_object({}) { |field, hash| hash[field.key] = field.value }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Model
|
|
5
|
+
# A contact's data and validations only — no config, no token
|
|
6
|
+
# provider, no HTTP, and no create endpoint of its own (Intacct has no
|
|
7
|
+
# standalone "create a contact" endpoint documented; contacts are
|
|
8
|
+
# referenced by id from vendor/customer's `contacts`/`contact_list`
|
|
9
|
+
# fields, which stay raw Hashes there). This is a small typed helper
|
|
10
|
+
# for building that nested payload with validation.
|
|
11
|
+
class Contact < IntacctRest::Model::Base
|
|
12
|
+
ATTRIBUTES = IntacctRest::Configuration::DEFAULT_CONTACT_ATTRIBUTES
|
|
13
|
+
|
|
14
|
+
attr_accessor(*ATTRIBUTES.keys)
|
|
15
|
+
|
|
16
|
+
validate :presence, %i[id]
|
|
17
|
+
validate :kind_of, :string, %i[id international_tax_id]
|
|
18
|
+
validate :kind_of, :boolean, %i[show_in_contact_list]
|
|
19
|
+
validate :kind_of, :hash, %i[tax electronic_invoice_details electronic_address]
|
|
20
|
+
|
|
21
|
+
# source: an arbitrary domain object (ActiveRecord record, OpenStruct,
|
|
22
|
+
# another Model::Contact, ...) to pull matching attributes off of via
|
|
23
|
+
# duck-typing. Explicit keyword attributes always win over whatever
|
|
24
|
+
# the source provided.
|
|
25
|
+
def initialize(source = nil, **attributes)
|
|
26
|
+
merged = source ? attributes_from(source).merge(attributes) : attributes
|
|
27
|
+
merged.each { |key, value| public_send(:"#{key}=", value) if respond_to?(:"#{key}=") }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Ruby-side attributes (unmapped) — useful for logging/introspection
|
|
31
|
+
# without the camelCase JSON translation.
|
|
32
|
+
def attributes
|
|
33
|
+
ATTRIBUTES.keys.each_with_object({}) { |attr, hash| hash[attr] = public_send(attr) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def payload
|
|
37
|
+
ATTRIBUTES.each_with_object({}) do |(attr, json_key), hash|
|
|
38
|
+
value = public_send(attr)
|
|
39
|
+
hash[json_key] = value unless value.nil?
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def attributes_from(source)
|
|
46
|
+
ATTRIBUTES.keys.each_with_object({}) do |attr, hash|
|
|
47
|
+
hash[attr] = source.public_send(attr) if source.respond_to?(attr)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Model
|
|
5
|
+
# Currency details nested inside an invoice header or invoice line
|
|
6
|
+
# (baseCurrency/txnCurrency/exchangeRate). Intacct has no standalone
|
|
7
|
+
# "create a currency" endpoint anywhere in its REST API — this exists
|
|
8
|
+
# purely as a small, validated way to build that nested Hash, e.g.:
|
|
9
|
+
#
|
|
10
|
+
# currency = IntacctRest::Model::Currency.new(txn_currency: "EUR")
|
|
11
|
+
# invoice.currency = currency.payload
|
|
12
|
+
#
|
|
13
|
+
# exchangeRate stays a raw Hash — not individually modeled.
|
|
14
|
+
class Currency < IntacctRest::Model::Base
|
|
15
|
+
ATTRIBUTES = IntacctRest::Configuration::DEFAULT_CURRENCY_ATTRIBUTES
|
|
16
|
+
|
|
17
|
+
attr_accessor(*ATTRIBUTES.keys)
|
|
18
|
+
|
|
19
|
+
validate :kind_of, :string, %i[base_currency txn_currency]
|
|
20
|
+
validate :kind_of, :hash, %i[exchange_rate]
|
|
21
|
+
|
|
22
|
+
# source: an arbitrary domain object to pull matching attributes off
|
|
23
|
+
# of via duck-typing. Explicit keyword attributes always win.
|
|
24
|
+
def initialize(source = nil, **attributes)
|
|
25
|
+
merged = source ? attributes_from(source).merge(attributes) : attributes
|
|
26
|
+
merged.each { |key, value| public_send(:"#{key}=", value) if respond_to?(:"#{key}=") }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def attributes
|
|
30
|
+
ATTRIBUTES.keys.each_with_object({}) { |attr, hash| hash[attr] = public_send(attr) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The camelCase Hash to embed into a parent's payload.
|
|
34
|
+
def payload
|
|
35
|
+
ATTRIBUTES.each_with_object({}) do |(attr, json_key), hash|
|
|
36
|
+
value = public_send(attr)
|
|
37
|
+
hash[json_key] = value unless value.nil?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def attributes_from(source)
|
|
44
|
+
ATTRIBUTES.keys.each_with_object({}) do |attr, hash|
|
|
45
|
+
hash[attr] = source.public_send(attr) if source.respond_to?(attr)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Model
|
|
5
|
+
# A customer's data and validations only — no config, no token
|
|
6
|
+
# provider, no HTTP. Build and inspect one freely; IntacctRest::Post
|
|
7
|
+
# (via IntacctRest::Endpoints::CreateCustomer) is what actually sends it.
|
|
8
|
+
#
|
|
9
|
+
# Nested objects (contacts, term, vendor, ...) are NOT individually
|
|
10
|
+
# modeled — pass them as raw Hashes using Intacct's native (camelCase)
|
|
11
|
+
# nested key names, e.g. term: { "id" => "Net 15" }. `vendor` is how a
|
|
12
|
+
# customer references an associated vendor record, same raw-Hash
|
|
13
|
+
# pattern: vendor: { "id" => "V-00014" }.
|
|
14
|
+
class Customer < IntacctRest::Model::Base
|
|
15
|
+
INTACCT_OBJECT = '/objects/accounts-receivable/customer'.freeze
|
|
16
|
+
|
|
17
|
+
WRITABLE_ATTRIBUTES = IntacctRest::Configuration::DEFAULT_CUSTOMER_WRITABLE_ATTRIBUTES
|
|
18
|
+
|
|
19
|
+
# Set by IntacctRest::Post after a successful create — writable here
|
|
20
|
+
# even though Intacct itself treats them as read-only, since this is
|
|
21
|
+
# the only place that result gets recorded.
|
|
22
|
+
READONLY_ATTRIBUTES = %i[
|
|
23
|
+
key href total_due last_invoice_created_date last_statement_generated_date
|
|
24
|
+
subscription_end_date entity web_url audit
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
attr_accessor(*WRITABLE_ATTRIBUTES.keys)
|
|
28
|
+
attr_accessor(*READONLY_ATTRIBUTES)
|
|
29
|
+
attr_reader :custom_fields
|
|
30
|
+
|
|
31
|
+
validate :presence, %i[name]
|
|
32
|
+
|
|
33
|
+
validate :kind_of, :string, %i[
|
|
34
|
+
id name status tax_id discount_percent advance_bill_by advance_bill_by_type
|
|
35
|
+
customer_resale_number delivery_options override_price_list currency notes
|
|
36
|
+
customer_restriction
|
|
37
|
+
]
|
|
38
|
+
validate :kind_of, :boolean, %i[is_on_hold email_opt_in is_one_time_use disable_refund]
|
|
39
|
+
validate :kind_of, :numeric, %i[credit_limit retainage_percentage]
|
|
40
|
+
validate :kind_of, :hash, %i[
|
|
41
|
+
customer_type parent sales_representative default_revenue_gl_account shipping_method
|
|
42
|
+
contacts restrictions term customer_message territory vendor account_group
|
|
43
|
+
account_label attachment price_list price_schedule override_offset_gl_account
|
|
44
|
+
customer_account_health
|
|
45
|
+
]
|
|
46
|
+
validate :kind_of, :array, %i[
|
|
47
|
+
contact_list customer_email_templates restricted_locations restricted_departments
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# source: an arbitrary domain object (ActiveRecord record, OpenStruct,
|
|
51
|
+
# another Model::Customer, ...) to pull matching attributes off of via
|
|
52
|
+
# duck-typing — any WRITABLE_ATTRIBUTES/READONLY_ATTRIBUTES key it
|
|
53
|
+
# responds to is copied over. Explicit keyword attributes always win
|
|
54
|
+
# over whatever the source provided.
|
|
55
|
+
def initialize(source = nil, custom_fields: [], **attributes)
|
|
56
|
+
self.custom_fields = custom_fields
|
|
57
|
+
merged = source ? attributes_from(source).merge(attributes) : attributes
|
|
58
|
+
merged.each { |key, value| public_send(:"#{key}=", value) if respond_to?(:"#{key}=") }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def custom_fields=(value)
|
|
62
|
+
@custom_fields = normalize_custom_fields(value)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def intacct_object
|
|
66
|
+
self.class::INTACCT_OBJECT
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Ruby-side attributes (unmapped, no custom fields) — useful for
|
|
70
|
+
# logging/introspection without the camelCase JSON translation.
|
|
71
|
+
def attributes
|
|
72
|
+
WRITABLE_ATTRIBUTES.keys.each_with_object({}) { |attr, hash| hash[attr] = public_send(attr) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def payload
|
|
76
|
+
WRITABLE_ATTRIBUTES.each_with_object({}) do |(attr, json_key), hash|
|
|
77
|
+
value = public_send(attr)
|
|
78
|
+
hash[json_key] = value unless value.nil?
|
|
79
|
+
end.merge(custom_fields_payload)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Called by IntacctRest::Post after a successful create.
|
|
83
|
+
def apply_result(result)
|
|
84
|
+
data = result.result
|
|
85
|
+
self.key = data['key'] if data['key']
|
|
86
|
+
self.href = data['href'] if data['href']
|
|
87
|
+
self.id = data['id'] if data['id'] && id.nil?
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def attributes_from(source)
|
|
93
|
+
(WRITABLE_ATTRIBUTES.keys + READONLY_ATTRIBUTES).each_with_object({}) do |attr, hash|
|
|
94
|
+
hash[attr] = source.public_send(attr) if source.respond_to?(attr)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def normalize_custom_fields(value)
|
|
99
|
+
case value
|
|
100
|
+
when Array
|
|
101
|
+
value.map { |v| v.is_a?(IntacctRest::CustomField) ? v : IntacctRest::CustomField.new(**v) }
|
|
102
|
+
when Hash
|
|
103
|
+
value.map { |name, val| IntacctRest::CustomField.new(name: name.to_s, value: val) }
|
|
104
|
+
when nil
|
|
105
|
+
[]
|
|
106
|
+
else
|
|
107
|
+
raise ArgumentError, "Unsupported custom_fields value: #{value.class}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def custom_fields_payload
|
|
112
|
+
custom_fields.each_with_object({}) { |field, hash| hash[field.key] = field.value }
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IntacctRest
|
|
4
|
+
module Model
|
|
5
|
+
# An AR invoice's data and validations only — no config, no token
|
|
6
|
+
# provider, no HTTP. Build and inspect one freely; IntacctRest::Post
|
|
7
|
+
# (via IntacctRest::Endpoints::CreateInvoice) is what actually sends it.
|
|
8
|
+
#
|
|
9
|
+
# Nested objects (customer, lines, currency, term, ...) are NOT
|
|
10
|
+
# individually modeled — pass them as raw Hashes using Intacct's
|
|
11
|
+
# native (camelCase) nested key names, e.g.:
|
|
12
|
+
#
|
|
13
|
+
# customer: { "id" => "C-00019" }
|
|
14
|
+
# lines: [{ "txnAmount" => "100.40", "glAccount" => { "id" => "5004" } }]
|
|
15
|
+
#
|
|
16
|
+
# Nested required fields (customer.id, lines[].txnAmount, ...) are not
|
|
17
|
+
# validated client-side — only top-level presence/type is. Intacct's
|
|
18
|
+
# own 400 response is the source of truth for those, same boundary
|
|
19
|
+
# already accepted for Vendor's nested objects.
|
|
20
|
+
class Invoice < IntacctRest::Model::Base
|
|
21
|
+
INTACCT_OBJECT = '/objects/accounts-receivable/invoice'.freeze
|
|
22
|
+
|
|
23
|
+
WRITABLE_ATTRIBUTES = IntacctRest::Configuration::DEFAULT_INVOICE_WRITABLE_ATTRIBUTES
|
|
24
|
+
|
|
25
|
+
# Set by IntacctRest::Post after a successful create — writable here
|
|
26
|
+
# even though Intacct itself treats them as read-only, since this is
|
|
27
|
+
# the only place that result gets recorded. `href` is appended even
|
|
28
|
+
# though the invoice object schema omits it — the actual 201
|
|
29
|
+
# response includes it, same as every other object here.
|
|
30
|
+
READONLY_ATTRIBUTES = %i[
|
|
31
|
+
key id record_type module_key due_in_days sales_document discount_cut_off_date
|
|
32
|
+
total_base_amount total_base_amount_due total_txn_amount total_txn_amount_due
|
|
33
|
+
dunning_count recurring_schedule payment_information is_system_generated_document
|
|
34
|
+
e_invoice_status dispute refuse download_url allow_online_payment provider_payment
|
|
35
|
+
retainage project_contract project_contract_billing architect
|
|
36
|
+
project_contract_billing_invoice_summary entity web_url audit href
|
|
37
|
+
].freeze
|
|
38
|
+
|
|
39
|
+
attr_accessor(*WRITABLE_ATTRIBUTES.keys)
|
|
40
|
+
attr_accessor(*READONLY_ATTRIBUTES)
|
|
41
|
+
attr_reader :custom_fields
|
|
42
|
+
|
|
43
|
+
validate :presence, %i[invoice_date due_date]
|
|
44
|
+
|
|
45
|
+
validate :kind_of, :string, %i[
|
|
46
|
+
invoice_number state reference_number description invoice_date due_date
|
|
47
|
+
invoice_type invoice_mode
|
|
48
|
+
]
|
|
49
|
+
validate :kind_of, :hash, %i[
|
|
50
|
+
currency customer_message contacts billback_template attachment customer term
|
|
51
|
+
tax_solution invoice_summary
|
|
52
|
+
]
|
|
53
|
+
validate :kind_of, :array, %i[lines]
|
|
54
|
+
|
|
55
|
+
# source: an arbitrary domain object (ActiveRecord record, OpenStruct,
|
|
56
|
+
# another Model::Invoice, ...) to pull matching attributes off of via
|
|
57
|
+
# duck-typing — any WRITABLE_ATTRIBUTES/READONLY_ATTRIBUTES key it
|
|
58
|
+
# responds to is copied over. Explicit keyword attributes always win
|
|
59
|
+
# over whatever the source provided.
|
|
60
|
+
def initialize(source = nil, custom_fields: [], **attributes)
|
|
61
|
+
self.custom_fields = custom_fields
|
|
62
|
+
merged = source ? attributes_from(source).merge(attributes) : attributes
|
|
63
|
+
merged.each { |key, value| public_send(:"#{key}=", value) if respond_to?(:"#{key}=") }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def custom_fields=(value)
|
|
67
|
+
@custom_fields = normalize_custom_fields(value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def intacct_object
|
|
71
|
+
self.class::INTACCT_OBJECT
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Ruby-side attributes (unmapped, no custom fields) — useful for
|
|
75
|
+
# logging/introspection without the camelCase JSON translation.
|
|
76
|
+
def attributes
|
|
77
|
+
WRITABLE_ATTRIBUTES.keys.each_with_object({}) { |attr, hash| hash[attr] = public_send(attr) }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def payload
|
|
81
|
+
WRITABLE_ATTRIBUTES.each_with_object({}) do |(attr, json_key), hash|
|
|
82
|
+
value = public_send(attr)
|
|
83
|
+
hash[json_key] = value unless value.nil?
|
|
84
|
+
end.merge(custom_fields_payload)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Called by IntacctRest::Post after a successful create.
|
|
88
|
+
def apply_result(result)
|
|
89
|
+
data = result.result
|
|
90
|
+
self.key = data['key'] if data['key']
|
|
91
|
+
self.href = data['href'] if data['href']
|
|
92
|
+
self.id = data['id'] if data['id'] && id.nil?
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def attributes_from(source)
|
|
98
|
+
(WRITABLE_ATTRIBUTES.keys + READONLY_ATTRIBUTES).each_with_object({}) do |attr, hash|
|
|
99
|
+
hash[attr] = source.public_send(attr) if source.respond_to?(attr)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def normalize_custom_fields(value)
|
|
104
|
+
case value
|
|
105
|
+
when Array
|
|
106
|
+
value.map { |v| v.is_a?(IntacctRest::CustomField) ? v : IntacctRest::CustomField.new(**v) }
|
|
107
|
+
when Hash
|
|
108
|
+
value.map { |name, val| IntacctRest::CustomField.new(name: name.to_s, value: val) }
|
|
109
|
+
when nil
|
|
110
|
+
[]
|
|
111
|
+
else
|
|
112
|
+
raise ArgumentError, "Unsupported custom_fields value: #{value.class}"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def custom_fields_payload
|
|
117
|
+
custom_fields.each_with_object({}) { |field, hash| hash[field.key] = field.value }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|