Files
jobtrackingapp/scripts/s06-acceptance-data.sh
cesnimda b37c0222a6 feat: Seeded acceptance-ready job data through the live API with determ…
- "scripts/s06-acceptance-data.sh"
- "scripts/s06-acceptance-data.test.sh"
- "README.md"
- ".gsd/KNOWLEDGE.md"
- ".gsd/DECISIONS.md"
- ".gsd/milestones/M001/slices/S06/tasks/T02-SUMMARY.md"

GSD-Task: S06/T02
2026-03-27 09:09:50 +01:00

295 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# Seed one deterministic acceptance-ready job into the live API.
#
# This script requires AUTH_TOKEN because local dev auth is enabled in this milestone.
# Retrieve a bearer token manually before running, for example by logging into the local
# API via POST /api/auth/login with the active local account for this environment, then:
# export AUTH_TOKEN="<bearer token>"
# bash scripts/s06-acceptance-data.sh
#
# If the placeholder dev credentials in appsettings.Development.json do not match the
# current database, use the real seeded account for this environment or a bearer token
# captured from an already-authenticated local browser session. The script never prints
# token values.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
API_BASE="${API_BASE:-http://localhost:5202/api}"
AUTH_TOKEN="${AUTH_TOKEN:-}"
CURL_TIMEOUT="${CURL_TIMEOUT:-20}"
CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-5}"
COMPANY_NAME="S06 Acceptance Labs"
JOB_TITLE="S06 Acceptance Backend Engineer"
JOB_URL="https://example.invalid/jobs/s06-acceptance-backend-engineer"
THREAD_ID="s06-acceptance-thread"
MESSAGE_ID="s06-acceptance-message-1"
FOLLOW_UP_AT="2026-03-10T09:00:00Z"
DATE_APPLIED="2026-02-28T10:00:00Z"
CORRESPONDENCE_AT="2026-03-09T11:30:00Z"
TMP_DIR="$(mktemp -d)"
trap 'rm -rf "$TMP_DIR"' EXIT
note() {
printf '%s\n' "$*"
}
fail() {
printf 'ERROR: %s\n' "$*" >&2
exit 1
}
if [[ -z "$AUTH_TOKEN" ]]; then
fail "AUTH_TOKEN is required. Export a bearer token first, then rerun."
fi
validate_json() {
local file="$1"
if ! python3 - "$file" <<'PY'
import json
import sys
with open(sys.argv[1], 'r', encoding='utf-8') as fh:
json.load(fh)
PY
then
printf 'Raw response body:\n' >&2
cat "$file" >&2
printf '\n' >&2
fail "Malformed JSON returned by API."
fi
}
request() {
local method="$1"
local name="$2"
local url="$3"
local body_file="$4"
local expect_json="$5"
local data_file="${6:-}"
local err_file="$TMP_DIR/${name// /-}.curl.err"
local status_file="$TMP_DIR/${name// /-}.status"
: >"$err_file"
local -a curl_args=(
-sS
--connect-timeout "$CONNECT_TIMEOUT"
--max-time "$CURL_TIMEOUT"
-X "$method"
-H 'Accept: application/json'
-H "Authorization: Bearer ${AUTH_TOKEN}"
-o "$body_file"
-w '%{http_code}'
)
if [[ -n "$data_file" ]]; then
curl_args+=(-H 'Content-Type: application/json' --data-binary "@$data_file")
fi
set +e
curl "${curl_args[@]}" "$url" >"$status_file" 2>"$err_file"
local curl_exit=$?
set -e
local status='000'
if [[ -s "$status_file" ]]; then
status="$(tr -d '\r\n' <"$status_file")"
fi
if [[ $curl_exit -ne 0 ]]; then
case "$curl_exit:$status" in
7:*|28:*|52:*|56:*|6:*)
printf 'curl: %s\n' "$(tr -d '\r' <"$err_file")" >&2
fail "Cannot reach ${url}. Start the API and rerun."
;;
22:401|22:403)
printf 'Response body:\n' >&2
cat "$body_file" >&2 || true
printf '\n' >&2
fail "AUTH_TOKEN appears invalid or lacks access for ${name}."
;;
22:5*)
printf 'Response body:\n' >&2
cat "$body_file" >&2 || true
printf '\n' >&2
fail "${name} failed with server error HTTP ${status}."
;;
22:*)
printf 'Response body:\n' >&2
cat "$body_file" >&2 || true
printf '\n' >&2
fail "${name} failed with HTTP ${status}."
;;
*)
printf 'curl: %s\n' "$(tr -d '\r' <"$err_file")" >&2
fail "${name} failed with curl exit ${curl_exit}."
;;
esac
fi
if [[ "$status" == 401 || "$status" == 403 ]]; then
printf 'Response body:\n' >&2
cat "$body_file" >&2 || true
printf '\n' >&2
fail "AUTH_TOKEN appears invalid or lacks access for ${name}."
fi
if [[ "$status" =~ ^5 ]]; then
printf 'Response body:\n' >&2
cat "$body_file" >&2 || true
printf '\n' >&2
fail "${name} failed with server error HTTP ${status}."
fi
if [[ "$expect_json" == "json" ]]; then
validate_json "$body_file"
fi
}
json_extract() {
local file="$1"
local expression="$2"
python3 - "$file" "$expression" <<'PY'
import json
import sys
file_path, expression = sys.argv[1], sys.argv[2]
with open(file_path, 'r', encoding='utf-8') as fh:
data = json.load(fh)
value = eval(expression, {'__builtins__': {'next': next, 'len': len}}, {'data': data})
if value is None:
print('')
elif isinstance(value, bool):
print('true' if value else 'false')
elif isinstance(value, (dict, list)):
print(json.dumps(value, separators=(',', ':')))
else:
print(value)
PY
}
json_string() {
python3 - "$1" <<'PY'
import json
import sys
print(json.dumps(sys.argv[1]))
PY
}
write_json() {
local path="$1"
shift
printf '%s\n' "$*" >"$path"
}
urlencode() {
python3 - "$1" <<'PY'
import sys
import urllib.parse
print(urllib.parse.quote(sys.argv[1]))
PY
}
note "Running preflight before seeding acceptance data."
bash "$SCRIPT_DIR/s06-preflight.sh" >/dev/null
company_body="$TMP_DIR/company.json"
write_json "$company_body" "{\"name\":$(json_string "$COMPANY_NAME"),\"location\":$(json_string "Oslo, Norway"),\"source\":$(json_string "LinkedIn")}"
company_response="$TMP_DIR/company-response.json"
request POST "company create" "$API_BASE/companies" "$company_response" json "$company_body"
company_id="$(json_extract "$company_response" 'data["id"]')"
[[ -n "$company_id" ]] || fail "Company create response did not include an id."
company_update_body="$TMP_DIR/company-update.json"
write_json "$company_update_body" "{\"name\":$(json_string "$COMPANY_NAME"),\"location\":$(json_string "Oslo, Norway"),\"source\":$(json_string "LinkedIn"),\"recruiterName\":$(json_string "Maria Recruiter"),\"recruiterEmail\":$(json_string "maria.recruiter@example.invalid"),\"recruiterLinkedIn\":$(json_string "https://www.linkedin.com/in/maria-recruiter"),\"lastContactedAt\":$(json_string "$CORRESPONDENCE_AT"),\"nextContactAt\":$(json_string "$FOLLOW_UP_AT"),\"pipelineStage\":$(json_string "Follow-up ready")}"
company_update_response="$TMP_DIR/company-update-response.json"
request PUT "company update" "$API_BASE/companies/$company_id" "$company_update_response" json "$company_update_body"
jobs_lookup="$TMP_DIR/jobs-lookup.json"
encoded_title="$(urlencode "$JOB_TITLE")"
request GET "job lookup" "$API_BASE/jobapplications?page=1&pageSize=15&q=$encoded_title" "$jobs_lookup" json
job_id="$(json_extract "$jobs_lookup" 'next((item["id"] for item in data.get("items", []) if item.get("jobTitle") == "S06 Acceptance Backend Engineer" and ((item.get("company") or {}).get("name") == "S06 Acceptance Labs")), "")')"
job_action="updated"
create_job_body="$TMP_DIR/create-job.json"
write_json "$create_job_body" "{\"jobTitle\":$(json_string "$JOB_TITLE"),\"companyId\":$company_id,\"status\":$(json_string "Waiting"),\"location\":$(json_string "Oslo / Hybrid"),\"salary\":$(json_string "NOK 900000"),\"nextAction\":$(json_string "Review saved package and send a manual recruiter follow-up from the linked thread."),\"followUpAt\":$(json_string "$FOLLOW_UP_AT"),\"notes\":$(json_string $'Acceptance seed notes for S06.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved acceptance application answer with concrete API and workflow evidence.\n<<<END_APPLICATION_ANSWER_DRAFT>>>'),\"description\":$(json_string "Lead the backend of an individual-first job tracking platform, maintain recruiter-thread continuity, and keep daily dashboard reminders trustworthy."),\"translatedDescription\":$(json_string "Backend role focused on workflow trust signals, saved application package reuse, and manual-send follow-up discipline."),\"descriptionLanguage\":$(json_string "en"),\"tags\":$(json_string "ASP.NET Core, React, SQLite, Workflow, Follow-up"),\"deadline\":$(json_string "2026-04-10T12:00:00Z"),\"coverLetterText\":$(json_string "Saved acceptance cover letter that references workflow trust signals, recruiter continuity, and measured backend delivery."),\"jobUrl\":$(json_string "$JOB_URL"),\"dateApplied\":$(json_string "$DATE_APPLIED"),\"feedbackRequestedAt\":null,\"hasResume\":true,\"hasCoverLetter\":true,\"hasPortfolio\":true,\"hasOtherAttachment\":false}"
if [[ -z "$job_id" ]]; then
create_job_response="$TMP_DIR/create-job-response.json"
request POST "job create" "$API_BASE/jobapplications" "$create_job_response" json "$create_job_body"
job_id="$(json_extract "$create_job_response" 'data["id"]')"
job_action="created"
fi
[[ -n "$job_id" ]] || fail "Job create/update flow did not yield a job id."
update_job_body="$TMP_DIR/update-job.json"
write_json "$update_job_body" "{\"jobTitle\":$(json_string "$JOB_TITLE"),\"companyId\":$company_id,\"status\":$(json_string "Waiting"),\"responseReceived\":false,\"responseDate\":null,\"location\":$(json_string "Oslo / Hybrid"),\"salary\":$(json_string "NOK 900000"),\"nextAction\":$(json_string "Review saved package and send a manual recruiter follow-up from the linked thread."),\"followUpAt\":$(json_string "$FOLLOW_UP_AT"),\"hasResume\":true,\"hasCoverLetter\":true,\"hasPortfolio\":true,\"hasOtherAttachment\":false,\"notes\":$(json_string $'Acceptance seed notes for S06.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved acceptance application answer with concrete API and workflow evidence.\n<<<END_APPLICATION_ANSWER_DRAFT>>>'),\"description\":$(json_string "Lead the backend of an individual-first job tracking platform, maintain recruiter-thread continuity, and keep daily dashboard reminders trustworthy."),\"translatedDescription\":$(json_string "Backend role focused on workflow trust signals, saved application package reuse, and manual-send follow-up discipline."),\"descriptionLanguage\":$(json_string "en"),\"tags\":$(json_string "ASP.NET Core, React, SQLite, Workflow, Follow-up"),\"deadline\":$(json_string "2026-04-10T12:00:00Z"),\"coverLetterText\":$(json_string "Saved acceptance cover letter that references workflow trust signals, recruiter continuity, and measured backend delivery."),\"jobUrl\":$(json_string "$JOB_URL"),\"dateApplied\":$(json_string "$DATE_APPLIED"),\"feedbackRequestedAt\":null,\"statusChangedAt\":null}"
update_job_response="$TMP_DIR/update-job-response.json"
request PUT "job update" "$API_BASE/jobapplications/$job_id" "$update_job_response" empty "$update_job_body"
save_cv_body="$TMP_DIR/save-cv.json"
write_json "$save_cv_body" "{\"tailoredCvText\":$(json_string "Saved acceptance tailored CV highlighting ASP.NET Core delivery, workflow trust signals, recruiter-thread continuity, and dashboard reminder ownership.")}"
save_cv_response="$TMP_DIR/save-cv-response.json"
request PUT "tailored CV save" "$API_BASE/jobapplications/$job_id/tailored-cv" "$save_cv_response" empty "$save_cv_body"
save_drafts_body="$TMP_DIR/save-drafts.json"
write_json "$save_drafts_body" "{\"coverLetterText\":$(json_string "Saved acceptance cover letter that references workflow trust signals, recruiter continuity, and measured backend delivery."),\"notes\":$(json_string $'Acceptance seed notes for S06.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved acceptance application answer with concrete API and workflow evidence.\n<<<END_APPLICATION_ANSWER_DRAFT>>>'),\"recruiterMessageDraft\":$(json_string "Saved acceptance recruiter message draft that acknowledges the prior thread and keeps the human send step explicit.")}"
save_drafts_response="$TMP_DIR/save-drafts-response.json"
request PUT "application drafts save" "$API_BASE/jobapplications/$job_id/application-drafts" "$save_drafts_response" empty "$save_drafts_body"
followup_body="$TMP_DIR/followup.json"
write_json "$followup_body" "{\"followUpAt\":$(json_string "$FOLLOW_UP_AT")}"
followup_response="$TMP_DIR/followup-response.json"
request PATCH "follow-up schedule" "$API_BASE/jobapplications/$job_id/followup" "$followup_response" empty "$followup_body"
correspondence_list="$TMP_DIR/correspondence-list.json"
request GET "correspondence list" "$API_BASE/correspondence/$job_id" "$correspondence_list" json
existing_correspondence_id="$(json_extract "$correspondence_list" 'next((item["id"] for item in data if item.get("externalMessageId") == "s06-acceptance-message-1"), "")')"
correspondence_action="existing"
if [[ -n "$existing_correspondence_id" ]]; then
existing_subject="$(json_extract "$correspondence_list" 'next((item.get("subject") or "" for item in data if item.get("externalMessageId") == "s06-acceptance-message-1"), "")')"
existing_content="$(json_extract "$correspondence_list" 'next((item.get("content") or "" for item in data if item.get("externalMessageId") == "s06-acceptance-message-1"), "")')"
if [[ "$existing_subject" != "Backend Engineer follow-up" || "$existing_content" != "Hi Casey,\n\nThanks again for applying. We reviewed your saved package and would welcome a short manual follow-up next week so we can confirm timeline and next steps.\n\nBest,\nMaria" ]]; then
delete_response="$TMP_DIR/delete-correspondence-response.json"
request DELETE "correspondence delete" "$API_BASE/correspondence/$existing_correspondence_id" "$delete_response" empty
existing_correspondence_id=""
correspondence_action="replaced"
fi
fi
if [[ -z "$existing_correspondence_id" ]]; then
create_correspondence_body="$TMP_DIR/create-correspondence.json"
write_json "$create_correspondence_body" "{\"jobApplicationId\":$job_id,\"from\":$(json_string "Maria Recruiter"),\"content\":$(json_string $'Hi Casey,\n\nThanks again for applying. We reviewed your saved package and would welcome a short manual follow-up next week so we can confirm timeline and next steps.\n\nBest,\nMaria'),\"subject\":$(json_string "Backend Engineer follow-up"),\"channel\":$(json_string "email"),\"date\":$(json_string "$CORRESPONDENCE_AT"),\"externalMessageId\":$(json_string "$MESSAGE_ID"),\"externalThreadId\":$(json_string "$THREAD_ID"),\"externalFrom\":$(json_string "maria.recruiter@example.invalid"),\"externalTo\":$(json_string "casey@example.invalid")}"
create_correspondence_response="$TMP_DIR/create-correspondence-response.json"
request POST "correspondence create" "$API_BASE/correspondence" "$create_correspondence_response" json "$create_correspondence_body"
existing_correspondence_id="$(json_extract "$create_correspondence_response" 'data["id"]')"
if [[ "$correspondence_action" != "replaced" ]]; then
correspondence_action="created"
fi
fi
request GET "job details" "$API_BASE/jobapplications/$job_id" "$TMP_DIR/job-details.json" json
request GET "readiness" "$API_BASE/jobapplications/$job_id/readiness" "$TMP_DIR/readiness.json" json
request GET "correspondence list final" "$API_BASE/correspondence/$job_id" "$TMP_DIR/correspondence-final.json" json
workflow_action="$(json_extract "$TMP_DIR/readiness.json" 'data["workflowSignal"]["actionKey"]')"
readiness_score="$(json_extract "$TMP_DIR/readiness.json" 'data["score"]')"
readiness_level="$(json_extract "$TMP_DIR/readiness.json" 'data["level"]')"
correspondence_count="$(json_extract "$TMP_DIR/correspondence-final.json" 'len(data)')"
completed_items="$(json_extract "$TMP_DIR/readiness.json" '" | ".join(data.get("completed", []))')"
reminder_items="$(json_extract "$TMP_DIR/readiness.json" '" | ".join(data.get("reminders", []))')"
note "seed.result=success"
note "seed.company.id=$company_id"
note "seed.job.id=$job_id"
note "seed.job.action=$job_action"
note "seed.correspondence.id=$existing_correspondence_id"
note "seed.correspondence.action=$correspondence_action"
note "seed.correspondence.count=$correspondence_count"
note "seed.readiness.score=$readiness_score"
note "seed.readiness.level=$readiness_level"
note "seed.workflow.action=$workflow_action"
note "seed.followUp.at=$FOLLOW_UP_AT"
note "seed.completed=$completed_items"
note "seed.reminders=$reminder_items"