d7f6c3700b
permit.io local pdp * docs and testing * build fix * docs * permit rights * accept self signed certs * fix eula error
2130 lines
81 KiB
Bash
Executable File
2130 lines
81 KiB
Bash
Executable File
#!/bin/bash
|
|
# =============================================================================
|
|
# text_extraction_integration_test.sh
|
|
# =============================================================================
|
|
#
|
|
# DESCRIPTION:
|
|
# This script performs end-to-end integration testing of the Query Orchestration
|
|
# API by executing a series of REST API calls using curl. It is designed to:
|
|
# 1. Demonstrate that all REST endpoints are working correctly
|
|
# 2. Show UI developers how to make API calls
|
|
# 3. Populate the system with sample data for UI development/testing
|
|
#
|
|
# USAGE:
|
|
# ./text_extraction_integration_test.sh <base_url>
|
|
#
|
|
# EXAMPLE:
|
|
# ./text_extraction_integration_test.sh http://localhost:8080
|
|
# ./text_extraction_integration_test.sh http://queryo-query-q0pz6j2syaox-1001614225.us-east-2.elb.amazonaws.com
|
|
#
|
|
# REQUIREMENTS:
|
|
# - bash 4.0+
|
|
# - curl
|
|
# - jq (for JSON parsing)
|
|
# - zip
|
|
# - The target service must NOT require authentication (auth bypass mode)
|
|
#
|
|
# NOTES ON DOCUMENT PROCESSING:
|
|
# When running against a local development environment (docker-compose), documents
|
|
# uploaded via batch or single upload go through an async queue processing pipeline:
|
|
# 1. Upload writes to S3
|
|
# 2. S3 event triggers store_event_runner
|
|
# 3. store_event_runner creates the document record
|
|
#
|
|
# This async processing may not complete during the script execution timeframe.
|
|
# When running against a deployed service with proper queue processing, documents
|
|
# should appear within seconds of upload.
|
|
#
|
|
# The script is designed to continue even when documents aren't immediately available,
|
|
# so it can demonstrate the API endpoints regardless of document processing status.
|
|
#
|
|
# API OPERATIONS PERFORMED (in order):
|
|
# ┌─────────────────────────────────────────────────────────────────────────┐
|
|
# │ PHASE 1: CLIENT SETUP │
|
|
# ├─────────────────────────────────────────────────────────────────────────┤
|
|
# │ Step 1: POST /client Create new client │
|
|
# │ Step 2: GET /client/{id} Verify client exists │
|
|
# │ Step 2.5: GET /clients List all clients │
|
|
# │ Step 3: PATCH /client/{id} Update client (can_sync) │
|
|
# │ Step 4: GET /client/{id}/status Get client sync status │
|
|
# ├─────────────────────────────────────────────────────────────────────────┤
|
|
# │ PHASE 2: DOCUMENT UPLOAD │
|
|
# ├─────────────────────────────────────────────────────────────────────────┤
|
|
# │ Step 5: (local) Create ZIP with 3 PDFs in folder structure │
|
|
# │ Step 6: POST /client/{id}/document/batch Upload ZIP batch │
|
|
# │ Step 7: GET /client/{id}/document/batch/{batchId} │
|
|
# │ Poll batch status │
|
|
# │ GET /client/{id}/document/batch List all batches │
|
|
# │ Step 8: GET /client/{id}/document List documents (expect 3)│
|
|
# │ Step 9-10: (skipped) Folder-based document retrieval │
|
|
# │ Step 11: POST /client/{id}/document Upload single PDF │
|
|
# │ Step 12: GET /client/{id}/document Verify total docs (4) │
|
|
# │ Step 12.5:GET /document/{id} Verify fileSizeBytes │
|
|
# │ Step 12.8:BUG TEST - Verify folder metrics BEFORE labeling │
|
|
# │ GET /client/{id}/folders?metrics=true │
|
|
# │ GET /folders/{folderId}/metrics │
|
|
# │ Assert known doc counts per folder │
|
|
# ├─────────────────────────────────────────────────────────────────────────┤
|
|
# │ PHASE 3: LABEL OPERATIONS │
|
|
# ├─────────────────────────────────────────────────────────────────────────┤
|
|
# │ Step 13-14: POST /documents/{docId}/labels Apply labels (x5): │
|
|
# │ GET /documents/{docId}/labels - Ingested │
|
|
# │ - OCR_Processed │
|
|
# │ - GenAI_Processed │
|
|
# │ - Doczy_AI_Completed │
|
|
# │ - Dashboard_Ready │
|
|
# │ Step 15: GET /client/{id}/folders?metrics=true │
|
|
# │ List folders w/ metrics │
|
|
# │ Step 16: GET /folders/{folderId}/metrics Get metrics per folder │
|
|
# │ Step 16.5:GET /folders/{folderId}/documents │
|
|
# │ Verify fileSizeBytes │
|
|
# │ Step 17: GET /labels/{label}/documents?clientId={id} │
|
|
# │ Get docs by label │
|
|
# │ Step 17.5:BUG TEST - Verify folder metrics AFTER labeling │
|
|
# │ GET /client/{id}/folders?metrics=true │
|
|
# │ GET /folders/{folderId}/metrics │
|
|
# │ Assert same known counts still hold │
|
|
# ├─────────────────────────────────────────────────────────────────────────┤
|
|
# │ PHASE 4: FIELD EXTRACTIONS │
|
|
# ├─────────────────────────────────────────────────────────────────────────┤
|
|
# │ Step 18: GET /field-extractions?documentId={docId} │
|
|
# │ Verify none exist │
|
|
# │ Step 19: POST /field-extractions Create extraction v1 │
|
|
# │ Step 20: GET /field-extractions/history?documentId={docId} │
|
|
# │ Verify 1 version │
|
|
# │ Step 21: POST /field-extractions Create extraction v2 │
|
|
# │ Step 22: GET /field-extractions/history?documentId={docId} │
|
|
# │ Verify 2 versions │
|
|
# │ Step 23: GET /field-extractions/version?documentId={docId}&version=N│
|
|
# │ Retrieve v1, v2, v999 │
|
|
# └─────────────────────────────────────────────────────────────────────────┘
|
|
#
|
|
# EXIT CODES:
|
|
# 0 - All tests passed
|
|
# 1 - Missing dependencies or invalid arguments
|
|
# 2 - API call failed
|
|
#
|
|
# =============================================================================
|
|
|
|
set -e # Exit on first error
|
|
|
|
# =============================================================================
|
|
# CONFIGURATION
|
|
# =============================================================================
|
|
|
|
# Colors for output (disable if not a terminal)
|
|
if [[ -t 1 ]]; then
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
else
|
|
RED=''
|
|
GREEN=''
|
|
YELLOW=''
|
|
BLUE=''
|
|
NC=''
|
|
fi
|
|
|
|
# Generate unique identifiers for this test run
|
|
TIMESTAMP=$(date +%s)
|
|
CLIENT_NAME="IntegrationTestClient_${TIMESTAMP}"
|
|
CLIENT_EXTERNAL_ID="int-test-${TIMESTAMP}"
|
|
|
|
# Variables to store IDs across test steps
|
|
CLIENT_ID=""
|
|
BATCH_ID=""
|
|
DOCUMENT_IDS=()
|
|
TARGET_DOC_ID=""
|
|
FOLDER_IDS=()
|
|
|
|
# BUG TEST: Expected folder document counts (known from the test's upload structure).
|
|
# The batch upload creates: /folder1/subfolder2/file1.pdf, /folder1/subfolder2/file2.pdf,
|
|
# /folder2/subfolder1/file3.pdf. The single upload goes to root "/".
|
|
# These are checked before labeling (step 12.8) and again after (step 17.5).
|
|
EXPECTED_FOLDER_COUNTS_BY_PATH='{ "/": 1, "/folder1": 0, "/folder1/subfolder2": 2, "/folder2": 0, "/folder2/subfolder1": 1 }'
|
|
|
|
# =============================================================================
|
|
# HELPER FUNCTIONS
|
|
# =============================================================================
|
|
|
|
# print_header - Prints a formatted section header
|
|
# Parameters:
|
|
# $1 - Step number
|
|
# $2 - Description
|
|
print_header() {
|
|
local step="$1"
|
|
local desc="$2"
|
|
echo ""
|
|
echo -e "${BLUE}=== Step ${step}: ${desc} ===${NC}"
|
|
}
|
|
|
|
# print_success - Prints a success message
|
|
# Parameters:
|
|
# $1 - Message
|
|
print_success() {
|
|
echo -e "${GREEN}✓ $1${NC}"
|
|
}
|
|
|
|
# print_error - Prints an error message and exits
|
|
# Parameters:
|
|
# $1 - Message
|
|
print_error() {
|
|
echo -e "${RED}✗ ERROR: $1${NC}"
|
|
exit 2
|
|
}
|
|
|
|
# print_warning - Prints a warning message
|
|
# Parameters:
|
|
# $1 - Message
|
|
print_warning() {
|
|
echo -e "${YELLOW}⚠ WARNING: $1${NC}"
|
|
}
|
|
|
|
# print_info - Prints an informational message
|
|
# Parameters:
|
|
# $1 - Message
|
|
print_info() {
|
|
echo -e " $1"
|
|
}
|
|
|
|
# check_response - Checks if the HTTP response code indicates success
|
|
# Parameters:
|
|
# $1 - Expected HTTP status code(s) (comma-separated, e.g., "200,201")
|
|
# $2 - Actual HTTP status code
|
|
# $3 - Response body (for error messages)
|
|
# $4 - Endpoint description
|
|
check_response() {
|
|
local expected="$1"
|
|
local actual="$2"
|
|
local body="$3"
|
|
local desc="$4"
|
|
|
|
# Check if actual code is in expected list
|
|
IFS=',' read -ra CODES <<< "$expected"
|
|
for code in "${CODES[@]}"; do
|
|
if [[ "$actual" == "$code" ]]; then
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
echo -e "${RED}✗ FAILED: $desc${NC}"
|
|
echo " Expected: $expected, Got: $actual"
|
|
echo " Response: $body"
|
|
exit 2
|
|
}
|
|
|
|
# api_call - Makes an API call and captures response
|
|
# Parameters:
|
|
# $1 - HTTP method (GET, POST, PATCH, DELETE)
|
|
# $2 - Endpoint path (e.g., "/client")
|
|
# $3 - Request body (optional, empty string for none)
|
|
# $4 - Content-Type header (optional, defaults to application/json)
|
|
# Returns:
|
|
# Sets global variables: HTTP_CODE, RESPONSE_BODY
|
|
api_call() {
|
|
local method="$1"
|
|
local endpoint="$2"
|
|
local body="$3"
|
|
local content_type="${4:-application/json}"
|
|
|
|
local url="${BASE_URL}${endpoint}"
|
|
local curl_opts=(-s -w "\n%{http_code}")
|
|
|
|
curl_opts+=(-X "$method")
|
|
|
|
if [[ -n "$body" ]]; then
|
|
curl_opts+=(-H "Content-Type: $content_type")
|
|
curl_opts+=(-d "$body")
|
|
fi
|
|
|
|
local response
|
|
local retries=3
|
|
local delay=1
|
|
|
|
# Retry loop for rate limiting (429 errors)
|
|
for ((i=1; i<=retries; i++)); do
|
|
response=$(curl "${curl_opts[@]}" "$url")
|
|
|
|
# Split response into body and status code
|
|
HTTP_CODE=$(echo "$response" | tail -n1)
|
|
RESPONSE_BODY=$(echo "$response" | sed '$d')
|
|
|
|
# If not rate limited, break out of retry loop
|
|
if [[ "$HTTP_CODE" != "429" ]]; then
|
|
break
|
|
fi
|
|
|
|
# Rate limited - wait and retry
|
|
if [[ $i -lt $retries ]]; then
|
|
print_info "Rate limited (429), waiting ${delay}s before retry $((i+1))/$retries..."
|
|
sleep "$delay"
|
|
delay=$((delay * 2)) # Exponential backoff
|
|
fi
|
|
done
|
|
|
|
# Small delay between all API calls to avoid rate limiting
|
|
sleep 0.1
|
|
}
|
|
|
|
# api_upload - Uploads a file using multipart/form-data
|
|
# Parameters:
|
|
# $1 - Endpoint path
|
|
# $2 - File path
|
|
# $3 - Form field name (e.g., "archive" or "file")
|
|
# Returns:
|
|
# Sets global variables: HTTP_CODE, RESPONSE_BODY
|
|
api_upload() {
|
|
local endpoint="$1"
|
|
local file_path="$2"
|
|
local field_name="$3"
|
|
|
|
local url="${BASE_URL}${endpoint}"
|
|
|
|
local response
|
|
response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
-F "${field_name}=@${file_path}" \
|
|
"$url")
|
|
|
|
HTTP_CODE=$(echo "$response" | tail -n1)
|
|
RESPONSE_BODY=$(echo "$response" | sed '$d')
|
|
}
|
|
|
|
# create_test_pdf - Creates a minimal valid PDF file with unique content
|
|
# Parameters:
|
|
# $1 - Output file path
|
|
# $2 - Text content to embed (used to make each PDF unique)
|
|
create_test_pdf() {
|
|
local output="$1"
|
|
local text="$2"
|
|
|
|
# Calculate stream length based on the text content
|
|
# The stream content will be: "BT\n/F1 12 Tf\n100 700 Td\n($text) Tj\nET"
|
|
# We need to account for variable text length
|
|
local stream_content="BT
|
|
/F1 12 Tf
|
|
100 700 Td
|
|
(${text}) Tj
|
|
ET"
|
|
local stream_length=${#stream_content}
|
|
|
|
cat > "$output" << PDFEOF
|
|
%PDF-1.4
|
|
%âãÏÓ
|
|
1 0 obj
|
|
<<
|
|
/Type /Catalog
|
|
/Pages 2 0 R
|
|
>>
|
|
endobj
|
|
2 0 obj
|
|
<<
|
|
/Type /Pages
|
|
/Kids [3 0 R]
|
|
/Count 1
|
|
>>
|
|
endobj
|
|
3 0 obj
|
|
<<
|
|
/Type /Page
|
|
/Parent 2 0 R
|
|
/MediaBox [0 0 612 792]
|
|
/Resources <<
|
|
/Font <<
|
|
/F1 <<
|
|
/Type /Font
|
|
/Subtype /Type1
|
|
/BaseFont /Helvetica
|
|
>>
|
|
>>
|
|
>>
|
|
/Contents 4 0 R
|
|
>>
|
|
endobj
|
|
4 0 obj
|
|
<<
|
|
/Length ${stream_length}
|
|
>>
|
|
stream
|
|
${stream_content}
|
|
endstream
|
|
endobj
|
|
xref
|
|
0 5
|
|
0000000000 65535 f
|
|
0000000015 00000 n
|
|
0000000066 00000 n
|
|
0000000125 00000 n
|
|
0000000330 00000 n
|
|
trailer
|
|
<<
|
|
/Size 5
|
|
/Root 1 0 R
|
|
>>
|
|
startxref
|
|
430
|
|
%%EOF
|
|
PDFEOF
|
|
}
|
|
|
|
# create_test_zip - Creates a ZIP with test PDF files
|
|
# Parameters:
|
|
# $1 - Output ZIP file path
|
|
create_test_zip() {
|
|
local output="$1"
|
|
local temp_dir
|
|
temp_dir=$(mktemp -d)
|
|
|
|
# Create folder structure
|
|
mkdir -p "${temp_dir}/folder1/subfolder2"
|
|
mkdir -p "${temp_dir}/folder2/subfolder1"
|
|
|
|
# Create PDF files with unique content
|
|
# IMPORTANT: Each PDF must have unique content to avoid deduplication by hash.
|
|
# The system deduplicates documents based on their hash - identical files become
|
|
# a single document. Using timestamps and UUIDs ensures uniqueness.
|
|
local ts=$(date +%s%N) # Nanosecond timestamp for uniqueness
|
|
create_test_pdf "${temp_dir}/folder1/subfolder2/file1.pdf" "Doc1-${ts}-folder1-subfolder2-AAAA"
|
|
create_test_pdf "${temp_dir}/folder1/subfolder2/file2.pdf" "Doc2-${ts}-folder1-subfolder2-BBBB"
|
|
create_test_pdf "${temp_dir}/folder2/subfolder1/file3.pdf" "Doc3-${ts}-folder2-subfolder1-CCCC"
|
|
|
|
# Create ZIP (using relative paths, output must be absolute path)
|
|
local abs_output
|
|
if [[ "$output" = /* ]]; then
|
|
abs_output="$output"
|
|
else
|
|
abs_output="$(pwd)/$output"
|
|
fi
|
|
(cd "$temp_dir" && zip -r "$abs_output" folder1 folder2)
|
|
|
|
# Cleanup
|
|
rm -rf "$temp_dir"
|
|
}
|
|
|
|
# =============================================================================
|
|
# VALIDATION
|
|
# =============================================================================
|
|
|
|
# Check for required tools
|
|
check_dependencies() {
|
|
local missing=()
|
|
|
|
if ! command -v curl &> /dev/null; then
|
|
missing+=("curl")
|
|
fi
|
|
|
|
if ! command -v jq &> /dev/null; then
|
|
missing+=("jq")
|
|
fi
|
|
|
|
if ! command -v zip &> /dev/null; then
|
|
missing+=("zip")
|
|
fi
|
|
|
|
if [[ ${#missing[@]} -gt 0 ]]; then
|
|
echo -e "${RED}ERROR: Missing required tools: ${missing[*]}${NC}"
|
|
echo "Please install the missing tools and try again."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Validate command line arguments
|
|
validate_args() {
|
|
if [[ $# -lt 1 ]]; then
|
|
echo "Usage: $0 <base_url>"
|
|
echo ""
|
|
echo "Examples:"
|
|
echo " $0 http://localhost:8080"
|
|
echo " $0 http://queryo-query-q0pz6j2syaox-1001614225.us-east-2.elb.amazonaws.com"
|
|
exit 1
|
|
fi
|
|
|
|
BASE_URL="${1%/}" # Remove trailing slash if present
|
|
|
|
echo -e "${BLUE}========================================${NC}"
|
|
echo -e "${BLUE}Text Extraction Integration Test${NC}"
|
|
echo -e "${BLUE}========================================${NC}"
|
|
echo ""
|
|
echo "Base URL: ${BASE_URL}"
|
|
echo "Client Name: ${CLIENT_NAME}"
|
|
echo "Client ID: ${CLIENT_EXTERNAL_ID}"
|
|
echo ""
|
|
}
|
|
|
|
# =============================================================================
|
|
# PHASE 1: CLIENT SETUP (Steps 1-4)
|
|
# =============================================================================
|
|
|
|
# Step 1: Create a new client via POST /client
|
|
step_01_create_client() {
|
|
print_header "1" "Create a new client via POST /client"
|
|
|
|
local body
|
|
body=$(cat << EOF
|
|
{
|
|
"id": "${CLIENT_EXTERNAL_ID}",
|
|
"name": "${CLIENT_NAME}"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
print_info "Request: POST /client"
|
|
print_info "Body: $body"
|
|
|
|
api_call "POST" "/client" "$body"
|
|
check_response "201" "$HTTP_CODE" "$RESPONSE_BODY" "Create client"
|
|
|
|
CLIENT_ID=$(echo "$RESPONSE_BODY" | jq -r '.id')
|
|
print_success "Client created successfully"
|
|
print_info "Client ID: $CLIENT_ID"
|
|
}
|
|
|
|
# Step 2: Verify client exists via GET /client/{id}
|
|
step_02_verify_client() {
|
|
print_header "2" "Verify client exists via GET /client/{id}"
|
|
|
|
print_info "Request: GET /client/${CLIENT_ID}"
|
|
|
|
api_call "GET" "/client/${CLIENT_ID}" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "Get client"
|
|
|
|
local name
|
|
name=$(echo "$RESPONSE_BODY" | jq -r '.name')
|
|
local can_sync
|
|
can_sync=$(echo "$RESPONSE_BODY" | jq -r '.can_sync')
|
|
|
|
print_success "Client verified successfully"
|
|
print_info "Name: $name"
|
|
print_info "Can Sync: $can_sync"
|
|
}
|
|
|
|
# Step 2.5: List all clients via GET /clients
|
|
step_02_5_list_clients() {
|
|
print_header "2.5" "List all clients via GET /clients"
|
|
|
|
print_info "Request: GET /clients"
|
|
|
|
api_call "GET" "/clients" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "List clients"
|
|
|
|
local total_count
|
|
total_count=$(echo "$RESPONSE_BODY" | jq -r '.totalCount')
|
|
|
|
print_success "Clients listed successfully"
|
|
print_info "Total clients in system: $total_count"
|
|
|
|
# Verify our client is in the list
|
|
local found
|
|
found=$(echo "$RESPONSE_BODY" | jq -r --arg id "$CLIENT_ID" '.clients[] | select(.id == $id) | .id')
|
|
|
|
if [[ "$found" == "$CLIENT_ID" ]]; then
|
|
print_success "Newly created client found in list"
|
|
else
|
|
print_warning "Newly created client not found in list"
|
|
fi
|
|
|
|
# Print all clients
|
|
echo "$RESPONSE_BODY" | jq -r '.clients[] | " - ID: \(.id), Name: \(.name), CanSync: \(.can_sync)"'
|
|
}
|
|
|
|
# Step 3: Update client to allow sync via PATCH /client/{id}
|
|
step_03_update_client() {
|
|
print_header "3" "Update client to allow sync via PATCH /client/{id}"
|
|
|
|
local body='{"can_sync": true}'
|
|
|
|
print_info "Request: PATCH /client/${CLIENT_ID}"
|
|
print_info "Body: $body"
|
|
|
|
api_call "PATCH" "/client/${CLIENT_ID}" "$body"
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "Update client"
|
|
|
|
print_success "Client updated: can_sync = true"
|
|
}
|
|
|
|
# Step 4: Confirm client status via GET /client/{id}/status
|
|
step_04_confirm_status() {
|
|
print_header "4" "Confirm client status via GET /client/{id}/status"
|
|
|
|
print_info "Request: GET /client/${CLIENT_ID}/status"
|
|
|
|
api_call "GET" "/client/${CLIENT_ID}/status" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "Get client status"
|
|
|
|
local status
|
|
status=$(echo "$RESPONSE_BODY" | jq -r '.status')
|
|
|
|
print_success "Client status retrieved"
|
|
print_info "Status: $status"
|
|
}
|
|
|
|
# =============================================================================
|
|
# PHASE 2: DOCUMENT UPLOAD (Steps 5-12)
|
|
# =============================================================================
|
|
|
|
# Step 5: Create a ZIP with 3 PDFs in folder structures
|
|
step_05_create_zip() {
|
|
print_header "5" "Create a ZIP with 3 PDFs in folder structures"
|
|
|
|
ZIP_FILE=$(mktemp).zip
|
|
create_test_zip "$ZIP_FILE"
|
|
|
|
print_success "ZIP file created: $ZIP_FILE"
|
|
print_info "Contents:"
|
|
print_info " - folder1/subfolder2/file1.pdf"
|
|
print_info " - folder1/subfolder2/file2.pdf"
|
|
print_info " - folder2/subfolder1/file1.pdf"
|
|
|
|
# Show file size
|
|
local size
|
|
size=$(ls -lh "$ZIP_FILE" | awk '{print $5}')
|
|
print_info "Size: $size"
|
|
}
|
|
|
|
# Step 6: Upload batch via POST /client/{id}/document/batch
|
|
step_06_upload_batch() {
|
|
print_header "6" "Upload batch via POST /client/${CLIENT_ID}/document/batch"
|
|
|
|
print_info "Request: POST /client/${CLIENT_ID}/document/batch"
|
|
print_info "File: $ZIP_FILE"
|
|
|
|
api_upload "/client/${CLIENT_ID}/document/batch" "$ZIP_FILE" "archive"
|
|
check_response "202" "$HTTP_CODE" "$RESPONSE_BODY" "Upload batch"
|
|
|
|
BATCH_ID=$(echo "$RESPONSE_BODY" | jq -r '.batch_id')
|
|
local status
|
|
status=$(echo "$RESPONSE_BODY" | jq -r '.status')
|
|
local status_url
|
|
status_url=$(echo "$RESPONSE_BODY" | jq -r '.status_url')
|
|
|
|
print_success "Batch upload accepted"
|
|
print_info "Batch ID: $BATCH_ID"
|
|
print_info "Status: $status"
|
|
print_info "Status URL: $status_url"
|
|
|
|
# Cleanup temp file
|
|
rm -f "$ZIP_FILE"
|
|
}
|
|
|
|
# Step 7: Verify batch via GET /client/{id}/document/batch/{batch_id}
|
|
step_07_verify_batch() {
|
|
print_header "7" "Verify batch via GET /client/${CLIENT_ID}/document/batch/${BATCH_ID}"
|
|
|
|
print_info "Request: GET /client/${CLIENT_ID}/document/batch/${BATCH_ID}"
|
|
|
|
# Poll for batch completion (max 30 attempts, 1 second apart)
|
|
local attempts=0
|
|
local max_attempts=30
|
|
local batch_status="processing"
|
|
|
|
while [[ "$batch_status" == "processing" && $attempts -lt $max_attempts ]]; do
|
|
api_call "GET" "/client/${CLIENT_ID}/document/batch/${BATCH_ID}" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "Get batch status"
|
|
|
|
batch_status=$(echo "$RESPONSE_BODY" | jq -r '.status')
|
|
local processed
|
|
processed=$(echo "$RESPONSE_BODY" | jq -r '.processed_documents')
|
|
local total
|
|
total=$(echo "$RESPONSE_BODY" | jq -r '.total_documents')
|
|
|
|
print_info "Attempt $((attempts+1)): status=$batch_status, processed=$processed/$total"
|
|
|
|
if [[ "$batch_status" == "processing" ]]; then
|
|
sleep 1
|
|
fi
|
|
|
|
((attempts++))
|
|
done
|
|
|
|
if [[ "$batch_status" == "completed" ]]; then
|
|
print_success "Batch processing completed"
|
|
elif [[ "$batch_status" == "failed" ]]; then
|
|
print_warning "Batch processing failed"
|
|
local failed_files
|
|
failed_files=$(echo "$RESPONSE_BODY" | jq -r '.failed_filenames[]?' 2>/dev/null || echo "none")
|
|
print_info "Failed files: $failed_files"
|
|
else
|
|
print_warning "Batch still processing after $max_attempts attempts"
|
|
fi
|
|
|
|
# Also list all batches for this client
|
|
print_info ""
|
|
print_info "Listing all batches for client..."
|
|
api_call "GET" "/client/${CLIENT_ID}/document/batch" ""
|
|
local total_batches
|
|
total_batches=$(echo "$RESPONSE_BODY" | jq -r '.total_count')
|
|
print_info "Total batches for client: $total_batches"
|
|
}
|
|
|
|
# Step 8: Get documents via GET /client/{id}/document and verify 3 documents
|
|
step_08_get_documents() {
|
|
print_header "8" "Get documents via GET /client/${CLIENT_ID}/document (expect 3 documents)"
|
|
|
|
print_info "Request: GET /client/${CLIENT_ID}/document"
|
|
|
|
# Poll until we have documents (they may still be processing)
|
|
# Note: In local testing, batch processing may not complete since it requires
|
|
# async queue processing. We'll continue even with 0 documents from batch.
|
|
local attempts=0
|
|
local max_attempts=10
|
|
local doc_count=0
|
|
|
|
while [[ $doc_count -lt 3 && $attempts -lt $max_attempts ]]; do
|
|
api_call "GET" "/client/${CLIENT_ID}/document" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "List documents"
|
|
|
|
doc_count=$(echo "$RESPONSE_BODY" | jq 'length')
|
|
print_info "Attempt $((attempts+1)): Found $doc_count document(s)"
|
|
|
|
if [[ $doc_count -lt 3 ]]; then
|
|
sleep 1
|
|
fi
|
|
|
|
((attempts++))
|
|
done
|
|
|
|
if [[ $doc_count -ge 3 ]]; then
|
|
print_success "Found expected number of documents"
|
|
else
|
|
print_warning "Expected 3 documents, found $doc_count (batch processing may be async)"
|
|
print_info "Note: In local testing, batch documents process asynchronously via queues"
|
|
fi
|
|
|
|
# Store document IDs for later use
|
|
DOCUMENT_IDS=($(echo "$RESPONSE_BODY" | jq -r '.[].id'))
|
|
|
|
print_info "Documents:"
|
|
for doc_id in "${DOCUMENT_IDS[@]}"; do
|
|
local hash
|
|
hash=$(echo "$RESPONSE_BODY" | jq -r --arg id "$doc_id" '.[] | select(.id == $id) | .hash')
|
|
local file_size
|
|
file_size=$(echo "$RESPONSE_BODY" | jq -r --arg id "$doc_id" '.[] | select(.id == $id) | .fileSizeBytes // "N/A"')
|
|
print_info " - ID: $doc_id, Hash: $hash, Size: ${file_size} bytes"
|
|
done
|
|
|
|
# Set target document for label operations
|
|
if [[ ${#DOCUMENT_IDS[@]} -gt 0 ]]; then
|
|
TARGET_DOC_ID="${DOCUMENT_IDS[0]}"
|
|
print_info "Target document for label operations: $TARGET_DOC_ID"
|
|
fi
|
|
}
|
|
|
|
# Steps 9-10: Folder-based document retrieval (skipped)
|
|
step_09_10_folder_documents() {
|
|
print_header "9-10" "Folder-based document retrieval (SKIPPED)"
|
|
|
|
print_info "Note: Folder-based document retrieval requires folder IDs from processing"
|
|
print_info "Skipping folder-specific document retrieval - documents verified at client level"
|
|
print_success "Steps 9-10 skipped as expected"
|
|
}
|
|
|
|
# Step 11: POST a single document to root
|
|
step_11_upload_single() {
|
|
print_header "11" "Upload single document to root"
|
|
|
|
# Create a single PDF file
|
|
SINGLE_PDF=$(mktemp).pdf
|
|
create_test_pdf "$SINGLE_PDF" "Single root document"
|
|
|
|
print_info "Request: POST /client/${CLIENT_ID}/document"
|
|
print_info "File: single_root_document.pdf"
|
|
|
|
# Use multipart form upload - the file field must be named "file"
|
|
# and the content type for the file should be application/octet-stream (let curl detect it)
|
|
local url="${BASE_URL}/client/${CLIENT_ID}/document"
|
|
|
|
local response
|
|
response=$(curl -s -w "\n%{http_code}" -X POST \
|
|
-F "file=@${SINGLE_PDF};type=application/octet-stream;filename=single_root_document.pdf" \
|
|
"$url")
|
|
|
|
HTTP_CODE=$(echo "$response" | tail -n1)
|
|
RESPONSE_BODY=$(echo "$response" | sed '$d')
|
|
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "Upload single document"
|
|
|
|
print_success "Single document uploaded successfully"
|
|
|
|
# Cleanup
|
|
rm -f "$SINGLE_PDF"
|
|
}
|
|
|
|
# Step 12: Verify documents total (expecting at least 1 from single upload)
|
|
step_12_verify_total() {
|
|
print_header "12" "Verify documents total (expecting at least 1)"
|
|
|
|
print_info "Request: GET /client/${CLIENT_ID}/document"
|
|
|
|
# Poll until we have at least 1 document
|
|
local attempts=0
|
|
local max_attempts=15
|
|
local doc_count=0
|
|
|
|
while [[ $doc_count -lt 1 && $attempts -lt $max_attempts ]]; do
|
|
api_call "GET" "/client/${CLIENT_ID}/document" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "List documents"
|
|
|
|
doc_count=$(echo "$RESPONSE_BODY" | jq 'length')
|
|
print_info "Attempt $((attempts+1)): Found $doc_count document(s)"
|
|
|
|
if [[ $doc_count -lt 1 ]]; then
|
|
sleep 1
|
|
fi
|
|
|
|
((attempts++))
|
|
done
|
|
|
|
if [[ $doc_count -ge 4 ]]; then
|
|
print_success "Verified 4+ documents total (batch processing complete)"
|
|
elif [[ $doc_count -ge 1 ]]; then
|
|
print_success "Verified $doc_count document(s) (single upload successful)"
|
|
print_info "Note: Batch documents may still be processing asynchronously"
|
|
else
|
|
print_warning "Expected at least 1 document, found $doc_count"
|
|
fi
|
|
|
|
# Update document IDs
|
|
DOCUMENT_IDS=($(echo "$RESPONSE_BODY" | jq -r '.[].id'))
|
|
|
|
if [[ ${#DOCUMENT_IDS[@]} -gt 0 ]]; then
|
|
TARGET_DOC_ID="${DOCUMENT_IDS[0]}"
|
|
fi
|
|
|
|
print_info "Final document list:"
|
|
for doc_id in "${DOCUMENT_IDS[@]}"; do
|
|
local hash
|
|
hash=$(echo "$RESPONSE_BODY" | jq -r --arg id "$doc_id" '.[] | select(.id == $id) | .hash')
|
|
local file_size
|
|
file_size=$(echo "$RESPONSE_BODY" | jq -r --arg id "$doc_id" '.[] | select(.id == $id) | .fileSizeBytes // "N/A"')
|
|
print_info " - ID: $doc_id, Hash: $hash, Size: ${file_size} bytes"
|
|
done
|
|
}
|
|
|
|
# Step 12.5: Verify file sizes are present in document details
|
|
step_12_5_verify_file_sizes() {
|
|
print_header "12.5" "Verify file sizes are present in document details"
|
|
|
|
if [[ ${#DOCUMENT_IDS[@]} -eq 0 ]]; then
|
|
print_warning "No documents available - skipping file size verification"
|
|
return
|
|
fi
|
|
|
|
local docs_with_size=0
|
|
local docs_without_size=0
|
|
|
|
for doc_id in "${DOCUMENT_IDS[@]}"; do
|
|
print_info ""
|
|
print_info "Request: GET /document/${doc_id}"
|
|
|
|
api_call "GET" "/document/${doc_id}" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local file_size
|
|
file_size=$(echo "$RESPONSE_BODY" | jq -r '.fileSizeBytes // "null"')
|
|
local filename
|
|
filename=$(echo "$RESPONSE_BODY" | jq -r '.filename // "unknown"')
|
|
|
|
if [[ "$file_size" != "null" && "$file_size" != "" ]]; then
|
|
print_success "Document $doc_id has fileSizeBytes: $file_size bytes (filename: $filename)"
|
|
((docs_with_size++))
|
|
else
|
|
print_warning "Document $doc_id is missing fileSizeBytes (filename: $filename)"
|
|
((docs_without_size++))
|
|
fi
|
|
else
|
|
print_warning "Failed to get document $doc_id: $HTTP_CODE"
|
|
fi
|
|
done
|
|
|
|
print_info ""
|
|
if [[ $docs_with_size -gt 0 ]]; then
|
|
print_success "File size verification: $docs_with_size document(s) have file sizes"
|
|
fi
|
|
if [[ $docs_without_size -gt 0 ]]; then
|
|
print_warning "File size verification: $docs_without_size document(s) missing file sizes"
|
|
print_info "Note: Legacy documents or documents still processing may not have file sizes"
|
|
fi
|
|
}
|
|
|
|
# Step 12.8: BUG TEST - Verify folder metrics BEFORE labeling
|
|
# Confirms the known document counts per folder are correct before any labels
|
|
# are applied. Step 17.5 re-checks these same counts AFTER labeling to detect
|
|
# the reported bug where labeling a document causes it to disappear from
|
|
# folder metrics (totalDocuments count decreases).
|
|
# Expected counts come from the fixed test upload structure:
|
|
# / = 1 (single upload), /folder1 = 0, /folder1/subfolder2 = 2, /folder2 = 0, /folder2/subfolder1 = 1
|
|
step_12_8_verify_folder_metrics_before_labels() {
|
|
print_header "12.8" "BUG TEST: Verify folder metrics BEFORE labeling"
|
|
|
|
if [[ ${#DOCUMENT_IDS[@]} -lt 4 ]]; then
|
|
print_warning "Expected 4 documents but have ${#DOCUMENT_IDS[@]} - skipping pre-label metrics check"
|
|
return
|
|
fi
|
|
|
|
print_info "Verifying expected document counts per folder before any labels are applied."
|
|
print_info "Expected: ${EXPECTED_FOLDER_COUNTS_BY_PATH}"
|
|
|
|
local bugs_found=0
|
|
|
|
# Test via /client/{id}/folders?metrics=true
|
|
print_info ""
|
|
print_info "Request: GET /client/${CLIENT_ID}/folders?metrics=true"
|
|
|
|
api_call "GET" "/client/${CLIENT_ID}/folders?metrics=true" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "Get client folders with metrics"
|
|
|
|
# Populate FOLDER_IDS for use by later steps
|
|
FOLDER_IDS=($(echo "$RESPONSE_BODY" | jq -r '.folders[].id'))
|
|
|
|
local folder_count
|
|
folder_count=$(echo "$RESPONSE_BODY" | jq '.folders | length')
|
|
print_info "Found $folder_count folders"
|
|
|
|
# Check each folder's totalDocuments against expected values
|
|
for row in $(echo "$RESPONSE_BODY" | jq -c '.folders[]'); do
|
|
local path
|
|
path=$(echo "$row" | jq -r '.path')
|
|
local actual
|
|
actual=$(echo "$row" | jq -r '.metrics.totalDocuments // 0')
|
|
local expected
|
|
expected=$(echo "$EXPECTED_FOLDER_COUNTS_BY_PATH" | jq -r --arg p "$path" '.[$p] // "UNKNOWN"')
|
|
|
|
if [[ "$expected" == "UNKNOWN" ]]; then
|
|
print_warning "Unexpected folder '$path' (not in expected map) - totalDocuments=$actual"
|
|
elif [[ "$actual" != "$expected" ]]; then
|
|
echo -e "${RED} FAIL: Folder '$path' totalDocuments=$actual (expected $expected)${NC}"
|
|
bugs_found=$((bugs_found + 1))
|
|
else
|
|
print_success "Folder '$path': totalDocuments=$actual (correct)"
|
|
fi
|
|
done
|
|
|
|
# Also verify via /folders/{folderId}/metrics for each folder
|
|
print_info ""
|
|
print_info "Cross-checking via GET /folders/{folderId}/metrics"
|
|
|
|
for folder_id in "${FOLDER_IDS[@]}"; do
|
|
api_call "GET" "/folders/${folder_id}/metrics" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local actual
|
|
actual=$(echo "$RESPONSE_BODY" | jq -r '.totalDocuments')
|
|
print_info " Folder $folder_id: totalDocuments=$actual"
|
|
else
|
|
print_warning " Failed to get metrics for folder $folder_id: $HTTP_CODE"
|
|
fi
|
|
done
|
|
|
|
if [[ $bugs_found -gt 0 ]]; then
|
|
print_error "Pre-label folder metrics do not match expected counts ($bugs_found mismatches)"
|
|
else
|
|
print_success "All folder document counts match expected values before labeling"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# PHASE 3: LABEL OPERATIONS (Steps 13-17)
|
|
# =============================================================================
|
|
|
|
# Step 13-14: Apply all labels one by one and verify after each
|
|
step_13_14_apply_labels() {
|
|
print_header "13-14" "Apply and verify labels on document: ${TARGET_DOC_ID}"
|
|
|
|
if [[ -z "$TARGET_DOC_ID" ]]; then
|
|
print_warning "No target document available - skipping label operations"
|
|
print_info "This may happen if document upload/processing hasn't completed"
|
|
return
|
|
fi
|
|
|
|
local labels=("Ingested" "OCR_Processed" "GenAI_Processed" "Doczy_AI_Completed" "Dashboard_Ready")
|
|
local applied_count=0
|
|
|
|
for label in "${labels[@]}"; do
|
|
print_info ""
|
|
print_info "Applying label: $label"
|
|
|
|
local body
|
|
body=$(cat << EOF
|
|
{
|
|
"label": "${label}",
|
|
"appliedBy": "integration-test@example.com"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
print_info "Request: POST /documents/${TARGET_DOC_ID}/labels"
|
|
|
|
api_call "POST" "/documents/${TARGET_DOC_ID}/labels" "$body"
|
|
|
|
if [[ "$HTTP_CODE" == "201" ]]; then
|
|
local record_id
|
|
record_id=$(echo "$RESPONSE_BODY" | jq -r '.id')
|
|
print_success "Label '$label' applied: recordID=$record_id"
|
|
((applied_count++))
|
|
else
|
|
print_warning "Failed to apply label '$label': $RESPONSE_BODY"
|
|
continue
|
|
fi
|
|
|
|
# Verify labels via GET
|
|
print_info "Verifying labels via GET /documents/${TARGET_DOC_ID}/labels"
|
|
|
|
api_call "GET" "/documents/${TARGET_DOC_ID}/labels" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local label_count
|
|
label_count=$(echo "$RESPONSE_BODY" | jq '.labels | length')
|
|
print_info " Labels on document: $label_count (expected: $applied_count)"
|
|
else
|
|
print_warning " Failed to get labels: $HTTP_CODE"
|
|
fi
|
|
done
|
|
|
|
print_success "Applied $applied_count labels to document"
|
|
}
|
|
|
|
# Step 15: List all folders for client via GET /clients/{clientId}/folders?metrics=true
|
|
step_15_list_folders() {
|
|
print_header "15" "List folders for client via GET /client/${CLIENT_ID}/folders?metrics=true"
|
|
|
|
print_info "Request: GET /client/${CLIENT_ID}/folders?metrics=true"
|
|
|
|
api_call "GET" "/client/${CLIENT_ID}/folders?metrics=true" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local folder_count
|
|
folder_count=$(echo "$RESPONSE_BODY" | jq '.folders | length')
|
|
|
|
print_success "Folders listed successfully (with metrics)"
|
|
print_info "Total folders: $folder_count"
|
|
|
|
if [[ $folder_count -gt 0 ]]; then
|
|
FOLDER_IDS=($(echo "$RESPONSE_BODY" | jq -r '.folders[].id'))
|
|
|
|
print_info "Folder hierarchy with inline metrics:"
|
|
echo "$RESPONSE_BODY" | jq -r '.folders[] | " - \(.path) (ID: \(.id))"'
|
|
echo "$RESPONSE_BODY" | jq -r '.folders[] | " Metrics: totalDocuments=\(.metrics.totalDocuments // 0), byLabel=\(.metrics.byLabel // {})"'
|
|
else
|
|
print_info "Note: No folders found - documents may be at root level"
|
|
fi
|
|
else
|
|
print_warning "Failed to list folders: $HTTP_CODE"
|
|
print_info "Response: $RESPONSE_BODY"
|
|
fi
|
|
}
|
|
|
|
# Step 16: Get metrics for each folder via GET /folders/{folderId}/metrics
|
|
step_16_folder_metrics() {
|
|
print_header "16" "Get metrics for each folder"
|
|
|
|
if [[ ${#FOLDER_IDS[@]} -eq 0 ]]; then
|
|
print_info "No folders to get metrics for (skipping)"
|
|
return
|
|
fi
|
|
|
|
for folder_id in "${FOLDER_IDS[@]}"; do
|
|
print_info ""
|
|
print_info "Request: GET /folders/${folder_id}/metrics"
|
|
|
|
api_call "GET" "/folders/${folder_id}/metrics" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local total_docs
|
|
total_docs=$(echo "$RESPONSE_BODY" | jq -r '.totalDocuments')
|
|
local by_label
|
|
by_label=$(echo "$RESPONSE_BODY" | jq -c '.byLabel')
|
|
|
|
print_success "Folder $folder_id metrics:"
|
|
print_info " Total documents: $total_docs"
|
|
print_info " By label: $by_label"
|
|
else
|
|
print_warning "Failed to get metrics for folder $folder_id: $HTTP_CODE"
|
|
fi
|
|
done
|
|
}
|
|
|
|
# Step 16.5: Verify file sizes in folder documents
|
|
step_16_5_folder_document_sizes() {
|
|
print_header "16.5" "Verify file sizes in folder document responses"
|
|
|
|
if [[ ${#FOLDER_IDS[@]} -eq 0 ]]; then
|
|
print_info "No folders available - skipping folder document file size verification"
|
|
return
|
|
fi
|
|
|
|
local total_docs_with_size=0
|
|
local total_docs_without_size=0
|
|
|
|
for folder_id in "${FOLDER_IDS[@]}"; do
|
|
print_info ""
|
|
print_info "Request: GET /folders/${folder_id}/documents"
|
|
|
|
api_call "GET" "/folders/${folder_id}/documents" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local doc_count
|
|
doc_count=$(echo "$RESPONSE_BODY" | jq '.documents | length')
|
|
|
|
if [[ $doc_count -gt 0 ]]; then
|
|
print_info "Folder $folder_id has $doc_count document(s)"
|
|
|
|
# Check each document for file size
|
|
for ((i=0; i<doc_count; i++)); do
|
|
local doc_id
|
|
doc_id=$(echo "$RESPONSE_BODY" | jq -r ".documents[$i].id")
|
|
local file_size
|
|
file_size=$(echo "$RESPONSE_BODY" | jq -r ".documents[$i].fileSizeBytes // \"null\"")
|
|
local filename
|
|
filename=$(echo "$RESPONSE_BODY" | jq -r ".documents[$i].filename // \"unknown\"")
|
|
|
|
if [[ "$file_size" != "null" && "$file_size" != "" ]]; then
|
|
print_success " Document $doc_id: fileSizeBytes=$file_size (filename: $filename)"
|
|
((total_docs_with_size++))
|
|
else
|
|
print_warning " Document $doc_id: missing fileSizeBytes (filename: $filename)"
|
|
((total_docs_without_size++))
|
|
fi
|
|
done
|
|
else
|
|
print_info "Folder $folder_id has no documents"
|
|
fi
|
|
else
|
|
print_warning "Failed to get documents for folder $folder_id: $HTTP_CODE"
|
|
fi
|
|
done
|
|
|
|
print_info ""
|
|
if [[ $total_docs_with_size -gt 0 ]]; then
|
|
print_success "Folder documents file size check: $total_docs_with_size document(s) have file sizes"
|
|
fi
|
|
if [[ $total_docs_without_size -gt 0 ]]; then
|
|
print_warning "Folder documents file size check: $total_docs_without_size document(s) missing file sizes"
|
|
fi
|
|
}
|
|
|
|
# Step 17: Get documents by label via GET /labels/{labelName}/documents
|
|
step_17_documents_by_label() {
|
|
print_header "17" "Get documents by label via GET /labels/{labelName}/documents"
|
|
|
|
local label="Ingested"
|
|
|
|
print_info "Request: GET /labels/${label}/documents?clientId=${CLIENT_ID}"
|
|
|
|
api_call "GET" "/labels/${label}/documents?clientId=${CLIENT_ID}" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local doc_count
|
|
doc_count=$(echo "$RESPONSE_BODY" | jq '.documents | length')
|
|
|
|
print_success "Documents with label '$label': $doc_count"
|
|
|
|
if [[ $doc_count -gt 0 ]]; then
|
|
echo "$RESPONSE_BODY" | jq -r '.documents[] | " - ID: \(.id), Hash: \(.hash), Size: \(.fileSizeBytes // "N/A") bytes"'
|
|
fi
|
|
else
|
|
print_warning "Failed to get documents by label: $HTTP_CODE"
|
|
print_info "Response: $RESPONSE_BODY"
|
|
fi
|
|
}
|
|
|
|
# Step 17.5: BUG TEST - Verify folder metrics AFTER labeling
|
|
# Re-checks folder document counts using the same expected values from step 12.8.
|
|
# If labeling a document caused its folder's totalDocuments to change, this FAILS.
|
|
# Tests both endpoints: /client/{id}/folders?metrics=true AND /folders/{id}/metrics
|
|
step_17_5_verify_metrics_after_labels() {
|
|
print_header "17.5" "BUG TEST: Verify folder metrics AFTER labeling (should be unchanged)"
|
|
|
|
if [[ ${#FOLDER_IDS[@]} -eq 0 ]]; then
|
|
print_warning "No folder IDs available - skipping post-label metrics check"
|
|
return
|
|
fi
|
|
|
|
print_info "Labels were applied in steps 13-14. Verifying that folder document counts"
|
|
print_info "are still the same as the expected values: ${EXPECTED_FOLDER_COUNTS_BY_PATH}"
|
|
|
|
local bugs_found=0
|
|
|
|
# Test 1: /client/{id}/folders?metrics=true
|
|
print_info ""
|
|
print_info "Test 1: GET /client/${CLIENT_ID}/folders?metrics=true"
|
|
|
|
api_call "GET" "/client/${CLIENT_ID}/folders?metrics=true" ""
|
|
check_response "200" "$HTTP_CODE" "$RESPONSE_BODY" "Get client folders with metrics (post-label)"
|
|
|
|
for row in $(echo "$RESPONSE_BODY" | jq -c '.folders[]'); do
|
|
local path
|
|
path=$(echo "$row" | jq -r '.path')
|
|
local actual
|
|
actual=$(echo "$row" | jq -r '.metrics.totalDocuments // 0')
|
|
local expected
|
|
expected=$(echo "$EXPECTED_FOLDER_COUNTS_BY_PATH" | jq -r --arg p "$path" '.[$p] // "UNKNOWN"')
|
|
|
|
if [[ "$expected" == "UNKNOWN" ]]; then
|
|
print_warning "Unexpected folder '$path' - totalDocuments=$actual"
|
|
elif [[ "$actual" != "$expected" ]]; then
|
|
echo -e "${RED} BUG: Folder '$path' totalDocuments=$actual (expected $expected) - labeling changed the count!${NC}"
|
|
bugs_found=$((bugs_found + 1))
|
|
else
|
|
print_success "Folder '$path': totalDocuments=$actual (correct after labeling)"
|
|
fi
|
|
done
|
|
|
|
# Test 2: /folders/{folderId}/metrics for each folder
|
|
print_info ""
|
|
print_info "Test 2: GET /folders/{folderId}/metrics for each folder"
|
|
|
|
for folder_id in "${FOLDER_IDS[@]}"; do
|
|
api_call "GET" "/folders/${folder_id}/metrics" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local actual
|
|
actual=$(echo "$RESPONSE_BODY" | jq -r '.totalDocuments')
|
|
print_success "Folder $folder_id: totalDocuments=$actual"
|
|
else
|
|
print_warning "Failed to get metrics for folder $folder_id: $HTTP_CODE"
|
|
fi
|
|
done
|
|
|
|
# Final verdict
|
|
print_info ""
|
|
if [[ $bugs_found -gt 0 ]]; then
|
|
print_error "BUG TEST FAILED: $bugs_found folder metric(s) changed after labeling a document"
|
|
else
|
|
print_success "BUG TEST PASSED: All folder document counts correct after labeling"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# PHASE 4: FIELD EXTRACTIONS (Steps 18-23)
|
|
# =============================================================================
|
|
|
|
# Step 18: Verify no field extraction exists for document
|
|
step_18_verify_no_extraction() {
|
|
print_header "18" "Verify no field extraction exists"
|
|
|
|
if [[ -z "$TARGET_DOC_ID" ]]; then
|
|
print_warning "No target document available - skipping field extraction verification"
|
|
return
|
|
fi
|
|
|
|
print_info "Request: GET /field-extractions?documentId=${TARGET_DOC_ID}"
|
|
|
|
api_call "GET" "/field-extractions?documentId=${TARGET_DOC_ID}" ""
|
|
|
|
if [[ "$HTTP_CODE" == "404" ]]; then
|
|
print_success "Confirmed: No field extraction exists for document (404)"
|
|
elif [[ "$HTTP_CODE" == "200" ]]; then
|
|
local version
|
|
version=$(echo "$RESPONSE_BODY" | jq -r '.version')
|
|
print_warning "Field extraction already exists: version=$version"
|
|
else
|
|
print_info "Got status $HTTP_CODE: $RESPONSE_BODY"
|
|
fi
|
|
}
|
|
|
|
# Step 19: Create field extraction via POST /field-extractions
|
|
# Creates version 1 with all 18 singleFields and 92 arrayFields populated
|
|
step_19_create_extraction() {
|
|
print_header "19" "Create field extraction via POST /field-extractions"
|
|
|
|
if [[ -z "$TARGET_DOC_ID" ]]; then
|
|
print_warning "No target document available - skipping field extraction creation"
|
|
return
|
|
fi
|
|
|
|
local body
|
|
body=$(cat << 'EOF'
|
|
{
|
|
"documentId": "PLACEHOLDER_DOC_ID",
|
|
"singleFields": {
|
|
"fileName": "test_document_v1.pdf",
|
|
"contractTitle": "Master Service Agreement v1",
|
|
"aareteDerivedAmendmentNum": 1,
|
|
"clientName": "Acme Healthcare Systems",
|
|
"payerName": "BlueCross BlueShield of California",
|
|
"payerState": "CA",
|
|
"providerState": "NY",
|
|
"filenameTin": "12-3456789",
|
|
"provGroupTin": "98-7654321",
|
|
"provGroupNpi": "1234567890",
|
|
"provGroupNameFull": "Premier Medical Group LLC",
|
|
"provOtherTin": "11-2233445",
|
|
"provOtherNpi": "0987654321",
|
|
"provOtherNameFull": "Secondary Care Associates",
|
|
"aareteDerivedEffectiveDt": "2024-01-01",
|
|
"aareteDerivedTerminationDt": "2025-12-31",
|
|
"autoRenewalInd": true,
|
|
"autoRenewalTerm": "12 months with 90-day notice"
|
|
},
|
|
"arrayFields": [
|
|
{
|
|
"exhibitTitle": "Exhibit A - Inpatient Services",
|
|
"exhibitPage": "1",
|
|
"reimbProvTin": "12-3456789",
|
|
"reimbProvNpi": "1234567890",
|
|
"reimbProvName": "Premier Medical Group LLC",
|
|
"reimbEffectiveDt": "2024-01-01",
|
|
"reimbTerminationDt": "2025-12-31",
|
|
"aareteDerivedClaimTypeCd": "IP",
|
|
"aareteDerivedProduct": "Commercial PPO",
|
|
"aareteDerivedLob": "Commercial",
|
|
"aareteDerivedProgram": "Standard",
|
|
"aareteDerivedNetwork": "Preferred",
|
|
"aareteDerivedProvType": "Acute Care Hospital",
|
|
"provTaxonomyCd": "282N00000X",
|
|
"provTaxonomyCdDesc": "General Acute Care Hospital",
|
|
"provSpecialtyCd": "001",
|
|
"provSpecialtyCdDesc": "General Practice",
|
|
"placeOfServiceCd": "21",
|
|
"placeOfServiceCdDesc": "Inpatient Hospital",
|
|
"billTypeCd": "111",
|
|
"billTypeCdDesc": "Hospital Inpatient Admit",
|
|
"patientAgeMin": "0",
|
|
"patientAgeMax": "120",
|
|
"reimbTerm": "Per DRG with outlier provisions",
|
|
"lobProgramRelationship": "Commercial-Standard",
|
|
"lobProductRelationship": "Commercial-PPO",
|
|
"carveoutInd": false,
|
|
"carveoutCd": "",
|
|
"lesserOfInd": true,
|
|
"greaterOfInd": false,
|
|
"aareteDerivedReimbMethod": "DRG",
|
|
"unitOfMeasure": "Admission",
|
|
"reimbPctRate": 95.5,
|
|
"reimbFeeRate": 15000.00,
|
|
"reimbConversionFactor": 1.25,
|
|
"triggerCapThresholdAmt": 500000.00,
|
|
"triggerBaseThreshold": 100000.00,
|
|
"defaultInd": true,
|
|
"additionDesc": "Annual rate increase based on CPI",
|
|
"additionMaxFeeRateInc": 1500.00,
|
|
"additionMaxPctRateInc": 3.5,
|
|
"aareteDerivedAdditionRateChangeTimeline": "Annual on contract anniversary",
|
|
"aareteDerivedFeeSchedule": "Medicare",
|
|
"aareteDerivedFeeScheduleVersion": "2024",
|
|
"serviceTerm": "All inpatient acute care services",
|
|
"cpt4ProcCd": "99223",
|
|
"cpt4ProcCdDesc": "Initial hospital care high severity",
|
|
"cpt4ProcMod": "25",
|
|
"cpt4ProcModDesc": "Significant separate E/M service",
|
|
"revenueCd": "0120",
|
|
"revenueCdDesc": "Room and Board Semi-Private",
|
|
"diagCd": "A41.9",
|
|
"diagCdDesc": "Sepsis unspecified organism",
|
|
"ndcCd": "00069-0150-01",
|
|
"ndcCdDesc": "Amoxicillin 500mg capsules",
|
|
"claimAdmitTypeCd": "1",
|
|
"authAdmitTypeDesc": "Emergency",
|
|
"claimStatusCd": "A",
|
|
"claimStatusCdDesc": "Approved",
|
|
"grouperType": "MS-DRG",
|
|
"grouperCd": "871",
|
|
"grouperCdDesc": "Septicemia or severe sepsis w/o MV >96 hours w MCC",
|
|
"grouperPctRate": 100.0,
|
|
"grouperBaseRate": 12500.00,
|
|
"aareteDerivedGrouperVersion": "v41.0",
|
|
"grouperAlternativeLevelOfCare": "Standard Acute",
|
|
"grouperSeverityInd": true,
|
|
"grouperSeverity": "Major",
|
|
"grouperRiskOfMortalitySubclass": "3",
|
|
"grouperTransferInd": false,
|
|
"grouperReadmissionsInd": false,
|
|
"grouperHacInd": false,
|
|
"outlierTerm": "Cost outlier at 2x fixed loss threshold",
|
|
"outlierFirstDollarInd": false,
|
|
"rangeNbrDays": "1-365",
|
|
"outlierFixedLossNbrDaysThreshold": 30.0,
|
|
"outlierFixedLossThreshold": 50000.00,
|
|
"outlierMaximum": 1000000.00,
|
|
"outlierMaximumFrequency": 2.0,
|
|
"outlierPctRate": 80.0,
|
|
"outlierExclusionCd": "TRANSPLANT",
|
|
"outlierExclusionCdDesc": "Organ transplant cases excluded",
|
|
"facilityAdjustmentTerm": "Standard facility adjustments apply",
|
|
"dshInd": true,
|
|
"dshPctRate": 15.5,
|
|
"dshFeeRate": 2500.00,
|
|
"imeInd": true,
|
|
"imePctRate": 5.75,
|
|
"imeFeeRate": 1000.00,
|
|
"ntapInd": false,
|
|
"ntapPctRate": 0.0,
|
|
"ntapFeeRate": 0.00,
|
|
"ucInd": true,
|
|
"ucPctRate": 250.0,
|
|
"ucFeeRate": 0.00,
|
|
"gmeInd": true,
|
|
"gmePctRate": 2.5,
|
|
"gmeFeeRate": 500.00,
|
|
"rateEscalatorInd": true,
|
|
"rateEscalatorDesc": "Annual CPI-U adjustment capped at 3%",
|
|
"rateEscalatorMaxRateIncPct": 3.0,
|
|
"rateEscalatorRateChangeTimeline": 12.0,
|
|
"stopLossTerm": "Individual stop loss at $500K",
|
|
"stopLossFirstDollarInd": false,
|
|
"stopLossRangeNbrDays": 365.0,
|
|
"stopLossFixedLossThreshold": 500000.00,
|
|
"stopLossMaximum": 2000000.00,
|
|
"stopLossMaximumFrequency": 1.0,
|
|
"stopLossDailyMaxRate": 10000.00,
|
|
"stopLossPctRateOnExcessCharges": 60.0,
|
|
"stopLossExclusionCd": "COSMETIC",
|
|
"stopLossExclusionDesc": "Cosmetic procedures excluded from stop loss"
|
|
},
|
|
{
|
|
"exhibitTitle": "Exhibit B - Outpatient Services",
|
|
"exhibitPage": "15",
|
|
"reimbProvTin": "12-3456789",
|
|
"reimbProvNpi": "1234567890",
|
|
"reimbProvName": "Premier Medical Group LLC",
|
|
"reimbEffectiveDt": "2024-01-01",
|
|
"reimbTerminationDt": "2025-12-31",
|
|
"aareteDerivedClaimTypeCd": "OP",
|
|
"aareteDerivedProduct": "Commercial PPO",
|
|
"aareteDerivedLob": "Commercial",
|
|
"aareteDerivedProgram": "Standard",
|
|
"aareteDerivedNetwork": "Preferred",
|
|
"aareteDerivedProvType": "Outpatient Clinic",
|
|
"provTaxonomyCd": "261QM0801X",
|
|
"provTaxonomyCdDesc": "Ambulatory Surgery Center",
|
|
"provSpecialtyCd": "002",
|
|
"provSpecialtyCdDesc": "Surgical Services",
|
|
"placeOfServiceCd": "22",
|
|
"placeOfServiceCdDesc": "Outpatient Hospital",
|
|
"billTypeCd": "131",
|
|
"billTypeCdDesc": "Hospital Outpatient",
|
|
"patientAgeMin": "0",
|
|
"patientAgeMax": "120",
|
|
"reimbTerm": "Fee Schedule based on Medicare OPPS",
|
|
"lobProgramRelationship": "Commercial-Standard",
|
|
"lobProductRelationship": "Commercial-PPO",
|
|
"carveoutInd": true,
|
|
"carveoutCd": "IMPLANTS",
|
|
"lesserOfInd": true,
|
|
"greaterOfInd": false,
|
|
"aareteDerivedReimbMethod": "Fee Schedule",
|
|
"unitOfMeasure": "Procedure",
|
|
"reimbPctRate": 150.0,
|
|
"reimbFeeRate": 0.00,
|
|
"reimbConversionFactor": 75.50,
|
|
"triggerCapThresholdAmt": 100000.00,
|
|
"triggerBaseThreshold": 25000.00,
|
|
"defaultInd": false,
|
|
"additionDesc": "Implant costs at invoice plus 10%",
|
|
"additionMaxFeeRateInc": 500.00,
|
|
"additionMaxPctRateInc": 2.0,
|
|
"aareteDerivedAdditionRateChangeTimeline": "Quarterly review",
|
|
"aareteDerivedFeeSchedule": "Medicare OPPS",
|
|
"aareteDerivedFeeScheduleVersion": "2024-Q1",
|
|
"serviceTerm": "Outpatient surgical and diagnostic services",
|
|
"cpt4ProcCd": "29881",
|
|
"cpt4ProcCdDesc": "Arthroscopy knee surgical",
|
|
"cpt4ProcMod": "RT",
|
|
"cpt4ProcModDesc": "Right side",
|
|
"revenueCd": "0360",
|
|
"revenueCdDesc": "Operating Room Services",
|
|
"diagCd": "M17.11",
|
|
"diagCdDesc": "Primary osteoarthritis right knee",
|
|
"ndcCd": "00409-1966-01",
|
|
"ndcCdDesc": "Bupivacaine injection",
|
|
"claimAdmitTypeCd": "3",
|
|
"authAdmitTypeDesc": "Elective",
|
|
"claimStatusCd": "A",
|
|
"claimStatusCdDesc": "Approved",
|
|
"grouperType": "APC",
|
|
"grouperCd": "5114",
|
|
"grouperCdDesc": "Level 4 Musculoskeletal Procedures",
|
|
"grouperPctRate": 150.0,
|
|
"grouperBaseRate": 3500.00,
|
|
"aareteDerivedGrouperVersion": "2024.1",
|
|
"grouperAlternativeLevelOfCare": "Ambulatory",
|
|
"grouperSeverityInd": false,
|
|
"grouperSeverity": "Minor",
|
|
"grouperRiskOfMortalitySubclass": "1",
|
|
"grouperTransferInd": false,
|
|
"grouperReadmissionsInd": false,
|
|
"grouperHacInd": false,
|
|
"outlierTerm": "No outlier provisions for outpatient",
|
|
"outlierFirstDollarInd": false,
|
|
"rangeNbrDays": "1",
|
|
"outlierFixedLossNbrDaysThreshold": 0.0,
|
|
"outlierFixedLossThreshold": 0.00,
|
|
"outlierMaximum": 0.00,
|
|
"outlierMaximumFrequency": 0.0,
|
|
"outlierPctRate": 0.0,
|
|
"outlierExclusionCd": "",
|
|
"outlierExclusionCdDesc": "",
|
|
"facilityAdjustmentTerm": "No facility adjustments for outpatient",
|
|
"dshInd": false,
|
|
"dshPctRate": 0.0,
|
|
"dshFeeRate": 0.00,
|
|
"imeInd": false,
|
|
"imePctRate": 0.0,
|
|
"imeFeeRate": 0.00,
|
|
"ntapInd": false,
|
|
"ntapPctRate": 0.0,
|
|
"ntapFeeRate": 0.00,
|
|
"ucInd": true,
|
|
"ucPctRate": 200.0,
|
|
"ucFeeRate": 0.00,
|
|
"gmeInd": false,
|
|
"gmePctRate": 0.0,
|
|
"gmeFeeRate": 0.00,
|
|
"rateEscalatorInd": true,
|
|
"rateEscalatorDesc": "Annual Medicare OPPS update",
|
|
"rateEscalatorMaxRateIncPct": 2.5,
|
|
"rateEscalatorRateChangeTimeline": 12.0,
|
|
"stopLossTerm": "N/A for outpatient",
|
|
"stopLossFirstDollarInd": false,
|
|
"stopLossRangeNbrDays": 0.0,
|
|
"stopLossFixedLossThreshold": 0.00,
|
|
"stopLossMaximum": 0.00,
|
|
"stopLossMaximumFrequency": 0.0,
|
|
"stopLossDailyMaxRate": 0.00,
|
|
"stopLossPctRateOnExcessCharges": 0.0,
|
|
"stopLossExclusionCd": "",
|
|
"stopLossExclusionDesc": ""
|
|
}
|
|
],
|
|
"createdBy": "integration-test@example.com"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
# Replace placeholder with actual document ID
|
|
body="${body//PLACEHOLDER_DOC_ID/$TARGET_DOC_ID}"
|
|
|
|
print_info "Request: POST /field-extractions"
|
|
print_info "Document ID: ${TARGET_DOC_ID}"
|
|
print_info "Payload includes all 18 singleFields and 92 arrayFields per item"
|
|
|
|
api_call "POST" "/field-extractions" "$body"
|
|
|
|
if [[ "$HTTP_CODE" == "201" ]]; then
|
|
local version
|
|
version=$(echo "$RESPONSE_BODY" | jq -r '.version')
|
|
print_success "Field extraction created: version=$version"
|
|
else
|
|
print_warning "Failed to create field extraction: $HTTP_CODE"
|
|
print_info "Response: $RESPONSE_BODY"
|
|
fi
|
|
}
|
|
|
|
# Step 20: Verify single extraction via GET /field-extractions/history
|
|
step_20_verify_history_1() {
|
|
print_header "20" "Verify single extraction via GET /field-extractions/history"
|
|
|
|
if [[ -z "$TARGET_DOC_ID" ]]; then
|
|
print_warning "No target document available - skipping field extraction history"
|
|
return
|
|
fi
|
|
|
|
print_info "Request: GET /field-extractions/history?documentId=${TARGET_DOC_ID}"
|
|
|
|
api_call "GET" "/field-extractions/history?documentId=${TARGET_DOC_ID}" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local version_count
|
|
version_count=$(echo "$RESPONSE_BODY" | jq '.versions | length')
|
|
|
|
if [[ $version_count -eq 1 ]]; then
|
|
print_success "Verified: 1 version in history"
|
|
else
|
|
print_warning "Expected 1 version, found $version_count"
|
|
fi
|
|
|
|
echo "$RESPONSE_BODY" | jq -r '.versions[] | " - Version: \(.version), Created: \(.createdAt), By: \(.createdBy)"'
|
|
else
|
|
print_warning "Failed to get history: $HTTP_CODE"
|
|
print_info "Response: $RESPONSE_BODY"
|
|
fi
|
|
}
|
|
|
|
# Step 21: Modify and create new version via POST /field-extractions
|
|
# Creates version 2 with updated values for all 18 singleFields and 92 arrayFields
|
|
# Includes 3 array items to demonstrate different scenarios
|
|
step_21_create_version_2() {
|
|
print_header "21" "Create second version of field extraction"
|
|
|
|
if [[ -z "$TARGET_DOC_ID" ]]; then
|
|
print_warning "No target document available - skipping field extraction v2 creation"
|
|
return
|
|
fi
|
|
|
|
local body
|
|
body=$(cat << 'EOF'
|
|
{
|
|
"documentId": "PLACEHOLDER_DOC_ID",
|
|
"singleFields": {
|
|
"fileName": "test_document_v2_amended.pdf",
|
|
"contractTitle": "Master Service Agreement - First Amendment v2",
|
|
"aareteDerivedAmendmentNum": 2,
|
|
"clientName": "Acme Healthcare Systems (Updated)",
|
|
"payerName": "BlueCross BlueShield of New York",
|
|
"payerState": "NY",
|
|
"providerState": "CA",
|
|
"filenameTin": "12-3456789",
|
|
"provGroupTin": "98-7654321",
|
|
"provGroupNpi": "1234567890",
|
|
"provGroupNameFull": "Premier Medical Group LLC - Western Region",
|
|
"provOtherTin": "22-3344556",
|
|
"provOtherNpi": "1122334455",
|
|
"provOtherNameFull": "Tertiary Specialty Associates",
|
|
"aareteDerivedEffectiveDt": "2024-07-01",
|
|
"aareteDerivedTerminationDt": "2026-06-30",
|
|
"autoRenewalInd": false,
|
|
"autoRenewalTerm": "Manual renewal required 120 days prior"
|
|
},
|
|
"arrayFields": [
|
|
{
|
|
"exhibitTitle": "Exhibit A - Inpatient Services (Revised)",
|
|
"exhibitPage": "1",
|
|
"reimbProvTin": "12-3456789",
|
|
"reimbProvNpi": "1234567890",
|
|
"reimbProvName": "Premier Medical Group LLC",
|
|
"reimbEffectiveDt": "2024-07-01",
|
|
"reimbTerminationDt": "2026-06-30",
|
|
"aareteDerivedClaimTypeCd": "IP",
|
|
"aareteDerivedProduct": "Commercial HMO",
|
|
"aareteDerivedLob": "Commercial",
|
|
"aareteDerivedProgram": "Enhanced",
|
|
"aareteDerivedNetwork": "Tier 1",
|
|
"aareteDerivedProvType": "Acute Care Hospital",
|
|
"provTaxonomyCd": "282N00000X",
|
|
"provTaxonomyCdDesc": "General Acute Care Hospital",
|
|
"provSpecialtyCd": "001",
|
|
"provSpecialtyCdDesc": "General Practice",
|
|
"placeOfServiceCd": "21",
|
|
"placeOfServiceCdDesc": "Inpatient Hospital",
|
|
"billTypeCd": "111",
|
|
"billTypeCdDesc": "Hospital Inpatient Admit",
|
|
"patientAgeMin": "0",
|
|
"patientAgeMax": "120",
|
|
"reimbTerm": "Per DRG with enhanced outlier provisions",
|
|
"lobProgramRelationship": "Commercial-Enhanced",
|
|
"lobProductRelationship": "Commercial-HMO",
|
|
"carveoutInd": false,
|
|
"carveoutCd": "",
|
|
"lesserOfInd": true,
|
|
"greaterOfInd": false,
|
|
"aareteDerivedReimbMethod": "DRG",
|
|
"unitOfMeasure": "Admission",
|
|
"reimbPctRate": 98.0,
|
|
"reimbFeeRate": 16500.00,
|
|
"reimbConversionFactor": 1.35,
|
|
"triggerCapThresholdAmt": 550000.00,
|
|
"triggerBaseThreshold": 110000.00,
|
|
"defaultInd": true,
|
|
"additionDesc": "Annual rate increase based on CPI-U Medical Care",
|
|
"additionMaxFeeRateInc": 1650.00,
|
|
"additionMaxPctRateInc": 3.75,
|
|
"aareteDerivedAdditionRateChangeTimeline": "Annual on July 1",
|
|
"aareteDerivedFeeSchedule": "Medicare",
|
|
"aareteDerivedFeeScheduleVersion": "2024-v2",
|
|
"serviceTerm": "All inpatient acute care services including ICU",
|
|
"cpt4ProcCd": "99223",
|
|
"cpt4ProcCdDesc": "Initial hospital care high severity",
|
|
"cpt4ProcMod": "25",
|
|
"cpt4ProcModDesc": "Significant separate E/M service",
|
|
"revenueCd": "0120",
|
|
"revenueCdDesc": "Room and Board Semi-Private",
|
|
"diagCd": "J18.9",
|
|
"diagCdDesc": "Pneumonia unspecified organism",
|
|
"ndcCd": "00069-0150-01",
|
|
"ndcCdDesc": "Amoxicillin 500mg capsules",
|
|
"claimAdmitTypeCd": "1",
|
|
"authAdmitTypeDesc": "Emergency",
|
|
"claimStatusCd": "A",
|
|
"claimStatusCdDesc": "Approved",
|
|
"grouperType": "MS-DRG",
|
|
"grouperCd": "193",
|
|
"grouperCdDesc": "Simple pneumonia and pleurisy w MCC",
|
|
"grouperPctRate": 105.0,
|
|
"grouperBaseRate": 13000.00,
|
|
"aareteDerivedGrouperVersion": "v42.0",
|
|
"grouperAlternativeLevelOfCare": "Standard Acute",
|
|
"grouperSeverityInd": true,
|
|
"grouperSeverity": "Major",
|
|
"grouperRiskOfMortalitySubclass": "2",
|
|
"grouperTransferInd": false,
|
|
"grouperReadmissionsInd": false,
|
|
"grouperHacInd": false,
|
|
"outlierTerm": "Cost outlier at 1.75x fixed loss threshold",
|
|
"outlierFirstDollarInd": false,
|
|
"rangeNbrDays": "1-365",
|
|
"outlierFixedLossNbrDaysThreshold": 25.0,
|
|
"outlierFixedLossThreshold": 45000.00,
|
|
"outlierMaximum": 1200000.00,
|
|
"outlierMaximumFrequency": 3.0,
|
|
"outlierPctRate": 85.0,
|
|
"outlierExclusionCd": "TRANSPLANT",
|
|
"outlierExclusionCdDesc": "Organ transplant cases excluded",
|
|
"facilityAdjustmentTerm": "Enhanced facility adjustments apply",
|
|
"dshInd": true,
|
|
"dshPctRate": 16.0,
|
|
"dshFeeRate": 2750.00,
|
|
"imeInd": true,
|
|
"imePctRate": 6.0,
|
|
"imeFeeRate": 1100.00,
|
|
"ntapInd": true,
|
|
"ntapPctRate": 2.0,
|
|
"ntapFeeRate": 350.00,
|
|
"ucInd": true,
|
|
"ucPctRate": 275.0,
|
|
"ucFeeRate": 0.00,
|
|
"gmeInd": true,
|
|
"gmePctRate": 3.0,
|
|
"gmeFeeRate": 600.00,
|
|
"rateEscalatorInd": true,
|
|
"rateEscalatorDesc": "Annual CPI-U Medical Care adjustment capped at 4%",
|
|
"rateEscalatorMaxRateIncPct": 4.0,
|
|
"rateEscalatorRateChangeTimeline": 12.0,
|
|
"stopLossTerm": "Individual stop loss at $600K",
|
|
"stopLossFirstDollarInd": false,
|
|
"stopLossRangeNbrDays": 365.0,
|
|
"stopLossFixedLossThreshold": 600000.00,
|
|
"stopLossMaximum": 2500000.00,
|
|
"stopLossMaximumFrequency": 1.0,
|
|
"stopLossDailyMaxRate": 12000.00,
|
|
"stopLossPctRateOnExcessCharges": 65.0,
|
|
"stopLossExclusionCd": "COSMETIC",
|
|
"stopLossExclusionDesc": "Cosmetic and experimental procedures excluded"
|
|
},
|
|
{
|
|
"exhibitTitle": "Exhibit B - Outpatient Services (Revised)",
|
|
"exhibitPage": "20",
|
|
"reimbProvTin": "12-3456789",
|
|
"reimbProvNpi": "1234567890",
|
|
"reimbProvName": "Premier Medical Group LLC",
|
|
"reimbEffectiveDt": "2024-07-01",
|
|
"reimbTerminationDt": "2026-06-30",
|
|
"aareteDerivedClaimTypeCd": "OP",
|
|
"aareteDerivedProduct": "Commercial HMO",
|
|
"aareteDerivedLob": "Commercial",
|
|
"aareteDerivedProgram": "Enhanced",
|
|
"aareteDerivedNetwork": "Tier 1",
|
|
"aareteDerivedProvType": "Outpatient Clinic",
|
|
"provTaxonomyCd": "261QM0801X",
|
|
"provTaxonomyCdDesc": "Ambulatory Surgery Center",
|
|
"provSpecialtyCd": "002",
|
|
"provSpecialtyCdDesc": "Surgical Services",
|
|
"placeOfServiceCd": "22",
|
|
"placeOfServiceCdDesc": "Outpatient Hospital",
|
|
"billTypeCd": "131",
|
|
"billTypeCdDesc": "Hospital Outpatient",
|
|
"patientAgeMin": "0",
|
|
"patientAgeMax": "120",
|
|
"reimbTerm": "Fee Schedule based on Medicare OPPS plus 55%",
|
|
"lobProgramRelationship": "Commercial-Enhanced",
|
|
"lobProductRelationship": "Commercial-HMO",
|
|
"carveoutInd": true,
|
|
"carveoutCd": "IMPLANTS-DEVICES",
|
|
"lesserOfInd": true,
|
|
"greaterOfInd": false,
|
|
"aareteDerivedReimbMethod": "Fee Schedule",
|
|
"unitOfMeasure": "Procedure",
|
|
"reimbPctRate": 155.0,
|
|
"reimbFeeRate": 0.00,
|
|
"reimbConversionFactor": 78.25,
|
|
"triggerCapThresholdAmt": 125000.00,
|
|
"triggerBaseThreshold": 30000.00,
|
|
"defaultInd": false,
|
|
"additionDesc": "Implant and device costs at invoice plus 15%",
|
|
"additionMaxFeeRateInc": 600.00,
|
|
"additionMaxPctRateInc": 2.5,
|
|
"aareteDerivedAdditionRateChangeTimeline": "Quarterly review",
|
|
"aareteDerivedFeeSchedule": "Medicare OPPS",
|
|
"aareteDerivedFeeScheduleVersion": "2024-Q3",
|
|
"serviceTerm": "Outpatient surgical diagnostic and interventional",
|
|
"cpt4ProcCd": "27447",
|
|
"cpt4ProcCdDesc": "Total knee arthroplasty",
|
|
"cpt4ProcMod": "LT",
|
|
"cpt4ProcModDesc": "Left side",
|
|
"revenueCd": "0360",
|
|
"revenueCdDesc": "Operating Room Services",
|
|
"diagCd": "M17.12",
|
|
"diagCdDesc": "Primary osteoarthritis left knee",
|
|
"ndcCd": "00409-1966-01",
|
|
"ndcCdDesc": "Bupivacaine injection",
|
|
"claimAdmitTypeCd": "3",
|
|
"authAdmitTypeDesc": "Elective",
|
|
"claimStatusCd": "A",
|
|
"claimStatusCdDesc": "Approved",
|
|
"grouperType": "APC",
|
|
"grouperCd": "5115",
|
|
"grouperCdDesc": "Level 5 Musculoskeletal Procedures",
|
|
"grouperPctRate": 155.0,
|
|
"grouperBaseRate": 4200.00,
|
|
"aareteDerivedGrouperVersion": "2024.2",
|
|
"grouperAlternativeLevelOfCare": "Ambulatory",
|
|
"grouperSeverityInd": false,
|
|
"grouperSeverity": "Moderate",
|
|
"grouperRiskOfMortalitySubclass": "1",
|
|
"grouperTransferInd": false,
|
|
"grouperReadmissionsInd": false,
|
|
"grouperHacInd": false,
|
|
"outlierTerm": "No outlier provisions for outpatient",
|
|
"outlierFirstDollarInd": false,
|
|
"rangeNbrDays": "1",
|
|
"outlierFixedLossNbrDaysThreshold": 0.0,
|
|
"outlierFixedLossThreshold": 0.00,
|
|
"outlierMaximum": 0.00,
|
|
"outlierMaximumFrequency": 0.0,
|
|
"outlierPctRate": 0.0,
|
|
"outlierExclusionCd": "",
|
|
"outlierExclusionCdDesc": "",
|
|
"facilityAdjustmentTerm": "No facility adjustments for outpatient",
|
|
"dshInd": false,
|
|
"dshPctRate": 0.0,
|
|
"dshFeeRate": 0.00,
|
|
"imeInd": false,
|
|
"imePctRate": 0.0,
|
|
"imeFeeRate": 0.00,
|
|
"ntapInd": false,
|
|
"ntapPctRate": 0.0,
|
|
"ntapFeeRate": 0.00,
|
|
"ucInd": true,
|
|
"ucPctRate": 225.0,
|
|
"ucFeeRate": 0.00,
|
|
"gmeInd": false,
|
|
"gmePctRate": 0.0,
|
|
"gmeFeeRate": 0.00,
|
|
"rateEscalatorInd": true,
|
|
"rateEscalatorDesc": "Annual Medicare OPPS update plus 2%",
|
|
"rateEscalatorMaxRateIncPct": 3.0,
|
|
"rateEscalatorRateChangeTimeline": 12.0,
|
|
"stopLossTerm": "N/A for outpatient",
|
|
"stopLossFirstDollarInd": false,
|
|
"stopLossRangeNbrDays": 0.0,
|
|
"stopLossFixedLossThreshold": 0.00,
|
|
"stopLossMaximum": 0.00,
|
|
"stopLossMaximumFrequency": 0.0,
|
|
"stopLossDailyMaxRate": 0.00,
|
|
"stopLossPctRateOnExcessCharges": 0.0,
|
|
"stopLossExclusionCd": "",
|
|
"stopLossExclusionDesc": ""
|
|
},
|
|
{
|
|
"exhibitTitle": "Exhibit C - Professional Services (New)",
|
|
"exhibitPage": "35",
|
|
"reimbProvTin": "22-3344556",
|
|
"reimbProvNpi": "1122334455",
|
|
"reimbProvName": "Tertiary Specialty Associates",
|
|
"reimbEffectiveDt": "2024-07-01",
|
|
"reimbTerminationDt": "2026-06-30",
|
|
"aareteDerivedClaimTypeCd": "PROF",
|
|
"aareteDerivedProduct": "Commercial HMO",
|
|
"aareteDerivedLob": "Commercial",
|
|
"aareteDerivedProgram": "Enhanced",
|
|
"aareteDerivedNetwork": "Tier 1",
|
|
"aareteDerivedProvType": "Physician Group",
|
|
"provTaxonomyCd": "207R00000X",
|
|
"provTaxonomyCdDesc": "Internal Medicine",
|
|
"provSpecialtyCd": "011",
|
|
"provSpecialtyCdDesc": "Internal Medicine",
|
|
"placeOfServiceCd": "11",
|
|
"placeOfServiceCdDesc": "Office",
|
|
"billTypeCd": "N/A",
|
|
"billTypeCdDesc": "Professional claim",
|
|
"patientAgeMin": "18",
|
|
"patientAgeMax": "120",
|
|
"reimbTerm": "RBRVS Medicare Fee Schedule plus 20%",
|
|
"lobProgramRelationship": "Commercial-Enhanced",
|
|
"lobProductRelationship": "Commercial-HMO",
|
|
"carveoutInd": false,
|
|
"carveoutCd": "",
|
|
"lesserOfInd": true,
|
|
"greaterOfInd": false,
|
|
"aareteDerivedReimbMethod": "Fee Schedule",
|
|
"unitOfMeasure": "Service",
|
|
"reimbPctRate": 120.0,
|
|
"reimbFeeRate": 0.00,
|
|
"reimbConversionFactor": 45.75,
|
|
"triggerCapThresholdAmt": 50000.00,
|
|
"triggerBaseThreshold": 10000.00,
|
|
"defaultInd": false,
|
|
"additionDesc": "Annual RBRVS update applied",
|
|
"additionMaxFeeRateInc": 100.00,
|
|
"additionMaxPctRateInc": 2.0,
|
|
"aareteDerivedAdditionRateChangeTimeline": "Annual on January 1",
|
|
"aareteDerivedFeeSchedule": "Medicare RBRVS",
|
|
"aareteDerivedFeeScheduleVersion": "2024",
|
|
"serviceTerm": "Professional evaluation and management services",
|
|
"cpt4ProcCd": "99214",
|
|
"cpt4ProcCdDesc": "Office visit established moderate",
|
|
"cpt4ProcMod": "",
|
|
"cpt4ProcModDesc": "",
|
|
"revenueCd": "N/A",
|
|
"revenueCdDesc": "Professional claim",
|
|
"diagCd": "E11.9",
|
|
"diagCdDesc": "Type 2 diabetes mellitus without complications",
|
|
"ndcCd": "",
|
|
"ndcCdDesc": "",
|
|
"claimAdmitTypeCd": "N/A",
|
|
"authAdmitTypeDesc": "Not applicable",
|
|
"claimStatusCd": "A",
|
|
"claimStatusCdDesc": "Approved",
|
|
"grouperType": "N/A",
|
|
"grouperCd": "N/A",
|
|
"grouperCdDesc": "Not applicable for professional",
|
|
"grouperPctRate": 0.0,
|
|
"grouperBaseRate": 0.00,
|
|
"aareteDerivedGrouperVersion": "N/A",
|
|
"grouperAlternativeLevelOfCare": "N/A",
|
|
"grouperSeverityInd": false,
|
|
"grouperSeverity": "N/A",
|
|
"grouperRiskOfMortalitySubclass": "N/A",
|
|
"grouperTransferInd": false,
|
|
"grouperReadmissionsInd": false,
|
|
"grouperHacInd": false,
|
|
"outlierTerm": "N/A for professional services",
|
|
"outlierFirstDollarInd": false,
|
|
"rangeNbrDays": "N/A",
|
|
"outlierFixedLossNbrDaysThreshold": 0.0,
|
|
"outlierFixedLossThreshold": 0.00,
|
|
"outlierMaximum": 0.00,
|
|
"outlierMaximumFrequency": 0.0,
|
|
"outlierPctRate": 0.0,
|
|
"outlierExclusionCd": "",
|
|
"outlierExclusionCdDesc": "",
|
|
"facilityAdjustmentTerm": "N/A for professional services",
|
|
"dshInd": false,
|
|
"dshPctRate": 0.0,
|
|
"dshFeeRate": 0.00,
|
|
"imeInd": false,
|
|
"imePctRate": 0.0,
|
|
"imeFeeRate": 0.00,
|
|
"ntapInd": false,
|
|
"ntapPctRate": 0.0,
|
|
"ntapFeeRate": 0.00,
|
|
"ucInd": true,
|
|
"ucPctRate": 150.0,
|
|
"ucFeeRate": 0.00,
|
|
"gmeInd": false,
|
|
"gmePctRate": 0.0,
|
|
"gmeFeeRate": 0.00,
|
|
"rateEscalatorInd": true,
|
|
"rateEscalatorDesc": "Annual Medicare RBRVS update",
|
|
"rateEscalatorMaxRateIncPct": 2.0,
|
|
"rateEscalatorRateChangeTimeline": 12.0,
|
|
"stopLossTerm": "N/A for professional services",
|
|
"stopLossFirstDollarInd": false,
|
|
"stopLossRangeNbrDays": 0.0,
|
|
"stopLossFixedLossThreshold": 0.00,
|
|
"stopLossMaximum": 0.00,
|
|
"stopLossMaximumFrequency": 0.0,
|
|
"stopLossDailyMaxRate": 0.00,
|
|
"stopLossPctRateOnExcessCharges": 0.0,
|
|
"stopLossExclusionCd": "",
|
|
"stopLossExclusionDesc": ""
|
|
}
|
|
],
|
|
"createdBy": "integration-test@example.com"
|
|
}
|
|
EOF
|
|
)
|
|
|
|
# Replace placeholder with actual document ID
|
|
body="${body//PLACEHOLDER_DOC_ID/$TARGET_DOC_ID}"
|
|
|
|
print_info "Request: POST /field-extractions"
|
|
print_info "Document ID: ${TARGET_DOC_ID}"
|
|
print_info "Payload includes all 18 singleFields and 92 arrayFields per item (3 items)"
|
|
|
|
api_call "POST" "/field-extractions" "$body"
|
|
|
|
if [[ "$HTTP_CODE" == "201" ]]; then
|
|
local version
|
|
version=$(echo "$RESPONSE_BODY" | jq -r '.version')
|
|
print_success "Field extraction v2 created: version=$version"
|
|
else
|
|
print_warning "Failed to create field extraction v2: $HTTP_CODE"
|
|
print_info "Response: $RESPONSE_BODY"
|
|
fi
|
|
}
|
|
|
|
# Step 22: Verify two versions via GET /field-extractions/history
|
|
step_22_verify_history_2() {
|
|
print_header "22" "Verify two versions in history"
|
|
|
|
if [[ -z "$TARGET_DOC_ID" ]]; then
|
|
print_warning "No target document available - skipping field extraction history"
|
|
return
|
|
fi
|
|
|
|
print_info "Request: GET /field-extractions/history?documentId=${TARGET_DOC_ID}"
|
|
|
|
api_call "GET" "/field-extractions/history?documentId=${TARGET_DOC_ID}" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
local version_count
|
|
version_count=$(echo "$RESPONSE_BODY" | jq '.versions | length')
|
|
|
|
if [[ $version_count -eq 2 ]]; then
|
|
print_success "Verified: 2 versions in history"
|
|
else
|
|
print_warning "Expected 2 versions, found $version_count"
|
|
fi
|
|
|
|
echo "$RESPONSE_BODY" | jq -r '.versions[] | " - Version: \(.version), Created: \(.createdAt), By: \(.createdBy)"'
|
|
else
|
|
print_warning "Failed to get history: $HTTP_CODE"
|
|
print_info "Response: $RESPONSE_BODY"
|
|
fi
|
|
}
|
|
|
|
# Step 23: Retrieve specific versions via GET /field-extractions/version
|
|
# Prints all field values to verify round-trip data integrity
|
|
step_23_get_specific_versions() {
|
|
print_header "23" "Retrieve specific versions via GET /field-extractions/version"
|
|
|
|
if [[ -z "$TARGET_DOC_ID" ]]; then
|
|
print_warning "No target document available - skipping specific version retrieval"
|
|
return
|
|
fi
|
|
|
|
# Get version 1
|
|
print_info ""
|
|
print_info "Testing retrieval of version 1..."
|
|
print_info "Request: GET /field-extractions/version?documentId=${TARGET_DOC_ID}&version=1"
|
|
|
|
api_call "GET" "/field-extractions/version?documentId=${TARGET_DOC_ID}&version=1" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
print_success "Version 1 retrieved successfully"
|
|
print_info ""
|
|
print_info "=== VERSION 1 FULL ROUND-TRIP DATA ==="
|
|
print_info ""
|
|
print_info "--- Single Fields (18 fields) ---"
|
|
echo "$RESPONSE_BODY" | jq -r '.singleFields | to_entries[] | " \(.key): \(.value)"'
|
|
print_info ""
|
|
print_info "--- Array Fields (2 items, 92 fields each) ---"
|
|
local array_count
|
|
array_count=$(echo "$RESPONSE_BODY" | jq '.arrayFields | length')
|
|
print_info " Total array items: $array_count"
|
|
|
|
for ((i=0; i<array_count; i++)); do
|
|
print_info ""
|
|
local exhibit_title
|
|
exhibit_title=$(echo "$RESPONSE_BODY" | jq -r ".arrayFields[$i].exhibitTitle")
|
|
print_info " --- Array Item $((i+1)): $exhibit_title ---"
|
|
echo "$RESPONSE_BODY" | jq -r ".arrayFields[$i] | to_entries[] | \" \(.key): \(.value)\""
|
|
done
|
|
else
|
|
print_warning "Failed to get version 1: $HTTP_CODE"
|
|
fi
|
|
|
|
# Get version 2
|
|
print_info ""
|
|
print_info "Testing retrieval of version 2..."
|
|
print_info "Request: GET /field-extractions/version?documentId=${TARGET_DOC_ID}&version=2"
|
|
|
|
api_call "GET" "/field-extractions/version?documentId=${TARGET_DOC_ID}&version=2" ""
|
|
|
|
if [[ "$HTTP_CODE" == "200" ]]; then
|
|
print_success "Version 2 retrieved successfully"
|
|
print_info ""
|
|
print_info "=== VERSION 2 FULL ROUND-TRIP DATA ==="
|
|
print_info ""
|
|
print_info "--- Single Fields (18 fields) ---"
|
|
echo "$RESPONSE_BODY" | jq -r '.singleFields | to_entries[] | " \(.key): \(.value)"'
|
|
print_info ""
|
|
print_info "--- Array Fields (3 items, 92 fields each) ---"
|
|
local array_count
|
|
array_count=$(echo "$RESPONSE_BODY" | jq '.arrayFields | length')
|
|
print_info " Total array items: $array_count"
|
|
|
|
for ((i=0; i<array_count; i++)); do
|
|
print_info ""
|
|
local exhibit_title
|
|
exhibit_title=$(echo "$RESPONSE_BODY" | jq -r ".arrayFields[$i].exhibitTitle")
|
|
print_info " --- Array Item $((i+1)): $exhibit_title ---"
|
|
echo "$RESPONSE_BODY" | jq -r ".arrayFields[$i] | to_entries[] | \" \(.key): \(.value)\""
|
|
done
|
|
else
|
|
print_warning "Failed to get version 2: $HTTP_CODE"
|
|
fi
|
|
|
|
# Test non-existent version (should return 404)
|
|
print_info ""
|
|
print_info "Testing retrieval of non-existent version 999..."
|
|
print_info "Request: GET /field-extractions/version?documentId=${TARGET_DOC_ID}&version=999"
|
|
|
|
api_call "GET" "/field-extractions/version?documentId=${TARGET_DOC_ID}&version=999" ""
|
|
|
|
if [[ "$HTTP_CODE" == "404" ]]; then
|
|
print_success "Non-existent version correctly returned 404"
|
|
else
|
|
print_warning "Expected 404 for non-existent version, got $HTTP_CODE"
|
|
fi
|
|
}
|
|
|
|
# =============================================================================
|
|
# MAIN EXECUTION
|
|
# =============================================================================
|
|
|
|
main() {
|
|
check_dependencies
|
|
validate_args "$@"
|
|
|
|
# Phase 1: Client Setup
|
|
echo ""
|
|
echo -e "${BLUE}=== PHASE 1: CLIENT SETUP ===${NC}"
|
|
step_01_create_client
|
|
step_02_verify_client
|
|
step_02_5_list_clients
|
|
step_03_update_client
|
|
step_04_confirm_status
|
|
|
|
# Phase 2: Document Upload
|
|
echo ""
|
|
echo -e "${BLUE}=== PHASE 2: DOCUMENT UPLOAD ===${NC}"
|
|
step_05_create_zip
|
|
step_06_upload_batch
|
|
step_07_verify_batch
|
|
step_08_get_documents
|
|
step_09_10_folder_documents
|
|
step_11_upload_single
|
|
step_12_verify_total
|
|
step_12_5_verify_file_sizes
|
|
step_12_8_verify_folder_metrics_before_labels
|
|
|
|
# Phase 3: Label Operations
|
|
echo ""
|
|
echo -e "${BLUE}=== PHASE 3: LABEL OPERATIONS ===${NC}"
|
|
step_13_14_apply_labels
|
|
step_15_list_folders
|
|
step_16_folder_metrics
|
|
step_16_5_folder_document_sizes
|
|
step_17_documents_by_label
|
|
step_17_5_verify_metrics_after_labels
|
|
|
|
# Phase 4: Field Extractions
|
|
echo ""
|
|
echo -e "${BLUE}=== PHASE 4: FIELD EXTRACTIONS ===${NC}"
|
|
step_18_verify_no_extraction
|
|
step_19_create_extraction
|
|
step_20_verify_history_1
|
|
step_21_create_version_2
|
|
step_22_verify_history_2
|
|
step_23_get_specific_versions
|
|
|
|
# Summary
|
|
echo ""
|
|
echo -e "${BLUE}========================================${NC}"
|
|
echo -e "${GREEN}All integration tests completed!${NC}"
|
|
echo -e "${BLUE}========================================${NC}"
|
|
echo ""
|
|
echo "Summary:"
|
|
echo " Client ID: $CLIENT_ID"
|
|
echo " Client Name: $CLIENT_NAME"
|
|
echo " Documents Created: ${#DOCUMENT_IDS[@]}"
|
|
echo " Target Document: $TARGET_DOC_ID"
|
|
echo ""
|
|
echo "The system is now populated with test data for UI development."
|
|
}
|
|
|
|
# Run main function with all arguments
|
|
main "$@"
|