FASE 4-5-6: Infraestructura, CRM, Service Orders, Notificaciones, Ahorro, Logistica, API Publica

FASE 4:
- Redis cache de stock con fallback graceful
- Multi-moneda (MXN/USD) con contabilidad en MXN
- Proveedores y ordenes de compra completo
- Meilisearch 1.5M+ partes indexadas
- Metabase KPIs con dashboard auto-generado

FASE 5:
- CRM mejorado: activities, tags, loyalty program, analytics
- Imagenes de partes: upload, resize, thumbnails WebP
- Ordenes de servicio Kanban: received->diagnosis->repair->ready->delivered
- Garantias/RMA, alertas de reorden, multi-sucursal
- Stubs BNPL (APLAZO) y ERP Sync (Aspel/Contpaqi)

FASE 6:
- Notificaciones automaticas: push/WhatsApp/email/in-app
- Reportes de ahorro vs retail_price
- Logistica + tracking: DHL, FedEx, Estafeta, 99min, Uber
- API Publica: API keys, rate limiting, catalog search

Migraciones: v1.9-v3.0
Tests: 93/93 pasando
Backup: nexus_backup_20260427_045859.tar.gz
This commit is contained in:
Nexus Dev
2026-04-27 05:23:30 +00:00
parent b70cb3042b
commit 9ff3dc4c8b
71 changed files with 10939 additions and 420 deletions

View File

