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
@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
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?)
ActiveRecord::Associations::Preloader.new(

@ -28,8 +28,8 @@ class SubmitFormController < ApplicationController
Submitters::MaybeUpdateDefaultValues.call(@submitter, current_user)
# Fetch ATS prefill values if task_assignment_id is provided
@ats_prefill_values = fetch_ats_prefill_values_if_available
# Fetch prefill values if available
@prefill_values = fetch_prefill_values_if_available
@attachments_index = build_attachments_index(submission)
@ -102,23 +102,23 @@ class SubmitFormController < ApplicationController
.preload(:blob).index_by(&:uuid)
end
def fetch_ats_prefill_values_if_available
# ATS passes values directly as Base64-encoded JSON parameters
return {} if params[:ats_values].blank?
def fetch_prefill_values_if_available
# External system passes values directly as Base64-encoded JSON parameters
return {} if params[:prefill_values].blank?
# 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
decoded_json = Base64.urlsafe_decode64(params[:ats_values])
decoded_json = Base64.urlsafe_decode64(params[:prefill_values])
# Security: Limit decoded JSON size as well
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
ats_values.is_a?(Hash) ? ats_values : {}
prefill_values.is_a?(Hash) ? prefill_values : {}
rescue StandardError
{}
end

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

@ -1,53 +1,53 @@
# frozen_string_literal: true
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.
#
# @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
# # With params[:ats_fields] = Base64.urlsafe_encode64(['employee_first_name', 'employee_email'].to_json)
# extract_ats_prefill_fields
# # With params[:prefill_fields] = Base64.urlsafe_encode64(['employee_first_name', 'employee_email'].to_json)
# extract_prefill_fields
# # => ['employee_first_name', 'employee_email']
def extract_ats_prefill_fields
AtsPrefill.extract_fields(params)
def extract_prefill_fields
Prefill.extract_fields(params)
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.
# Existing submitter values always take precedence over ATS values to prevent overwriting
# This method combines externally-provided prefill values with values already entered by submitters.
# Existing submitter values always take precedence over prefill 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 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
# @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
# 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 = [
# { 'uuid' => 'field-uuid-1', 'prefill' => 'employee_first_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' }
# # Note: 'John' is preserved over 'Jane' because submitter value takes precedence
def merge_ats_prefill_values(submitter_values, ats_values, template_fields = nil)
AtsPrefill.merge_values(submitter_values, ats_values, template_fields)
def merge_prefill_values(submitter_values, prefill_values, template_fields = nil)
Prefill.merge_values(submitter_values, prefill_values, template_fields)
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
# 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
# @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)
# # => 'field-uuid-123'
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
# 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
# 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_ats_fields_cache
AtsPrefill.clear_cache
def clear_prefill_fields_cache
Prefill.clear_cache
end
# Legacy method aliases for backward compatibility
alias build_field_lookup_cache merge_ats_prefill_values
alias build_field_lookup_cache merge_prefill_values
private
@ -78,33 +78,33 @@ module PrefillFieldsHelper
# These now delegate to the service layer for consistency
def read_from_cache(cache_key)
AtsPrefill::CacheManager.read_from_cache(cache_key)
Prefill::CacheManager.read_from_cache(cache_key)
end
def parse_ats_fields_param(ats_fields_param)
def parse_prefill_fields_param(prefill_fields_param)
# This is now handled internally by FieldExtractor
# 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
def validate_and_filter_field_names(field_names)
# This is now handled internally by FieldExtractor
# 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
def valid_ats_field_name?(name)
def valid_prefill_field_name?(name)
# This is now handled internally by FieldExtractor
# 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
def ats_fields_cache_key(ats_fields_param)
AtsPrefill::CacheManager.generate_cache_key('ats_fields', ats_fields_param)
def prefill_fields_cache_key(prefill_fields_param)
Prefill::CacheManager.generate_cache_key('prefill_fields', prefill_fields_param)
end
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
def cache_and_return_empty(cache_key)
@ -113,7 +113,7 @@ module PrefillFieldsHelper
end
def field_lookup_cache_key(template_fields)
signature = AtsPrefill::FieldMapper.send(:build_cache_signature, template_fields)
AtsPrefill::CacheManager.generate_cache_key('field_mapping', signature)
signature = Prefill::FieldMapper.send(:build_cache_signature, template_fields)
Prefill::CacheManager.generate_cache_key('field_mapping', signature)
end
end

@ -256,14 +256,14 @@
</label>
</div>
<div
v-if="availableAtsFields && availableAtsFields.length > 0"
v-if="availablePrefillFields && availablePrefillFields.length > 0"
class="py-1.5 px-1 relative"
@click.stop
>
<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"
data-testid="ats-fields-dropdown"
data-testid="prefill-fields-dropdown"
@change="[field.prefill = $event.target.value || undefined, !field.prefill && delete field.prefill, save()]"
>
<option
@ -273,12 +273,12 @@
{{ '' }}
</option>
<option
v-for="atsField in availableAtsFields"
:key="atsField"
:value="atsField"
:selected="field.prefill === atsField"
v-for="prefillField in availablePrefillFields"
:key="prefillField"
:value="prefillField"
:selected="field.prefill === prefillField"
>
{{ formatAtsFieldName(atsField) }}
{{ formatPrefillFieldName(prefillField) }}
</option>
</select>
<label
@ -526,14 +526,14 @@ export default {
}
},
computed: {
availableAtsFields () {
return this.template.available_ats_fields || []
availablePrefillFields () {
return this.template.available_prefill_fields || []
},
availableAtsFieldsForType () {
if (!this.template.ats_fields_by_type || !this.field.type) {
availablePrefillFieldsForType () {
if (!this.template.prefill_fields_by_type || !this.field.type) {
return []
}
return this.template.ats_fields_by_type[this.field.type] || []
return this.template.prefill_fields_by_type[this.field.type] || []
},
schemaAttachmentsIndexes () {
return (this.template.schema || []).reduce((acc, item, index) => {
@ -594,7 +594,7 @@ export default {
}
},
methods: {
formatAtsFieldName (fieldName) {
formatPrefillFieldName (fieldName) {
// Convert snake_case to Title Case for display
return fieldName
.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
describe '#merge_ats_prefill_values' do
describe '#merge_prefill_values' do
let(:submitter_values) do
{
'field-1-uuid' => 'Existing First Name',
@ -90,7 +90,7 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
}
end
let(:ats_values) do
let(:prefill_values) do
{
'employee_first_name' => 'John',
'employee_last_name' => 'Doe',
@ -100,7 +100,7 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
context 'when template_fields is provided' 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(
'field-1-uuid' => 'Existing First Name', # Should not be overwritten
@ -111,15 +111,15 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end
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')
end
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')
end
@ -127,19 +127,19 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
context 'when template_fields is nil' 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)
end
end
context 'when ats_values is blank' do
it 'returns original submitter_values for nil ats_values' do
result = helper.merge_ats_prefill_values(submitter_values, nil, template_fields)
context 'when prefill_values is blank' do
it 'returns original submitter_values for nil prefill_values' do
result = helper.merge_prefill_values(submitter_values, nil, template_fields)
expect(result).to eq(submitter_values)
end
it 'returns original submitter_values for empty ats_values' do
result = helper.merge_ats_prefill_values(submitter_values, {}, template_fields)
it 'returns original submitter_values for empty prefill_values' do
result = helper.merge_prefill_values(submitter_values, {}, template_fields)
expect(result).to eq(submitter_values)
end
end
@ -154,7 +154,7 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end
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(
'field-1-uuid' => 'John', # Should be filled from ATS (was blank)
@ -166,48 +166,48 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
end
end
describe '#extract_ats_prefill_fields' do
describe '#extract_prefill_fields' do
before do
allow(helper).to receive(:params).and_return(params)
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(: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
result = helper.extract_ats_prefill_fields
result = helper.extract_prefill_fields
expect(result).to eq(fields)
end
it 'caches the result' do
# 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
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)
end
end
context 'when ats_fields parameter is missing' do
context 'when prefill_fields parameter is missing' do
let(:params) { {} }
it 'returns an empty array' do
result = helper.extract_ats_prefill_fields
result = helper.extract_prefill_fields
expect(result).to eq([])
end
end
context 'when ats_fields parameter is invalid' do
let(:params) { { ats_fields: 'invalid-base64' } }
context 'when prefill_fields parameter is invalid' do
let(:params) { { prefill_fields: 'invalid-base64' } }
it 'returns an empty array' do
result = helper.extract_ats_prefill_fields
result = helper.extract_prefill_fields
expect(result).to eq([])
end
end
@ -226,9 +226,9 @@ RSpec.describe PrefillFieldsHelper, type: :helper do
]
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)
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