CP-10361 - Due to Docuseal being a public repo, we want to rename these values/fields to be generic instead of labeling 'ats'

pull/544/head
Bernardo Anderson 4 months ago
parent d4a0dd379a
commit 24cf871bcc

@ -17,10 +17,10 @@ class SubmissionsController < ApplicationController
def show def show
@submission = Submissions.preload_with_pages(@submission) @submission = Submissions.preload_with_pages(@submission)
@available_ats_fields = extract_ats_prefill_fields @available_prefill_fields = extract_prefill_fields
# Optional: store in session for persistence across requests # Optional: store in session for persistence across requests
session[:ats_prefill_fields] = @available_ats_fields if @available_ats_fields.any? session[:prefill_fields] = @available_prefill_fields if @available_prefill_fields.any?
unless @submission.submitters.all?(&:completed_at?) unless @submission.submitters.all?(&:completed_at?)
ActiveRecord::Associations::Preloader.new( ActiveRecord::Associations::Preloader.new(

@ -28,8 +28,8 @@ class SubmitFormController < ApplicationController
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user) Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
# Fetch ATS prefill values if task_assignment_id is provided # Fetch prefill values if available
@ats_prefill_values = fetch_ats_prefill_values_if_available @prefill_values = fetch_prefill_values_if_available
@attachments_index = build_attachments_index(submission) @attachments_index = build_attachments_index(submission)
@ -102,23 +102,23 @@ class SubmitFormController < ApplicationController
.preload(:blob).index_by(&:uuid) .preload(:blob).index_by(&:uuid)
end end
def fetch_ats_prefill_values_if_available def fetch_prefill_values_if_available
# ATS passes values directly as Base64-encoded JSON parameters # External system passes values directly as Base64-encoded JSON parameters
return {} if params[:ats_values].blank? return {} if params[:prefill_values].blank?
# Security: Limit input size to prevent DoS attacks (64KB limit) # Security: Limit input size to prevent DoS attacks (64KB limit)
return {} if params[:ats_values].bytesize > 65_536 return {} if params[:prefill_values].bytesize > 65_536
begin begin
decoded_json = Base64.urlsafe_decode64(params[:ats_values]) decoded_json = Base64.urlsafe_decode64(params[:prefill_values])
# Security: Limit decoded JSON size as well # Security: Limit decoded JSON size as well
return {} if decoded_json.bytesize > 32_768 return {} if decoded_json.bytesize > 32_768
ats_values = JSON.parse(decoded_json) prefill_values = JSON.parse(decoded_json)
# Validate that we got a hash # Validate that we got a hash
ats_values.is_a?(Hash) ? ats_values : {} prefill_values.is_a?(Hash) ? prefill_values : {}
rescue StandardError rescue StandardError
{} {}
end end

@ -42,8 +42,8 @@ class TemplatesController < ApplicationController
associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]] associations: [schema_documents: [:blob, { preview_images_attachments: :blob }]]
).call ).call
# Process ATS fields for template editing # Process prefill fields for template editing
@available_ats_fields = extract_ats_prefill_fields @available_prefill_fields = extract_prefill_fields
@template_data = @template_data =
@template.as_json.merge( @template.as_json.merge(
@ -51,7 +51,7 @@ class TemplatesController < ApplicationController
methods: %i[metadata signed_uuid], methods: %i[metadata signed_uuid],
include: { preview_images: { methods: %i[url metadata filename] } } include: { preview_images: { methods: %i[url metadata filename] } }
), ),
available_ats_fields: @available_ats_fields available_prefill_fields: @available_prefill_fields
).to_json ).to_json
render :edit, layout: 'plain' render :edit, layout: 'plain'

@ -1,53 +1,53 @@
# frozen_string_literal: true # frozen_string_literal: true
module PrefillFieldsHelper module PrefillFieldsHelper
# Extracts and validates ATS prefill field names from Base64-encoded parameters # Extracts and validates prefill field names from Base64-encoded parameters
# #
# This method decodes the ats_fields parameter, validates the field names against # This method decodes the prefill_fields parameter, validates the field names against
# allowed patterns, and caches the results to improve performance on repeated requests. # allowed patterns, and caches the results to improve performance on repeated requests.
# #
# @return [Array<String>] Array of valid ATS field names, empty array if none found or on error # @return [Array<String>] Array of valid prefill field names, empty array if none found or on error
# #
# @example # @example
# # With params[:ats_fields] = Base64.urlsafe_encode64(['employee_first_name', 'employee_email'].to_json) # # With params[:prefill_fields] = Base64.urlsafe_encode64(['employee_first_name', 'employee_email'].to_json)
# extract_ats_prefill_fields # extract_prefill_fields
# # => ['employee_first_name', 'employee_email'] # # => ['employee_first_name', 'employee_email']
def extract_ats_prefill_fields def extract_prefill_fields
AtsPrefill.extract_fields(params) Prefill.extract_fields(params)
end end
# Merges ATS prefill values with existing submitter values # Merges prefill values with existing submitter values
# #
# This method combines ATS-provided prefill values with values already entered by submitters. # This method combines externally-provided prefill values with values already entered by submitters.
# Existing submitter values always take precedence over ATS values to prevent overwriting # Existing submitter values always take precedence over prefill values to prevent overwriting
# user input. Uses optimized field lookup caching for better performance. # user input. Uses optimized field lookup caching for better performance.
# #
# @param submitter_values [Hash] Existing values entered by submitters, keyed by field UUID # @param submitter_values [Hash] Existing values entered by submitters, keyed by field UUID
# @param ats_values [Hash] Prefill values from ATS system, keyed by ATS field name # @param prefill_values [Hash] Prefill values from external system, keyed by prefill field name
# @param template_fields [Array<Hash>, nil] Template field definitions containing UUID and prefill mappings # @param template_fields [Array<Hash>, nil] Template field definitions containing UUID and prefill mappings
# @return [Hash] Merged values with submitter values taking precedence over ATS values # @return [Hash] Merged values with submitter values taking precedence over prefill values
# #
# @example # @example
# submitter_values = { 'field-uuid-1' => 'John' } # submitter_values = { 'field-uuid-1' => 'John' }
# ats_values = { 'employee_first_name' => 'Jane', 'employee_last_name' => 'Doe' } # prefill_values = { 'employee_first_name' => 'Jane', 'employee_last_name' => 'Doe' }
# template_fields = [ # template_fields = [
# { 'uuid' => 'field-uuid-1', 'prefill' => 'employee_first_name' }, # { 'uuid' => 'field-uuid-1', 'prefill' => 'employee_first_name' },
# { 'uuid' => 'field-uuid-2', 'prefill' => 'employee_last_name' } # { 'uuid' => 'field-uuid-2', 'prefill' => 'employee_last_name' }
# ] # ]
# #
# merge_ats_prefill_values(submitter_values, ats_values, template_fields) # merge_prefill_values(submitter_values, prefill_values, template_fields)
# # => { 'field-uuid-1' => 'John', 'field-uuid-2' => 'Doe' } # # => { 'field-uuid-1' => 'John', 'field-uuid-2' => 'Doe' }
# # Note: 'John' is preserved over 'Jane' because submitter value takes precedence # # Note: 'John' is preserved over 'Jane' because submitter value takes precedence
def merge_ats_prefill_values(submitter_values, ats_values, template_fields = nil) def merge_prefill_values(submitter_values, prefill_values, template_fields = nil)
AtsPrefill.merge_values(submitter_values, ats_values, template_fields) Prefill.merge_values(submitter_values, prefill_values, template_fields)
end end
# Finds field UUID by matching ATS field name to template field's prefill attribute # Finds field UUID by matching prefill field name to template field's prefill attribute
# #
# This method provides backward compatibility and is now optimized to use # This method provides backward compatibility and is now optimized to use
# the cached lookup when possible. # the cached lookup when possible.
# #
# @param field_name [String] ATS field name to look up # @param field_name [String] Prefill field name to look up
# @param template_fields [Array<Hash>, nil] Template field definitions # @param template_fields [Array<Hash>, nil] Template field definitions
# @return [String, nil] Field UUID if found, nil otherwise # @return [String, nil] Field UUID if found, nil otherwise
# #
@ -55,22 +55,22 @@ module PrefillFieldsHelper
# find_field_uuid_by_name('employee_first_name', template_fields) # find_field_uuid_by_name('employee_first_name', template_fields)
# # => 'field-uuid-123' # # => 'field-uuid-123'
def find_field_uuid_by_name(field_name, template_fields = nil) def find_field_uuid_by_name(field_name, template_fields = nil)
AtsPrefill.find_field_uuid(field_name, template_fields) Prefill.find_field_uuid(field_name, template_fields)
end end
# Clears ATS fields cache (useful for testing or manual cache invalidation) # Clears prefill fields cache (useful for testing or manual cache invalidation)
# #
# Since Rails cache doesn't provide easy enumeration of keys, this method # Since Rails cache doesn't provide easy enumeration of keys, this method
# relies on TTL for automatic cleanup. This method is provided for potential # relies on TTL for automatic cleanup. This method is provided for potential
# future use or testing scenarios where immediate cache invalidation is needed. # future use or testing scenarios where immediate cache invalidation is needed.
# #
# @return [void] # @return [void]
def clear_ats_fields_cache def clear_prefill_fields_cache
AtsPrefill.clear_cache Prefill.clear_cache
end end
# Legacy method aliases for backward compatibility # Legacy method aliases for backward compatibility
alias build_field_lookup_cache merge_ats_prefill_values alias build_field_lookup_cache merge_prefill_values
private private
@ -78,33 +78,33 @@ module PrefillFieldsHelper
# These now delegate to the service layer for consistency # These now delegate to the service layer for consistency
def read_from_cache(cache_key) def read_from_cache(cache_key)
AtsPrefill::CacheManager.read_from_cache(cache_key) Prefill::CacheManager.read_from_cache(cache_key)
end end
def parse_ats_fields_param(ats_fields_param) def parse_prefill_fields_param(prefill_fields_param)
# This is now handled internally by FieldExtractor # This is now handled internally by FieldExtractor
# Kept for backward compatibility but not recommended for direct use # Kept for backward compatibility but not recommended for direct use
AtsPrefill::FieldExtractor.send(:parse_encoded_fields, ats_fields_param) Prefill::FieldExtractor.send(:parse_encoded_fields, prefill_fields_param)
end end
def validate_and_filter_field_names(field_names) def validate_and_filter_field_names(field_names)
# This is now handled internally by FieldExtractor # This is now handled internally by FieldExtractor
# Kept for backward compatibility but not recommended for direct use # Kept for backward compatibility but not recommended for direct use
AtsPrefill::FieldExtractor.send(:validate_field_names, field_names) Prefill::FieldExtractor.send(:validate_field_names, field_names)
end end
def valid_ats_field_name?(name) def valid_prefill_field_name?(name)
# This is now handled internally by FieldExtractor # This is now handled internally by FieldExtractor
# Kept for backward compatibility but not recommended for direct use # Kept for backward compatibility but not recommended for direct use
AtsPrefill::FieldExtractor.send(:valid_ats_field_name?, name) Prefill::FieldExtractor.send(:valid_prefill_field_name?, name)
end end
def ats_fields_cache_key(ats_fields_param) def prefill_fields_cache_key(prefill_fields_param)
AtsPrefill::CacheManager.generate_cache_key('ats_fields', ats_fields_param) Prefill::CacheManager.generate_cache_key('prefill_fields', prefill_fields_param)
end end
def cache_result(cache_key, value, ttl) def cache_result(cache_key, value, ttl)
AtsPrefill::CacheManager.write_to_cache(cache_key, value, ttl) Prefill::CacheManager.write_to_cache(cache_key, value, ttl)
end end
def cache_and_return_empty(cache_key) def cache_and_return_empty(cache_key)
@ -113,7 +113,7 @@ module PrefillFieldsHelper
end end
def field_lookup_cache_key(template_fields) def field_lookup_cache_key(template_fields)
signature = AtsPrefill::FieldMapper.send(:build_cache_signature, template_fields) signature = Prefill::FieldMapper.send(:build_cache_signature, template_fields)
AtsPrefill::CacheManager.generate_cache_key('field_mapping', signature) Prefill::CacheManager.generate_cache_key('field_mapping', signature)
end end
end end

