diff --git a/custom_connectors/custom_auth/allocadia.rb b/custom_connectors/custom_auth/allocadia.rb
new file mode 100644
index 00000000..fd81ebf8
--- /dev/null
+++ b/custom_connectors/custom_auth/allocadia.rb
@@ -0,0 +1,1235 @@
+{
+ title: 'Allocadia',
+
+ methods: {
+ make_schema_builder_fields_sticky: lambda do |input|
+ input.map do |field|
+ if field[:properties].present?
+ field[:properties] = call('make_schema_builder_fields_sticky',
+ field[:properties])
+ elsif field['properties'].present?
+ field['properties'] = call('make_schema_builder_fields_sticky',
+ field['properties'])
+ end
+ field[:sticky] = true
+ field
+ end
+ end,
+
+ # convert a cells object from a line item to the readable format
+ format_item_cells: lambda do |input|
+ columns = input[:columns]
+ cells = input[:cells]
+
+ new_cells = {}
+
+ cells&.each do |id,cell|
+ col = columns[id]
+
+ unless col.blank? || cell['value'].blank?
+ new_cells[cell['columnName']] = call(:format_cell_value, { column: col, value: cell['value'] })
+ # Need to use name rather than ID for YoY support, etc.
+ #new_cells["f_#{id}"] = call(:format_cell_value, { column: col, value: cell['value'] })
+ end
+ end
+
+ Hash[ new_cells.sort_by { |key, val| key } ]
+ end,
+
+ # convert cell values from the Allocadia API into a readable format
+ format_cell_value: lambda do |input|
+ col = input[:column]
+ value = input[:value]
+
+ case col['type']
+ when 'DROPDOWN'
+ value = col['choices'].select {|choice| choice['id'] == value}.dig(0, 'label')
+
+ when 'MULTISELECT'
+ ms_array = []
+ value&.each do |id,pcnt|
+ choice_name = col['choices'].select {|choice| choice['id'] == id}.dig(0, 'label')
+ ms_array << choice_name + "::" + pcnt
+ end
+ value = ms_array.join(',')
+
+ when 'LINK'
+ # if it's a link format value as either "label", "url", or "label (url)" depending on what's present
+ label = value['label']
+ url = value['url']
+ if (label && url)
+ value = label + " (" + url + ")"
+ elsif (label)
+ value = label
+ else
+ value = url
+ end
+ end
+ value
+ end,
+
+ # convert an input cell into the format the Allocadia API expects
+ format_cell_to_allocadia: lambda do |input|
+ col = input[:column]
+ value = input[:value]
+
+ if (value.blank? && col['required'])
+ error("[#{col['name']}] is a required field. Cannot clear value.")
+ end
+
+ if value.present?
+ case col['type']
+ when 'DROPDOWN'
+ value = col['choices'].select {|choice| choice['label'] == value}.dig(0, 'id')
+
+ when 'MULTISELECT'
+ ms_obj = {}
+ value&.split(",").each do |ms_val|
+ choice_name_pcnt_arr = ms_val.split("::")
+ choice_id = col['choices'].select {|choice| choice['label'] == choice_name_pcnt_arr[0]}.dig(0, 'id')
+ ms_obj[choice_id] = choice_name_pcnt_arr[1].to_f
+ end
+ value = ms_obj
+
+ when 'LINK'
+ url = nil
+ label = value
+ # if the string ends in parentheses and the start inside parentheses is http then assume it's in label (url) format
+ label_url = value&.match(/(.*) (\()(http.*)(\))/i) # /i will ignore case
+ if (label_url.present?)
+ label = label_url[1]
+ url = label_url[3]
+ end
+
+ # if it's not in label (url) format but appears to be a url then treat it as a url with no label
+ if (!url && label&.match(/^http.*/i))
+ url = label
+ label = nil
+ end
+ value = {}
+ value['label'] = label
+ value['url'] = url
+ end
+ end
+ value.blank? ? nil : value # treat empty string the same as nil
+ end,
+
+ # recursively check if the item is part of the hierarchy by following the parentId up the hierarchy
+ item_in_hierarchy: lambda do |input|
+ root_id = input[:root_id]
+ item = input[:item]
+ all_items = input[:all_items]
+
+ if item['parentId'] == nil
+ item['id'] == root_id
+ elsif item['parentId'] == root_id
+ true
+ else
+ parent = all_items.select {|find_item| find_item['id'] == item['parentId']}[0]
+ call(:item_in_hierarchy, { root_id: root_id, all_items: all_items, item: parent })
+ end
+ end,
+
+ # retrieve all of the columns that are applicable to line items - "filter" input optional
+ get_line_item_columns_raw: lambda do |input|
+ get("/v1/budgets/#{input[:budgetId]}/columns?$filter=location ne \"ACTUAL\" and location ne \"PO\" and location ne \"ROLLUP\" and location ne \"BUDGET\" and location ne \"OTHER\"#{input[:filter]}")
+ end,
+
+ # retrieve all of the columns that are applicable to line items, in a hash key'd by id
+ get_line_item_columns_key_id: lambda do |input|
+ cols = {}
+ call(:get_line_item_columns_raw, { budgetId: input[:budgetId], filter: input[:filter] } ).each do |col|
+ cols[col['id']] = col
+ end
+ cols
+ end,
+
+ # retrieve all of the columns that are applicable to line items, in a hash key'd by name
+ get_line_item_columns_key_name: lambda do |input|
+ cols = {}
+ call(:get_line_item_columns_raw, { budgetId: input[:budgetId], filter: input[:filter] } ).each do |col|
+ cols[col['name']] = col
+ end
+ cols
+ end,
+ },
+
+ connection: {
+ fields: [
+ {
+ name: 'username',
+ hint: 'Allocadia app login username',
+ optional: false
+ },
+ {
+ name: 'password',
+ hint: 'Allocadia app login password',
+ optional: false,
+ control_type: 'password'
+ },
+ {
+ name: 'environment',
+ default: 'api-staging',
+ control_type: 'select',
+ pick_list: [
+ %w[North\ America api-na],
+ %w[Europe api-eu],
+ %w[Staging api-staging],
+ %w[Europe\ Staging api-eu-staging],
+ %w[Dev api-dev]
+ ],
+ optional: false
+ }
+ ],
+
+ base_uri: lambda { |connection|
+ domain = (connection['environment'].include? 'dev') ? 'allocadia.technology' : 'allocadia.com'
+ "https://#{connection['environment']}.#{domain}"
+ },
+
+ authorization: {
+ type: 'custom_auth',
+
+ acquire: lambda do |connection|
+ domain = (connection['environment'].include? 'dev') ? 'allocadia.technology' : 'allocadia.com'
+ post("https://#{connection['environment']}.#{domain}/v1/token",
+ username: connection['username'],
+ password: connection['password']).compact
+ end,
+
+ refresh_on: [401],
+
+ apply: lambda { |connection|
+ # if token doesn't exist yet use a dummy value so that we get a 401 and not a 400 error
+ headers('Authorization' => "token #{connection['token'] ? connection['token'] : '1234'}")
+ }
+ }
+ },
+
+ test: ->(_connection) { post("/v1/token",
+ username: _connection['username'],
+ password: _connection['password']) },
+
+
+ # todo - look to add hints where applicable
+ object_definitions: {
+
+ choice_add: {
+ fields: lambda do |_connection, _config_fields|
+ [
+ { name: 'label', optional: false },
+ { name: 'externalAssociations',
+ type: 'array',
+ of: 'object',
+ properties:
+ [
+ { name: 'externalId', sticky: true },
+ { name: 'type', default: 'CAMPAIGN' },
+ ]
+ }
+ ]
+ end
+ },
+
+ choice: {
+ fields: lambda do |_connection, _config_fields|
+ [
+ {
+ control_type: 'text',
+ label: 'Choice ID',
+ type: 'string',
+ name: 'choiceId',
+ optional: false
+ },
+ {
+ control_type: 'text',
+ label: 'Label',
+ type: 'string',
+ name: 'label',
+ optional: false
+ },
+ {
+ control_type: 'text',
+ label: 'External ID',
+ type: 'string',
+ name: 'externalId',
+ optional: true
+ }
+ ]
+ end
+ },
+
+ column: {
+ fields: lambda do |_connection, _config_fields|
+ [
+ {
+ control_type: 'text',
+ label: 'Column ID',
+ type: 'string',
+ name: 'columnId',
+ optional: false
+ },
+ {
+ control_type: 'text',
+ label: 'Name',
+ type: 'string',
+ name: 'name',
+ optional: false
+ },
+ {
+ control_type: 'text',
+ label: 'Type',
+ type: 'string',
+ name: 'type',
+ optional: false
+ },
+ {
+ control_type: 'select',
+ pick_list: 'column_locations',
+ label: 'Location',
+ toggle_hint: 'Select from option list',
+ toggle_field: {
+ label: 'Location',
+ control_type: 'text',
+ toggle_hint: 'Use custom value',
+ type: 'boolean',
+ name: 'location'
+ },
+ type: 'string',
+ name: 'location'
+ },
+ {
+ label: 'Choices',
+ type: 'array',
+ of: 'object',
+ name: 'choices',
+ properties: #TODO - can we reuse the choice object definition here?
+ [
+ {
+ control_type: 'text',
+ label: 'Choice ID',
+ type: 'string',
+ name: 'choiceId'
+ },
+ {
+ control_type: 'text',
+ label: 'Label',
+ type: 'string',
+ name: 'label'
+ },
+ {
+ control_type: 'text',
+ label: 'External ID',
+ type: 'string',
+ name: 'externalId'
+ }
+ ]
+ }
+ ]
+ end
+ },
+
+ budget: {
+ fields: lambda do |_connection, _config_fields|
+ [
+ {
+ sticky: true,
+ control_type: 'text',
+ label: 'Folder/Budget ID',
+ type: 'string',
+ name: 'budgetId',
+ optional: false
+ },
+ {
+ control_type: 'text',
+ label: 'Folder/Budget Name',
+ type: 'string',
+ name: 'name',
+ optional: false
+ },
+ {
+ control_type: 'text',
+ label: 'Parent ID',
+ type: 'string',
+ name: 'parentId'
+ },
+ {
+ control_type: 'text',
+ label: 'Currency',
+ type: 'string',
+ name: 'currency'
+ },
+ {
+ control_type: 'text',
+ label: 'Notes',
+ type: 'string',
+ name: 'notes'
+ },
+ {
+ control_type: 'checkbox',
+ label: 'Folder',
+ toggle_hint: 'Select from option list',
+ toggle_field: {
+ label: 'Folder',
+ control_type: 'text',
+ toggle_hint: 'Use custom value',
+ type: 'boolean',
+ name: 'folder'
+ },
+ type: 'boolean',
+ name: 'folder'
+ },
+ {
+ control_type: 'date_time',
+ label: 'Created date',
+ render_input: 'date_time_conversion',
+ parse_output: 'date_time_conversion',
+ type: 'date_time',
+ name: 'createdDate'
+ },
+ {
+ control_type: 'date_time',
+ label: 'Updated date',
+ render_input: 'date_time_conversion',
+ parse_output: 'date_time_conversion',
+ type: 'date_time',
+ name: 'updatedDate'
+ }
+ ]
+ end
+ },
+
+ line_item: {
+ fields: lambda do |_connection, config_fields|
+ budget_id = config_fields['budgetId'] ? config_fields['budgetId'] : config_fields['updateBudgetId']
+ # if this an update, only get non-readOnly columns
+ read_only_filter = config_fields['updateBudgetId'] ? ' and readOnly eq false' : ''
+
+ cells_prop =
+ if budget_id && (budget_id&.to_i) != 0
+ line_item_columns = call(:get_line_item_columns_raw, budgetId: budget_id, filter: read_only_filter)
+ line_item_columns&.map do |field|
+ case field['type']
+ when 'CURRENCY', 'NUMBER'
+ {
+ name: field['name'],
+ label: field['name'],
+ sticky: true,
+ control_type: 'number',
+ render_input: 'float_conversion',
+ parse_output: 'float_conversion',
+ type: 'number'
+ }.compact
+ else
+ {
+ name: field['name'],
+ label: field['name'],
+ sticky: true,
+ control_type: 'text',
+ type: 'string'
+ }.compact
+ end
+ end
+ end || []
+ [
+ {
+ name: 'itemId',
+ label: 'Item ID',
+ control_type: 'text',
+ type: 'string'
+ },
+ {
+ name: 'name',
+ label: 'Name',
+ control_type: 'text',
+ type: 'string'
+ },
+ {
+ name: 'type',
+ label: 'Type',
+ default: 'LINE_ITEM',
+ control_type: 'select',
+ pick_list: 'line_item_types',
+ toggle_hint: 'Select from list',
+ toggle_field: {
+ name: 'type',
+ label: 'Type',
+ hint: 'Allowed values are: LINE_ITEM, CATEGORY, PLACEHOLDER',
+ toggle_hint: 'Use custom value',
+ control_type: 'text',
+ type: 'string'
+ }
+ },
+ {
+ control_type: 'text',
+ label: 'Budget ID',
+ type: 'string',
+ name: 'budgetId'
+ },
+ {
+ control_type: 'text',
+ label: 'Parent ID',
+ type: 'string',
+ name: 'parentId'
+ },
+ {
+ control_type: 'text',
+ label: 'Path',
+ type: 'string',
+ name: 'path'
+ },
+ {
+ name: 'createdDate',
+ label: 'Created date',
+ control_type: 'date_time',
+ render_input: 'date_time_conversion',
+ parse_output: 'date_time_conversion',
+ type: 'date_time'
+ },
+ {
+ name: 'updatedDate',
+ label: 'Updated date',
+ control_type: 'date_time',
+ render_input: 'date_time_conversion',
+ parse_output: 'date_time_conversion',
+ type: 'date_time'
+ },
+ {
+ name: 'cells',
+ sticky: true,
+ type: cells_prop.size > 0 ? 'object' : 'string',
+ properties: cells_prop
+ },
+
+ ]
+ end
+ },
+
+ filter: {
+ fields: lambda do |_connection, _config_fields|
+ [
+ {
+ name: 'filter',
+ label: 'Filter using custom criteria',
+ sticky: true,
+ hint: 'Data can be filtered based upon property and ' \
+ 'sub-property values. Strings must be double quoted and ' \
+ 'all expressions must evaluate to a boolean value.
' \
+ 'Supported operators: eq (Equal), ne (Not ' \
+ 'equal), gt (Greater than), ge (Greater ' \
+ 'than or equal), lt (Less than), le ' \
+ '(Less than or equal), and, and or.
' \
+ 'For example: updatedDate ge "2017-03-10T00:00:00.000Z" and ' \
+ 'updatedDate lt "2017-03-11T00:00:00.000Z"'
+ }
+ ]
+ end
+ },
+
+ custom_action_input: {
+ fields: lambda do |connection, config_fields|
+ input_schema = parse_json(config_fields.dig('input', 'schema') || '[]')
+
+ [
+ {
+ name: 'path',
+ optional: false,
+ hint: "Base URI is https://#{connection['environment']}" \
+ '.allocadia.com - path will be appended to this URI. ' \
+ 'Use absolute URI to override this base URI.'
+ },
+ (
+ if %w[get delete].include?(config_fields['verb'])
+ {
+ name: 'input',
+ type: 'object',
+ control_type: 'form-schema-builder',
+ sticky: input_schema.blank?,
+ label: 'URL parameters',
+ add_field_label: 'Add URL parameter',
+ properties: [
+ {
+ name: 'schema',
+ extends_schema: true,
+ sticky: input_schema.blank?
+ },
+ (
+ if input_schema.present?
+ {
+ name: 'data',
+ type: 'object',
+ properties: call('make_schema_builder_fields_sticky',
+ input_schema)
+ }
+ end
+ )
+ ].compact
+ }
+ else
+ {
+ name: 'input',
+ type: 'object',
+ properties: [
+ {
+ name: 'schema',
+ extends_schema: true,
+ schema_neutral: true,
+ control_type: 'schema-designer',
+ sample_data_type: 'json_input',
+ sticky: input_schema.blank?,
+ label: 'Request body parameters',
+ add_field_label: 'Add request body parameter'
+ },
+ (
+ if input_schema.present?
+ {
+ name: 'data',
+ type: 'object',
+ properties: input_schema
+ .each { |field| field[:sticky] = true }
+ }
+ end
+ )
+ ].compact
+ }
+ end
+ ),
+ {
+ name: 'output',
+ control_type: 'schema-designer',
+ sample_data_type: 'json_http',
+ extends_schema: true,
+ schema_neutral: true,
+ sticky: true
+ }
+ ]
+ end
+ },
+
+ custom_action_output: {
+ fields: lambda do |_connection, config_fields|
+ parse_json(config_fields['output'] || '[]')
+ end
+ },
+
+ },
+
+ actions: {
+
+ custom_action: {
+ description: "Custom http action " \
+ "in Allocadia",
+ help: {
+ body: 'Build your own Allocadia action with an HTTP request. The ' \
+ 'request will be authorized with your Allocadia connection.',
+ learn_more_url: "https://api-na.allocadia.com/v1/docs/",
+ learn_more_text: 'Allocadia API Documentation'
+ },
+
+ execute: lambda do |_connection, input|
+ verb = input['verb']
+ error("#{verb} not supported") if %w[get post put delete].exclude?(verb)
+ data = input.dig('input', 'data').presence || {}
+
+ case verb
+ when 'get'
+ response =
+ get(input['path'], data)
+ .after_error_response(/.*/) do |_code, body, _header, message|
+ error("#{message}: #{body}")
+ end.compact
+
+ if response.is_a?(Array)
+ array_name = parse_json(input['output'] || '[]')
+ .dig(0, 'name') || 'array'
+ { array_name.to_s => response }
+ elsif response.is_a?(Hash)
+ response
+ else
+ error('API response is not a JSON')
+ end
+ when 'post'
+ post(input['path'], data)
+ .after_response do |code, body, headers|
+ # if /3\d{2} | 4\d{2} | 5\d{2}/.match?(code)
+ if code.to_s.match?(/[3-5]\d{2}/)
+ error("#{code}: #{body}")
+ else
+ {
+ location: headers['location'],
+ id: headers['location'].split('/').last
+ }
+ end
+ # .after_error_response(/.*/) do |_code, body, _header, message|
+ # error("#{message}: #{body}")
+ end.compact
+ when 'put'
+ put(input['path'], data)
+ .after_error_response(/.*/) do |_code, body, _header, message|
+ error("#{message}: #{body}")
+ end.compact
+ when 'delete'
+ delete(input['path'], data)
+ .after_error_response(/.*/) do |_code, body, _header, message|
+ error("#{message}: #{body}")
+ end.compact
+ end
+ end,
+
+ config_fields: [{
+ name: 'verb',
+ label: 'Request type',
+ hint: 'Select HTTP method of the request',
+ optional: false,
+ control_type: 'select',
+ pick_list: %w[get post put delete].map { |verb| [verb.upcase, verb] }
+ }],
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['custom_action_input']
+ end,
+
+ output_fields: lambda do |object_definitions|
+ object_definitions['custom_action_output']
+ end
+ },
+
+ get_choices_by_column_id: {
+ description: "Get choices by column ID " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+
+ raw_choices = get("/v1/budgets/#{input['budgetId']}/columns/#{input['columnId']}/choices")
+ {
+ choices: raw_choices&.map do |field|
+ {
+ choiceId: field['id'],
+ label: field['label'],
+ externalId: field.dig('externalAssociations', 0, 'externalId')
+ }
+ end
+ }
+ end,
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['budget'].only('budgetId').concat(object_definitions['column'].only('columnId')).required('budgetId','columnId')
+ end,
+
+ output_fields: lambda do |object_definitions|
+ [{
+ name: 'choices',
+ type: 'array',
+ of: 'object',
+ properties: object_definitions['choice']
+ }]
+ end,
+
+ },
+
+ get_choices_by_column_name: {
+ description: "Get choices by column name " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+
+ all_columns_obj = []
+
+ locationFilter = input['location'] ? "location eq \"#{input['location']}\" and " : ""
+
+ column_names = input['columnNames'].pluck('columnName')
+
+ column_names.each do |columnName|
+ column_obj = get("/v1/budgets/#{input['budgetId']}/columns?$filter=#{locationFilter}name eq \"#{columnName}\"").first || error("Column #{columnName} not found")
+
+ raw_choices = get("/v1/budgets/#{input['budgetId']}/columns/#{column_obj['id']}/choices")
+ choices = raw_choices&.map do |choice|
+ {
+ choiceId: choice['id'],
+ label: choice['label'],
+ externalId: choice.dig('externalAssociations', 0, 'externalId')
+ }
+ end
+
+ column_obj['choices'] = choices
+ all_columns_obj << column_obj
+ end
+
+ {
+ columns: all_columns_obj
+ }
+ end,
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['budget'].only('budgetId').
+ concat([
+ {
+ name: 'columnNames',
+ type: :array,
+ of: :object,
+ properties: [{ name: 'columnName' }]
+ }
+ ]).concat(object_definitions['column'].only('location'))
+ end,
+
+ output_fields: lambda do |object_definitions|
+ [{
+ name: 'columns',
+ type: 'array',
+ of: 'object',
+ properties: object_definitions['column']
+ }]
+ end,
+
+ },
+
+ add_choice: {
+ description: "Add column choice " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+
+ # if the externalId in the first entry is blank, remove the external associations
+ if input.dig('externalAssociations',0,'externalId').blank?
+ input.delete('externalAssociations')
+ end
+
+ post("/v1/budgets/#{input.delete('budgetId')}/columns/#{input.delete('columnId')}/choices", input)
+ .after_response do |code, body, headers|
+ # if /3\d{2} | 4\d{2} | 5\d{2}/.match?(code)
+ if code.to_s.match?(/[3-5]\d{2}/)
+ error("#{code}: #{body}")
+ else
+ { choiceId: headers['location'].split('/').last }
+ end
+ end
+
+ end,
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['budget'].only('budgetId').concat(object_definitions['column'].only('columnId')).concat(object_definitions['choice_add'])
+ end,
+
+ output_fields: lambda do |object_definitions|
+ [{
+ name: 'choiceId',
+ }]
+ end,
+
+ },
+
+ get_item_by_id: {
+ description: "Get item by ID " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+
+ item = get("/v1/lineitems/#{input['itemId']}")
+ columns = call(:get_line_item_columns_raw, { budgetId: item['budgetId'] })
+
+ if input['primaryExternalColumnName']
+ primaryColId = columns.select {|col| col['name'] == input['primaryExternalColumnName'] }[0]['id']
+ choices = get("/v1/budgets/#{item['budgetId']}/columns/#{primaryColId}/choices")
+ if (item['cells'][primaryColId])
+ choiceId = item['cells'][primaryColId]['value']
+ choice = choices.select {|ch| ch['id'] == choiceId }[0]
+ externalId = choice['externalAssociations'].size > 0 ? choice['externalAssociations'][0]['externalId'] : nil
+ item['primaryExternalId'] = externalId
+ end
+ end
+
+ cols = {}
+ columns.each do |col|
+ cols[col['id']] = col
+ end
+ item['cells'] = call(:format_item_cells, { columns: cols, cells: item['cells'] })
+ item['itemId'] = item.delete('id')
+ item.except('_links')
+ end,
+
+ config_fields: [{
+ name: 'budgetId',
+ label: 'Budget',
+ optional: false,
+ control_type: 'select',
+ type: 'text',
+ pick_list: 'budgets',
+ toggle_hint: 'Select from list',
+ toggle_field: {
+ name: 'budgetId',
+ label: 'Budget ID',
+ toggle_hint: 'Use custom value',
+ hint: 'Enter N/A if budget ID not known',
+ control_type: 'text',
+ type: 'string'
+ }
+ }],
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['line_item'].only('itemId').required('itemId').concat([{ name: 'primaryExternalColumnName' }])
+
+ end,
+
+ output_fields: lambda do |object_definitions|
+ object_definitions['line_item'].concat([{ name: 'primaryExternalId' }])
+ end,
+
+ },
+
+ get_budget_by_id: {
+ description: "Get budget by ID " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+ budget = get("/v1/budgets/#{input['budgetId']}")
+ budget['budgetId'] = budget.delete('id')
+ budget.except('_links')
+ end,
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['budget'].only('budgetId')
+
+ end,
+
+ output_fields: lambda do |object_definitions|
+ object_definitions['budget']
+ end,
+
+ },
+
+ get_all_budgets: {
+ description: "Get all budgets by folder ID " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+ budget_filter = input['filter'] ? "?$filter=#{input['filter']}" : ""
+ all_budgets = get("/v1/budgets#{budget_filter}")
+ budgets_in_hierarchy = input['folderId'].blank? ? all_budgets : all_budgets.select {|item| call(:item_in_hierarchy, { root_id: input['folderId'], all_items: all_budgets, item: item }) }
+
+ budgets_in_hierarchy.each do |budget|
+ budget['budgetId'] = budget.delete 'id'
+ budget.delete '_links'
+ end
+
+ { budgets: budgets_in_hierarchy }
+ end,
+
+ input_fields: lambda do |object_definitions|
+ [{
+ name: 'folderId',
+ label: 'Folder ID',
+ }].concat(object_definitions['filter'])
+ end,
+
+
+ output_fields: lambda do |object_definitions|
+ [{
+ name: 'budgets',
+ type: 'array',
+ of: 'object',
+ properties: object_definitions['budget']
+ }]
+ end,
+
+ },
+
+ get_line_items_by_budget: {
+ description: "Get all line items in budget " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+ filter = input['filter'] ? "?$filter=#{input['filter']}" : ""
+ columns = call(:get_line_item_columns_key_id, { budgetId: input['budgetId'] })
+ budget_items = get("/v1/budgets/#{input['budgetId']}/lineitems#{filter}")
+
+ budget_items&.delete_if { |item| item['parentId'] == nil } # filter out grand total row
+
+ budget_items.each do |item|
+ item['itemId'] = item.delete 'id'
+ item.delete '_links'
+ item['cells'] = call(:format_item_cells, { columns: columns, cells: item['cells'] })
+ end
+
+ { items: budget_items }
+ end,
+
+ # reduces output to the first 100 items plus the last one?
+ summarize_output: 'items',
+
+ config_fields: [{
+ name: 'budgetId',
+ label: 'Budget',
+ optional: false,
+ control_type: 'select',
+ type: 'text',
+ pick_list: 'budgets',
+ toggle_hint: 'Select from list',
+ toggle_field: {
+ name: 'budgetId',
+ label: 'Budget ID',
+ toggle_hint: 'Use custom value',
+ control_type: 'text',
+ type: 'string'
+ }
+ }],
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['filter']
+ end,
+
+ output_fields: lambda do |object_definitions|
+ [{
+ name: 'items',
+ type: 'array',
+ of: 'object',
+ properties: object_definitions['line_item']
+ }]
+ end,
+ },
+
+ search_line_items: {
+ description: "Search line items in folder " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+ item_filter = input['filter'] ? "?$filter=#{input['filter']}" : ""
+ all_budgets = get("/v1/budgets")
+ budgets_in_hierarchy = all_budgets.select {|budget| call(:item_in_hierarchy, { root_id: input['folderId'], all_items: all_budgets, item: budget }) }
+ budgets_in_hierarchy = budgets_in_hierarchy.select {|budget| budget['folder'] == false } # include budgets, not folders
+ columns = call(:get_line_item_columns_key_id, { budgetId: input['folderId'] })
+
+ all_items = []
+
+ budgets_in_hierarchy.each do |budget|
+ budget_items = get("/v1/budgets/#{budget['id']}/lineitems#{item_filter}")
+
+ budget_items.each do |item|
+ unless item['parentId'] == nil # filter out grand total row
+ item['itemId'] = item.delete 'id'
+ item.delete '_links'
+ item['cells'] = call(:format_item_cells, { columns: columns, cells: item['cells'] })
+ all_items << item
+ end
+ end
+ end
+
+ { items: all_items }
+ end,
+
+ config_fields: [{
+ name: 'folderId',
+ label: 'Folder',
+ optional: false,
+ control_type: 'select',
+ type: 'text',
+ pick_list: 'folders',
+ toggle_hint: 'Select from folder list',
+ toggle_field: {
+ name: 'folderId',
+ label: 'Folder ID',
+ toggle_hint: 'Use custom value',
+ control_type: 'text',
+ type: 'string'
+ }
+ }],
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['filter']
+ end,
+
+ output_fields: lambda do |object_definitions|
+ [{
+ name: 'items',
+ type: 'array',
+ of: 'object',
+ properties: object_definitions['line_item']
+ }]
+ end,
+ },
+
+ # TODO - this is incomplete - waiting on a "wait" function to be available
+ search_line_items_large_hierarchy: {
+ description: "Search line items in folder for large hierarchy " \
+ "in Allocadia",
+
+ execute: lambda do |_connection, input|
+ itemfilter = input['filter'] ? "?$filter=#{input['filter']}" : ""
+ #columns = call(:get_line_item_columns_key_id, { budgetId: input['budgetId'] })
+
+ budget_items = get("/v1/lineitems/#{itemfilter}").after_response do |code, body, headers|
+ location = headers['location']
+ job_id = location&.split('/')[-1]
+ job = get("/v1/jobs/lineitems/#{job_id}")
+ error(job)
+ end
+
+ all_items = []
+
+ budget_items.each do |item|
+ unless item['parentId'] == nil # filter out grand total row
+ item['itemId'] = item.delete 'id'
+ item.delete '_links'
+ # item['cells'] = call(:format_item_cells, { columns: columns, cells: item['cells'] })
+ all_items << item
+ end
+ end
+
+ { items: all_items }
+ end,
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['filter']
+ end,
+
+ output_fields: lambda do |object_definitions|
+ [{
+ name: 'items',
+ type: 'array',
+ of: 'object',
+ properties: object_definitions['line_item']
+ }]
+ end,
+ },
+
+ update_line_item: {
+ description: "Update line item" \
+ " in Allocadia",
+
+ execute: lambda do |_connection, input|
+ columns = call(:get_line_item_columns_key_name, { budgetId: input.delete('updateBudgetId') })
+
+ # handle case where it could be a cells object coming in as a string
+ if (input['cells']&.is_a?(String))
+ input['cells'] = parse_json(input['cells'])
+ end
+
+ input['cells'] = input['cells']&.
+ compact&.
+ map { |key, value| { columns[key]['id'] => { 'value' => call(:format_cell_to_allocadia, { value: value, column: columns[key] } ) } } }&.
+ inject(:merge)
+
+ put("/v1/lineitems" \
+ "/#{input.delete('itemId')}", input.compact)
+ .after_error_response(/.*/) do |_code, body, _header, message|
+ error("#{message}: #{body}")
+ end || {}
+ end,
+
+ config_fields: [{
+ name: 'updateBudgetId',
+ label: 'Budget',
+ optional: false,
+ control_type: 'select',
+ type: 'text',
+ pick_list: 'budgets',
+ toggle_hint: 'Select from list',
+ toggle_field: {
+ name: 'updateBudgetId',
+ label: 'Budget ID',
+ toggle_hint: 'Use custom value',
+ control_type: 'text',
+ type: 'string'
+ }
+ }],
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['line_item'].only('itemId','name','cells').required('itemId')
+ end
+ },
+
+ add_line_item: {
+ description: "Add line item" \
+ " in Allocadia",
+
+ execute: lambda do |_connection, input|
+ columns = call(:get_line_item_columns_key_name, { budgetId: input['updateBudgetId'] })
+
+ # handle case where it could be a cells object coming in as a string
+ if (input['cells']&.is_a?(String))
+ input['cells'] = parse_json(input['cells'])
+ end
+
+ input['cells'] = input['cells']&.
+ compact&.
+ map { |key, value|
+ { columns[key]['id'] =>
+ { 'value' =>
+ call(:format_cell_to_allocadia,
+ value: value, column: columns[key])
+ }
+ }
+ }&.
+ inject(:merge)
+
+ post("/v1/budgets/#{input.delete('updateBudgetId')}/lineitems",
+ input.compact)
+ .after_response do |code, body, headers|
+ if code.to_s.match?(/[3-5]\d{2}/)
+ error("#{code}: #{body}")
+ else
+ { itemId: headers['location'].split('/').last }
+ end
+ end
+ end,
+
+ config_fields: [{
+ name: 'updateBudgetId',
+ label: 'Budget',
+ optional: false,
+ control_type: 'select',
+ type: 'text',
+ pick_list: 'budgets',
+ toggle_hint: 'Select from list',
+ toggle_field: {
+ name: 'updateBudgetId',
+ label: 'Budget ID',
+ toggle_hint: 'Use custom value',
+ control_type: 'text',
+ type: 'string'
+ }
+ }],
+
+ input_fields: lambda do |object_definitions|
+ object_definitions['line_item']
+ .only('name', 'type', 'parentId', 'cells').required('name', 'type')
+ end,
+
+ output_fields: lambda do |object_definitions|
+ object_definitions['line_item'].only('itemId')
+ end
+ }
+
+ },
+
+ pick_lists: {
+ foldersbudgets: lambda do |_connection|
+ get('/v1/budgets')&.pluck('name', 'id') || []
+ end,
+
+ budgets: lambda do |_connection|
+ get('/v1/budgets?$filter=folder eq false')&.pluck('name', 'id') || []
+ end,
+
+ folders: lambda do |_connection|
+ get('/v1/budgets?$filter=folder eq true')&.pluck('name', 'id') || []
+ end,
+
+ line_item_types: lambda do |_connection|
+ [%w[Line\ item LINE_ITEM],
+ %w[Category CATEGORY],
+ %w[Placeholder PLACEHOLDER]]
+ end,
+
+ column_locations: lambda do |_connection|
+ [
+ %w[ACTUAL ACTUAL],
+ %w[BUDGET BUDGET],
+ %w[DETAILS DETAILS],
+ %w[GRID GRID],
+ %w[OTHER OTHER],
+ %w[PO PO],
+ %w[ROLLUP ROLLUP]
+ ]
+ end
+ }
+}