diff --git a/Dockerfile.template b/Dockerfile.template new file mode 100644 index 0000000..df2d37e --- /dev/null +++ b/Dockerfile.template @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin ${BIN_NAME} + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/${BIN_NAME} ./${BIN_NAME} + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./${BIN_NAME}"] diff --git a/apps/catering_services/Dockerfile b/apps/catering_services/Dockerfile new file mode 100644 index 0000000..a639bdb --- /dev/null +++ b/apps/catering_services/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin catering_services + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/catering_services ./catering_services + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./catering_services"] diff --git a/apps/companies/Dockerfile b/apps/companies/Dockerfile new file mode 100644 index 0000000..cc3b169 --- /dev/null +++ b/apps/companies/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin companies + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/companies ./companies + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./companies"] diff --git a/apps/customers/Dockerfile b/apps/customers/Dockerfile new file mode 100644 index 0000000..3e27656 --- /dev/null +++ b/apps/customers/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin customers + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/customers ./customers + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./customers"] diff --git a/apps/developers/Dockerfile b/apps/developers/Dockerfile new file mode 100644 index 0000000..d77dd62 --- /dev/null +++ b/apps/developers/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin developers + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/developers ./developers + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./developers"] diff --git a/apps/employees/Dockerfile b/apps/employees/Dockerfile new file mode 100644 index 0000000..ab70738 --- /dev/null +++ b/apps/employees/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin employees + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/employees ./employees + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./employees"] diff --git a/apps/fitness_trainers/Dockerfile b/apps/fitness_trainers/Dockerfile new file mode 100644 index 0000000..c2a42df --- /dev/null +++ b/apps/fitness_trainers/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin fitness_trainers + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/fitness_trainers ./fitness_trainers + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./fitness_trainers"] diff --git a/apps/gateway/Dockerfile b/apps/gateway/Dockerfile new file mode 100644 index 0000000..60cf01c --- /dev/null +++ b/apps/gateway/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin gateway + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/gateway ./gateway + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./gateway"] diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index 7522d0a..b779592 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -85,7 +85,10 @@ impl Services { || path.starts_with("/api/packages") || path.starts_with("/api/support") || path.starts_with("/api/admin/roles") + || path.starts_with("/api/admin/users") || path.starts_with("/api/admin/verifications") + || path.starts_with("/api/admin/approvals") + || path.starts_with("/api/admin/approval-cases") || path.starts_with("/api/admin/external-roles") || path.starts_with("/api/admin/dashboard-config") || path.starts_with("/api/admin/onboarding-config") @@ -179,11 +182,14 @@ impl Services { else if path.starts_with("/api/admin/runtime-configs") { Some(self.users_url.clone()) } + // Catch-all for any other admin endpoints → users service + else if path.starts_with("/api/admin/") { + Some(self.users_url.clone()) + } else { None } } - } fn build_cors() -> CorsLayer { diff --git a/apps/graphic_designers/Dockerfile b/apps/graphic_designers/Dockerfile new file mode 100644 index 0000000..fd8e776 --- /dev/null +++ b/apps/graphic_designers/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin graphic_designers + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/graphic_designers ./graphic_designers + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./graphic_designers"] diff --git a/apps/job_seekers/Dockerfile b/apps/job_seekers/Dockerfile new file mode 100644 index 0000000..67cc77e --- /dev/null +++ b/apps/job_seekers/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin job_seekers + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/job_seekers ./job_seekers + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./job_seekers"] diff --git a/apps/makeup_artists/Dockerfile b/apps/makeup_artists/Dockerfile new file mode 100644 index 0000000..0951681 --- /dev/null +++ b/apps/makeup_artists/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin makeup_artists + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/makeup_artists ./makeup_artists + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./makeup_artists"] diff --git a/apps/payments/Dockerfile b/apps/payments/Dockerfile new file mode 100644 index 0000000..b203377 --- /dev/null +++ b/apps/payments/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin payments + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/payments ./payments + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./payments"] diff --git a/apps/photographers/Dockerfile b/apps/photographers/Dockerfile new file mode 100644 index 0000000..7db2b3d --- /dev/null +++ b/apps/photographers/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin photographers + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/photographers ./photographers + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./photographers"] diff --git a/apps/social_media_managers/Dockerfile b/apps/social_media_managers/Dockerfile new file mode 100644 index 0000000..740169f --- /dev/null +++ b/apps/social_media_managers/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin social_media_managers + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/social_media_managers ./social_media_managers + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./social_media_managers"] diff --git a/apps/tutors/Dockerfile b/apps/tutors/Dockerfile new file mode 100644 index 0000000..d82c9e5 --- /dev/null +++ b/apps/tutors/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin tutors + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/tutors ./tutors + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./tutors"] diff --git a/apps/ugc_content_creators/Dockerfile b/apps/ugc_content_creators/Dockerfile new file mode 100644 index 0000000..b34a955 --- /dev/null +++ b/apps/ugc_content_creators/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin ugc_content_creators + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/ugc_content_creators ./ugc_content_creators + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./ugc_content_creators"] diff --git a/apps/users/Dockerfile b/apps/users/Dockerfile new file mode 100644 index 0000000..2c48ec0 --- /dev/null +++ b/apps/users/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin users + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/users ./users + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./users"] diff --git a/apps/users/src/handlers/admin.rs b/apps/users/src/handlers/admin.rs index 85222b7..4b28b10 100644 --- a/apps/users/src/handlers/admin.rs +++ b/apps/users/src/handlers/admin.rs @@ -13,10 +13,10 @@ use sqlx::FromRow; pub fn router() -> Router { Router::new() - .route("/users", get(list_users)) + .route("/", get(list_users)) .route("/customers", get(list_customers)) .route("/candidates", get(list_candidates)) - .route("/users/{id}/status", axum::routing::patch(update_user_status)) + .route("/{id}/status", axum::routing::patch(update_user_status)) } #[derive(Deserialize)] diff --git a/apps/video_editors/Dockerfile b/apps/video_editors/Dockerfile new file mode 100644 index 0000000..471c8d8 --- /dev/null +++ b/apps/video_editors/Dockerfile @@ -0,0 +1,41 @@ +# Build stage +FROM rust:1.79-slim AS builder + +WORKDIR /usr/src/app + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + pkg-config \ + libsqlite3-dev \ + && rm -rf /var/lib/apt/lists/* + +# Copy manifests +COPY Cargo.toml ./ +COPY crates ./crates +COPY apps ./apps + +# Build the application (release mode for smaller binary) +RUN cargo build --release --bin video_editors + +# Runtime stage +FROM debian:bookworm-slim AS runtime + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + ca-certificates \ + libsqlite3-0 \ + && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN useradd -m -u 1000 appuser + +WORKDIR /app + +# Copy binary from builder +COPY --from=builder /usr/src/app/target/release/video_editors ./video_editors + +# Switch to non-root user +USER appuser + +# Run the binary +CMD ["./video_editors"] diff --git a/docker-compose.yml b/docker-compose.yml index c7004c5..768a5d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,12 +12,27 @@ services: - '5432:5432' volumes: - pgdata:/var/lib/postgresql/data + - ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql healthcheck: test: ['CMD-SHELL', 'pg_isready -U nxtgauge -d nxtgauge_db'] interval: 5s timeout: 5s retries: 10 + redis: + image: redis:7-alpine + restart: unless-stopped + ports: + - '6379:6379' + volumes: + - redisdata:/data + command: redis-server --appendonly yes + healthcheck: + test: ['CMD', 'redis-cli', 'ping'] + interval: 5s + timeout: 5s + retries: 10 + # ── Core Services ──────────────────────────────────────────────────────── gateway: @@ -40,11 +55,14 @@ services: FITNESS_TRAINERS_SERVICE_URL: http://fitness_trainers:8092 CATERING_SERVICES_SERVICE_URL: http://catering_services:8093 PAYMENTS_SERVICE_URL: http://payments:8094 + REDIS_URL: redis://redis:6379 ports: - '8000:8000' depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy users: build: @@ -53,6 +71,7 @@ services: environment: PORT: 8080 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} JWT_EXPIRY_MINUTES: 15 REFRESH_TOKEN_EXPIRY_DAYS: 30 @@ -64,6 +83,8 @@ services: depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy companies: build: @@ -72,10 +93,13 @@ services: environment: PORT: 8081 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy job_seekers: build: @@ -84,10 +108,13 @@ services: environment: PORT: 8082 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy customers: build: @@ -96,10 +123,13 @@ services: environment: PORT: 8083 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy # ── 9 Profession Services ───────────────────────────────────────────────── @@ -110,10 +140,13 @@ services: environment: PORT: 8085 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy makeup_artists: build: @@ -122,10 +155,13 @@ services: environment: PORT: 8086 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy tutors: build: @@ -134,10 +170,13 @@ services: environment: PORT: 8087 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy developers: build: @@ -146,10 +185,13 @@ services: environment: PORT: 8088 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy video_editors: build: @@ -158,10 +200,13 @@ services: environment: PORT: 8089 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy graphic_designers: build: @@ -170,10 +215,13 @@ services: environment: PORT: 8090 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy social_media_managers: build: @@ -182,10 +230,13 @@ services: environment: PORT: 8091 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy fitness_trainers: build: @@ -194,10 +245,13 @@ services: environment: PORT: 8092 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy catering_services: build: @@ -206,10 +260,13 @@ services: environment: PORT: 8093 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy # ── Payments ────────────────────────────────────────────────────────────── @@ -220,12 +277,46 @@ services: environment: PORT: 8094 DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 JWT_SECRET: ${JWT_SECRET} RAZORPAY_KEY_ID: ${RAZORPAY_KEY_ID:-} RAZORPAY_KEY_SECRET: ${RAZORPAY_KEY_SECRET:-} depends_on: postgres: condition: service_healthy + redis: + condition: service_healthy + + employees: + build: + context: . + dockerfile: apps/employees/Dockerfile + environment: + PORT: 8095 + DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 + JWT_SECRET: ${JWT_SECRET} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + + ugc_content_creators: + build: + context: . + dockerfile: apps/ugc_content_creators/Dockerfile + environment: + PORT: 8096 + DATABASE_URL: postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@postgres:5432/nxtgauge_db + REDIS_URL: redis://redis:6379 + JWT_SECRET: ${JWT_SECRET} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy volumes: pgdata: + redisdata: diff --git a/gateway.pid b/gateway.pid new file mode 100644 index 0000000..bf2b153 --- /dev/null +++ b/gateway.pid @@ -0,0 +1 @@ +97314 diff --git a/scripts/init-db.sql b/scripts/init-db.sql new file mode 100644 index 0000000..d795c51 --- /dev/null +++ b/scripts/init-db.sql @@ -0,0 +1,1321 @@ +-- 1. ROLES +CREATE TABLE IF NOT EXISTS roles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + key VARCHAR(255) UNIQUE NOT NULL, + name VARCHAR(255) NOT NULL, + audience VARCHAR(50) NOT NULL, -- INTERNAL or EXTERNAL + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 2. ONBOARDING CONFIGS +CREATE TABLE IF NOT EXISTS onboarding_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + schema_json JSONB NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_active BOOLEAN NOT NULL DEFAULT true, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Only one active onboarding config per role at a time +CREATE UNIQUE INDEX IF NOT EXISTS idx_active_onboarding_per_role + ON onboarding_configs(role_id) WHERE is_active = true; + +-- 3. DASHBOARD CONFIGS +CREATE TABLE IF NOT EXISTS dashboard_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + audience VARCHAR(50) NOT NULL, -- INTERNAL or EXTERNAL + config_json JSONB NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_active BOOLEAN NOT NULL DEFAULT true, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Only one active dashboard config per role+audience combination +CREATE UNIQUE INDEX IF NOT EXISTS idx_active_dashboard_per_role_audience + ON dashboard_configs(role_id, audience) WHERE is_active = true; + +-- 4. RUNTIME CONFIGS +CREATE TABLE IF NOT EXISTS runtime_configs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + config_json JSONB NOT NULL, + version INTEGER NOT NULL DEFAULT 1, + is_active BOOLEAN NOT NULL DEFAULT true, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Only one active runtime config per role at a time +CREATE UNIQUE INDEX IF NOT EXISTS idx_active_runtime_per_role + ON runtime_configs(role_id) WHERE is_active = true; +-- 1. USERS +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, PENDING, SUSPENDED + role_id UUID REFERENCES roles(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 2. REFRESH TOKENS +CREATE TABLE IF NOT EXISTS refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash VARCHAR(255) UNIQUE NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Index for fast token lookups +CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash); +CREATE TABLE IF NOT EXISTS photographer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Photographer Specific Fields + portfolio_url VARCHAR(255), + equipment_list TEXT, + years_of_experience INT, + hourly_rate INTEGER, -- in paise (INR × 100) + specialties TEXT[], -- e.g., ["wedding", "portrait", "commercial"] + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Ensure a user can only have one photographer profile + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS tutor_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Tutor Specific Fields + subjects_taught TEXT[], -- e.g., ["math", "physics", "computer science"] + education_level VARCHAR(255), + certifications TEXT, + years_of_experience INT, + hourly_rate INTEGER, -- in paise (INR × 100) + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Ensure a user can only have one tutor profile + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS company_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Company Specific Fields + company_name VARCHAR(255) NOT NULL, + registration_number VARCHAR(100), + industry VARCHAR(150), + website_url VARCHAR(255), + employee_count INT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Ensure a user can only have one company profile + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS job_seeker_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Job Seeker + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS customer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Customer + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS makeup_artist_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Makeup Artist + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS developer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Developer + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS video_editor_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Video Editor + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS graphic_designer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Graphic Designer + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS social_media_manager_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Social Media Manager + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS fitness_trainer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Fitness Trainer + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +CREATE TABLE IF NOT EXISTS catering_service_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + + -- Generic Fields for Catering Service + bio TEXT, + experience_years INT, + custom_data JSONB DEFAULT '{}'::jsonb, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + UNIQUE(user_id) +); +-- Add missing columns to users table +ALTER TABLE users + ADD COLUMN IF NOT EXISTS full_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS phone VARCHAR(20) UNIQUE, + ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS phone_verified BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +-- user_roles: many-to-many, a user can hold multiple external roles +CREATE TABLE IF NOT EXISTS user_roles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, SUSPENDED + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(user_id, role_id) +); + +-- role_permissions +CREATE TABLE IF NOT EXISTS role_permissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + permission_key VARCHAR(100) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(role_id, permission_key) +); + +-- departments for internal staff +CREATE TABLE IF NOT EXISTS departments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- designations for internal staff +CREATE TABLE IF NOT EXISTS designations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(100) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- employees (internal staff records) +CREATE TABLE IF NOT EXISTS employees ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + role_id UUID NOT NULL REFERENCES roles(id), + department_id UUID REFERENCES departments(id), + designation_id UUID REFERENCES designations(id), + employee_code VARCHAR(50), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- onboarding_submissions: tracks verification submissions +CREATE TABLE IF NOT EXISTS onboarding_submissions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role_id UUID NOT NULL REFERENCES roles(id), + config_id UUID REFERENCES onboarding_configs(id), + data_json JSONB, + status VARCHAR(50) NOT NULL DEFAULT 'DRAFT', + submitted_at TIMESTAMPTZ, + reviewed_at TIMESTAMPTZ, + reviewed_by UUID REFERENCES users(id), + rejection_reason TEXT, + document_request TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- submission_documents: uploaded files for onboarding +CREATE TABLE IF NOT EXISTS submission_documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + submission_id UUID NOT NULL REFERENCES onboarding_submissions(id) ON DELETE CASCADE, + document_type VARCHAR(100) NOT NULL, + file_url VARCHAR(500) NOT NULL, + file_name VARCHAR(255), + uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); +CREATE INDEX IF NOT EXISTS idx_user_roles_status ON user_roles(status); +CREATE INDEX IF NOT EXISTS idx_onboarding_submissions_user_id ON onboarding_submissions(user_id); +CREATE INDEX IF NOT EXISTS idx_onboarding_submissions_status ON onboarding_submissions(status); +-- Complete company profile (replacing the minimal stub) +ALTER TABLE company_profiles + ADD COLUMN IF NOT EXISTS business_type VARCHAR(100), + ADD COLUMN IF NOT EXISTS gst_number VARCHAR(50), + ADD COLUMN IF NOT EXISTS contact_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS contact_email VARCHAR(255), + ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(20), + ADD COLUMN IF NOT EXISTS address_line1 VARCHAR(500), + ADD COLUMN IF NOT EXISTS city VARCHAR(100), + ADD COLUMN IF NOT EXISTS state VARCHAR(100), + ADD COLUMN IF NOT EXISTS country VARCHAR(100) NOT NULL DEFAULT 'India', + ADD COLUMN IF NOT EXISTS postal_code VARCHAR(20), + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', + ADD COLUMN IF NOT EXISTS free_job_slots INTEGER NOT NULL DEFAULT 1, + ADD COLUMN IF NOT EXISTS purchased_job_slots INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS free_contact_views INTEGER NOT NULL DEFAULT 30, + ADD COLUMN IF NOT EXISTS purchased_contact_views INTEGER NOT NULL DEFAULT 0; + +-- Jobs +CREATE TABLE IF NOT EXISTS jobs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + company_id UUID NOT NULL REFERENCES company_profiles(id) ON DELETE CASCADE, + title VARCHAR(200) NOT NULL, + category VARCHAR(100), + description TEXT NOT NULL, + location VARCHAR(255) NOT NULL, + job_type VARCHAR(50) NOT NULL DEFAULT 'FULL_TIME', -- FULL_TIME, PART_TIME, CONTRACT + salary_min INTEGER, -- in paise + salary_max INTEGER, -- in paise + experience_years INTEGER, + skills TEXT[] DEFAULT '{}', + status VARCHAR(50) NOT NULL DEFAULT 'DRAFT', + -- DRAFT, PENDING_APPROVAL, LIVE, EXPIRED, CLOSED, REJECTED + rejection_reason TEXT, + expires_at TIMESTAMPTZ, + approved_at TIMESTAMPTZ, + approved_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Applications (Job Seeker → Job) +CREATE TABLE IF NOT EXISTS applications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + job_id UUID NOT NULL REFERENCES jobs(id) ON DELETE CASCADE, + job_seeker_id UUID NOT NULL REFERENCES job_seeker_profiles(id) ON DELETE CASCADE, + cover_letter TEXT, + resume_url VARCHAR(500), + status VARCHAR(50) NOT NULL DEFAULT 'APPLIED', + -- APPLIED, SHORTLISTED, INTERVIEW, OFFERED, HIRED, REJECTED, WITHDRAWN + applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + contact_viewed BOOLEAN NOT NULL DEFAULT false, + UNIQUE(job_id, job_seeker_id) +); + +CREATE INDEX IF NOT EXISTS idx_jobs_company_id ON jobs(company_id); +CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); +CREATE INDEX IF NOT EXISTS idx_applications_job_id ON applications(job_id); +CREATE INDEX IF NOT EXISTS idx_applications_job_seeker_id ON applications(job_seeker_id); +CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status); +-- Add missing fields to job_seeker_profiles +ALTER TABLE job_seeker_profiles + ADD COLUMN IF NOT EXISTS full_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS summary TEXT, + ADD COLUMN IF NOT EXISTS experience_years INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS skills TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS resume_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS active_application_count INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE'; + +-- Requirements (customer leads) +CREATE TABLE IF NOT EXISTS requirements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + customer_id UUID NOT NULL REFERENCES customer_profiles(id) ON DELETE CASCADE, + profession_key VARCHAR(50) NOT NULL, + title VARCHAR(200) NOT NULL, + description TEXT NOT NULL, + location VARCHAR(255) NOT NULL, + budget INTEGER, -- in paise + preferred_date DATE, + extra_data_json JSONB, + status VARCHAR(50) NOT NULL DEFAULT 'DRAFT', + -- DRAFT, PENDING_APPROVAL, OPEN, CLOSED, EXPIRED, REJECTED + rejection_reason TEXT, + request_count INTEGER NOT NULL DEFAULT 0, + accepted_count INTEGER NOT NULL DEFAULT 0, + expires_at TIMESTAMPTZ, + approved_at TIMESTAMPTZ, + approved_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- professionals unified table (parent for all 9 profession subtypes) +CREATE TABLE IF NOT EXISTS professionals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + profession_key VARCHAR(50) NOT NULL, + display_name VARCHAR(255) NOT NULL, + location VARCHAR(255), + bio TEXT, + extra_data_json JSONB, + status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Lead requests (professional → requirement) +CREATE TABLE IF NOT EXISTS lead_requests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + requirement_id UUID NOT NULL REFERENCES requirements(id) ON DELETE CASCADE, + professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + -- PENDING, ACCEPTED, REJECTED, EXPIRED, CANCELLED + tracecoins_reserved INTEGER NOT NULL DEFAULT 25, + expires_at TIMESTAMPTZ NOT NULL, + requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + resolved_at TIMESTAMPTZ, + UNIQUE(requirement_id, professional_id) +); + +-- Add missing fields to customer_profiles +ALTER TABLE customer_profiles + ADD COLUMN IF NOT EXISTS full_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS phone VARCHAR(20), + ADD COLUMN IF NOT EXISTS city VARCHAR(100), + ADD COLUMN IF NOT EXISTS area VARCHAR(100), + ADD COLUMN IF NOT EXISTS preferred_professions TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS active_requirement_count INTEGER NOT NULL DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE'; + +CREATE INDEX IF NOT EXISTS idx_requirements_customer_id ON requirements(customer_id); +CREATE INDEX IF NOT EXISTS idx_requirements_status ON requirements(status); +CREATE INDEX IF NOT EXISTS idx_requirements_profession_key ON requirements(profession_key); +CREATE INDEX IF NOT EXISTS idx_lead_requests_requirement_id ON lead_requests(requirement_id); +CREATE INDEX IF NOT EXISTS idx_lead_requests_professional_id ON lead_requests(professional_id); +CREATE INDEX IF NOT EXISTS idx_lead_requests_status ON lead_requests(status); +CREATE INDEX IF NOT EXISTS idx_professionals_profession_key ON professionals(profession_key); +-- Portfolio items (for professionals) +CREATE TABLE IF NOT EXISTS portfolio_items ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + description TEXT, + tags TEXT[] DEFAULT '{}', + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Portfolio images (multiple images per portfolio item) +CREATE TABLE IF NOT EXISTS portfolio_images ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + portfolio_item_id UUID NOT NULL REFERENCES portfolio_items(id) ON DELETE CASCADE, + file_url VARCHAR(500) NOT NULL, + display_order INTEGER NOT NULL DEFAULT 0 +); + +-- Services (offered by professionals) +CREATE TABLE IF NOT EXISTS services ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + price INTEGER NOT NULL DEFAULT 0, -- in paise + duration_minutes INTEGER, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tracecoin wallets (one per user) +CREATE TABLE IF NOT EXISTS tracecoin_wallets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + balance INTEGER NOT NULL DEFAULT 0, + reserved INTEGER NOT NULL DEFAULT 0, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Tracecoin ledger (IMMUTABLE — never update or delete) +CREATE TABLE IF NOT EXISTS tracecoin_ledger ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + wallet_id UUID NOT NULL REFERENCES tracecoin_wallets(id), + type VARCHAR(20) NOT NULL, -- CREDIT, DEBIT, RESERVE, RELEASE + amount INTEGER NOT NULL, + reason VARCHAR(100) NOT NULL, -- LEAD_REQUEST, LEAD_ACCEPTED, PURCHASE, ADMIN_CREDIT, LEAD_EXPIRED + reference_id UUID, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Pricing packages (Tracecoin bundles, job slots, contact views) +CREATE TABLE IF NOT EXISTS pricing_packages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + role_key VARCHAR(50) NOT NULL, + package_type VARCHAR(50) NOT NULL, -- JOB_POSTING, CONTACT_VIEWS, TRACECOIN_BUNDLE + tracecoins_amount INTEGER NOT NULL DEFAULT 0, + price_inr INTEGER NOT NULL, -- in paise + description TEXT, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Payments (Razorpay transactions) +CREATE TABLE IF NOT EXISTS payments ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + package_id UUID NOT NULL REFERENCES pricing_packages(id), + razorpay_order_id VARCHAR(100), + razorpay_payment_id VARCHAR(100), + amount_inr INTEGER NOT NULL, + tracecoins_credited INTEGER NOT NULL DEFAULT 0, + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, SUCCESS, FAILED + verified_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Invoices (generated for every successful payment) +CREATE TABLE IF NOT EXISTS invoices ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + payment_id UUID NOT NULL REFERENCES payments(id), + user_id UUID NOT NULL REFERENCES users(id), + invoice_number VARCHAR(50) NOT NULL UNIQUE, + subtotal INTEGER NOT NULL, -- in paise + gst_amount INTEGER NOT NULL, -- in paise + total INTEGER NOT NULL, -- in paise + status VARCHAR(20) NOT NULL DEFAULT 'ISSUED', -- ISSUED, PAID + issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + file_url VARCHAR(500) +); + +CREATE INDEX IF NOT EXISTS idx_portfolio_items_professional_id ON portfolio_items(professional_id); +CREATE INDEX IF NOT EXISTS idx_services_professional_id ON services(professional_id); +CREATE INDEX IF NOT EXISTS idx_tracecoin_ledger_wallet_id ON tracecoin_ledger(wallet_id); +CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id); +CREATE INDEX IF NOT EXISTS idx_invoices_user_id ON invoices(user_id); +-- Notifications (in-app) +CREATE TABLE IF NOT EXISTS notifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + body TEXT, + type VARCHAR(50), -- APPROVAL, LEAD, JOB, PAYMENT + reference_id UUID, + is_read BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Email logs (audit trail) +CREATE TABLE IF NOT EXISTS email_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id), + trigger VARCHAR(100) NOT NULL, -- PROFILE_APPROVED, JOB_APPROVED, etc. + to_email VARCHAR(255) NOT NULL, + subject VARCHAR(500), + status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, SENT, FAILED + sent_at TIMESTAMPTZ +); + +CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id); +CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read); +CREATE INDEX IF NOT EXISTS idx_email_logs_user_id ON email_logs(user_id); +-- Drop the generic professionals table approach; use per-profession profile tables +-- Portfolio and services stay shared (referenced by user_id + profession_key) + +-- 1. PHOTOGRAPHER PROFILES +CREATE TABLE IF NOT EXISTS photographer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + specialties TEXT[] DEFAULT '{}', -- e.g. ['Wedding', 'Portrait', 'Commercial'] + camera_brands TEXT[] DEFAULT '{}', -- e.g. ['Sony', 'Canon'] + studio_available BOOLEAN NOT NULL DEFAULT false, + outdoor_shoots BOOLEAN NOT NULL DEFAULT true, + travel_radius_km INTEGER DEFAULT 50, + starting_price_inr INTEGER DEFAULT 0, -- in paise + -- Verification & status + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, SUSPENDED + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 2. TUTOR PROFILES +CREATE TABLE IF NOT EXISTS tutor_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + subjects TEXT[] DEFAULT '{}', -- e.g. ['Math', 'Physics', 'Hindi'] + board_types TEXT[] DEFAULT '{}', -- e.g. ['CBSE', 'ICSE', 'IB'] + qualification VARCHAR(255), -- e.g. 'B.Tech IIT Delhi' + teaches_online BOOLEAN NOT NULL DEFAULT true, + teaches_offline BOOLEAN NOT NULL DEFAULT true, + experience_years INTEGER DEFAULT 0, + hourly_rate_inr INTEGER DEFAULT 0, -- in paise + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 3. MAKEUP ARTIST PROFILES +CREATE TABLE IF NOT EXISTS makeup_artist_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + specializations TEXT[] DEFAULT '{}', -- e.g. ['Bridal', 'Editorial', 'SFX'] + kit_brands TEXT[] DEFAULT '{}', -- e.g. ['MAC', 'NARS', 'NYX'] + home_service BOOLEAN NOT NULL DEFAULT true, + studio_available BOOLEAN NOT NULL DEFAULT false, + starting_price_inr INTEGER DEFAULT 0, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 4. DEVELOPER PROFILES +CREATE TABLE IF NOT EXISTS developer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + tech_stack TEXT[] DEFAULT '{}', -- e.g. ['Rust', 'React', 'PostgreSQL'] + github_url VARCHAR(500), + portfolio_url VARCHAR(500), + experience_years INTEGER DEFAULT 0, + availability VARCHAR(50) DEFAULT 'FULL_TIME', -- FULL_TIME, PART_TIME, FREELANCE + hourly_rate_inr INTEGER DEFAULT 0, + remote_ok BOOLEAN NOT NULL DEFAULT true, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 5. VIDEO EDITOR PROFILES +CREATE TABLE IF NOT EXISTS video_editor_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + software_skills TEXT[] DEFAULT '{}', -- e.g. ['Premiere Pro', 'DaVinci Resolve'] + style_tags TEXT[] DEFAULT '{}', -- e.g. ['Cinematic', 'Corporate', 'Reels'] + turnaround_days INTEGER DEFAULT 7, + reel_url VARCHAR(500), + starting_price_inr INTEGER DEFAULT 0, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 6. GRAPHIC DESIGNER PROFILES +CREATE TABLE IF NOT EXISTS graphic_designer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + design_tools TEXT[] DEFAULT '{}', -- e.g. ['Figma', 'Illustrator', 'Photoshop'] + style_tags TEXT[] DEFAULT '{}', -- e.g. ['Minimalist', 'Bold', 'Corporate'] + brand_experience BOOLEAN NOT NULL DEFAULT false, + portfolio_url VARCHAR(500), + starting_price_inr INTEGER DEFAULT 0, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 7. SOCIAL MEDIA MANAGER PROFILES +CREATE TABLE IF NOT EXISTS social_media_manager_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + platforms TEXT[] DEFAULT '{}', -- e.g. ['Instagram', 'LinkedIn', 'YouTube'] + industries TEXT[] DEFAULT '{}', -- e.g. ['F&B', 'Fashion', 'Real Estate'] + content_types TEXT[] DEFAULT '{}', -- e.g. ['Reels', 'Carousels', 'Stories'] + avg_follower_growth_pct INTEGER DEFAULT 0, + starting_price_inr INTEGER DEFAULT 0, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 8. FITNESS TRAINER PROFILES +CREATE TABLE IF NOT EXISTS fitness_trainer_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + disciplines TEXT[] DEFAULT '{}', -- e.g. ['Yoga', 'HIIT', 'Zumba', 'CrossFit'] + certifications TEXT[] DEFAULT '{}', -- e.g. ['ACE', 'NASM', 'Yoga Alliance RYT'] + online_sessions BOOLEAN NOT NULL DEFAULT true, + home_visits BOOLEAN NOT NULL DEFAULT false, + gym_based BOOLEAN NOT NULL DEFAULT false, + per_session_rate_inr INTEGER DEFAULT 0, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 9. CATERING SERVICES PROFILES +CREATE TABLE IF NOT EXISTS catering_service_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + business_name VARCHAR(255) NOT NULL, + bio TEXT, + location VARCHAR(255), + -- Profession-specific + cuisine_types TEXT[] DEFAULT '{}', -- e.g. ['North Indian', 'Continental', 'Vegan'] + event_types TEXT[] DEFAULT '{}', -- e.g. ['Wedding', 'Corporate', 'Birthday'] + min_guests INTEGER DEFAULT 10, + max_guests INTEGER DEFAULT 500, + has_setup_team BOOLEAN NOT NULL DEFAULT true, + has_serving_staff BOOLEAN NOT NULL DEFAULT true, + price_per_head_inr INTEGER DEFAULT 0, -- in paise + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Shared: portfolio_items now uses user_id + profession_key (no foreign key to professionals) +-- Drop the professionals-table FK if it was added before +ALTER TABLE portfolio_items + ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE, + ADD COLUMN IF NOT EXISTS profession_key VARCHAR(50); + +ALTER TABLE services + ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE, + ADD COLUMN IF NOT EXISTS profession_key VARCHAR(50); + +-- Lead requests: use user_id instead of professional_id foreign key +ALTER TABLE lead_requests + ADD COLUMN IF NOT EXISTS professional_user_id UUID REFERENCES users(id) ON DELETE CASCADE; + +-- Backfill columns when legacy minimal profile tables already exist. +-- This keeps migrations idempotent while upgrading old schemas to the new profile shape. +ALTER TABLE photographer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS bio TEXT, + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS specialties TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS camera_brands TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS outdoor_shoots BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS travel_radius_km INTEGER DEFAULT 50, + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE tutor_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS bio TEXT, + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS subjects TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS board_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS qualification VARCHAR(255), + ADD COLUMN IF NOT EXISTS teaches_online BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS teaches_offline BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE makeup_artist_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS specializations TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS kit_brands TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS home_service BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE developer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS tech_stack TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS github_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS availability VARCHAR(50) DEFAULT 'FULL_TIME', + ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS remote_ok BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE video_editor_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS software_skills TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS turnaround_days INTEGER DEFAULT 7, + ADD COLUMN IF NOT EXISTS reel_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE graphic_designer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS design_tools TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS brand_experience BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE social_media_manager_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS platforms TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS industries TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS content_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS avg_follower_growth_pct INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE fitness_trainer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS disciplines TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS certifications TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS online_sessions BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS home_visits BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS gym_based BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS per_session_rate_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE catering_service_profiles + ADD COLUMN IF NOT EXISTS business_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS cuisine_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS event_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS min_guests INTEGER DEFAULT 10, + ADD COLUMN IF NOT EXISTS max_guests INTEGER DEFAULT 500, + ADD COLUMN IF NOT EXISTS has_setup_team BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS has_serving_staff BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS price_per_head_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE lead_requests + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); +-- Indexes +CREATE INDEX IF NOT EXISTS idx_photographer_profiles_status ON photographer_profiles(status); +CREATE INDEX IF NOT EXISTS idx_tutor_profiles_status ON tutor_profiles(status); +CREATE INDEX IF NOT EXISTS idx_makeup_artist_profiles_status ON makeup_artist_profiles(status); +CREATE INDEX IF NOT EXISTS idx_developer_profiles_status ON developer_profiles(status); +CREATE INDEX IF NOT EXISTS idx_video_editor_profiles_status ON video_editor_profiles(status); +CREATE INDEX IF NOT EXISTS idx_graphic_designer_profiles_status ON graphic_designer_profiles(status); +CREATE INDEX IF NOT EXISTS idx_social_media_manager_profiles_status ON social_media_manager_profiles(status); +CREATE INDEX IF NOT EXISTS idx_fitness_trainer_profiles_status ON fitness_trainer_profiles(status); +CREATE INDEX IF NOT EXISTS idx_catering_service_profiles_status ON catering_service_profiles(status); +CREATE INDEX IF NOT EXISTS idx_portfolio_items_user_id ON portfolio_items(user_id); +CREATE INDEX IF NOT EXISTS idx_services_user_id ON services(user_id); +-- Add email verification and password reset columns to users table +ALTER TABLE users + ADD COLUMN IF NOT EXISTS email_verification_token VARCHAR(255), + ADD COLUMN IF NOT EXISTS email_verification_expires_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS reset_password_token VARCHAR(255), + ADD COLUMN IF NOT EXISTS reset_password_expires_at TIMESTAMPTZ; + +-- Add index for token lookups +CREATE INDEX IF NOT EXISTS idx_users_email_verification_token ON users(email_verification_token); +CREATE INDEX IF NOT EXISTS idx_users_reset_password_token ON users(reset_password_token); +-- Reviews: customers leave reviews on professionals after an accepted lead +CREATE TABLE IF NOT EXISTS reviews ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + lead_request_id UUID NOT NULL REFERENCES lead_requests(id) ON DELETE CASCADE UNIQUE, + customer_id UUID NOT NULL REFERENCES customer_profiles(id) ON DELETE CASCADE, + professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, + rating SMALLINT NOT NULL CHECK (rating >= 1 AND rating <= 5), + comment TEXT, + is_published BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_reviews_professional_id ON reviews(professional_id); +CREATE INDEX IF NOT EXISTS idx_reviews_customer_id ON reviews(customer_id); +-- Knowledge Base categories +CREATE TABLE IF NOT EXISTS kb_categories ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + slug VARCHAR(255) NOT NULL UNIQUE, + description TEXT, + display_order INTEGER NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Knowledge Base articles +CREATE TABLE IF NOT EXISTS kb_articles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + category_id UUID NOT NULL REFERENCES kb_categories(id) ON DELETE CASCADE, + title VARCHAR(500) NOT NULL, + slug VARCHAR(500) NOT NULL UNIQUE, + body TEXT NOT NULL, + target_roles TEXT[] DEFAULT '{}', -- empty = visible to all + is_published BOOLEAN NOT NULL DEFAULT false, + views INTEGER NOT NULL DEFAULT 0, + created_by UUID REFERENCES users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_kb_articles_category_id ON kb_articles(category_id); +CREATE INDEX IF NOT EXISTS idx_kb_articles_slug ON kb_articles(slug); +-- Support tickets +CREATE TABLE IF NOT EXISTS support_tickets ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + subject VARCHAR(500) NOT NULL, + category VARCHAR(50) NOT NULL DEFAULT 'GENERAL', -- GENERAL, BILLING, ACCOUNT, LEAD, JOB + status VARCHAR(20) NOT NULL DEFAULT 'OPEN', -- OPEN, IN_PROGRESS, RESOLVED, CLOSED + priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL', -- LOW, NORMAL, HIGH, URGENT + assigned_to UUID REFERENCES users(id), + resolved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Support ticket messages +CREATE TABLE IF NOT EXISTS support_ticket_messages ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ticket_id UUID NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE, + sender_id UUID NOT NULL REFERENCES users(id), + body TEXT NOT NULL, + is_internal BOOLEAN NOT NULL DEFAULT false, -- true = staff-only note + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_support_tickets_user_id ON support_tickets(user_id); +CREATE INDEX IF NOT EXISTS idx_support_tickets_status ON support_tickets(status); +CREATE INDEX IF NOT EXISTS idx_support_ticket_messages_ticket_id ON support_ticket_messages(ticket_id); +-- Discount coupons for Tracecoin and package purchases +CREATE TABLE IF NOT EXISTS coupons ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + code VARCHAR(50) NOT NULL UNIQUE, + description TEXT, + discount_type VARCHAR(20) NOT NULL, -- PERCENT, FLAT + discount_value INTEGER NOT NULL, -- percent (0-100) or paise + applies_to VARCHAR(50) NOT NULL DEFAULT 'ALL', -- ALL, TRACECOIN_BUNDLE, JOB_POSTING, CONTACT_VIEWS + min_order_amount INTEGER NOT NULL DEFAULT 0, -- paise + max_uses INTEGER, -- NULL = unlimited + uses_count INTEGER NOT NULL DEFAULT 0, + per_user_limit INTEGER NOT NULL DEFAULT 1, + valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(), + valid_until TIMESTAMPTZ, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Track which users used which coupons +CREATE TABLE IF NOT EXISTS coupon_uses ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + coupon_id UUID NOT NULL REFERENCES coupons(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + payment_id UUID REFERENCES payments(id), + used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (coupon_id, user_id) +); + +CREATE INDEX IF NOT EXISTS idx_coupons_code ON coupons(code); +CREATE INDEX IF NOT EXISTS idx_coupon_uses_user_id ON coupon_uses(user_id); +-- Onboarding state per user per role +-- Tracks progress through the schema-driven onboarding form +CREATE TABLE IF NOT EXISTS onboarding_states ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, + status VARCHAR(20) NOT NULL DEFAULT 'NOT_STARTED', -- NOT_STARTED | IN_PROGRESS | COMPLETED + progress_json JSONB NOT NULL DEFAULT '{}', + completed_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- One onboarding state record per user per role +CREATE UNIQUE INDEX IF NOT EXISTS idx_onboarding_state_user_role + ON onboarding_states(user_id, role_id); +-- Make display_name / business_name nullable so upserts can work +-- without forcing the name on every call. +-- Add custom_data JSONB to every profession table so all onboarding +-- form fields are preserved even if they don't have a dedicated column. + +ALTER TABLE photographer_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE tutor_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE makeup_artist_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE developer_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE video_editor_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE graphic_designer_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE social_media_manager_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE fitness_trainer_profiles ALTER COLUMN display_name DROP NOT NULL; +ALTER TABLE catering_service_profiles ALTER COLUMN business_name DROP NOT NULL; + +ALTER TABLE photographer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE tutor_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE makeup_artist_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE developer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE video_editor_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE graphic_designer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE social_media_manager_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE fitness_trainer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +ALTER TABLE catering_service_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; +-- Enforce immutable tracecoin ledger: no UPDATE/DELETE allowed. + +CREATE OR REPLACE FUNCTION prevent_tracecoin_ledger_mutation() +RETURNS trigger AS $$ +BEGIN + RAISE EXCEPTION 'tracecoin_ledger is immutable; % is not allowed', TG_OP; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trg_prevent_tracecoin_ledger_update ON tracecoin_ledger; +CREATE TRIGGER trg_prevent_tracecoin_ledger_update +BEFORE UPDATE ON tracecoin_ledger +FOR EACH ROW +EXECUTE FUNCTION prevent_tracecoin_ledger_mutation(); + +DROP TRIGGER IF EXISTS trg_prevent_tracecoin_ledger_delete ON tracecoin_ledger; +CREATE TRIGGER trg_prevent_tracecoin_ledger_delete +BEFORE DELETE ON tracecoin_ledger +FOR EACH ROW +EXECUTE FUNCTION prevent_tracecoin_ledger_mutation(); +UPDATE company_profiles +SET status = 'APPROVED' +WHERE status = 'ACTIVE'; + +UPDATE customer_profiles +SET status = 'APPROVED' +WHERE status = 'ACTIVE'; +-- Extend roles table for internal role management +ALTER TABLE roles + ADD COLUMN IF NOT EXISTS description TEXT, + ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id) ON DELETE SET NULL, + ADD COLUMN IF NOT EXISTS can_approve_requests BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS can_manage_system_settings BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE departments + ADD COLUMN IF NOT EXISTS code VARCHAR(64), + ADD COLUMN IF NOT EXISTS description TEXT, + ADD COLUMN IF NOT EXISTS department_head VARCHAR(255), + ADD COLUMN IF NOT EXISTS department_email VARCHAR(255), + ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) NOT NULL DEFAULT 'INTERNAL', + ADD COLUMN IF NOT EXISTS transfers_enabled BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); + +UPDATE departments +SET updated_at = COALESCE(updated_at, created_at, NOW()); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_departments_code_unique + ON departments (LOWER(code)) + WHERE code IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_departments_is_active + ON departments (is_active); +ALTER TABLE designations + ADD COLUMN IF NOT EXISTS code VARCHAR(64), + ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id) ON DELETE SET NULL, + ADD COLUMN IF NOT EXISTS description TEXT, + ADD COLUMN IF NOT EXISTS level VARCHAR(100), + ADD COLUMN IF NOT EXISTS can_manage_team BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS can_approve BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); + +UPDATE designations +SET updated_at = COALESCE(updated_at, created_at, NOW()); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_designations_code_unique + ON designations (LOWER(code)) + WHERE code IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_designations_is_active + ON designations (is_active); + +CREATE INDEX IF NOT EXISTS idx_designations_department_id + ON designations (department_id); +-- UP: 20260402030000_strict_employee_separation.up.sql + +-- Drop old employees table (was linked to users — replacing with standalone auth) +DROP TABLE IF EXISTS employees CASCADE; + +-- 1. EMPLOYEES (Standalone Table - Not Linked to 'users') +CREATE TABLE employees ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + first_name VARCHAR(100) NOT NULL, + last_name VARCHAR(100) NOT NULL, + email VARCHAR(255) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + employee_code VARCHAR(50) UNIQUE, + department_id UUID REFERENCES departments(id) ON DELETE SET NULL, + designation_id UUID REFERENCES designations(id) ON DELETE SET NULL, + role_code VARCHAR(50) NOT NULL DEFAULT 'STAFF', + status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', + joined_at DATE NOT NULL DEFAULT CURRENT_DATE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 2. EMPLOYEE SESSIONS (Standalone Auth) +CREATE TABLE IF NOT EXISTS employee_sessions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + employee_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE, + token_hash VARCHAR(255) UNIQUE NOT NULL, + expires_at TIMESTAMPTZ NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_employees_email ON employees(email); +CREATE INDEX IF NOT EXISTS idx_employees_status ON employees(status); +CREATE INDEX IF NOT EXISTS idx_employee_sessions_token ON employee_sessions(token_hash); +-- Up migration: Create activity_logs table +CREATE TABLE IF NOT EXISTS activity_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + actor_id UUID NOT NULL, -- User or Employee who performed the action + actor_type VARCHAR(20) NOT NULL, -- 'USER' or 'EMPLOYEE' + entity_id UUID NOT NULL, -- Target of the action (User ID, Job ID, etc.) + entity_type VARCHAR(50) NOT NULL, -- 'USER', 'JOB', 'REQUIREMENT', 'EMPLOYEE', etc. + action VARCHAR(100) NOT NULL, -- 'APPROVE', 'REJECT', 'STATUS_CHANGE', 'DELETE', etc. + metadata JSONB, -- Optional extra context: { "old_status": "PENDING", "new_status": "APPROVED", "reason": "..." } + ip_address VARCHAR(45), + user_agent TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_activity_logs_entity ON activity_logs (entity_type, entity_id); +CREATE INDEX IF NOT EXISTS idx_activity_logs_actor ON activity_logs (actor_type, actor_id); +CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs (created_at DESC); +ALTER TABLE kb_articles + ADD COLUMN IF NOT EXISTS summary TEXT, + ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}'; +-- Allow admin-created tickets with no linked user +ALTER TABLE support_tickets + ALTER COLUMN user_id DROP NOT NULL; + +-- Add description body and requester info for admin-created cases +ALTER TABLE support_tickets + ADD COLUMN IF NOT EXISTS description TEXT, + ADD COLUMN IF NOT EXISTS requester_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS requester_email VARCHAR(255); +-- Extend reviews table to support admin-created reviews and admin moderation +ALTER TABLE reviews + ALTER COLUMN lead_request_id DROP NOT NULL, + ALTER COLUMN customer_id DROP NOT NULL, + ALTER COLUMN professional_id DROP NOT NULL, + ADD COLUMN IF NOT EXISTS title VARCHAR(255), + ADD COLUMN IF NOT EXISTS subject_type VARCHAR(50) NOT NULL DEFAULT 'PLATFORM', + ADD COLUMN IF NOT EXISTS subject_id VARCHAR(255), + ADD COLUMN IF NOT EXISTS reviewer_name VARCHAR(255), + ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED'; + +-- Sync status with is_published for existing rows +UPDATE reviews SET status = CASE WHEN is_published THEN 'PUBLISHED' ELSE 'HIDDEN' END; +-- Add title and role_keys to coupons for admin UI +ALTER TABLE coupons + ADD COLUMN IF NOT EXISTS title VARCHAR(255), + ADD COLUMN IF NOT EXISTS role_keys TEXT[] NOT NULL DEFAULT '{}'; + +-- Backfill title from description +UPDATE coupons SET title = description WHERE title IS NULL AND description IS NOT NULL; +-- Admin-managed automatic discounts (applied before coupon codes) +CREATE TABLE IF NOT EXISTS discounts ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + title VARCHAR(255) NOT NULL, + scope VARCHAR(20) NOT NULL DEFAULT 'ROLE', -- ROLE, PACKAGE + role_key VARCHAR(50), + package_id UUID REFERENCES pricing_packages(id) ON DELETE SET NULL, + discount_type VARCHAR(20) NOT NULL, -- PERCENT, FIXED + discount_value INTEGER NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +-- 10. UGC CONTENT CREATOR PROFILES +CREATE TABLE IF NOT EXISTS ugc_content_creator_profiles ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, + display_name VARCHAR(255) NOT NULL DEFAULT '', + bio TEXT, + location VARCHAR(255), + -- Profession-specific + platforms TEXT[] DEFAULT '{}', -- e.g. ['Instagram', 'YouTube', 'TikTok'] + content_niches TEXT[] DEFAULT '{}', -- e.g. ['Beauty', 'Tech', 'Food', 'Lifestyle'] + content_formats TEXT[] DEFAULT '{}', -- e.g. ['Reels', 'Unboxing', 'Reviews', 'GRWM'] + follower_count INTEGER DEFAULT 0, + avg_views_per_post INTEGER DEFAULT 0, + has_media_kit BOOLEAN NOT NULL DEFAULT false, + instagram_handle VARCHAR(100), + youtube_channel_url VARCHAR(500), + portfolio_url VARCHAR(500), + starting_price_inr INTEGER DEFAULT 0, -- in paise + custom_data JSONB, + -- Verification & status + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, SUSPENDED + rejection_reason TEXT, + approved_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_status ON ugc_content_creator_profiles(status); +CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_user_id ON ugc_content_creator_profiles(user_id); +-- 1. VERIFICATIONS TABLE +CREATE TABLE IF NOT EXISTS verifications ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + role_key VARCHAR(50) NOT NULL, + status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, UNDER_REVIEW, DOCUMENTS_REQUESTED, REVISION_REQUESTED, APPROVED, REJECTED + priority VARCHAR(10) NOT NULL DEFAULT 'LOW', -- HIGH, MEDIUM, LOW + case_type VARCHAR(50) NOT NULL, -- PROFILE, PORTFOLIO, JOB, REQUIREMENT + payload JSONB NOT NULL DEFAULT '{}', -- full submission data + documents JSONB NOT NULL DEFAULT '[]', -- list of documents [{id, title, url, status}] + notes TEXT, + rejection_reason TEXT, + assigned_to UUID REFERENCES users(id) ON DELETE SET NULL, -- Admin/Employee ID + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 2. VERIFICATION LOGS (History of actions) +CREATE TABLE IF NOT EXISTS verification_logs ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + verification_id UUID NOT NULL REFERENCES verifications(id) ON DELETE CASCADE, + action VARCHAR(50) NOT NULL, -- STATUS_CHANGE, NOTE_ADDED, DOCS_REQUESTED, REASSIGNED + actor_id UUID REFERENCES users(id) ON DELETE SET NULL, + old_status VARCHAR(50), + new_status VARCHAR(50), + message TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +-- 3. INDEXES +CREATE INDEX IF NOT EXISTS idx_verifications_user_id ON verifications(user_id); +CREATE INDEX IF NOT EXISTS idx_verifications_status ON verifications(status); +CREATE INDEX IF NOT EXISTS idx_verifications_case_type ON verifications(case_type); +CREATE INDEX IF NOT EXISTS idx_verification_logs_ver_id ON verification_logs(verification_id); diff --git a/start-services.sh b/start-services.sh index c073011..1b47d96 100755 --- a/start-services.sh +++ b/start-services.sh @@ -5,6 +5,27 @@ set -a source .env set +a +# ── Initialize PostgreSQL database if needed ──────────────────────────────────── +echo "Initializing database..." + +# Use DATABASE_URL from .env to run init script +export PGPASSWORD=${POSTGRES_PASSWORD:-nxtgauge_dev} + +# Check if database is accessible and if the 'roles' table exists as a heuristic +if psql "${DATABASE_URL:-postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@localhost:5432/nxtgauge_db}" -c '\q' 2>/dev/null; then + # Try to see if the schema is already initialized (check for 'roles' table) + if ! psql "${DATABASE_URL:-postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@localhost:5432/nxtgauge_db}" -t -c "SELECT to_regname('roles');" 2>/dev/null | grep -q '^roles$'; then + echo "Applying database schema..." + psql "${DATABASE_URL:-postgresql://nxtgauge:${POSTGRES_PASSWORD:-nxtgauge_dev}@localhost:5432/nxtgauge_db}" -f scripts/init-db.sql + else + echo "Database schema already initialized." + fi +else + echo "ERROR: Cannot connect to PostgreSQL. Make sure PostgreSQL is running on localhost:5432." + echo "Start PostgreSQL and try again." + exit 1 +fi + echo "Building workspace..." cargo build --workspace diff --git a/users.pid b/users.pid new file mode 100644 index 0000000..cdd146d --- /dev/null +++ b/users.pid @@ -0,0 +1 @@ +96200