docs(service-booking): add spec, plans, mockup, and clone-verify script

Kickoff brief, design spec, both implementation plans (rates foundation +
booking wizard), the UI mockup, and the hands-off Westin clone-verify/deploy
script for the Technician Service Booking feature.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-06-04 00:20:36 -04:00
parent 774d21863e
commit f0400114f9
6 changed files with 2242 additions and 0 deletions

163
scripts/verify_service_booking.sh Executable file
View File

@@ -0,0 +1,163 @@
#!/usr/bin/env bash
# =============================================================================
# verify_service_booking.sh
#
# HANDS-OFF clone-verify (and, behind a flag, deploy) for the Technician
# Service Booking feature (fusion_tasks + fusion_claims) on the Westin host.
#
# It automates the documented "Westin Prod — Clone-Verify / Deploy" procedure
# (see Odoo-Modules/CLAUDE.md) end-to-end:
# 1. refresh the branch checkout on the host
# 2. clone the live DB to a throwaway test DB (+ the orphaned-tax-FK cleanup)
# 3. stage the branch modules into the _test shadow prefix (prod untouched)
# 4. install/upgrade + run the module tests on the clone (PASS/FAIL gate)
# 5. (only with --deploy AND green tests) back up, swap, -u prod, restart
# 6. always clean up the clone + staging
#
# Verify-only by default. Deploy is OFF unless you pass --deploy.
#
# RUN IT ON THE WESTIN HOST:
# ssh odoo-westin # (via your usual jump)
# # one-time: put the branch on the host, e.g.
# # git clone <remote> /opt/odoo/staging/Odoo-Modules (or scp the tree there)
# bash verify_service_booking.sh # verify only
# DEPLOY=1 bash verify_service_booking.sh --deploy # verify, then deploy on green
#
# Prereq: the feature code must already be implemented on $BRANCH. This script
# does NOT write code — it verifies/deploys what's on the branch.
# =============================================================================
set -Eeuo pipefail
# ----------------------------- CONFIG (env-overridable) ----------------------
APP="${APP:-odoo-dev-app}" # Odoo app container
DBC="${DBC:-odoo-dev-db}" # Postgres container
PROD_DB="${PROD_DB:-westin-v19}" # live DB (cloned, never -u'd unless --deploy)
CLONE_DB="${CLONE_DB:-westin-v19-svcbook}" # throwaway verify DB
PGPW="${PGPW:-DevSecure2025!}"
PGUSER="${PGUSER:-odoo}"
MODULES="${MODULES:-fusion_tasks,fusion_claims}" # comma list for -u
TEST_TAGS="${TEST_TAGS:-/fusion_tasks,/fusion_claims}"
MOD_DIRS=(fusion_tasks fusion_claims) # dirs to stage/deploy
BRANCH="${BRANCH:-claude/technician-service-booking}"
SRC="${SRC:-/opt/odoo/staging/Odoo-Modules}" # host checkout of the branch
STAGE="${STAGE:-/opt/odoo/custom-addons/_test}" # shadow prefix (CLAUDE.md)
LIVE_ADDONS="${LIVE_ADDONS:-/opt/odoo/custom-addons}"
BACKUPS="${BACKUPS:-/opt/odoo/backups}" # OUTSIDE the addons path
CONF="${CONF:-/etc/odoo/odoo.conf}"
# _test prefix SHADOWS prod (first match wins); deps load from the real path.
ADDONS_PATH="/usr/lib/python3/dist-packages/odoo/addons,/usr/lib/python3/dist-packages/addons,${STAGE},/mnt/enterprise-addons,/mnt/extra-addons"
LIVE_ADDONS_PATH="/usr/lib/python3/dist-packages/odoo/addons,/usr/lib/python3/dist-packages/addons,/mnt/enterprise-addons,/mnt/extra-addons"
DEPLOY=0
[[ "${1:-}" == "--deploy" || "${DEPLOY:-0}" == "1" ]] && DEPLOY=1
STAMP="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo manual)"
LOG="/tmp/svcbook_verify_${STAMP}.log"
c() { printf '\n\033[1;36m== %s ==\033[0m\n' "$*"; } # section
ok() { printf '\033[1;32m%s\033[0m\n' "$*"; }
err() { printf '\033[1;31m%s\033[0m\n' "$*" >&2; }
dexec() { docker exec "$@"; }
psql_clone() { dexec -e PGPASSWORD="$PGPW" "$DBC" psql -U "$PGUSER" -d "$CLONE_DB" -v ON_ERROR_STOP=1 "$@"; }
# ----------------------------- CLEANUP TRAP ----------------------------------
cleanup() {
c "Cleanup"
rm -rf "${STAGE:?}/"* 2>/dev/null || true
dexec -e PGPASSWORD="$PGPW" "$DBC" dropdb -U "$PGUSER" --if-exists "$CLONE_DB" 2>/dev/null || true
ok "Dropped clone $CLONE_DB, cleared $STAGE"
}
trap 'err "FAILED (line $LINENO). See $LOG"; cleanup' ERR
trap 'cleanup' EXIT
# ----------------------------- 0. SANITY -------------------------------------
c "Pre-flight"
docker ps --format '{{.Names}}' | grep -qx "$APP" || { err "container $APP not running"; exit 1; }
docker ps --format '{{.Names}}' | grep -qx "$DBC" || { err "container $DBC not running"; exit 1; }
if [[ -d "$SRC/.git" ]]; then
git -C "$SRC" fetch --quiet origin "$BRANCH" && git -C "$SRC" checkout --quiet "$BRANCH" && git -C "$SRC" pull --quiet --ff-only origin "$BRANCH"
ok "Branch $BRANCH @ $(git -C "$SRC" rev-parse --short HEAD)"
else
err "WARNING: $SRC is not a git checkout — staging whatever is on disk there."
fi
for m in "${MOD_DIRS[@]}"; do [[ -d "$SRC/$m" ]] || { err "missing module dir: $SRC/$m"; exit 1; }; done
# ----------------------------- 1. CLONE THE DB -------------------------------
c "Clone $PROD_DB -> $CLONE_DB (read-only on prod)"
dexec -e PGPASSWORD="$PGPW" "$DBC" sh -c \
"dropdb -U $PGUSER --if-exists $CLONE_DB; createdb -U $PGUSER -O $PGUSER $CLONE_DB && pg_dump -U $PGUSER $PROD_DB | psql -U $PGUSER -q -d $CLONE_DB" \
>>"$LOG" 2>&1
ok "Cloned."
# ----------------------------- 2. ORPHAN-TAX-FK CLEANUP (clone only) ---------
# westin-v19 has ~3300 orphaned tax m2m rows under validated FKs; a plain
# pg_dump|psql clone can't rebuild the validating FK over them -> Odoo fails to
# load the registry. Safe to delete ON THE CLONE only. (CLAUDE.md gotcha.)
c "Orphaned-tax-FK cleanup (clone only)"
psql_clone -c "DELETE FROM product_taxes_rel WHERE tax_id NOT IN (SELECT id FROM account_tax);" >>"$LOG" 2>&1 || true
psql_clone -c "DELETE FROM product_supplier_taxes_rel WHERE tax_id NOT IN (SELECT id FROM account_tax);" >>"$LOG" 2>&1 || true
# sweep any other %_rel table carrying a tax_id column
psql_clone -t -A -c "SELECT table_name FROM information_schema.columns WHERE column_name='tax_id' AND table_name LIKE '%\\_rel';" 2>/dev/null \
| while read -r t; do [[ -n "$t" ]] && psql_clone -c "DELETE FROM ${t} WHERE tax_id NOT IN (SELECT id FROM account_tax);" >>"$LOG" 2>&1 || true; done
ok "Orphan FKs cleared on clone."
# ----------------------------- 3. STAGE MODULES (shadow) ---------------------
c "Stage modules into $STAGE (shadows prod, prod files untouched)"
mkdir -p "$STAGE"
for m in "${MOD_DIRS[@]}"; do rm -rf "${STAGE:?}/$m"; cp -r "$SRC/$m" "$STAGE/$m"; done
ok "Staged: ${MOD_DIRS[*]}"
# ----------------------------- 4. INSTALL/UPGRADE + TESTS (clone) -----------
# Test-runner gotchas on the prod-config container (CLAUDE.md / fusion_repairs):
# --test-enable SILENTLY SKIPS without --workers 0; log_level=warn hides test
# output -> add --log-level=test. The EXIT CODE is authoritative.
run_odoo() { # $1 = extra args
dexec "$APP" odoo -d "$CLONE_DB" \
--db_host db --db_port 5432 --db_user "$PGUSER" --db_password "$PGPW" \
--addons-path="$ADDONS_PATH" --stop-after-init --no-http $1
}
c "Install/upgrade on clone (catches install/render errors)"
if run_odoo "-u $MODULES" >>"$LOG" 2>&1; then ok "Upgrade OK"; else err "UPGRADE FAILED — see $LOG"; tail -40 "$LOG"; exit 2; fi
c "Run module tests on clone"
if run_odoo "-u $MODULES --test-enable --test-tags $TEST_TAGS --workers 0 --log-level=test" >>"$LOG" 2>&1; then
TESTS_OK=1; ok "TESTS PASSED"
else
TESTS_OK=0; err "TESTS FAILED (exit $?)"; grep -E 'FAIL|ERROR|Traceback' "$LOG" | tail -40 || true
fi
echo
c "VERIFY RESULT"
if [[ "${TESTS_OK:-0}" == "1" ]]; then ok "✅ Clone-verify GREEN (full log: $LOG)"; else err "❌ Clone-verify RED (full log: $LOG)"; fi
# ----------------------------- 5. DEPLOY (gated) -----------------------------
if [[ "$DEPLOY" == "1" ]]; then
if [[ "${TESTS_OK:-0}" != "1" ]]; then err "Not deploying — tests are red."; exit 3; fi
c "DEPLOY to $PROD_DB (tests green)"
mkdir -p "$BACKUPS"
# DB backup (-Fc) + module dir backups OUTSIDE the addons path
dexec -e PGPASSWORD="$PGPW" "$DBC" pg_dump -Fc -U "$PGUSER" "$PROD_DB" > "$BACKUPS/${PROD_DB}_${STAMP}.dump"
for m in "${MOD_DIRS[@]}"; do [[ -d "$LIVE_ADDONS/$m" ]] && cp -r "$LIVE_ADDONS/$m" "$BACKUPS/${m}_${STAMP}"; done
ok "Backed up DB + module dirs to $BACKUPS"
# swap branch modules into the real addons
for m in "${MOD_DIRS[@]}"; do rm -rf "${LIVE_ADDONS:?}/$m"; cp -r "$SRC/$m" "$LIVE_ADDONS/$m"; done
# -u prod, gated on exit 0
if dexec "$APP" odoo -d "$PROD_DB" --db_host db --db_port 5432 --db_user "$PGUSER" --db_password "$PGPW" \
--addons-path="$LIVE_ADDONS_PATH" -u "$MODULES" --stop-after-init --no-http >>"$LOG" 2>&1; then
dexec -e PGPASSWORD="$PGPW" "$DBC" psql -U "$PGUSER" -d "$PROD_DB" -c \
"DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%';" >>"$LOG" 2>&1 || true
docker restart "$APP" >>"$LOG" 2>&1
ok "🚀 Deployed + assets cleared + $APP restarted."
else
err "PROD -u FAILED — restoring module dirs, NOT restarting."
for m in "${MOD_DIRS[@]}"; do rm -rf "${LIVE_ADDONS:?}/$m"; [[ -d "$BACKUPS/${m}_${STAMP}" ]] && cp -r "$BACKUPS/${m}_${STAMP}" "$LIVE_ADDONS/$m"; done
err "Restore the DB if needed: pg_restore from $BACKUPS/${PROD_DB}_${STAMP}.dump"
exit 4
fi
else
echo
ok "Verify-only run (no deploy). Re-run with --deploy to ship on green."
fi