@@ -1,8 +1,7 @@
#!/bin/bash
# ============================================================
# Nexus Autoparts POS — Automated Installer
# Nexus Autoparts POS — Automated Installer v2.0
# Works on: Debian 12+, Ubuntu 22.04+, Raspberry Pi OS (64-bit)
# Usage: curl -s https://raw.githubusercontent.com/.../install.sh | bash
# ============================================================
set -euo pipefail
@@ -15,11 +14,6 @@ BOLD='\033[1m'
NC='\033[0m'
INSTALL_DIR="/opt/nexus-pos"
SERVICE_NAME="nexus-pos"
DB_USER="nexus"
DB_PASS="nexus_autoparts_2026"
DB_NAME="nexus_autoparts"
POS_PORT=5001
LOG_FILE="/var/log/nexus-pos-install.log"
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
@@ -32,7 +26,7 @@ banner() {
echo ""
echo -e "${BOLD}${CYAN}"
echo " ========================================"
echo " Nexus Autoparts POS — Installer v1.0"
echo " Nexus Autoparts POS — Installer v2.0"
echo " ========================================"
echo -e "${NC}"
}
@@ -45,49 +39,42 @@ cleanup_on_error() {
}
trap cleanup_on_error ERR
# Generate a secure random password
generate_secret() {
openssl rand -hex 32 2>/dev/null || python3 -c "import secrets; print(secrets.token_hex(32))"
}
# ============================================================
# 1. CHECK PREREQUISITES
# ============================================================
check_prerequisites() {
info "Checking prerequisites..."
# Must be Linux
if [[ "$(uname -s)" != "Linux" ]]; then
fatal "This installer only supports Linux (Debian/Ubuntu/Raspberry Pi OS)."
fi
# Must be root
if [[ $EUID -ne 0 ]]; then
fatal "This script must be run as root. Use: sudo bash install.sh"
fi
# Check distro
if [[ -f /etc/os-release ]]; then
. /etc/os-release
info "Detected OS: ${PRETTY_NAME:-$ID}"
log "OS: ${PRETTY_NAME:-$ID}"
else
warn "Could not detect OS version. Proceeding anyway."
fi
# Detect Raspberry Pi
IS_RPI=false
if grep -qi "raspberry" /proc/cpuinfo 2>/dev/null || grep -qi "raspberry" /sys/firmware/devicetree/base/model 2>/dev/null; then
IS_RPI=true
info "Raspberry Pi detected."
log "Raspberry Pi detected"
fi
# Check architecture
ARCH=$(uname -m)
info "Architecture: $ARCH"
if [[ "$ARCH" != "x86_64" && "$ARCH" != "aarch64" && "$ARCH" != "armv7l" ]]; then
warn "Untested architecture: $ARCH. Proceeding with caution."
fi
# Check internet
if ! ping -c 1 -W 3 8.8.8.8 &>/dev/null; then
fatal "No internet connection detected. Please connect and retry."
warn "No internet connection detected. Some features may not work."
fi
ok "Prerequisites check passed."
@@ -101,27 +88,27 @@ install_packages() {
apt-get update -qq >> "$LOG_FILE" 2>&1
PACKAGES=(
python3
python3-pip
python3-venv
postgresql
postgresql-client
git
nginx
libpq-dev
gcc
python3-dev
curl
python3 python3-pip python3-venv
postgresql postgresql-client
redis-server
git nginx curl
libpq-dev gcc python3-dev
)
# On Raspberry Pi, add some extras for lxml
if [[ "$IS_RPI" == true ]]; then
PACKAGES+=(libxml2-dev libxslt1-dev zlib1g-dev)
fi
info "Installing system packages: ${PACKAGES[*]}"
info "Installing system packages..."
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${PACKAGES[@]}" >> "$LOG_FILE" 2>&1
# Install Node.js for WhatsApp bridge (LTS)
if ! command -v node &>/dev/null; then
info "Installing Node.js LTS..."
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >> "$LOG_FILE" 2>&1
apt-get install -y -qq nodejs >> "$LOG_FILE" 2>&1
fi
ok "System packages installed."
}
@@ -131,19 +118,33 @@ install_packages() {
configure_postgresql() {
info "Configuring PostgreSQL..."
# Ensure PostgreSQL is running
systemctl enable postgresql >> "$LOG_FILE" 2>&1
systemctl start postgresql >> "$LOG_FILE" 2>&1
# Create user if not exists
info "Configuring Redis..."
systemctl enable redis-server >> "$LOG_FILE" 2>&1
systemctl start redis-server >> "$LOG_FILE" 2>&1
# Generate random DB password
DB_PASS=$(generate_secret)
DB_USER="nexus"
DB_NAME="nexus_autoparts"
# Save credentials for later
echo "DB_USER=$DB_USER" > /tmp/nexus_install_vars
echo "DB_PASS=$DB_PASS" >> /tmp/nexus_install_vars
echo "DB_NAME=$DB_NAME" >> /tmp/nexus_install_vars
# Create user
if sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='${DB_USER}'" | grep -q 1; then
info "PostgreSQL user '${DB_USER}' already exists."
info "PostgreSQL user '${DB_USER}' exists. Updating password..."
sudo -u postgres psql -c "ALTER USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;" >> "$LOG_FILE" 2>&1
else
sudo -u postgres psql -c "CREATE USER ${DB_USER} WITH PASSWORD '${DB_PASS}' CREATEDB;" >> "$LOG_FILE" 2>&1
ok "PostgreSQL user '${DB_USER}' created."
fi
# Create master database if not exists
# Create master database
if sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='${DB_NAME}'" | grep -q 1; then
info "Database '${DB_NAME}' already exists."
else
@@ -151,14 +152,11 @@ configure_postgresql() {
ok "Database '${DB_NAME}' created."
fi
# Grant CREATEDB to user (idempotent)
sudo -u postgres psql -c "ALTER USER ${DB_USER} CREATEDB;" >> "$LOG_FILE" 2>&1
# Ensure pg_hba.conf allows md5 auth for local connections
PG_HBA=$(sudo -u postgres psql -tAc "SHOW hba_file" 2>/dev/null | head -1)
if [[ -n "$PG_HBA" ]] && ! grep -q "nexus" "$PG_HBA" 2>/dev/null; then
# Add md5 auth line for nexus user before the first local line
sed -i "/^# TYPE/a local all ${DB_USER} md5" "$PG_HBA" 2>/dev/null || true
# Ensure md5 auth for local connections
PG_HBA=$(sudo -u postgres psql -tAc "SHOW hba_file" 2>/dev/null | head -1 | xargs)
if [[ -n "$PG_HBA" ]] && ! grep -q "local.*all.*${DB_USER}.*scram-sha-256" "$PG_HBA" 2>/dev/null; then
# Add scram-sha-256 line before the first peer/trust line
sed -i "/^local.*all.*all.*peer/i local all ${DB_USER} scram-sha-256" "$PG_HBA" 2>/dev/null || true
systemctl reload postgresql >> "$LOG_FILE" 2>&1
fi
@@ -166,104 +164,79 @@ configure_postgresql() {
}
# ============================================================
# 4. CLONE REPOSITORY
# 4. SETUP APPLICATION
# ============================================================
clone_repo() {
setup_app() {
info "Setting up application in ${INSTALL_DIR}..."
if [[ -d "${INSTALL_DIR}" ]]; then
warn "${INSTALL_DIR} already exists."
echo -en "${YELLOW} Overwrite? [y/N]: ${NC}"
read -r overwrite
if [[ "${overwrite,,}" == "y" ]]; then
rm -rf "${INSTALL_DIR}"
else
info "Keeping existing installation. Will update in place."
fi
warn "${INSTALL_DIR} already exists. Updating in place..."
else
mkdir -p "${INSTALL_DIR}"
fi
if [[ ! -d "${INSTALL_DIR}" ]]; then
# If running from the repo itself, copy it
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "${SCRIPT_DIR}/pos/app.py" ]]; then
info "Copying from local source: ${SCRIPT_DIR}"
cp -a "${SCRIPT_DIR}" "${INSTALL_DIR}"
else
info "Cloning from GitHub..."
git clone https://github.com/consultoria-as/nexus-autoparts.git "${INSTALL_DIR}" >> "$LOG_FILE" 2>&1
fi
# Copy from local source (the repo where this script lives)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "${SCRIPT_DIR}/pos/app.py" ]]; then
info "Copying from local source: ${SCRIPT_DIR}"
rsync -a --delete --exclude='venv' --exclude='__pycache__' --exclude='.git' \
"${SCRIPT_DIR}/" "${INSTALL_DIR}/" >> "$LOG_FILE" 2>&1
else
fatal "Could not find local source. Run this script from the project root."
fi
ok "Application files ready at ${INSTALL_DIR}."
}
# ============================================================
# 5. INSTALL PYTHON DEPENDENCIES
# ============================================================
install_python_deps() {
info "Creating Python virtual environment..."
# Start Meilisearch and Metabase (optional but recommended)
if command -v docker &>/dev/null; then
info "Starting Meilisearch..."
cd "${INSTALL_DIR}/docker" && docker compose -f docker-compose.meilisearch.yml up -d >> "$LOG_FILE" 2>&1
ok "Meilisearch container started."
info "Starting Metabase..."
cd "${INSTALL_DIR}/docker" && docker compose -f docker-compose.metabase.yml up -d >> "$LOG_FILE" 2>&1
ok "Metabase container started."
else
warn "Docker not found. Meilisearch and Metabase will not be available."
fi
# Create virtual environment
python3 -m venv "${INSTALL_DIR}/venv" >> "$LOG_FILE" 2>&1
info "Installing Python dependencies..."
"${INSTALL_DIR}/venv/bin/pip" install --upgrade pip >> "$LOG_FILE" 2>&1
"${INSTALL_DIR}/venv/bin/pip" install -r "${INSTALL_DIR}/pos/requirements.txt" >> "$LOG_FILE" 2>&1
"${INSTALL_DIR}/venv/bin/pip" install gunicorn >> "$LOG_FILE" 2>&1
ok "Python dependencies installed."
ok "Application files and Python dependencies ready."
}
# ============================================================
# 6. INTERACTIVE SETUP
# 5. INTERACTIVE SETUP
# ============================================================
interactive_setup() {
echo ""
echo -e "${BOLD}${CYAN}--- Business Setup ---${NC}"
echo ""
# Business name
echo -en "${BOLD} Business name${NC} (e.g., Refaccionaria Lopez): "
read -r BUSINESS_NAME
if [[ -z "$BUSINESS_NAME" ]]; then
BUSINESS_NAME="Mi Refaccionaria"
warn "Using default: ${BUSINESS_NAME}"
fi
[[ -z "$BUSINESS_NAME" ]] && BUSINESS_NAME="Mi Refaccionaria"
# RFC
echo -en "${BOLD} RFC${NC} (optional, press Enter to skip): "
echo -en "${BOLD} RFC${NC} (optional): "
read -r BUSINESS_RFC
if [[ -z "$BUSINESS_RFC" ]]; then
BUSINESS_RFC=""
info "RFC skipped."
fi
# Owner name
echo -en "${BOLD} Owner name${NC}: "
read -r OWNER_NAME
if [[ -z "$OWNER_NAME" ]]; then
OWNER_NAME="Administrador"
warn "Using default: ${OWNER_NAME}"
fi
[[ -z "$OWNER_NAME" ]] && OWNER_NAME="Administrador"
# Owner PIN
while true; do
echo -en "${BOLD} Owner PIN${NC} (4 digits): "
read -rs OWNER_PIN
echo ""
if [[ "$OWNER_PIN" =~ ^[0-9]{4}$ ]]; then
break
else
warn "PIN must be exactly 4 digits. Try again."
fi
[[ "$OWNER_PIN" =~ ^[0-9]{4}$ ]] && break
warn "PIN must be exactly 4 digits."
done
# Domain/IP
DEFAULT_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
echo -en "${BOLD} Domain or IP${NC} for access [${DEFAULT_IP:-localhost}]: "
echo -en "${BOLD} Domain or IP${NC} [${DEFAULT_IP:-localhost}]: "
read -r ACCESS_HOST
if [[ -z "$ACCESS_HOST" ]]; then
ACCESS_HOST="${DEFAULT_IP:-localhost}"
fi
[[ -z "$ACCESS_HOST" ]] && ACCESS_HOST="${DEFAULT_IP:-localhost}"
echo ""
echo -e "${BOLD} Summary:${NC}"
@@ -275,9 +248,24 @@ interactive_setup() {
echo ""
echo -en "${BOLD} Proceed? [Y/n]: ${NC}"
read -r confirm
if [[ "${confirm,,}" == "n" ]]; then
fatal "Installation cancelled by user."
fi
[[ "${confirm,,}" == "n" ]] && fatal "Installation cancelled."
}
# ============================================================
# 6. LOAD MASTER SCHEMA & SEED DATA
# ============================================================
load_master_schema() {
info "Loading master database schema (vehicles + catalog)..."
source /tmp/nexus_install_vars
# Load PostgreSQL schema (no TecDoc)
sudo -u postgres psql "${DB_NAME}" -f "${INSTALL_DIR}/sql/schema_master_postgres.sql" >> "$LOG_FILE" 2>&1
# Load initial catalog seed (categories, brands, models, years)
sudo -u postgres psql "${DB_NAME}" -f "${INSTALL_DIR}/pos/seed/initial_catalog.sql" >> "$LOG_FILE" 2>&1
ok "Master schema and seed data loaded."
}
# ============================================================
@@ -286,25 +274,34 @@ interactive_setup() {
provision_tenant() {
info "Provisioning tenant database..."
source /tmp/nexus_install_vars
cd "${INSTALL_DIR}/pos"
# Build a small Python script to avoid quoting issues in bash
cat > /tmp/_nexus_provision.py << PYEOF
export MASTER_DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}"
export TENANT_DB_URL_TEMPLATE="postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}"
export POS_JWT_SECRET="$(generate_secret)"
export DATABASE_URL="${MASTER_DB_URL}"
export JWT_SECRET="${POS_JWT_SECRET}"
# Build provision script
cat > /tmp/_nexus_provision.py << 'PYEOF'
import sys, os
sys.path.insert(0, '${INSTALL_DIR}/pos')
os.chdir('${INSTALL_DIR}/pos')
sys.path.insert(0, os.environ['INSTALL_DIR'] + '/pos')
os.chdir(os.environ['INSTALL_DIR'] + '/pos')
from services.tenant_manager import provision_tenant
rfc_val = os.environ.get('NX_RFC') or None
result = provision_tenant(
name=os.environ['NX_BUSINESS'],
rfc=rfc_val,
rfc=os.environ.get('NX_RFC') or None,
owner_name=os.environ['NX_OWNER'],
owner_pin=os.environ['NX_PIN']
)
print(f"Tenant created: id={result['tenant_id']}, db={result['db_name']}")
print(f"Tenant created: id={result['tenant_id']}, db={result['db_name']}, subdomain={result['subdomain']}")
PYEOF
INSTALL_DIR="${INSTALL_DIR}" \
NX_BUSINESS="$BUSINESS_NAME" \
NX_RFC="$BUSINESS_RFC" \
NX_OWNER="$OWNER_NAME" \
@@ -317,13 +314,26 @@ PYEOF
}
# ============================================================
# 8. APPLY MIGRATIONS (v1.1)
# 8. APPLY MIGRATIONS TO ALL TENANTS
# ============================================================
apply_migrations() {
info "Applying database migrations..."
source /tmp/nexus_install_vars
export MASTER_DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}"
export TENANT_DB_URL_TEMPLATE="postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}"
cd "${INSTALL_DIR}/pos"
# Run master migrations
"${INSTALL_DIR}/venv/bin/python3" -c "
import sys
sys.path.insert(0, '${INSTALL_DIR}/pos')
from migrations.runner_master import run_master_migrations
run_master_migrations()
" >> "$LOG_FILE" 2>&1
# Run tenant migrations
"${INSTALL_DIR}/venv/bin/python3" -c "
import sys
sys.path.insert(0, '${INSTALL_DIR}/pos')
@@ -331,30 +341,85 @@ from migrations.runner import run_migrations
run_migrations()
" >> "$LOG_FILE" 2>&1
ok "Migrations applied."
ok "All migrations applied."
}
# ============================================================
# 9. CREATE SYSTEMD SERVICE
# 9. GENERATE .env FILE
# ============================================================
create_systemd_service() {
info "Creating systemd service..."
generate_env() {
info "Generating environment configuration..."
cat > /etc/systemd/system/${SERVICE_NAME}.service << SERVICEEOF
source /tmp/nexus_install_vars
POS_SECRET=$(generate_secret)
WEB_SECRET=$(generate_secret)
WPP_SECRET=$(generate_secret)
cat > "${INSTALL_DIR}/.env" << ENVEOF
# Nexus Autoparts — Generated on $(date)
# DO NOT COMMIT THIS FILE TO GIT
DATABASE_URL=postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}
MASTER_DB_URL=postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}
TENANT_DB_URL_TEMPLATE=postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}
JWT_SECRET=${WEB_SECRET}
POS_JWT_SECRET=${POS_SECRET}
WHATSAPP_BRIDGE_KEY=${WPP_SECRET}
REDIS_URL=redis://localhost:6379/0
REDIS_ENABLED=true
REDIS_STOCK_TTL=300
MEILI_URL=http://localhost:7700
MEILI_API_KEY=$(generate_secret)
METABASE_URL=http://localhost:3000
METABASE_ADMIN_EMAIL=admin@${SUBDOMAIN}.local
METABASE_ADMIN_PASS=$(generate_secret)
METABASE_DB_PASS=$(generate_secret)
DEFAULT_CURRENCY=MXN
EXCHANGE_RATE_USD_MXN=17.5
ENVEOF
chmod 600 "${INSTALL_DIR}/.env"
ok ".env file created with secure secrets."
}
# ============================================================
# 10. CREATE SYSTEMD SERVICES
# ============================================================
create_systemd_services() {
info "Creating systemd services..."
source /tmp/nexus_install_vars
# Create nexus user
if ! id -u nexus &>/dev/null; then
useradd -r -s /bin/false -d "${INSTALL_DIR}" nexus >> "$LOG_FILE" 2>&1
fi
chown -R nexus:nexus "${INSTALL_DIR}"
# POS Service
cat > /etc/systemd/system/nexus-pos.service << SERVICEEOF
[Unit]
Description=Nexus Autoparts POS
After=network.target postgresql.service
Requires=postgresql.service
After=network.target postgresql.service redis-server.service
Wants=postgresql.service redis-server.service
[Service]
Type=simple
User=www-data
Group=www-data
Type=notify
User=nexus
Group=nexus
WorkingDirectory=${INSTALL_DIR}/pos
Environment=PATH=${INSTALL_DIR}/venv/bin:/usr/bin:/bin
Environment=MASTER_DB_URL=postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}
Environment=TENANT_DB_URL_TEMPLATE=postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}
ExecStart=${INSTALL_DIR}/venv/bin/gunicorn --bind 127.0.0.1:${POS_PORT} --workers 3 --timeout 120 "app:create_app()"
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=${INSTALL_DIR}/.env
ExecStartPre=/bin/mkdir -p /var/log/nexus-pos
ExecStart=${INSTALL_DIR}/venv/bin/gunicorn -c ${INSTALL_DIR}/pos/gunicorn.conf.py "app:create_app()"
Restart=always
RestartSec=5
StandardOutput=journal
@@ -364,64 +429,123 @@ StandardError=journal
WantedBy=multi-user.target
SERVICEEOF
# Set ownership
chown -R www-data:www-data "${INSTALL_DIR}"
# Web Dashboard Service
cat > /etc/systemd/system/nexus-web.service << SERVICEEOF
[Unit]
Description=Nexus Autoparts Web Publica
After=network.target postgresql.service redis-server.service
Wants=postgresql.service redis-server.service
[Service]
Type=simple
User=nexus
Group=nexus
WorkingDirectory=${INSTALL_DIR}/dashboard
Environment=PATH=${INSTALL_DIR}/venv/bin:/usr/bin:/bin
Environment=PYTHONUNBUFFERED=1
EnvironmentFile=${INSTALL_DIR}/.env
ExecStart=${INSTALL_DIR}/venv/bin/python3 server.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
SERVICEEOF
# WhatsApp Bridge Service
cat > /etc/systemd/system/nexus-whatsapp.service << SERVICEEOF
[Unit]
Description=Nexus WhatsApp Bridge (Baileys)
After=network.target
[Service]
Type=simple
User=nexus
Group=nexus
WorkingDirectory=${INSTALL_DIR}
Environment=NODE_ENV=production
ExecStart=/usr/bin/node ${INSTALL_DIR}/pos/whatsapp-bridge-server.js
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
SERVICEEOF
systemctl daemon-reload >> "$LOG_FILE" 2>&1
ok "Systemd service created: ${SERVICE_NAME}.service"
ok "Systemd services created: nexus-pos, nexus-web, nexus-whatsapp"
}
# ============================================================
# 10. CONFIGURE NGINX
# 11. CONFIGURE NGINX
# ============================================================
configure_nginx() {
info "Configuring nginx reverse proxy..."
info "Configuring nginx..."
# Remove default site
rm -f /etc/nginx/sites-enabled/default 2>/dev/null || true
cat > /etc/nginx/sites-available/nexus-pos << NGINXEOF
cat > /etc/nginx/sites-available/nexus << 'NGINXEOF'
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=pos_login:10m rate=10r/s;
upstream nexus_main {
server 127.0.0.1:5000;
}
upstream nexus_pos {
server 127.0.0.1:5001;
}
server {
listen 80;
server_name ${ACCESS_HOST};
server_name _; # Catch-all
client_max_body_size 20M;
# POS application
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options SAMEORIGIN always;
# Web publica / landing
location / {
proxy_pass http://nexus_main;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# POS paths
location /pos/ {
proxy_pass http://127.0.0.1:${POS_PORT}/pos/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_read_timeout 300s;
proxy_pass http://nexus_pos;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# API endpoints
location /api/ {
proxy_pass http://127.0.0.1:${POS_PORT}/api/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
# Redirect root to POS login
location = / {
return 302 /pos/login;
# Rate limit login endpoint
location /pos/api/auth/login {
limit_req zone=pos_login burst=5 nodelay;
proxy_pass http://nexus_pos;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Health check
location /health {
proxy_pass http://127.0.0.1:${POS_PORT}/pos/health;
proxy_pass http://nexus_pos/pos/health;
}
}
NGINXEOF
ln -sf /etc/nginx/sites-available/nexus-pos /etc/nginx/sites-enabled/nexus-pos
ln -sf /etc/nginx/sites-available/nexus /etc/nginx/sites-enabled/nexus
# Test nginx config
if nginx -t >> "$LOG_FILE" 2>&1; then
ok "Nginx configuration valid."
else
@@ -431,32 +555,66 @@ NGINXEOF
}
# ============================================================
# 11. START SERVICES
# 12. START SERVICES
# ============================================================
start_services() {
info "Starting services..."
systemctl enable postgresql >> "$LOG_FILE" 2>&1
systemctl enable nginx >> "$LOG_FILE" 2>&1
systemctl restart nginx >> "$LOG_FILE" 2>&1
ok "Nginx started."
systemctl enable "${SERVICE_NAME}" >> "$LOG_FILE" 2>&1
systemctl start "${SERVICE_NAME}" >> "$LOG_FILE" 2>&1
systemctl enable nexus-pos >> "$LOG_FILE" 2>&1
systemctl start nexus-pos >> "$LOG_FILE" 2>&1
ok "Nexus POS service started."
# Wait a moment and verify
sleep 2
if systemctl is-active --quiet "${SERVICE_NAME}"; then
ok "Service is running."
systemctl enable nexus-web >> "$LOG_FILE" 2>&1
systemctl start nexus-web >> "$LOG_FILE" 2>&1
ok "Nexus Web service started."
systemctl enable nexus-whatsapp >> "$LOG_FILE" 2>&1
systemctl start nexus-whatsapp >> "$LOG_FILE" 2>&1
ok "Nexus WhatsApp bridge started."
sleep 3
# Verify POS is running
if systemctl is-active --quiet nexus-pos; then
ok "All services are running."
else
warn "Service may not have started correctly. Check: journalctl -u ${SERVICE_NAME}"
warn "nexus-pos may not have started correctly. Check: journalctl -u nexus-pos"
fi
}
# ============================================================
# 12. PRINT SUCCESS
# 13. HEALTH CHECK
# ============================================================
run_health_check() {
info "Running post-installation health check..."
source /tmp/nexus_install_vars
export MASTER_DB_URL="postgresql://${DB_USER}:${DB_PASS}@localhost/${DB_NAME}"
export TENANT_DB_URL_TEMPLATE="postgresql://${DB_USER}:${DB_PASS}@localhost/{db_name}"
export DATABASE_URL="${MASTER_DB_URL}"
cd "${INSTALL_DIR}"
"${INSTALL_DIR}/venv/bin/pip" install requests >> "$LOG_FILE" 2>&1 || true
if "${INSTALL_DIR}/venv/bin/python3" "${INSTALL_DIR}/scripts/health_check.py"; then
ok "Health check passed."
else
warn "Health check found issues. Review output above."
fi
}
# ============================================================
# 14. PRINT SUCCESS
# ============================================================
print_success() {
source /tmp/nexus_install_vars
echo ""
echo -e "${BOLD}${GREEN}"
echo " ========================================"
@@ -464,23 +622,28 @@ print_success() {
echo " ========================================"
echo -e "${NC}"
echo ""
echo -e " ${BOLD}Access URL:${NC} http://${ACCESS_HOST}/pos/login"
echo -e " ${BOLD}POS Login:${NC} http://${ACCESS_HOST}/pos/login"
echo -e " ${BOLD}Web Catalog:${NC} http://${ACCESS_HOST}/"
echo -e " ${BOLD}Business:${NC} ${BUSINESS_NAME}"
echo -e " ${BOLD}Owner:${NC} ${OWNER_NAME}"
echo -e " ${BOLD}PIN:${NC} **** (the 4-digit PIN you entered)"
echo -e " ${BOLD}PIN:${NC} ${OWNER_PIN}"
echo ""
echo -e " ${BOLD}Service:${NC} systemctl status ${SERVICE_NAME}"
echo -e " ${BOLD}Logs:${NC} journalctl -u ${SERVICE_NAME} -f"
echo -e " ${BOLD}Install log:${NC} ${LOG_FILE}"
echo -e " ${BOLD}Services:${NC}"
echo " POS: systemctl status nexus-pos"
echo " Web: systemctl status nexus-web"
echo " WhatsApp: systemctl status nexus-whatsapp"
echo " Logs: journalctl -u nexus-pos -f"
echo ""
echo -e " ${BOLD}Database:${NC}"
echo " Host: localhost"
echo " User: ${DB_USER}"
echo " Master DB: ${DB_NAME}"
echo " Host: localhost"
echo " User: ${DB_USER}"
echo " Pass: ${DB_PASS}"
echo " Master DB: ${DB_NAME}"
echo ""
echo -e " ${BOLD}Files:${NC} ${INSTALL_DIR}/"
echo -e " ${BOLD}.env:${NC} ${INSTALL_DIR}/.env"
echo ""
echo -e " ${YELLOW}To uninstall:${NC} sudo bash ${INSTALL_DIR}/uninstall.sh"
echo -e " ${YELLOW}Save the database password securely!${NC}"
echo ""
}
@@ -489,24 +652,27 @@ print_success() {
# ============================================================
main() {
banner
# Init log
mkdir -p "$(dirname "$LOG_FILE")"
echo "=== Nexus POS Install started at $(date) ===" > "$LOG_FILE"
echo "=== Nexus POS Install v2.0 started at $(date) ===" > "$LOG_FILE"
check_prerequisites
install_packages
configure_postgresql
clone_repo
install_python_deps
setup_app
interactive_setup
load_master_schema
provision_tenant
apply_migrations
create_systemd_service
generate_env
create_systemd_services
configure_nginx
start_services
run_health_check
print_success
# Cleanup temp vars
rm -f /tmp/nexus_install_vars
log "Installation completed successfully."
}