187 lines
5.4 KiB
Bash
Executable File
187 lines
5.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# Poll ServiceM8 form responses, parse Quote Template responses, then apply any
|
|
# newly/unapplied parsed quote responses to ServiceM8 jobMaterials.
|
|
#
|
|
# Default window: last 24 hours from script start, in local system time.
|
|
# Override with:
|
|
# --since 'YYYY-MM-DD HH:MM:SS'
|
|
# --hours 48
|
|
#
|
|
# By default this wrapper applies unapplied parsed responses. Use --dry-run to
|
|
# run the poll and preview each pending apply without writing to ServiceM8.
|
|
# The apply script still refuses duplicate applies unless --force is explicitly
|
|
# passed through. It also checks ServiceM8 for existing jobMaterial rows on
|
|
# the target job and blocks the apply unless --force-remote-existing is passed.
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
POLL_SCRIPT="$SCRIPT_DIR/poll_form_responses_since.py"
|
|
APPLY_SCRIPT="$SCRIPT_DIR/apply_polled_quote_template_jobmaterials.py"
|
|
DB_PATH="${WEBHOOK_POLL_DB_PATH:-$SCRIPT_DIR/servicem8_formresponse_poll.db}"
|
|
LOG_DIR="${WEBHOOK_RUN_LOG_DIR:-$SCRIPT_DIR/logs}"
|
|
QUOTE_TEMPLATE_FORM_UUID="${SERVICEM8_QUOTE_TEMPLATE_FORM_UUID:-3621b6be-1d19-4756-9ab4-9d5e4120f6d9}"
|
|
|
|
SINCE=""
|
|
HOURS="24"
|
|
FORCE="0"
|
|
FORCE_REMOTE_EXISTING="0"
|
|
RECHECK_REMOTE_EXISTING="0"
|
|
DRY_RUN="0"
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $0 [--since 'YYYY-MM-DD HH:MM:SS'] [--hours N] [--dry-run] [--force] [--force-remote-existing] [--recheck-remote-existing]
|
|
|
|
Examples:
|
|
$0
|
|
$0 --hours 48
|
|
$0 --since '2026-05-04 08:00:00'
|
|
$0 --dry-run --hours 48
|
|
|
|
This will:
|
|
1. Poll /formresponse.json using timestamp gt SINCE
|
|
2. Store/parse Quote Template responses into $DB_PATH
|
|
3. Apply parsed responses that do not already have generated materials recorded
|
|
|
|
With --dry-run, step 3 previews the ServiceM8 jobMaterial payloads only; it does
|
|
not write to ServiceM8 or mark responses as applied.
|
|
|
|
Safety:
|
|
--force keeps the existing local duplicate override behaviour.
|
|
--force-remote-existing allows creating rows even when the remote ServiceM8
|
|
job already has jobMaterial rows; the incident is still recorded.
|
|
--recheck-remote-existing revisits rows previously blocked because remote
|
|
jobMaterial rows existed. If the remote rows are gone, the apply can proceed.
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--since)
|
|
SINCE="${2:-}"
|
|
shift 2
|
|
;;
|
|
--hours)
|
|
HOURS="${2:-}"
|
|
shift 2
|
|
;;
|
|
--force)
|
|
FORCE="1"
|
|
shift
|
|
;;
|
|
--force-remote-existing)
|
|
FORCE_REMOTE_EXISTING="1"
|
|
shift
|
|
;;
|
|
--recheck-remote-existing)
|
|
RECHECK_REMOTE_EXISTING="1"
|
|
shift
|
|
;;
|
|
--dry-run)
|
|
DRY_RUN="1"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$SINCE" ]]; then
|
|
SINCE="$(date -d "-${HOURS} hours" '+%Y-%m-%d %H:%M:%S')"
|
|
fi
|
|
|
|
mkdir -p "$LOG_DIR"
|
|
RUN_ID="$(date '+%Y%m%d-%H%M%S')"
|
|
LOG_FILE="$LOG_DIR/poll-and-apply-$RUN_ID.log"
|
|
|
|
exec > >(tee -a "$LOG_FILE") 2>&1
|
|
|
|
echo "== ServiceM8 Quote Template poll/apply run =="
|
|
echo "Started: $(date --iso-8601=seconds)"
|
|
echo "Since: $SINCE"
|
|
echo "Mode: $([[ "$DRY_RUN" == "1" ]] && echo "dry-run" || echo "apply")"
|
|
echo "Remote existing: $([[ "$FORCE_REMOTE_EXISTING" == "1" ]] && echo "force" || ([[ "$RECHECK_REMOTE_EXISTING" == "1" ]] && echo "recheck" || echo "block"))"
|
|
echo "DB: $DB_PATH"
|
|
echo "Log: $LOG_FILE"
|
|
echo
|
|
|
|
echo "== Polling form responses =="
|
|
"$POLL_SCRIPT" --since "$SINCE" --summary-limit 20
|
|
|
|
echo
|
|
echo "== Finding unapplied parsed Quote Template responses =="
|
|
mapfile -t FORM_RESPONSE_UUIDS < <(
|
|
python3 - "$DB_PATH" "$QUOTE_TEMPLATE_FORM_UUID" "$RECHECK_REMOTE_EXISTING" "$FORCE_REMOTE_EXISTING" <<'PY'
|
|
import sqlite3
|
|
import sys
|
|
|
|
poll_db, quote_form_uuid, recheck_remote_existing, force_remote_existing = sys.argv[1:5]
|
|
include_remote_blocked = recheck_remote_existing == "1" or force_remote_existing == "1"
|
|
status_clause = "coalesce(q.process_status, '') != 'applied'"
|
|
if not include_remote_blocked:
|
|
status_clause += " and coalesce(q.process_status, '') != 'blocked_remote_existing'"
|
|
|
|
conn = sqlite3.connect(poll_db)
|
|
conn.row_factory = sqlite3.Row
|
|
rows = conn.execute(
|
|
f"""
|
|
select q.form_response_uuid
|
|
from quote_template_form_responses q
|
|
left join form_responses_raw r on r.uuid = q.form_response_uuid
|
|
where q.form_uuid = ?
|
|
and {status_clause}
|
|
order by coalesce(r.timestamp, q.discovered_at) asc, q.form_response_uuid asc
|
|
""",
|
|
(quote_form_uuid,),
|
|
).fetchall()
|
|
for row in rows:
|
|
print(row["form_response_uuid"])
|
|
PY
|
|
)
|
|
|
|
if [[ ${#FORM_RESPONSE_UUIDS[@]} -eq 0 ]]; then
|
|
echo "No unapplied Quote Template responses found."
|
|
echo "Finished: $(date --iso-8601=seconds)"
|
|
exit 0
|
|
fi
|
|
|
|
printf 'Found %d unapplied Quote Template response(s):\n' "${#FORM_RESPONSE_UUIDS[@]}"
|
|
printf ' - %s\n' "${FORM_RESPONSE_UUIDS[@]}"
|
|
|
|
echo
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
echo "== Dry-run preview only; no ServiceM8 writes =="
|
|
APPLY_ARGS=(--pretty)
|
|
else
|
|
echo "== Applying to ServiceM8 =="
|
|
APPLY_ARGS=(--apply --pretty)
|
|
fi
|
|
if [[ "$FORCE" == "1" ]]; then
|
|
APPLY_ARGS+=(--force)
|
|
fi
|
|
if [[ "$FORCE_REMOTE_EXISTING" == "1" ]]; then
|
|
APPLY_ARGS+=(--force-remote-existing)
|
|
fi
|
|
|
|
for uuid in "${FORM_RESPONSE_UUIDS[@]}"; do
|
|
echo
|
|
if [[ "$DRY_RUN" == "1" ]]; then
|
|
echo "-- Dry-run form_response_uuid=$uuid --"
|
|
else
|
|
echo "-- Applying form_response_uuid=$uuid --"
|
|
fi
|
|
"$APPLY_SCRIPT" --uuid "$uuid" "${APPLY_ARGS[@]}"
|
|
done
|
|
|
|
echo
|
|
echo "Finished: $(date --iso-8601=seconds)"
|
|
echo "Log: $LOG_FILE"
|