@ -256,14 +256,14 @@
</label> </label>
</div> </div>
<div <div
v-if="availableAtsFields && availableAtsFields.length > 0" v-if="availablePrefillFields && availablePrefillFields.length > 0"
class="py-1.5 px-1 relative" class="py-1.5 px-1 relative"
@click.stop @click.stop
> >
<select <select
:placeholder="t('ats_field')" :placeholder="t('prefill_field')"
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent" class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0 bg-transparent"
data-testid="ats-fields-dropdown" data-testid="prefill-fields-dropdown"
@change="[field.prefill = $event.target.value || undefined, !field.prefill && delete field.prefill, save()]" @change="[field.prefill = $event.target.value || undefined, !field.prefill && delete field.prefill, save()]"
> >
<option <option
@ -273,12 +273,12 @@
{{ '' }} {{ '' }}
</option> </option>
<option <option
v-for="atsField in availableAtsFields" v-for="prefillField in availablePrefillFields"
:key="atsField" :key="prefillField"
:value="atsField" :value="prefillField"
:selected="field.prefill === atsField" :selected="field.prefill === prefillField"
> >
{{ formatAtsFieldName(atsField) }} {{ formatPrefillFieldName(prefillField) }}
</option> </option>
</select> </select>
<label <label
@ -526,14 +526,14 @@ export default {
} }
}, },
computed: { computed: {
availableAtsFields () { availablePrefillFields () {
return this.template.available_ats_fields || [] return this.template.available_prefill_fields || []
}, },
availableAtsFieldsForType () { availablePrefillFieldsForType () {
if (!this.template.ats_fields_by_type || !this.field.type) { if (!this.template.prefill_fields_by_type || !this.field.type) {
return [] return []
} }
return this.template.ats_fields_by_type[this.field.type] || [] return this.template.prefill_fields_by_type[this.field.type] || []
}, },
schemaAttachmentsIndexes () { schemaAttachmentsIndexes () {
return (this.template.schema || []).reduce((acc, item, index) => { return (this.template.schema || []).reduce((acc, item, index) => {
@ -594,7 +594,7 @@ export default {
} }
}, },
methods: { methods: {
formatAtsFieldName (fieldName) { formatPrefillFieldName (fieldName) {
// Convert snake_case to Title Case for display // Convert snake_case to Title Case for display
return fieldName return fieldName
.split('_') .split('_')

@ -1,96 +0,0 @@
# frozen_string_literal: true
require_relative 'ats_prefill/cache_manager'
require_relative 'ats_prefill/field_extractor'
require_relative 'ats_prefill/field_mapper'
require_relative 'ats_prefill/value_merger'
# AtsPrefill provides a clean facade for ATS (Applicant Tracking System) prefill functionality.
# This module encapsulates the complexity of extracting, validating, mapping, and merging
# ATS field values with existing submitter data.
#
# The module follows the service object pattern established in DocuSeal's codebase,
# providing focused, testable, and reusable components for ATS integration.
#
# @example Basic usage
# # Extract valid field names from request parameters
# field_names = AtsPrefill.extract_fields(params)
#
# # Merge ATS values with existing submitter values
# merged_values = AtsPrefill.merge_values(submitter_values, ats_values, template_fields)
#
# # Find specific field UUID by name
# field_uuid = AtsPrefill.find_field_uuid('employee_first_name', template_fields)
module AtsPrefill
# Extracts and validates ATS field names from request parameters
#
# @param params [ActionController::Parameters] Request parameters containing ats_fields
# @return [Array<String>] Array of valid ATS field names
#
# @example
# AtsPrefill.extract_fields(params)
# # => ['employee_first_name', 'employee_email']
def extract_fields(params)
FieldExtractor.call(params)
end
# Merges ATS prefill values with existing submitter values
#
# Existing submitter values always take precedence over ATS values to prevent
# overwriting user input.
#
# @param submitter_values [Hash] Existing values entered by submitters
# @param ats_values [Hash] Prefill values from ATS system
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [Hash] Merged values with submitter values taking precedence
#
# @example
# AtsPrefill.merge_values(
# { 'field-1' => 'John' },
# { 'employee_first_name' => 'Jane', 'employee_last_name' => 'Doe' },
# template_fields
# )
# # => { 'field-1' => 'John', 'field-2' => 'Doe' }
def merge_values(submitter_values, ats_values, template_fields = nil)
ValueMerger.call(submitter_values, ats_values, template_fields)
end
# Finds field UUID by matching ATS field name to template field's prefill attribute
#
# @param field_name [String] ATS field name to look up
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [String, nil] Field UUID if found, nil otherwise
#
# @example
# AtsPrefill.find_field_uuid('employee_first_name', template_fields)
# # => 'field-uuid-123'
def find_field_uuid(field_name, template_fields)
FieldMapper.find_field_uuid(field_name, template_fields)
end
# Creates field mapping for direct access to the mapping hash
#
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [Hash] Mapping of ATS field names to field UUIDs
#
# @example
# AtsPrefill.build_field_mapping(template_fields)
# # => { 'employee_first_name' => 'field-1', 'employee_last_name' => 'field-2' }
def build_field_mapping(template_fields)
FieldMapper.call(template_fields)
end
# Clears ATS-related caches (useful for testing or manual cache invalidation)
#
# Since Rails cache doesn't provide easy enumeration of keys, this method
# relies on TTL for automatic cleanup. This method is provided for potential
# future use or testing scenarios where immediate cache invalidation is needed.
#
# @return [void]
def clear_cache
# Since we can't easily enumerate cache keys, we'll rely on TTL for cleanup
# This method is provided for potential future use or testing
end
module_function :extract_fields, :merge_values, :find_field_uuid, :build_field_mapping, :clear_cache
end

@ -1,85 +0,0 @@
# frozen_string_literal: true
module AtsPrefill
module CacheManager
# Cache TTL for ATS field parsing (1 hour)
FIELD_EXTRACTION_TTL = 3600
# Cache TTL for field UUID lookup optimization (30 minutes)
FIELD_MAPPING_TTL = 1800
# Maximum number of cached entries to prevent memory bloat
MAX_CACHE_ENTRIES = 1000
module_function
# Fetches field extraction results from cache or computes them
#
# @param cache_key [String] The cache key to use
# @yield Block that computes the value if not cached
# @return [Array<String>] Array of valid field names
def fetch_field_extraction(cache_key, &)
fetch_with_fallback(cache_key, FIELD_EXTRACTION_TTL, &)
end
# Fetches field mapping results from cache or computes them
#
# @param cache_key [String] The cache key to use
# @yield Block that computes the value if not cached
# @return [Hash] Mapping of field names to UUIDs
def fetch_field_mapping(cache_key, &)
fetch_with_fallback(cache_key, FIELD_MAPPING_TTL, &)
end
# Generates a secure cache key using SHA256 hash
#
# @param prefix [String] Cache key prefix
# @param data [String] Data to hash for the key
# @return [String] Secure cache key
def generate_cache_key(prefix, data)
hash = Digest::SHA256.hexdigest(data.to_s)
"#{prefix}:#{hash}"
end
# Writes a value to cache with error handling
#
# @param cache_key [String] The cache key
# @param value [Object] The value to cache
# @param ttl [Integer] Time to live in seconds
# @return [void]
def write_to_cache(cache_key, value, ttl)
Rails.cache.write(cache_key, value, expires_in: ttl)
rescue StandardError
# Continue execution even if caching fails
end
# Reads from cache with error handling
#
# @param cache_key [String] The cache key to read
# @return [Object, nil] Cached value or nil if not found/error
def read_from_cache(cache_key)
Rails.cache.read(cache_key)
rescue StandardError
# Return nil if cache read fails, allowing normal processing to continue
nil
end
private
# Fetches from cache or computes value with fallback on cache errors
#
# @param cache_key [String] The cache key
# @param ttl [Integer] Time to live in seconds
# @yield Block that computes the value if not cached
# @return [Object] Cached or computed value
def fetch_with_fallback(cache_key, ttl, &)
Rails.cache.fetch(cache_key, expires_in: ttl, &)
rescue StandardError
# Fallback to computation if cache fails
yield
end
module_function :fetch_field_extraction, :fetch_field_mapping, :generate_cache_key, :write_to_cache,
:read_from_cache, :fetch_with_fallback
end
end

@ -1,77 +0,0 @@
# frozen_string_literal: true
module AtsPrefill
module FieldExtractor
# Valid field name pattern for security validation
VALID_FIELD_PATTERN = /\A(employee|manager|account|location)_[a-z]+(?:_[a-z]+)*\z/
# Extracts and validates ATS prefill field names from Base64-encoded parameters
#
# This method decodes the ats_fields parameter, validates the field names against
# allowed patterns, and caches the results to improve performance on repeated requests.
#
# @param params [ActionController::Parameters] Request parameters
# @return [Array<String>] Array of valid ATS field names, empty array if none found or on error
#
# @example
# # With params[:ats_fields] = Base64.urlsafe_encode64(['employee_first_name', 'employee_email'].to_json)
# AtsPrefill::FieldExtractor.call(params)
# # => ['employee_first_name', 'employee_email']
def call(params)
return [] if params[:ats_fields].blank?
cache_key = CacheManager.generate_cache_key('ats_fields', params[:ats_fields])
CacheManager.fetch_field_extraction(cache_key) do
extract_and_validate_fields(params[:ats_fields])
end
end
# Extracts and validates field names from encoded parameter
#
# @param encoded_param [String] Base64-encoded JSON string containing field names
# @return [Array<String>] Array of valid field names
def extract_and_validate_fields(encoded_param)
field_names = parse_encoded_fields(encoded_param)
return [] if field_names.nil?
validate_field_names(field_names)
end
# Parses and decodes the ATS fields parameter
#
# @param encoded_param [String] Base64-encoded JSON string containing field names
# @return [Array<String>, nil] Array of field names if parsing succeeds, nil on error
def parse_encoded_fields(encoded_param)
decoded_json = Base64.urlsafe_decode64(encoded_param)
JSON.parse(decoded_json)
rescue StandardError
# Return nil if Base64 decoding or JSON parsing fails
nil
end
# Validates and filters field names to only include allowed patterns
#
# @param field_names [Array] Array of field names to validate
# @return [Array<String>] Array of valid field names, empty array if input is invalid
def validate_field_names(field_names)
# Validate that we got an array of strings
return [] unless field_names.is_a?(Array) && field_names.all?(String)
# Filter to only expected field name patterns
field_names.select { |name| valid_ats_field_name?(name) }
end
# Checks if a field name matches the valid ATS field pattern
#
# @param name [String] Field name to validate
# @return [Boolean] True if field name is valid, false otherwise
def valid_ats_field_name?(name)
# Only allow expected field name patterns (security)
name.match?(VALID_FIELD_PATTERN)
end
module_function :call, :extract_and_validate_fields, :parse_encoded_fields, :validate_field_names,
:valid_ats_field_name?
end
end

@ -1,84 +0,0 @@
# frozen_string_literal: true
module AtsPrefill
module FieldMapper
# Creates optimized mapping between ATS field names and template field UUIDs
#
# Creates a hash mapping ATS field names to template field UUIDs for O(1) lookup
# performance instead of O(n) linear search. Results are cached to improve
# performance across multiple requests.
#
# @param template_fields [Array<Hash>, nil] Template field definitions containing UUID and prefill mappings
# @return [Hash] Mapping of ATS field names to field UUIDs
#
# @example
# template_fields = [
# { 'uuid' => 'field-1', 'prefill' => 'employee_first_name' },
# { 'uuid' => 'field-2', 'prefill' => 'employee_last_name' }
# ]
# AtsPrefill::FieldMapper.call(template_fields)
# # => { 'employee_first_name' => 'field-1', 'employee_last_name' => 'field-2' }
def call(template_fields)
return {} if template_fields.blank?
cache_key = CacheManager.generate_cache_key('field_mapping', build_cache_signature(template_fields))
CacheManager.fetch_field_mapping(cache_key) do
build_field_mapping(template_fields)
end
end
# Finds field UUID by matching ATS field name to template field's prefill attribute
#
# This method provides backward compatibility and is now optimized to use
# the cached lookup when possible.
#
# @param field_name [String] ATS field name to look up
# @param template_fields [Array<Hash>, nil] Template field definitions
# @return [String, nil] Field UUID if found, nil otherwise
#
# @example
# find_field_uuid('employee_first_name', template_fields)
# # => 'field-uuid-123'
def find_field_uuid(field_name, template_fields = nil)
return nil if field_name.blank? || template_fields.blank?
# Use optimized lookup cache
field_mapping = call(template_fields)
field_mapping[field_name]
end
private
# Builds a cache signature from template fields for consistent caching
#
# @param template_fields [Array<Hash>] Template field definitions
# @return [String] Cache signature based on field UUIDs and prefill attributes
def build_cache_signature(template_fields)
return '' if template_fields.blank?
# Extract relevant data for cache key generation - format matches test expectations
template_fields
.filter_map do |field|
"#{field['uuid']}:#{field['prefill']}" if field['uuid'].present? && field['prefill'].present?
end
.sort
.join('|')
end
# Builds the actual field mapping hash
#
# @param template_fields [Array<Hash>] Template field definitions
# @return [Hash] Mapping of ATS field names to field UUIDs
def build_field_mapping(template_fields)
template_fields.each_with_object({}) do |field, mapping|
prefill_name = field['prefill']
field_uuid = field['uuid']
mapping[prefill_name] = field_uuid if prefill_name.present? && field_uuid.present?
end
end
module_function :call, :find_field_uuid, :build_cache_signature, :build_field_mapping
end
end

@ -1,62 +0,0 @@
# frozen_string_literal: true
module AtsPrefill
module ValueMerger
# Merges ATS prefill values with existing submitter values
#
# This method combines ATS-provided prefill values with values already entered by submitters.
# Existing submitter values always take precedence over ATS values to prevent overwriting
# user input. Uses optimized field lookup caching for better performance.
#
# @param submitter_values [Hash] Existing values entered by submitters, keyed by field UUID
# @param ats_values [Hash] Prefill values from ATS system, keyed by ATS field name
# @param template_fields [Array<Hash>, nil] Template field definitions containing UUID and prefill mappings
# @return [Hash] Merged values with submitter values taking precedence over ATS values
#
# @example
# submitter_values = { 'field-uuid-1' => 'John' }
# ats_values = { 'employee_first_name' => 'Jane', 'employee_last_name' => 'Doe' }
# template_fields = [
# { 'uuid' => 'field-uuid-1', 'prefill' => 'employee_first_name' },
# { 'uuid' => 'field-uuid-2', 'prefill' => 'employee_last_name' }
# ]
#
# AtsPrefill::ValueMerger.call(submitter_values, ats_values, template_fields)
# # => { 'field-uuid-1' => 'John', 'field-uuid-2' => 'Doe' }
# # Note: 'John' is preserved over 'Jane' because submitter value takes precedence
def call(submitter_values, ats_values, template_fields = nil)
return submitter_values if ats_values.blank?
# Build optimized lookup cache for better performance with large field sets
field_mapping = FieldMapper.call(template_fields)
merge_values(submitter_values, ats_values, field_mapping)
end
private
# Merges ATS values into submitter values for fields that are blank
#
# @param submitter_values [Hash] Current submitter field values
# @param ats_values [Hash] ATS field values to merge
# @param field_mapping [Hash] Mapping of ATS field names to template field UUIDs
# @return [Hash] Updated submitter values
def merge_values(submitter_values, ats_values, field_mapping)
return submitter_values if ats_values.blank? || field_mapping.blank?
ats_values.each do |ats_field_name, ats_value|
field_uuid = field_mapping[ats_field_name]
next unless field_uuid
# Only merge if the submitter value is blank (nil or empty string)
# Note: false and 0 are valid values that should not be overwritten
current_value = submitter_values[field_uuid]
submitter_values[field_uuid] = ats_value if current_value.nil? || current_value == ''
end
submitter_values
end
module_function :call, :merge_values
end
end

@ -82,7 +82,7 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end end
end end
describe '#merge_ats_prefill_values' do describe '#merge_prefill_values' do
let(:submitter_values) do let(:submitter_values) do
{ {
'field-1-uuid' => 'Existing First Name', 'field-1-uuid' => 'Existing First Name',
@ -90,7 +90,7 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
} }
end end
let(:ats_values) do let(:prefill_values) do
{ {
'employee_first_name' => 'John', 'employee_first_name' => 'John',
'employee_last_name' => 'Doe', 'employee_last_name' => 'Doe',
@ -100,7 +100,7 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
context 'when template_fields is provided' do context 'when template_fields is provided' do
it 'merges ATS values for fields that do not have existing submitter values' do it 'merges ATS values for fields that do not have existing submitter values' do
result = helper.merge_ats_prefill_values(submitter_values, ats_values, template_fields) result = helper.merge_prefill_values(submitter_values, prefill_values, template_fields)
expect(result).to include( expect(result).to include(
'field-1-uuid' => 'Existing First Name', # Should not be overwritten 'field-1-uuid' => 'Existing First Name', # Should not be overwritten
@ -111,15 +111,15 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end end
it 'does not overwrite existing submitter values' do it 'does not overwrite existing submitter values' do
result = helper.merge_ats_prefill_values(submitter_values, ats_values, template_fields) result = helper.merge_prefill_values(submitter_values, prefill_values, template_fields)
expect(result['field-1-uuid']).to eq('Existing First Name') expect(result['field-1-uuid']).to eq('Existing First Name')
end end
it 'ignores ATS values for fields without matching prefill attributes' do it 'ignores ATS values for fields without matching prefill attributes' do
ats_values_with_unknown = ats_values.merge('unknown_field' => 'Unknown Value') prefill_values_with_unknown = prefill_values.merge('unknown_field' => 'Unknown Value')
result = helper.merge_ats_prefill_values(submitter_values, ats_values_with_unknown, template_fields) result = helper.merge_prefill_values(submitter_values, prefill_values_with_unknown, template_fields)
expect(result.keys).not_to include('unknown_field') expect(result.keys).not_to include('unknown_field')
end end
@ -127,19 +127,19 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
context 'when template_fields is nil' do context 'when template_fields is nil' do
it 'returns original submitter_values unchanged' do it 'returns original submitter_values unchanged' do
result = helper.merge_ats_prefill_values(submitter_values, ats_values, nil) result = helper.merge_prefill_values(submitter_values, prefill_values, nil)
expect(result).to eq(submitter_values) expect(result).to eq(submitter_values)
end end
end end
context 'when ats_values is blank' do context 'when prefill_values is blank' do
it 'returns original submitter_values for nil ats_values' do it 'returns original submitter_values for nil prefill_values' do
result = helper.merge_ats_prefill_values(submitter_values, nil, template_fields) result = helper.merge_prefill_values(submitter_values, nil, template_fields)
expect(result).to eq(submitter_values) expect(result).to eq(submitter_values)
end end
it 'returns original submitter_values for empty ats_values' do it 'returns original submitter_values for empty prefill_values' do
result = helper.merge_ats_prefill_values(submitter_values, {}, template_fields) result = helper.merge_prefill_values(submitter_values, {}, template_fields)
expect(result).to eq(submitter_values) expect(result).to eq(submitter_values)
end end
end end
@ -154,7 +154,7 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end end
it 'fills blank submitter values with ATS values' do it 'fills blank submitter values with ATS values' do
result = helper.merge_ats_prefill_values(submitter_values_with_blanks, ats_values, template_fields) result = helper.merge_prefill_values(submitter_values_with_blanks, prefill_values, template_fields)
expect(result).to include( expect(result).to include(
'field-1-uuid' => 'John', # Should be filled from ATS (was blank) 'field-1-uuid' => 'John', # Should be filled from ATS (was blank)
@ -166,48 +166,48 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end end
end end
describe '#extract_ats_prefill_fields' do describe '#extract_prefill_fields' do
before do before do
allow(helper).to receive(:params).and_return(params) allow(helper).to receive(:params).and_return(params)
end end
context 'when ats_fields parameter is present' do context 'when prefill_fields parameter is present' do
let(:fields) { %w[employee_first_name employee_last_name employee_email] } let(:fields) { %w[employee_first_name employee_last_name employee_email] }
let(:encoded_fields) { Base64.urlsafe_encode64(fields.to_json) } let(:encoded_fields) { Base64.urlsafe_encode64(fields.to_json) }
let(:params) { { ats_fields: encoded_fields } } let(:params) { { prefill_fields: encoded_fields } }
it 'decodes and returns the ATS fields' do it 'decodes and returns the ATS fields' do
result = helper.extract_ats_prefill_fields result = helper.extract_prefill_fields
expect(result).to eq(fields) expect(result).to eq(fields)
end end
it 'caches the result' do it 'caches the result' do
# The implementation now uses AtsPrefill service which uses Rails.cache.fetch # The implementation now uses AtsPrefill service which uses Rails.cache.fetch
cache_key = AtsPrefill::CacheManager.generate_cache_key('ats_fields', encoded_fields) cache_key = Prefill::CacheManager.generate_cache_key('prefill_fields', encoded_fields)
# Mock the cache to verify it's being used # Mock the cache to verify it's being used
allow(Rails.cache).to receive(:fetch).and_call_original allow(Rails.cache).to receive(:fetch).and_call_original
helper.extract_ats_prefill_fields helper.extract_prefill_fields
expect(Rails.cache).to have_received(:fetch).with(cache_key, expires_in: 3600) expect(Rails.cache).to have_received(:fetch).with(cache_key, expires_in: 3600)
end end
end end
context 'when ats_fields parameter is missing' do context 'when prefill_fields parameter is missing' do
let(:params) { {} } let(:params) { {} }
it 'returns an empty array' do it 'returns an empty array' do
result = helper.extract_ats_prefill_fields result = helper.extract_prefill_fields
expect(result).to eq([]) expect(result).to eq([])
end end
end end
context 'when ats_fields parameter is invalid' do context 'when prefill_fields parameter is invalid' do
let(:params) { { ats_fields: 'invalid-base64' } } let(:params) { { prefill_fields: 'invalid-base64' } }
it 'returns an empty array' do it 'returns an empty array' do
result = helper.extract_ats_prefill_fields result = helper.extract_prefill_fields
expect(result).to eq([]) expect(result).to eq([])
end end
end end
@ -226,9 +226,9 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
] ]
encoded = Base64.urlsafe_encode64(fields.to_json) encoded = Base64.urlsafe_encode64(fields.to_json)
allow(helper).to receive(:params).and_return({ ats_fields: encoded }) allow(helper).to receive(:params).and_return({ prefill_fields: encoded })
result = helper.extract_ats_prefill_fields result = helper.extract_prefill_fields
expect(result).to eq(fields) expect(result).to eq(fields)
end end
end end

@ -1,233 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe 'ATS Prefill Integration', type: :request do
let(:account) { create(:account) }
let(:user) { create(:user, account: account) }
let(:template_folder) { create(:template_folder, account: account) }
let(:template_fields) do
[
{
'uuid' => 'field-1-uuid',
'name' => 'First Name',
'type' => 'text',
'prefill' => 'employee_first_name',
'submitter_uuid' => 'submitter-uuid-1'
},
{
'uuid' => 'field-2-uuid',
'name' => 'Last Name',
'type' => 'text',
'prefill' => 'employee_last_name',
'submitter_uuid' => 'submitter-uuid-1'
},
{
'uuid' => 'field-3-uuid',
'name' => 'Email',
'type' => 'text',
'prefill' => 'employee_email',
'submitter_uuid' => 'submitter-uuid-1'
},
{
'uuid' => 'field-4-uuid',
'name' => 'Signature',
'type' => 'signature',
'submitter_uuid' => 'submitter-uuid-1'
}
]
end
let(:template) do
create(:template,
account: account,
author: user,
folder: template_folder,
fields: template_fields,
submitters: [{ 'name' => 'First Party', 'uuid' => 'submitter-uuid-1' }])
end
let(:submission) do
create(:submission,
template: template,
account: account,
created_by_user: user,
template_fields: template_fields,
template_submitters: [{ 'name' => 'First Party', 'uuid' => 'submitter-uuid-1' }])
end
let(:submitter) do
create(:submitter,
submission: submission,
uuid: 'submitter-uuid-1',
name: 'John Doe',
email: 'john@example.com')
end
describe 'Controller ATS parameter processing' do
let(:controller) { SubmitFormController.new }
before do
allow(controller).to receive(:params).and_return(ActionController::Parameters.new(test_params))
end
context 'when ATS fields and values are provided via Base64 parameters' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(%w[employee_first_name employee_last_name employee_email].to_json),
ats_values: Base64.urlsafe_encode64({ 'employee_first_name' => 'John', 'employee_last_name' => 'Smith',
'employee_email' => 'john.smith@company.com' }.to_json)
}
end
it 'successfully decodes and processes ATS parameters' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({
'employee_first_name' => 'John',
'employee_last_name' => 'Smith',
'employee_email' => 'john.smith@company.com'
})
end
end
context 'when ats_values parameter contains invalid Base64' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
ats_values: 'invalid-base64!'
}
end
it 'handles Base64 decoding errors gracefully' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
context 'when ats_values parameter contains valid Base64 but invalid JSON' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
ats_values: Base64.urlsafe_encode64('invalid json')
}
end
it 'handles JSON parsing errors gracefully' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
context 'when ats_values parameter contains valid JSON but wrong data type' do
let(:test_params) do
{
ats_fields: Base64.urlsafe_encode64(['employee_first_name'].to_json),
ats_values: Base64.urlsafe_encode64('["not", "a", "hash"]')
}
end
it 'handles invalid data type gracefully' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
context 'when no ATS parameters are provided' do
let(:test_params) { {} }
it 'returns empty hash when no ATS parameters present' do
result = controller.send(:fetch_ats_prefill_values_if_available)
expect(result).to eq({})
end
end
end
describe 'Helper method integration' do
include PrefillFieldsHelper
it 'correctly maps ATS field names to template field UUIDs' do
result = find_field_uuid_by_name('employee_first_name', template_fields)
expect(result).to eq('field-1-uuid')
result = find_field_uuid_by_name('employee_last_name', template_fields)
expect(result).to eq('field-2-uuid')
result = find_field_uuid_by_name('nonexistent_field', template_fields)
expect(result).to be_nil
end
it 'correctly merges ATS values with existing submitter values' do
existing_values = { 'field-1-uuid' => 'Existing John' }
ats_values = { 'employee_first_name' => 'ATS John', 'employee_last_name' => 'ATS Smith' }
result = merge_ats_prefill_values(existing_values, ats_values, template_fields)
expect(result).to eq({
'field-1-uuid' => 'Existing John', # Should not override existing value
'field-2-uuid' => 'ATS Smith' # Should add new ATS value
})
end
it 'handles empty ATS values gracefully' do
existing_values = { 'field-1-uuid' => 'Existing John' }
ats_values = {}
result = merge_ats_prefill_values(existing_values, ats_values, template_fields)
expect(result).to eq({
'field-1-uuid' => 'Existing John'
})
end
it 'handles missing template fields gracefully' do
existing_values = {}
ats_values = { 'nonexistent_field' => 'Some Value' }
result = merge_ats_prefill_values(existing_values, ats_values, template_fields)
expect(result).to eq({})
end
end
describe 'End-to-end ATS prefill workflow' do
include PrefillFieldsHelper
it 'processes complete ATS prefill workflow from parameters to merged values' do
# Step 1: Simulate controller parameter processing
controller = SubmitFormController.new
ats_fields_data = %w[employee_first_name employee_last_name employee_email]
ats_values_data = {
'employee_first_name' => 'John',
'employee_last_name' => 'Smith',
'employee_email' => 'john.smith@company.com'
}
encoded_fields = Base64.urlsafe_encode64(ats_fields_data.to_json)
encoded_values = Base64.urlsafe_encode64(ats_values_data.to_json)
params = ActionController::Parameters.new({
ats_fields: encoded_fields,
ats_values: encoded_values
})
allow(controller).to receive(:params).and_return(params)
ats_values = controller.send(:fetch_ats_prefill_values_if_available)
# Step 2: Simulate existing submitter values
existing_submitter_values = { 'field-1-uuid' => 'Existing John' }
# Step 3: Merge ATS values with existing values
final_values = merge_ats_prefill_values(existing_submitter_values, ats_values, template_fields)
# Step 4: Verify final result
expect(final_values).to eq({
'field-1-uuid' => 'Existing John', # Existing value preserved
'field-2-uuid' => 'Smith', # ATS value applied
'field-3-uuid' => 'john.smith@company.com' # ATS value applied
})
end
end
end

@ -1,148 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::CacheManager do
describe '.generate_cache_key' do
it 'generates a consistent cache key with SHA256 hash' do
key1 = described_class.generate_cache_key('test', 'data')
key2 = described_class.generate_cache_key('test', 'data')
expect(key1).to eq(key2)
expect(key1).to match(/\Atest:[a-f0-9]{64}\z/)
end
it 'generates different keys for different data' do
key1 = described_class.generate_cache_key('test', 'data1')
key2 = described_class.generate_cache_key('test', 'data2')
expect(key1).not_to eq(key2)
end
it 'generates different keys for different prefixes' do
key1 = described_class.generate_cache_key('prefix1', 'data')
key2 = described_class.generate_cache_key('prefix2', 'data')
expect(key1).not_to eq(key2)
end
end
describe '.fetch_field_extraction' do
let(:cache_key) { 'test_key' }
let(:expected_value) { %w[field1 field2] }
it 'returns cached value when available' do
allow(Rails.cache).to receive(:fetch)
.with(cache_key, expires_in: described_class::FIELD_EXTRACTION_TTL)
.and_return(expected_value)
result = described_class.fetch_field_extraction(cache_key) { 'should not be called' }
expect(result).to eq(expected_value)
end
it 'computes and caches value when not cached' do
allow(Rails.cache).to receive(:fetch).with(cache_key, expires_in: described_class::FIELD_EXTRACTION_TTL).and_yield
result = described_class.fetch_field_extraction(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
it 'falls back to computation when cache fails' do
allow(Rails.cache).to receive(:fetch).and_raise(StandardError, 'Cache error')
result = described_class.fetch_field_extraction(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
end
describe '.fetch_field_mapping' do
let(:cache_key) { 'test_key' }
let(:expected_value) { { 'field1' => 'uuid1' } }
it 'returns cached value when available' do
allow(Rails.cache).to receive(:fetch)
.with(cache_key, expires_in: described_class::FIELD_MAPPING_TTL)
.and_return(expected_value)
result = described_class.fetch_field_mapping(cache_key) { 'should not be called' }
expect(result).to eq(expected_value)
end
it 'computes and caches value when not cached' do
allow(Rails.cache).to receive(:fetch).with(cache_key, expires_in: described_class::FIELD_MAPPING_TTL).and_yield
result = described_class.fetch_field_mapping(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
it 'falls back to computation when cache fails' do
allow(Rails.cache).to receive(:fetch).and_raise(StandardError, 'Cache error')
result = described_class.fetch_field_mapping(cache_key) { expected_value }
expect(result).to eq(expected_value)
end
end
describe '.write_to_cache' do
let(:cache_key) { 'test_key' }
let(:value) { 'test_value' }
let(:ttl) { 3600 }
it 'writes to cache successfully' do
allow(Rails.cache).to receive(:write)
described_class.write_to_cache(cache_key, value, ttl)
expect(Rails.cache).to have_received(:write).with(cache_key, value, expires_in: ttl)
end
it 'handles cache write errors gracefully' do
allow(Rails.cache).to receive(:write).and_raise(StandardError, 'Cache error')
expect { described_class.write_to_cache(cache_key, value, ttl) }.not_to raise_error
end
end
describe '.read_from_cache' do
let(:cache_key) { 'test_key' }
let(:cached_value) { 'cached_value' }
it 'reads from cache successfully' do
allow(Rails.cache).to receive(:read).with(cache_key).and_return(cached_value)
result = described_class.read_from_cache(cache_key)
expect(result).to eq(cached_value)
end
it 'returns nil when cache read fails' do
allow(Rails.cache).to receive(:read).and_raise(StandardError, 'Cache error')
result = described_class.read_from_cache(cache_key)
expect(result).to be_nil
end
it 'returns nil when key not found' do
allow(Rails.cache).to receive(:read).with(cache_key).and_return(nil)
result = described_class.read_from_cache(cache_key)
expect(result).to be_nil
end
end
describe 'constants' do
it 'defines expected TTL constants' do
expect(described_class::FIELD_EXTRACTION_TTL).to eq(3600)
expect(described_class::FIELD_MAPPING_TTL).to eq(1800)
expect(described_class::MAX_CACHE_ENTRIES).to eq(1000)
end
end
end

@ -1,191 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::FieldExtractor do
describe '.call' do
context 'when ats_fields parameter is present' do
let(:fields) { %w[employee_first_name employee_last_name employee_email] }
let(:encoded_fields) { Base64.urlsafe_encode64(fields.to_json) }
let(:params) { ActionController::Parameters.new(ats_fields: encoded_fields) }
it 'decodes and returns the ATS fields' do
result = described_class.call(params)
expect(result).to eq(fields)
end
it 'caches the result' do
cache_key = AtsPrefill::CacheManager.generate_cache_key('ats_fields', encoded_fields)
allow(AtsPrefill::CacheManager).to receive(:fetch_field_extraction).and_call_original
described_class.call(params)
expect(AtsPrefill::CacheManager).to have_received(:fetch_field_extraction).with(cache_key)
end
it 'returns cached result on subsequent calls' do
cache_key = AtsPrefill::CacheManager.generate_cache_key('ats_fields', encoded_fields)
cached_result = ['cached_field']
allow(AtsPrefill::CacheManager).to receive(:fetch_field_extraction).with(cache_key).and_return(cached_result)
result = described_class.call(params)
expect(result).to eq(cached_result)
end
end
context 'when ats_fields parameter is missing' do
let(:params) { ActionController::Parameters.new({}) }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when ats_fields parameter is blank' do
let(:params) { ActionController::Parameters.new(ats_fields: '') }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when ats_fields parameter is invalid' do
let(:params) { ActionController::Parameters.new(ats_fields: 'invalid-base64') }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when decoded JSON is not an array' do
let(:invalid_data) { { not: 'an array' } }
let(:encoded_invalid) { Base64.urlsafe_encode64(invalid_data.to_json) }
let(:params) { ActionController::Parameters.new(ats_fields: encoded_invalid) }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when array contains non-string values' do
let(:mixed_data) { ['employee_first_name', 123, 'employee_email'] }
let(:encoded_mixed) { Base64.urlsafe_encode64(mixed_data.to_json) }
let(:params) { ActionController::Parameters.new(ats_fields: encoded_mixed) }
it 'returns an empty array' do
result = described_class.call(params)
expect(result).to eq([])
end
end
context 'when validating field names' do
it 'accepts all valid field name patterns' do
valid_fields = %w[
employee_first_name
employee_middle_name
employee_last_name
employee_email
manager_firstname
manager_lastname
account_name
location_name
location_street
]
encoded = Base64.urlsafe_encode64(valid_fields.to_json)
params = ActionController::Parameters.new(ats_fields: encoded)
result = described_class.call(params)
expect(result).to eq(valid_fields)
end
it 'rejects invalid field name patterns' do
invalid_fields = %w[
invalid_field
employee
_employee_name
employee_name_
EMPLOYEE_NAME
employee-name
employee.name
malicious_script
admin_password
]
encoded = Base64.urlsafe_encode64(invalid_fields.to_json)
params = ActionController::Parameters.new(ats_fields: encoded)
result = described_class.call(params)
expect(result).to eq([])
end
it 'filters out invalid fields while keeping valid ones' do
mixed_fields = %w[
employee_first_name
invalid_field
manager_lastname
malicious_script
location_name
]
expected_valid = %w[employee_first_name manager_lastname location_name]
encoded = Base64.urlsafe_encode64(mixed_fields.to_json)
params = ActionController::Parameters.new(ats_fields: encoded)
result = described_class.call(params)
expect(result).to eq(expected_valid)
end
end
context 'when handling errors' do
it 'handles JSON parsing errors gracefully' do
invalid_json = Base64.urlsafe_encode64('invalid json')
params = ActionController::Parameters.new(ats_fields: invalid_json)
result = described_class.call(params)
expect(result).to eq([])
end
it 'handles Base64 decoding errors gracefully' do
params = ActionController::Parameters.new(ats_fields: 'invalid-base64!')
result = described_class.call(params)
expect(result).to eq([])
end
end
end
describe 'VALID_FIELD_PATTERN' do
it 'matches expected field patterns' do
valid_patterns = %w[
employee_first_name
manager_last_name
account_company_name
location_street_address
]
expect(valid_patterns).to all(match(described_class::VALID_FIELD_PATTERN))
end
it 'rejects invalid field patterns' do
invalid_patterns = %w[
invalid_field
employee
_employee_name
employee_name_
EMPLOYEE_NAME
employee-name
employee.name
123_field
field_123
]
invalid_patterns.each do |pattern|
expect(pattern).not_to match(described_class::VALID_FIELD_PATTERN)
end
end
end
end

@ -1,251 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::FieldMapper do
let(:template_fields) do
[
{
'uuid' => 'field-1-uuid',
'name' => 'First Name',
'type' => 'text',
'prefill' => 'employee_first_name'
},
{
'uuid' => 'field-2-uuid',
'name' => 'Last Name',
'type' => 'text',
'prefill' => 'employee_last_name'
},
{
'uuid' => 'field-3-uuid',
'name' => 'Email',
'type' => 'text',
'prefill' => 'employee_email'
},
{
'uuid' => 'field-4-uuid',
'name' => 'Signature',
'type' => 'signature'
# No prefill attribute
}
]
end
describe '.call' do
context 'when template_fields is provided' do
it 'returns correct mapping of prefill names to UUIDs' do
result = described_class.call(template_fields)
expect(result).to eq({
'employee_first_name' => 'field-1-uuid',
'employee_last_name' => 'field-2-uuid',
'employee_email' => 'field-3-uuid'
})
end
it 'excludes fields without prefill attributes' do
result = described_class.call(template_fields)
expect(result).not_to have_key('signature')
expect(result.values).not_to include('field-4-uuid')
end
it 'caches the result' do
cache_signature = template_fields
.filter_map do |f|
"#{f['uuid']}:#{f['prefill']}" if f['uuid'].present? && f['prefill'].present?
end
.sort
.join('|')
cache_key = AtsPrefill::CacheManager.generate_cache_key('field_mapping', cache_signature)
allow(AtsPrefill::CacheManager).to receive(:fetch_field_mapping).and_call_original
described_class.call(template_fields)
expect(AtsPrefill::CacheManager).to have_received(:fetch_field_mapping).with(cache_key)
end
it 'returns cached result on subsequent calls' do
cache_signature = template_fields
.filter_map do |f|
"#{f['uuid']}:#{f['prefill']}" if f['uuid'].present? && f['prefill'].present?
end
.sort
.join('|')
cache_key = AtsPrefill::CacheManager.generate_cache_key('field_mapping', cache_signature)
cached_result = { 'cached_field' => 'cached_uuid' }
allow(AtsPrefill::CacheManager).to receive(:fetch_field_mapping).with(cache_key).and_return(cached_result)
result = described_class.call(template_fields)
expect(result).to eq(cached_result)
end
end
context 'when template_fields is nil' do
it 'returns empty hash' do
result = described_class.call(nil)
expect(result).to eq({})
end
end
context 'when template_fields is empty' do
it 'returns empty hash' do
result = described_class.call([])
expect(result).to eq({})
end
end
context 'when fields have missing attributes' do
let(:incomplete_fields) do
[
{
'uuid' => 'field-1-uuid',
'prefill' => 'employee_first_name'
},
{
'uuid' => 'field-2-uuid'
# Missing prefill
},
{
'prefill' => 'employee_last_name'
# Missing uuid
},
{
'uuid' => '',
'prefill' => 'employee_email'
},
{
'uuid' => 'field-5-uuid',
'prefill' => ''
}
]
end
it 'only includes fields with both uuid and prefill present' do
result = described_class.call(incomplete_fields)
expect(result).to eq({
'employee_first_name' => 'field-1-uuid'
})
end
end
context 'with duplicate prefill names' do
let(:duplicate_fields) do
[
{
'uuid' => 'field-1-uuid',
'prefill' => 'employee_name'
},
{
'uuid' => 'field-2-uuid',
'prefill' => 'employee_name'
}
]
end
it 'uses the last occurrence for duplicate prefill names' do
result = described_class.call(duplicate_fields)
expect(result).to eq({
'employee_name' => 'field-2-uuid'
})
end
end
end
describe '.find_field_uuid' do
context 'when template_fields is provided' do
it 'returns the correct UUID for a matching ATS field name' do
uuid = described_class.find_field_uuid('employee_first_name', template_fields)
expect(uuid).to eq('field-1-uuid')
end
it 'returns the correct UUID for another matching ATS field name' do
uuid = described_class.find_field_uuid('employee_email', template_fields)
expect(uuid).to eq('field-3-uuid')
end
it 'returns nil for a non-matching ATS field name' do
uuid = described_class.find_field_uuid('non_existent_field', template_fields)
expect(uuid).to be_nil
end
it 'returns nil for a field without prefill attribute' do
uuid = described_class.find_field_uuid('signature', template_fields)
expect(uuid).to be_nil
end
it 'uses the cached field mapping' do
allow(described_class).to receive(:call).with(template_fields).and_return(
{ 'employee_first_name' => 'field-1-uuid' }
)
uuid = described_class.find_field_uuid('employee_first_name',
template_fields)
expect(uuid).to eq('field-1-uuid')
end
end
context 'when template_fields is nil' do
it 'returns nil' do
uuid = described_class.find_field_uuid('employee_first_name', nil)
expect(uuid).to be_nil
end
end
context 'when template_fields is empty' do
it 'returns nil' do
uuid = described_class.find_field_uuid('employee_first_name', [])
expect(uuid).to be_nil
end
end
context 'when field_name is blank' do
it 'returns nil for nil field_name' do
uuid = described_class.find_field_uuid(nil, template_fields)
expect(uuid).to be_nil
end
it 'returns nil for empty field_name' do
uuid = described_class.find_field_uuid('', template_fields)
expect(uuid).to be_nil
end
end
end
describe 'cache signature generation' do
it 'generates consistent cache signatures' do
signature1 = described_class.send(:build_cache_signature, template_fields)
signature2 = described_class.send(:build_cache_signature, template_fields)
expect(signature1).to eq(signature2)
end
it 'generates different signatures for different field sets' do
different_fields = [
{
'uuid' => 'different-uuid',
'prefill' => 'different_field'
}
]
signature1 = described_class.send(:build_cache_signature, template_fields)
signature2 = described_class.send(:build_cache_signature, different_fields)
expect(signature1).not_to eq(signature2)
end
it 'generates same signature regardless of field order' do
shuffled_fields = template_fields.shuffle
signature1 = described_class.send(:build_cache_signature, template_fields)
signature2 = described_class.send(:build_cache_signature, shuffled_fields)
expect(signature1).to eq(signature2)
end
end
end

@ -1,213 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill::ValueMerger do
let(:template_fields) do
[
{
'uuid' => 'field-1-uuid',
'name' => 'First Name',
'type' => 'text',
'prefill' => 'employee_first_name'
},
{
'uuid' => 'field-2-uuid',
'name' => 'Last Name',
'type' => 'text',
'prefill' => 'employee_last_name'
},
{
'uuid' => 'field-3-uuid',
'name' => 'Email',
'type' => 'text',
'prefill' => 'employee_email'
},
{
'uuid' => 'field-4-uuid',
'name' => 'Signature',
'type' => 'signature'
# No prefill attribute
}
]
end
describe '.call' do
let(:submitter_values) do
{
'field-1-uuid' => 'Existing First Name',
'field-4-uuid' => 'Existing Signature'
}
end
let(:ats_values) do
{
'employee_first_name' => 'John',
'employee_last_name' => 'Doe',
'employee_email' => 'john.doe@example.com'
}
end
context 'when template_fields is provided' do
it 'merges ATS values for fields that do not have existing submitter values' do
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to include(
'field-1-uuid' => 'Existing First Name', # Should not be overwritten
'field-2-uuid' => 'Doe', # Should be set from ATS
'field-3-uuid' => 'john.doe@example.com', # Should be set from ATS
'field-4-uuid' => 'Existing Signature' # Should remain unchanged
)
end
it 'does not overwrite existing submitter values' do
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq('Existing First Name')
end
it 'ignores ATS values for fields without matching prefill attributes' do
ats_values_with_unknown = ats_values.merge('unknown_field' => 'Unknown Value')
result = described_class.call(submitter_values, ats_values_with_unknown, template_fields)
expect(result.keys).not_to include('unknown_field')
end
it 'uses FieldMapper to get field mapping' do
expected_mapping = {
'employee_first_name' => 'field-1-uuid',
'employee_last_name' => 'field-2-uuid',
'employee_email' => 'field-3-uuid'
}
allow(AtsPrefill::FieldMapper).to receive(:call).and_return(expected_mapping)
described_class.call(submitter_values, ats_values, template_fields)
expect(AtsPrefill::FieldMapper).to have_received(:call).with(template_fields)
end
end
context 'when template_fields is nil' do
it 'returns original submitter_values unchanged' do
result = described_class.call(submitter_values, ats_values, nil)
expect(result).to eq(submitter_values)
end
end
context 'when ats_values is blank' do
it 'returns original submitter_values for nil ats_values' do
result = described_class.call(submitter_values, nil, template_fields)
expect(result).to eq(submitter_values)
end
it 'returns original submitter_values for empty ats_values' do
result = described_class.call(submitter_values, {}, template_fields)
expect(result).to eq(submitter_values)
end
end
context 'when submitter_values has blank values' do
let(:submitter_values_with_blanks) do
{
'field-1-uuid' => '',
'field-2-uuid' => nil,
'field-4-uuid' => 'Existing Signature'
}
end
it 'fills blank submitter values with ATS values' do
result = described_class.call(submitter_values_with_blanks, ats_values, template_fields)
expect(result).to include(
'field-1-uuid' => 'John', # Should be filled from ATS (was blank)
'field-2-uuid' => 'Doe', # Should be filled from ATS (was nil)
'field-3-uuid' => 'john.doe@example.com', # Should be set from ATS (was missing)
'field-4-uuid' => 'Existing Signature' # Should remain unchanged
)
end
it 'treats empty string as blank' do
submitter_values = { 'field-1-uuid' => '' }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq('John')
end
it 'treats nil as blank' do
submitter_values = { 'field-1-uuid' => nil }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq('John')
end
it 'does not treat false as blank' do
submitter_values = { 'field-1-uuid' => false }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to be(false)
end
it 'does not treat zero as blank' do
submitter_values = { 'field-1-uuid' => 0 }
ats_values = { 'employee_first_name' => 'John' }
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result['field-1-uuid']).to eq(0)
end
end
context 'when field mapping is empty' do
it 'returns original submitter values when no fields match' do
allow(AtsPrefill::FieldMapper).to receive(:call).and_return({})
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to eq(submitter_values)
end
end
context 'with complex scenarios' do
it 'handles multiple ATS values with partial existing submitter values' do
submitter_values = {
'field-1-uuid' => 'Keep This',
'field-2-uuid' => '',
'field-5-uuid' => 'Unrelated Field'
}
ats_values = {
'employee_first_name' => 'Should Not Override',
'employee_last_name' => 'Should Fill',
'employee_email' => 'Should Add',
'unknown_field' => 'Should Ignore'
}
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to eq({
'field-1-uuid' => 'Keep This',
'field-2-uuid' => 'Should Fill',
'field-3-uuid' => 'Should Add',
'field-5-uuid' => 'Unrelated Field'
})
end
it 'modifies the original submitter_values hash' do
original_values = submitter_values.dup
result = described_class.call(submitter_values, ats_values, template_fields)
expect(result).to be(submitter_values)
expect(submitter_values).not_to eq(original_values)
end
end
end
end

@ -1,175 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe AtsPrefill do
let(:template_fields) do
[
{
'uuid' => 'field-1-uuid',
'name' => 'First Name',
'type' => 'text',
'prefill' => 'employee_first_name'
},
{
'uuid' => 'field-2-uuid',
'name' => 'Last Name',
'type' => 'text',
'prefill' => 'employee_last_name'
}
]
end
describe '.extract_fields' do
let(:params) { ActionController::Parameters.new(ats_fields: 'encoded_data') }
it 'delegates to FieldExtractor' do
expected_result = %w[employee_first_name employee_last_name]
allow(AtsPrefill::FieldExtractor).to receive(:call).with(params).and_return(expected_result)
result = described_class.extract_fields(params)
expect(result).to eq(expected_result)
end
end
describe '.merge_values' do
let(:submitter_values) { { 'field-1-uuid' => 'John' } }
let(:ats_values) { { 'employee_last_name' => 'Doe' } }
it 'delegates to ValueMerger' do
expected_result = { 'field-1-uuid' => 'John', 'field-2-uuid' => 'Doe' }
allow(AtsPrefill::ValueMerger).to receive(:call).with(submitter_values, ats_values,
template_fields).and_return(expected_result)
result = described_class.merge_values(submitter_values, ats_values, template_fields)
expect(result).to eq(expected_result)
end
it 'handles nil template_fields' do
allow(AtsPrefill::ValueMerger).to receive(:call).with(submitter_values, ats_values,
nil).and_return(submitter_values)
result = described_class.merge_values(submitter_values, ats_values, nil)
expect(result).to eq(submitter_values)
end
end
describe '.find_field_uuid' do
let(:field_name) { 'employee_first_name' }
it 'delegates to FieldMapper.find_field_uuid' do
expected_uuid = 'field-1-uuid'
allow(AtsPrefill::FieldMapper).to receive(:find_field_uuid).with(field_name,
template_fields).and_return(expected_uuid)
result = described_class.find_field_uuid(field_name, template_fields)
expect(result).to eq(expected_uuid)
end
it 'returns nil for non-existent field' do
allow(AtsPrefill::FieldMapper).to receive(:find_field_uuid).with('non_existent', template_fields).and_return(nil)
result = described_class.find_field_uuid('non_existent', template_fields)
expect(result).to be_nil
end
end
describe '.build_field_mapping' do
it 'delegates to FieldMapper.call' do
expected_mapping = { 'employee_first_name' => 'field-1-uuid', 'employee_last_name' => 'field-2-uuid' }
allow(AtsPrefill::FieldMapper).to receive(:call).with(template_fields).and_return(expected_mapping)
result = described_class.build_field_mapping(template_fields)
expect(result).to eq(expected_mapping)
end
it 'handles nil template_fields' do
allow(AtsPrefill::FieldMapper).to receive(:call).with(nil).and_return({})
result = described_class.build_field_mapping(nil)
expect(result).to eq({})
end
end
describe '.clear_cache' do
it 'exists as a method for future use' do
expect { described_class.clear_cache }.not_to raise_error
end
it 'returns nil' do
result = described_class.clear_cache
expect(result).to be_nil
end
end
describe 'integration test' do
let(:params) do
fields = %w[employee_first_name employee_last_name]
encoded_fields = Base64.urlsafe_encode64(fields.to_json)
ActionController::Parameters.new(ats_fields: encoded_fields)
end
let(:submitter_values) { { 'field-1-uuid' => 'Existing Name' } }
let(:ats_values) { { 'employee_first_name' => 'John', 'employee_last_name' => 'Doe' } }
it 'works end-to-end with real service objects' do
# Extract fields
extracted_fields = described_class.extract_fields(params)
expect(extracted_fields).to eq(%w[employee_first_name employee_last_name])
# Build field mapping
field_mapping = described_class.build_field_mapping(template_fields)
expect(field_mapping).to eq({
'employee_first_name' => 'field-1-uuid',
'employee_last_name' => 'field-2-uuid'
})
# Find specific field UUID
uuid = described_class.find_field_uuid('employee_first_name', template_fields)
expect(uuid).to eq('field-1-uuid')
# Merge values
merged_values = described_class.merge_values(submitter_values, ats_values, template_fields)
expect(merged_values).to eq({
'field-1-uuid' => 'Existing Name', # Should not be overwritten
'field-2-uuid' => 'Doe' # Should be added from ATS
})
end
end
describe 'module structure' do
it 'includes all expected methods' do
expected_methods = %i[
extract_fields
merge_values
find_field_uuid
build_field_mapping
clear_cache
]
expected_methods.each do |method|
expect(described_class).to respond_to(method)
end
end
it 'is a module with module_function' do
expect(described_class).to be_a(Module)
# Check that methods are available as module methods
expect(described_class).to respond_to(:extract_fields)
expect(described_class.methods).to include(:extract_fields)
end
end
describe 'error handling' do
it 'propagates errors from underlying services' do
allow(AtsPrefill::FieldExtractor).to receive(:call).and_raise(StandardError, 'Test error')
expect { described_class.extract_fields({}) }.to raise_error(StandardError, 'Test error')
end
end
end
Loading…
Cancel
Save