-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathquickbooks_desktop_endpoint.rb
306 lines (243 loc) · 8.55 KB
/
quickbooks_desktop_endpoint.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
require 'endpoint_base'
require 'sinatra/reloader'
require 'securerandom'
require 'honeybadger'
require File.expand_path(File.dirname(__FILE__) + '/lib/quickbooks_desktop_integration')
ENDPOINTS = %w(
add_salesreceipts
add_payments
add_products
add_purchaseorders
add_orders
add_invoices
add_returns
add_customers
add_shipments
cancel_order
add_journals
add_vendors
add_noninventoryproducts
add_serviceproducts
add_salestaxproducts
add_discountproducts
add_otherchargeproducts
add_creditmemos
add_creditmemosaspayments
)
GET_ENDPOINTS = %w(
get_inventory
get_inventorywithsites
get_inventories
get_products
get_invoices
get_purchaseorders
get_customers
get_orders
get_salesreceipts
get_vendors
get_noninventoryproducts
get_serviceproducts
get_salestaxproducts
get_discountproducts
get_inventoryproducts
get_inventoryassemblyproducts
get_otherchargeproducts
get_creditmemos
)
CUSTOM_OBJECT_TYPES = %w(
inventorywithsites
serviceproducts
noninventoryproducts
salestaxproducts
discountproducts
inventoryproducts
inventoryassemblyproducts
otherchargeproducts
purchaseorders
salesreceipts
creditmemos
)
OBJECT_TYPES_MAPPING_DATA_OBJECT = {
'inventorywithsites' => 'inventories',
'otherchargeproducts' => 'products',
'serviceproducts' => 'products',
'salestaxproducts' => 'products',
'noninventoryproducts' => 'products',
'inventoryproducts' => 'products',
'discountproducts' => 'products',
'inventoryassemblyproducts' => 'products',
'purchaseorders' => 'purchase_orders',
'salesreceipts' => 'sales_receipts',
'creditmemos' => 'credit_memos'
}
class QuickbooksDesktopEndpoint < EndpointBase::Sinatra::Base
set :logging, true
# Force Sinatra to autoreload this file or any file in the lib directory
# when they change in development
configure :development do
register Sinatra::Reloader
also_reload './lib/**/*'
end
# Changing the endpoint paths might break internal logic as they're expected
# to be always in plural. e.g. products not product
ENDPOINTS.each do |path|
post "/#{path}" do
config = {
connection_id: request.env['HTTP_X_HUB_STORE'],
flow: "#{path}",
# NOTE could save us some time and http calls by not persisting configs
# on every call. Use same approach on polling instead by always setting
# this flag back to false on return?
quickbooks_force_config: 'true'
}.merge(@config).with_indifferent_access
Persistence::Settings.new(config).setup
@return_payload = nil
add_return_attributes_to_return_payload
unless already_has_guid?
generate_and_add_guid
end
add_flow_return_payload if @return_payload
integration = Persistence::Object.new(config, @payload)
integration.save
object_type = integration.payload_key.capitalize
result 200, "#{object_type} waiting for Quickbooks Desktop scheduler"
end
end
post '/get_notifications' do
config = {
connection_id: request.env['HTTP_X_HUB_STORE'],
flow: "get_notifications",
quickbooks_force_config: 'true'
}.merge(@config).with_indifferent_access
integration = Persistence::Object.new config, @payload
notifications = integration.get_notifications
add_value 'success', notifications['processed'] if !notifications['processed'].empty?
add_value 'fail', notifications['failed'] if !notifications['failed'].empty?
result 200, "Notifications retrieved"
end
post '/get_health_check' do
config = {
connection_id: request.env['HTTP_X_HUB_STORE'],
flow: "get_health_check",
quickbooks_force_config: 'true'
}.merge(@config).with_indifferent_access
s3_settings = Persistence::Settings.new(config)
if s3_settings.healthceck_is_failing?
result 500, "Health check was not successful"
else
result 200, "Health check was successful"
end
end
post '/set_inventory' do
config = {
connection_id: request.env['HTTP_X_HUB_STORE'],
flow: 'set_inventory'
}.merge(@config).with_indifferent_access
Persistence::Settings.new(config).setup
integration = Persistence::Object.new config, @payload
integration.save
notifications = integration.get_notifications
add_value 'success', notifications['processed'] if !notifications['processed'].empty?
add_value 'fail', notifications['failed'] if !notifications['failed'].empty?
object_type = integration.payload_key.capitalize
result 200, "Inventory waiting for Quickbooks Desktop scheduler"
end
GET_ENDPOINTS.each do |path|
post "/#{path}" do
object_type = path.split('_').last.pluralize
config = {
connection_id: request.env['HTTP_X_HUB_STORE'],
flow: path,
origin: 'quickbooks'
}.merge(@config).with_indifferent_access
s3_settings = Persistence::Settings.new(config)
s3_settings.setup
add_parameter 'quickbooks_force_config', false
persistence = Persistence::Polling.new config, @payload, object_type
records, done = persistence.process_waiting_records
integration = Persistence::Object.new config, @payload
notifications = integration.get_notifications
add_value 'success', notifications['processed'] if !notifications['processed'].empty?
add_value 'fail', notifications['failed'] if !notifications['failed'].empty?
if records.any?
names = records.inject([]) do |names, collection|
name = collection.keys.first
puts name
puts collection.values.first.inspect
records = collection.values.first
puts({connection_id: @config['connection_id'], flow: @config['flow'], records: records.inspect})
records = records.map{|record| allow_only_whitelisted_fields(record.with_indifferent_access) }
add_or_merge_value determine_name(name), records
names.push name
end
params = s3_settings.fetch(path).first[object_type]
add_parameter 'quickbooks_since', params['quickbooks_since']
status = done ? 200 : 206
result status, "Received #{names.uniq.join(', ')} records from quickbooks"
else
result 200
end
end
end
private
def determine_name(name)
plural_name = name.pluralize
return name unless CUSTOM_OBJECT_TYPES.include?(plural_name)
OBJECT_TYPES_MAPPING_DATA_OBJECT[plural_name]
end
# NOTE: ideally this would live in endpoint_base gem,
# but it is the first time it appears
# it expects config['fields_whitelist'] to be a string of comma separated attrs
# i.e. "id, list_id, external_guid"
def allow_only_whitelisted_fields(record)
return record unless @config['fields_whitelist']
puts({connection_id: @config['connection_id'], whitelisted_fields: @config['fields_whitelist'], flow: @config['flow'], record: record.inspect})
params_list = @config['fields_whitelist'].split(",").map(&:strip).map(&:to_sym)
# so id is not forgotten
params_list = (params_list << :id).uniq
new_record = {}
params_list.each do |param|
new_record[param] = record[param]
end
new_record
end
def add_flow_return_payload
payload = @return_payload.merge({
id: @payload[object_type][:id]
})
add_object determine_name(object_type).singularize, payload
end
def generate_and_add_guid
@return_payload ||= {}
guid = "{#{SecureRandom.uuid.upcase}}"
@payload[object_type][:external_guid] = guid
@return_payload[:external_guid] = guid
end
def add_return_attributes_to_return_payload
@return_payload = @payload[object_type][:return_to_fl] if @payload[object_type][:return_to_fl].is_a?(Hash)
end
def object_type
@payload[:parameters][:payload_type]
end
def already_has_guid?
# We need to tie the object in FL to the external ID
# UNLESS the object originated in QBE!
# You can't MOD an external_guid, so we don't set if the object has a QBE ID
(@payload[object_type][:external_guid] && @payload[object_type][:external_guid] != "") ||
(@payload[object_type][:qbe_id] && @payload[object_type][:qbe_id] != "")
end
# NOTE this lives in endpoint_base. Added here just so it's closer ..
# once we sure it's stable merge and push to endpoint_base/master
# and bump it here
def add_or_merge_value(name, value)
@attrs ||= {}
unless @attrs[name]
@attrs[name] = value
else
old_value = @attrs[name]
collection = (old_value + value).flatten
group = collection.group_by { |h| h[:id] || h['id'] }
@attrs[name] = group.map { |_k, v| v.reduce(:merge) }
end
end
end