Compare commits
24 commits
main
...
high-perfo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
189f4bca60 | ||
|
|
1956acdcf3 | ||
|
|
ec41f6dad2 | ||
|
|
f67f6c2514 | ||
|
|
051a980f14 | ||
|
|
0d35bf5649 | ||
|
|
123a157e04 | ||
|
|
d0b10eac8f | ||
|
|
418da25d37 | ||
|
|
b2c93f4e33 | ||
|
|
0bda2b2f10 | ||
|
|
8adc84699e | ||
|
|
758f0699ff | ||
|
|
d0b768d602 | ||
|
|
30346b02d1 | ||
|
|
30df37b127 | ||
|
|
e428fe268c | ||
|
|
d79aa50c77 | ||
|
|
fc772c2acb | ||
|
|
1b1d98ebee | ||
|
|
c7fe1b7ad3 | ||
|
|
319b384f0a | ||
|
|
2c6d102205 | ||
|
|
52e30a1b4b |
41 changed files with 1627 additions and 680 deletions
|
|
@ -11,7 +11,7 @@ Usage:
|
|||
This script:
|
||||
1. Updates the newTag for the specified service to the SHA
|
||||
2. Commits and pushes to the gitops repo
|
||||
3. ArgoCD detects the change and deploys
|
||||
3. Flux detects the change and deploys
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
|
@ -98,10 +98,14 @@ def main():
|
|||
image_name = f"nxtgauge-{args.service}"
|
||||
|
||||
# Find the right kustomization file based on service
|
||||
if "frontend" in args.service or "admin" in args.service:
|
||||
if "frontend" in args.service:
|
||||
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/overlays/prod/kustomization.yaml")
|
||||
if not os.path.exists(kustomization_path):
|
||||
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-frontend-solid/base/kustomization.yaml")
|
||||
elif "admin" in args.service:
|
||||
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-admin-solid/overlays/prod/kustomization.yaml")
|
||||
if not os.path.exists(kustomization_path):
|
||||
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-admin-solid/base/kustomization.yaml")
|
||||
elif "ai-assistant" in args.service:
|
||||
kustomization_path = os.path.join(args.repo, "apps/nxtgauge-ai-assistant/overlays/prod/kustomization.yaml")
|
||||
if not os.path.exists(kustomization_path):
|
||||
38
.forgejo/workflows/build-all.yaml
Normal file
38
.forgejo/workflows/build-all.yaml
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
name: build-all
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
build-gateway:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- run: |
|
||||
git clone --depth 1 http://forgejo-http.forgejo.svc.cluster.local:3000/ashwin/nxtgauge-backend-rust.git /tmp/repo
|
||||
cd /tmp/repo
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=gateway -t registry.nxtgauge.com/nxtgauge-rust-gateway:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-gateway:latest
|
||||
rm -rf /tmp/repo
|
||||
|
||||
build-users:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- run: |
|
||||
git clone --depth 1 http://forgejo-http.forgejo.svc.cluster.local:3000/ashwin/nxtgauge-backend-rust.git /tmp/repo
|
||||
cd /tmp/repo
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=users -t registry.nxtgauge.com/nxtgauge-rust-users:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-users:latest
|
||||
rm -rf /tmp/repo
|
||||
|
||||
build-cron:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- run: |
|
||||
git clone --depth 1 http://forgejo-http.forgejo.svc.cluster.local:3000/ashwin/nxtgauge-backend-rust.git /tmp/repo
|
||||
cd /tmp/repo
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=cron -t registry.nxtgauge.com/nxtgauge-rust-cron:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-cron:latest
|
||||
rm -rf /tmp/repo
|
||||
18
.forgejo/workflows/build-gateway.yaml
Normal file
18
.forgejo/workflows/build-gateway.yaml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
name: build-gateway
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Build
|
||||
run: |
|
||||
git clone --depth 1 http://forgejo-http.forgejo.svc.cluster.local:3000/ashwin/nxtgauge-backend-rust.git /tmp/repo
|
||||
cd /tmp/repo
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=gateway -t registry.nxtgauge.com/nxtgauge-rust-gateway:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-gateway:latest
|
||||
rm -rf /tmp/repo
|
||||
53
.forgejo/workflows/build-services.yaml
Normal file
53
.forgejo/workflows/build-services.yaml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: build-services
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
build-gateway:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build gateway
|
||||
run: |
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=gateway -t registry.nxtgauge.com/nxtgauge-rust-gateway:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-gateway:latest
|
||||
|
||||
build-users:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build users
|
||||
run: |
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=users -t registry.nxtgauge.com/nxtgauge-rust-users:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-users:latest
|
||||
|
||||
build-jobs:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build jobs
|
||||
run: |
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=jobs -t registry.nxtgauge.com/nxtgauge-rust-jobs:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-jobs:latest
|
||||
|
||||
build-leads:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build leads
|
||||
run: |
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=leads -t registry.nxtgauge.com/nxtgauge-rust-leads:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-leads:latest
|
||||
|
||||
build-cron:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build cron
|
||||
run: |
|
||||
docker build -f Dockerfile.simple --build-arg SERVICE_NAME=cron -t registry.nxtgauge.com/nxtgauge-rust-cron:latest .
|
||||
docker push registry.nxtgauge.com/nxtgauge-rust-cron:latest
|
||||
44
.forgejo/workflows/build-simple.yaml
Normal file
44
.forgejo/workflows/build-simple.yaml
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
name: build-all-services
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build and push all services
|
||||
run: |
|
||||
set -e
|
||||
REGISTRY="registry.nxtgauge.com"
|
||||
SHA="${{ github.sha }}"
|
||||
|
||||
SERVICES='gateway users companies jobs leads job-seekers customers payments employees photographers makeup-artists tutors developers video-editors graphic-designers social-media-managers fitness-trainers catering-services ugc-content-creators cron'
|
||||
|
||||
echo "Logging into registry..."
|
||||
echo "Ashwin@2026" | docker login $REGISTRY -u admin --password-stdin
|
||||
|
||||
for service in $SERVICES; do
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "Building $service..."
|
||||
echo "=================================="
|
||||
|
||||
docker build -f Dockerfile.simple \
|
||||
--build-arg SERVICE_NAME=$service \
|
||||
-t "$REGISTRY/nxtgauge-rust-$service:$SHA" \
|
||||
-t "$REGISTRY/nxtgauge-rust-$service:latest" \
|
||||
. || echo "Failed to build $service"
|
||||
|
||||
docker push "$REGISTRY/nxtgauge-rust-$service:$SHA" || echo "Failed to push $service:$SHA"
|
||||
docker push "$REGISTRY/nxtgauge-rust-$service:latest" || echo "Failed to push $service:latest"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "All services built and pushed!"
|
||||
168
.forgejo/workflows/build.yaml
Normal file
168
.forgejo/workflows/build.yaml
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
name: build-and-push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
services_csv: ${{ steps.detect.outputs.services_csv }}
|
||||
has_changes: ${{ steps.detect.outputs.has_changes }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed services
|
||||
id: detect
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if git rev-parse --verify HEAD^ >/dev/null 2>&1; then
|
||||
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
|
||||
else
|
||||
CHANGED_FILES=$(git ls-files)
|
||||
fi
|
||||
|
||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | tr '\n' ' ')
|
||||
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
ALL_SERVICES='gateway,users,companies,jobs,leads,job-seekers,customers,payments,employees,photographers,makeup-artists,tutors,developers,video-editors,graphic-designers,social-media-managers,fitness-trainers,catering-services,ugc-content-creators,cron'
|
||||
|
||||
# Force full build for explicit trigger commits
|
||||
if echo "$LAST_COMMIT_MSG" | grep -Eiq 'trigger build|force build|rebuild all'; then
|
||||
echo "services_csv=$ALL_SERVICES" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build everything for workflow/docker/shared backend changes
|
||||
if echo "$CHANGED_FILES" | grep -Eq '^(\.forgejo/workflows/|Dockerfile|Cargo\.toml|Cargo\.lock|crates/)'; then
|
||||
echo "services_csv=$ALL_SERVICES" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SERVICES=''
|
||||
add_service() {
|
||||
local svc="$1"
|
||||
case ",${SERVICES}," in
|
||||
*",${svc},"*) ;;
|
||||
*)
|
||||
if [ -z "$SERVICES" ]; then
|
||||
SERVICES="$svc"
|
||||
else
|
||||
SERVICES="$SERVICES,$svc"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
while IFS= read -r f; do
|
||||
case "$f" in
|
||||
apps/gateway/*) add_service "gateway" ;;
|
||||
apps/users/*) add_service "users" ;;
|
||||
apps/companies/*) add_service "companies" ;;
|
||||
apps/jobs/*) add_service "jobs" ;;
|
||||
apps/leads/*) add_service "leads" ;;
|
||||
apps/job_seekers/*) add_service "job-seekers" ;;
|
||||
apps/customers/*) add_service "customers" ;;
|
||||
apps/payments/*) add_service "payments" ;;
|
||||
apps/employees/*) add_service "employees" ;;
|
||||
apps/photographers/*) add_service "photographers" ;;
|
||||
apps/makeup_artists/*) add_service "makeup-artists" ;;
|
||||
apps/tutors/*) add_service "tutors" ;;
|
||||
apps/developers/*) add_service "developers" ;;
|
||||
apps/video_editors/*) add_service "video-editors" ;;
|
||||
apps/graphic_designers/*) add_service "graphic-designers" ;;
|
||||
apps/social_media_managers/*) add_service "social-media-managers" ;;
|
||||
apps/fitness_trainers/*) add_service "fitness-trainers" ;;
|
||||
apps/catering_services/*) add_service "catering-services" ;;
|
||||
apps/ugc_content_creators/*) add_service "ugc-content-creators" ;;
|
||||
apps/cron/*) add_service "cron" ;;
|
||||
esac
|
||||
done <<< "$CHANGED_FILES"
|
||||
|
||||
if [ -z "$SERVICES" ]; then
|
||||
echo "services_csv=" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "services_csv=$SERVICES" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.has_changes == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service:
|
||||
- gateway
|
||||
- users
|
||||
- companies
|
||||
- jobs
|
||||
- leads
|
||||
- job-seekers
|
||||
- customers
|
||||
- payments
|
||||
- employees
|
||||
- photographers
|
||||
- makeup-artists
|
||||
- tutors
|
||||
- developers
|
||||
- video-editors
|
||||
- graphic-designers
|
||||
- social-media-managers
|
||||
- fitness-trainers
|
||||
- catering-services
|
||||
- ugc-content-creators
|
||||
- cron
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
run: |
|
||||
export DOCKER_HOST=unix:///var/run/docker.sock
|
||||
docker version
|
||||
docker buildx create --use || true
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
- name: Login to Registry
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export DOCKER_HOST=unix:///var/run/docker.sock
|
||||
echo "Logging into registry.nxtgauge.com..."
|
||||
echo "Ashwin@2026" | docker login registry.nxtgauge.com -u admin --password-stdin
|
||||
|
||||
- name: Build and push
|
||||
env:
|
||||
SERVICES_CSV: ${{ needs.detect-changes.outputs.services_csv }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export DOCKER_HOST=unix:///var/run/docker.sock
|
||||
SHA="${{ github.sha }}"
|
||||
|
||||
if [ -n "$SERVICES_CSV" ] && ! echo ",$SERVICES_CSV," | grep -q ",${{ matrix.service }},"; then
|
||||
echo "Skipping unchanged service: ${{ matrix.service }}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Building ${{ matrix.service }}..."
|
||||
docker buildx build --push \
|
||||
-f Dockerfile.simple \
|
||||
--build-arg SERVICE_NAME=${{ matrix.service }} \
|
||||
-t "registry.nxtgauge.com/nxtgauge-rust-${{ matrix.service }}:${SHA}" \
|
||||
-t "registry.nxtgauge.com/nxtgauge-rust-${{ matrix.service }}:latest" \
|
||||
.
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
name: build-and-push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
services_csv: ${{ steps.detect.outputs.services_csv }}
|
||||
has_changes: ${{ steps.detect.outputs.has_changes }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed services
|
||||
id: detect
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
set_output() {
|
||||
local key="$1"
|
||||
local value="$2"
|
||||
if [ -n "${GITHUB_OUTPUT:-}" ]; then
|
||||
echo "$key=$value" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "::set-output name=$key::$value"
|
||||
}
|
||||
|
||||
if git rev-parse --verify HEAD^ >/dev/null 2>&1; then
|
||||
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
|
||||
else
|
||||
CHANGED_FILES=$(git ls-files)
|
||||
fi
|
||||
|
||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | tr '\n' ' ')
|
||||
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
ALL_SERVICES='gateway,users,companies,jobs,leads,job-seekers,customers,payments,employees,photographers,makeup-artists,tutors,developers,video-editors,graphic-designers,social-media-managers,fitness-trainers,catering-services,ugc-content-creators,cron'
|
||||
|
||||
# Force full build for explicit trigger commits.
|
||||
if echo "$LAST_COMMIT_MSG" | grep -Eiq 'trigger gitea pipeline|force build|rebuild all'; then
|
||||
set_output "services_csv" "$ALL_SERVICES"
|
||||
set_output "has_changes" "true"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build everything for workflow/docker/shared backend changes.
|
||||
if echo "$CHANGED_FILES" | grep -Eq '^(\.gitea/workflows/|Dockerfile|Dockerfile\.|Cargo\.toml|Cargo\.lock|crates/|scripts/)'; then
|
||||
set_output "services_csv" "$ALL_SERVICES"
|
||||
set_output "has_changes" "true"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SERVICES=''
|
||||
add_service() {
|
||||
local svc="$1"
|
||||
case ",${SERVICES}," in
|
||||
*",${svc},"*) ;;
|
||||
*)
|
||||
if [ -z "$SERVICES" ]; then
|
||||
SERVICES="$svc"
|
||||
else
|
||||
SERVICES="$SERVICES,$svc"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
while IFS= read -r f; do
|
||||
case "$f" in
|
||||
apps/gateway/*) add_service "gateway" ;;
|
||||
apps/users/*) add_service "users" ;;
|
||||
apps/companies/*) add_service "companies" ;;
|
||||
apps/jobs/*) add_service "jobs" ;;
|
||||
apps/leads/*) add_service "leads" ;;
|
||||
apps/job_seekers/*) add_service "job-seekers" ;;
|
||||
apps/customers/*) add_service "customers" ;;
|
||||
apps/payments/*) add_service "payments" ;;
|
||||
apps/employees/*) add_service "employees" ;;
|
||||
apps/photographers/*) add_service "photographers" ;;
|
||||
apps/makeup_artists/*) add_service "makeup-artists" ;;
|
||||
apps/tutors/*) add_service "tutors" ;;
|
||||
apps/developers/*) add_service "developers" ;;
|
||||
apps/video_editors/*) add_service "video-editors" ;;
|
||||
apps/graphic_designers/*) add_service "graphic-designers" ;;
|
||||
apps/social_media_managers/*) add_service "social-media-managers" ;;
|
||||
apps/fitness_trainers/*) add_service "fitness-trainers" ;;
|
||||
apps/catering_services/*) add_service "catering-services" ;;
|
||||
apps/ugc_content_creators/*) add_service "ugc-content-creators" ;;
|
||||
apps/cron/*) add_service "cron" ;;
|
||||
esac
|
||||
done <<< "$CHANGED_FILES"
|
||||
|
||||
if [ -z "$SERVICES" ]; then
|
||||
set_output "services_csv" ""
|
||||
set_output "has_changes" "false"
|
||||
else
|
||||
set_output "services_csv" "$SERVICES"
|
||||
set_output "has_changes" "true"
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.has_changes == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service:
|
||||
- gateway
|
||||
- users
|
||||
- companies
|
||||
- jobs
|
||||
- leads
|
||||
- job-seekers
|
||||
- customers
|
||||
- payments
|
||||
- employees
|
||||
- photographers
|
||||
- makeup-artists
|
||||
- tutors
|
||||
- developers
|
||||
- video-editors
|
||||
- graphic-designers
|
||||
- social-media-managers
|
||||
- fitness-trainers
|
||||
- catering-services
|
||||
- ugc-content-creators
|
||||
- cron
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
run: |
|
||||
export DOCKER_HOST=unix:///var/run/docker.sock
|
||||
docker version
|
||||
docker buildx create --use || true
|
||||
docker buildx inspect --bootstrap
|
||||
|
||||
- name: Login to Registry
|
||||
env:
|
||||
REGISTRY_HOSTPORT: ${{ secrets.REGISTRY_HOSTPORT }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export DOCKER_HOST=unix:///var/run/docker.sock
|
||||
test -n "$REGISTRY_HOSTPORT"
|
||||
for attempt in 1 2 3 4 5; do
|
||||
echo "Registry login attempt $attempt to $REGISTRY_HOSTPORT"
|
||||
if echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_HOSTPORT" -u "$REGISTRY_USERNAME" --password-stdin; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Registry login failed (attempt $attempt); retrying..."
|
||||
sleep $((attempt * 8))
|
||||
done
|
||||
|
||||
echo "Registry login failed after retries"
|
||||
exit 1
|
||||
|
||||
- name: Build and push
|
||||
env:
|
||||
REGISTRY_HOSTPORT: ${{ secrets.REGISTRY_HOSTPORT }}
|
||||
SERVICES_CSV: ${{ needs.detect-changes.outputs.services_csv }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export DOCKER_HOST=unix:///var/run/docker.sock
|
||||
if [ -n "$SERVICES_CSV" ] && ! echo ",$SERVICES_CSV," | grep -q ",${{ matrix.service }},"; then
|
||||
echo "Skipping unchanged service: ${{ matrix.service }}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
build_with_cache() {
|
||||
docker buildx build --push \
|
||||
-f Dockerfile.simple \
|
||||
--build-arg SERVICE_NAME=${{ matrix.service }} \
|
||||
--cache-from type=registry,ref=$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:buildcache \
|
||||
--cache-to type=registry,ref=$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:buildcache,mode=max \
|
||||
-t "$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:${{ gitea.sha }}" \
|
||||
-t "$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:high-performance-latest" \
|
||||
.
|
||||
}
|
||||
|
||||
build_without_cache_export() {
|
||||
docker buildx build --push \
|
||||
-f Dockerfile.simple \
|
||||
--build-arg SERVICE_NAME=${{ matrix.service }} \
|
||||
--cache-from type=registry,ref=$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:buildcache \
|
||||
-t "$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:${{ gitea.sha }}" \
|
||||
-t "$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:high-performance-latest" \
|
||||
.
|
||||
}
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
echo "Build attempt $attempt with cache export for ${{ matrix.service }}"
|
||||
if build_with_cache; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $attempt failed; retrying after backoff"
|
||||
sleep $((attempt * 10))
|
||||
done
|
||||
|
||||
echo "Falling back to build without cache export for ${{ matrix.service }}"
|
||||
if ! build_without_cache_export; then
|
||||
echo "Final fallback: push tags without cache"
|
||||
docker buildx build --push \
|
||||
-f Dockerfile.simple \
|
||||
--build-arg SERVICE_NAME=${{ matrix.service }} \
|
||||
-t "$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:${{ gitea.sha }}" \
|
||||
-t "$REGISTRY_HOSTPORT/nxtgauge-rust-${{ matrix.service }}:high-performance-latest" \
|
||||
.
|
||||
fi
|
||||
|
||||
- name: Prune old image tags (keep latest 1 SHA)
|
||||
if: success()
|
||||
continue-on-error: true
|
||||
env:
|
||||
REGISTRY_HOST: ${{ secrets.REGISTRY_HOSTPORT }}
|
||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 .gitea/scripts/registry_prune.py \
|
||||
--registry "$REGISTRY_HOST" \
|
||||
--repo "nxtgauge-rust-${{ matrix.service }}" \
|
||||
--username "$REGISTRY_USERNAME" \
|
||||
--password "$REGISTRY_PASSWORD" \
|
||||
--keep 1
|
||||
|
||||
- name: Update GitOps and trigger deployment
|
||||
if: always()
|
||||
continue-on-error: true
|
||||
env:
|
||||
GITEOPS_REPO: ${{ secrets.GITEOPS_REPO }}
|
||||
GITEOPS_SSH_KEY: ${{ secrets.GITEOPS_SSH_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "$GITEOPS_REPO" ]; then
|
||||
echo "GITEOPS_REPO secret not set, skipping GitOps update"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Clone gitops repo
|
||||
GITEOPS_DIR=$(mktemp -d)
|
||||
git clone "$GITEOPS_REPO" "$GITEOPS_DIR"
|
||||
cd "$GITEOPS_DIR"
|
||||
|
||||
# Set up SSH key for push
|
||||
mkdir -p ~/.ssh
|
||||
echo "$GITEOPS_SSH_KEY" > ~/.ssh/id_ed25519
|
||||
chmod 600 ~/.ssh/id_ed25519
|
||||
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
||||
|
||||
# Update gitops with new SHA
|
||||
python3 .gitea/scripts/update-gitops.py \
|
||||
--repo "$GITEOPS_DIR" \
|
||||
--service "${{ matrix.service }}" \
|
||||
--sha "${{ gitea.sha }}" \
|
||||
--message "chore: deploy ${{ matrix.service }}@${{ gitea.sha }}"
|
||||
|
||||
rm -rf "$GITEOPS_DIR"
|
||||
160
.github/workflows/build-and-push.yaml
vendored
Normal file
160
.github/workflows/build-and-push.yaml
vendored
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
name: Build and Push to Registry
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- high-performance
|
||||
- main
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
services_csv: ${{ steps.detect.outputs.services_csv }}
|
||||
has_changes: ${{ steps.detect.outputs.has_changes }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed services
|
||||
id: detect
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if git rev-parse --verify HEAD^ >/dev/null 2>&1; then
|
||||
CHANGED_FILES=$(git diff --name-only HEAD^ HEAD)
|
||||
else
|
||||
CHANGED_FILES=$(git ls-files)
|
||||
fi
|
||||
|
||||
LAST_COMMIT_MSG=$(git log -1 --pretty=%B | tr '\n' ' ')
|
||||
|
||||
echo "Changed files:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
ALL_SERVICES='gateway,users,companies,jobs,leads,job-seekers,customers,payments,employees,photographers,makeup-artists,tutors,developers,video-editors,graphic-designers,social-media-managers,fitness-trainers,catering-services,ugc-content-creators,cron'
|
||||
|
||||
# Force full build for explicit trigger commits
|
||||
if echo "$LAST_COMMIT_MSG" | grep -Eiq 'trigger build|force build|rebuild all'; then
|
||||
echo "services_csv=$ALL_SERVICES" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build everything for workflow/docker/shared backend changes
|
||||
if echo "$CHANGED_FILES" | grep -Eq '^(\.github/workflows/|Dockerfile|Cargo\.toml|Cargo\.lock|crates/)'; then
|
||||
echo "services_csv=$ALL_SERVICES" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SERVICES=''
|
||||
add_service() {
|
||||
local svc="$1"
|
||||
case ",${SERVICES}," in
|
||||
*",${svc},"*) ;;
|
||||
*)
|
||||
if [ -z "$SERVICES" ]; then
|
||||
SERVICES="$svc"
|
||||
else
|
||||
SERVICES="$SERVICES,$svc"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
while IFS= read -r f; do
|
||||
case "$f" in
|
||||
apps/gateway/*) add_service "gateway" ;;
|
||||
apps/users/*) add_service "users" ;;
|
||||
apps/companies/*) add_service "companies" ;;
|
||||
apps/jobs/*) add_service "jobs" ;;
|
||||
apps/leads/*) add_service "leads" ;;
|
||||
apps/job_seekers/*) add_service "job-seekers" ;;
|
||||
apps/customers/*) add_service "customers" ;;
|
||||
apps/payments/*) add_service "payments" ;;
|
||||
apps/employees/*) add_service "employees" ;;
|
||||
apps/photographers/*) add_service "photographers" ;;
|
||||
apps/makeup_artists/*) add_service "makeup-artists" ;;
|
||||
apps/tutors/*) add_service "tutors" ;;
|
||||
apps/developers/*) add_service "developers" ;;
|
||||
apps/video_editors/*) add_service "video-editors" ;;
|
||||
apps/graphic_designers/*) add_service "graphic-designers" ;;
|
||||
apps/social_media_managers/*) add_service "social-media-managers" ;;
|
||||
apps/fitness_trainers/*) add_service "fitness-trainers" ;;
|
||||
apps/catering_services/*) add_service "catering-services" ;;
|
||||
apps/ugc_content_creators/*) add_service "ugc-content-creators" ;;
|
||||
apps/cron/*) add_service "cron" ;;
|
||||
esac
|
||||
done <<< "$CHANGED_FILES"
|
||||
|
||||
if [ -z "$SERVICES" ]; then
|
||||
echo "services_csv=" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "services_csv=$SERVICES" >> "$GITHUB_OUTPUT"
|
||||
echo "has_changes=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.has_changes == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service:
|
||||
- gateway
|
||||
- users
|
||||
- companies
|
||||
- jobs
|
||||
- leads
|
||||
- job-seekers
|
||||
- customers
|
||||
- payments
|
||||
- employees
|
||||
- photographers
|
||||
- makeup-artists
|
||||
- tutors
|
||||
- developers
|
||||
- video-editors
|
||||
- graphic-designers
|
||||
- social-media-managers
|
||||
- fitness-trainers
|
||||
- catering-services
|
||||
- ugc-content-creators
|
||||
- cron
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: registry.nxtgauge.com
|
||||
username: admin
|
||||
password: Ashwin@2026
|
||||
|
||||
- name: Build and push
|
||||
env:
|
||||
SERVICES_CSV: ${{ needs.detect-changes.outputs.services_csv }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
SHA="${{ github.sha }}"
|
||||
|
||||
if [ -n "$SERVICES_CSV" ] && ! echo ",$SERVICES_CSV," | grep -q ",${{ matrix.service }},"; then
|
||||
echo "Skipping unchanged service: ${{ matrix.service }}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker buildx build --push \
|
||||
-f Dockerfile.simple \
|
||||
--build-arg SERVICE_NAME=${{ matrix.service }} \
|
||||
-t "registry.nxtgauge.com/nxtgauge-rust-${{ matrix.service }}:${SHA}" \
|
||||
-t "registry.nxtgauge.com/nxtgauge-rust-${{ matrix.service }}:latest" \
|
||||
.
|
||||
40
.github/workflows/sync-to-forgejo.yml
vendored
Normal file
40
.github/workflows/sync-to-forgejo.yml
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
name: sync-to-forgejo
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sync to Forgejo
|
||||
env:
|
||||
FORGEJO_SECRET: ${{ secrets.FORGEJO_SECRET || secrets.GITEA_SECRET }}
|
||||
FORGEJO_OWNER: ${{ secrets.FORGEJO_OWNER || 'ashwin' }}
|
||||
FORGEJO_USERNAME: ${{ secrets.FORGEJO_USERNAME || secrets.GITEA_USERNAME || 'ashwin' }}
|
||||
REPO: ${{ github.event.repository.name }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
export GIT_TRACE=1
|
||||
export GIT_CURL_VERBOSE=1
|
||||
|
||||
USER="${FORGEJO_USERNAME}"
|
||||
TARGET="https://ci.nxtgauge.com/${FORGEJO_OWNER}/${REPO}.git"
|
||||
AUTH="$(printf '%s' "${USER}:${FORGEJO_SECRET}" | base64 -w0)"
|
||||
|
||||
test -n "${FORGEJO_SECRET:-}" || (echo "FORGEJO_SECRET empty" && exit 1)
|
||||
curl -fsS -H "Authorization: Basic ${AUTH}" https://ci.nxtgauge.com/api/v1/user >/dev/null
|
||||
curl -fsS -X POST -H "Authorization: Basic ${AUTH}" "https://ci.nxtgauge.com/api/v1/repos/${FORGEJO_OWNER}/${REPO}/mirror-sync" >/dev/null
|
||||
|
||||
46
.github/workflows/sync-to-gitea.yml
vendored
46
.github/workflows/sync-to-gitea.yml
vendored
|
|
@ -1,46 +0,0 @@
|
|||
name: sync-to-gitea
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- high-performance
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Sync to Gitea
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.GITEA_SECRET }}
|
||||
REPO: ${{ github.event.repository.name }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
export GIT_TRACE=1
|
||||
export GIT_CURL_VERBOSE=1
|
||||
|
||||
USER="Admin"
|
||||
TARGET="https://ci.nxtgauge.com/Admin/${REPO}.git"
|
||||
AUTH="$(printf '%s' "${USER}:${GITEA_TOKEN}" | base64 -w0)"
|
||||
|
||||
test -n "${GITEA_TOKEN:-}" || (echo "GITEA_TOKEN empty" && exit 1)
|
||||
curl -fsS -H "Authorization: token ${GITEA_TOKEN}" https://ci.nxtgauge.com/api/v1/user >/dev/null
|
||||
curl -fsS -H "Authorization: Basic ${AUTH}" "${TARGET}/info/refs?service=git-receive-pack" >/dev/null
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global http.version HTTP/1.1
|
||||
git config --global http.postBuffer 524288000
|
||||
git remote remove gitea 2>/dev/null || true
|
||||
git remote add gitea "${TARGET}"
|
||||
|
||||
git -c http.extraheader="Authorization: Basic ${AUTH}" push gitea "HEAD:${BRANCH}" --force
|
||||
git -c http.extraheader="Authorization: Basic ${AUTH}" push gitea --tags --force
|
||||
196
Cargo.lock
generated
196
Cargo.lock
generated
|
|
@ -599,6 +599,12 @@ dependencies = [
|
|||
"fastrand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base16ct"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
|
|
@ -958,6 +964,18 @@ version = "0.8.21"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-bigint"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
|
|
@ -986,6 +1004,33 @@ dependencies = [
|
|||
"cmov",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures 0.2.17",
|
||||
"curve25519-dalek-derive",
|
||||
"digest 0.10.7",
|
||||
"fiat-crypto",
|
||||
"rustc_version",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek-derive"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "customers"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1116,6 +1161,44 @@ version = "1.0.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
|
||||
|
||||
[[package]]
|
||||
name = "ecdsa"
|
||||
version = "0.16.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
|
||||
dependencies = [
|
||||
"der",
|
||||
"digest 0.10.7",
|
||||
"elliptic-curve",
|
||||
"rfc6979",
|
||||
"signature",
|
||||
"spki",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
|
||||
dependencies = [
|
||||
"pkcs8",
|
||||
"signature",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519-dalek"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||
dependencies = [
|
||||
"curve25519-dalek",
|
||||
"ed25519",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
|
|
@ -1125,6 +1208,27 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "elliptic-curve"
|
||||
version = "0.13.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"crypto-bigint",
|
||||
"digest 0.10.7",
|
||||
"ff",
|
||||
"generic-array",
|
||||
"group",
|
||||
"hkdf",
|
||||
"pem-rfc7468",
|
||||
"pkcs8",
|
||||
"rand_core 0.6.4",
|
||||
"sec1",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "email"
|
||||
version = "0.1.0"
|
||||
|
|
@ -1228,6 +1332,22 @@ version = "2.4.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
|
||||
dependencies = [
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fiat-crypto"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
|
|
@ -1434,6 +1554,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
|||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1495,6 +1616,17 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "group"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
|
||||
dependencies = [
|
||||
"ff",
|
||||
"rand_core 0.6.4",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.3.27"
|
||||
|
|
@ -2048,11 +2180,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"ed25519-dalek",
|
||||
"getrandom 0.2.17",
|
||||
"hmac 0.12.1",
|
||||
"js-sys",
|
||||
"p256",
|
||||
"p384",
|
||||
"pem",
|
||||
"rand 0.8.6",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
"signature",
|
||||
"simple_asn1",
|
||||
"zeroize",
|
||||
|
|
@ -2451,6 +2590,30 @@ version = "0.5.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "p256"
|
||||
version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "p384"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
|
||||
dependencies = [
|
||||
"ecdsa",
|
||||
"elliptic-curve",
|
||||
"primeorder",
|
||||
"sha2 0.10.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking"
|
||||
version = "2.2.1"
|
||||
|
|
@ -2632,6 +2795,15 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "primeorder"
|
||||
version = "0.13.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
|
||||
dependencies = [
|
||||
"elliptic-curve",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
|
|
@ -2909,6 +3081,16 @@ dependencies = [
|
|||
"webpki-roots 1.0.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rfc6979"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
|
||||
dependencies = [
|
||||
"hmac 0.12.1",
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
|
|
@ -3080,6 +3262,20 @@ dependencies = [
|
|||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sec1"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
|
||||
dependencies = [
|
||||
"base16ct",
|
||||
"der",
|
||||
"generic-array",
|
||||
"pkcs8",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "3.7.0"
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ Required secrets:
|
|||
- `REGISTRY_USERNAME`
|
||||
- `REGISTRY_PASSWORD`
|
||||
|
||||
See `.gitea/workflows/README.md` for details.
|
||||
See `.forgejo/workflows/README.md` for details.
|
||||
# Trigger build Fri Jun 12 03:45:06 AM IST 2026
|
||||
|
|
|
|||
|
|
@ -186,12 +186,12 @@ async fn approve_company(
|
|||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
sqlx::query("UPDATE company_profiles SET status = 'ACTIVE', updated_at = NOW() WHERE id = $1")
|
||||
sqlx::query("UPDATE company_profiles SET status = 'APPROVED', updated_at = NOW() WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
Ok(Json(serde_json::json!({ "status": "ACTIVE" })))
|
||||
Ok(Json(serde_json::json!({ "status": "APPROVED" })))
|
||||
}
|
||||
|
||||
async fn reject_company(
|
||||
|
|
|
|||
|
|
@ -222,8 +222,7 @@ async fn submit_requirement(
|
|||
|
||||
async fn list_requests(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
_auth: AuthUser,
|
||||
auth: AuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let page = q.page.unwrap_or(1);
|
||||
|
|
@ -233,12 +232,12 @@ async fn list_requests(
|
|||
let rows_result = sqlx::query_as::<_, db::models::lead_request::LeadRequest>(
|
||||
r#"
|
||||
SELECT * FROM lead_requests
|
||||
WHERE user_role_profile_id = $1
|
||||
WHERE professional_user_id = $1
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.bind(auth.user_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
|
|
@ -271,7 +270,7 @@ async fn approve_request(
|
|||
Ok(updated) => {
|
||||
match TracecoinWalletRepository::try_debit_reserved_tracecoins(
|
||||
&state.pool,
|
||||
lead.user_role_profile_id,
|
||||
lead.user_role_profile_id.unwrap(),
|
||||
lead.tracecoins_reserved,
|
||||
lead.id,
|
||||
).await {
|
||||
|
|
@ -307,7 +306,7 @@ async fn reject_request(
|
|||
Ok(updated) => {
|
||||
match TracecoinWalletRepository::try_release_reserved_tracecoins(
|
||||
&state.pool,
|
||||
lead.user_role_profile_id,
|
||||
lead.user_role_profile_id.unwrap(),
|
||||
lead.tracecoins_reserved,
|
||||
lead.id,
|
||||
"LEAD_REJECTED",
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ impl Services {
|
|||
|
||||
|
||||
fn resolve_upstream(&self, path: &str) -> Option<String> {
|
||||
// Auth, users, roles, notifications, runtime-config, config, KB, support
|
||||
// Auth, users, roles, notifications, runtime-config, config, KB, support, reviews
|
||||
if path.starts_with("/api/auth")
|
||||
|| path.starts_with("/api/users")
|
||||
|| path.starts_with("/api/v1/users")
|
||||
|
|
@ -96,6 +96,7 @@ impl Services {
|
|||
|| path.starts_with("/api/kb")
|
||||
|| path.starts_with("/api/packages")
|
||||
|| path.starts_with("/api/support")
|
||||
|| path.starts_with("/api/reviews")
|
||||
|| path.starts_with("/api/admin/roles")
|
||||
|| path.starts_with("/api/admin/users")
|
||||
|| path.starts_with("/api/admin/verifications")
|
||||
|
|
|
|||
|
|
@ -380,6 +380,7 @@ async fn send_lead_request_ai(
|
|||
};
|
||||
|
||||
let expires_at = chrono::Utc::now() + chrono::Duration::hours(24);
|
||||
let customer_id = lead.2.clone();
|
||||
|
||||
let result = sqlx::query_as::<_, LeadRequestRow>(
|
||||
r#"
|
||||
|
|
@ -390,7 +391,7 @@ async fn send_lead_request_ai(
|
|||
)
|
||||
.bind(payload.lead_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(user_id)
|
||||
.bind(&customer_id) // customer_user_id from the lead
|
||||
.bind(tracecoins_cost)
|
||||
.bind(&ai_message)
|
||||
.bind(expires_at)
|
||||
|
|
@ -419,7 +420,7 @@ async fn send_lead_request_ai(
|
|||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(&customer_id) // notify the customer
|
||||
.bind("AI Auto-Respond Sent")
|
||||
.bind("Your AI-assisted response has been sent to the customer.")
|
||||
.bind("LEAD_REQUEST")
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
routing::{get, post},
|
||||
routing::{get, post, patch},
|
||||
Json, Router,
|
||||
};
|
||||
use reqwest::Client;
|
||||
|
|
@ -41,6 +41,14 @@ pub struct CreateLead {
|
|||
pub profession_key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateLead {
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
async fn list_leads(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Lead>>, StatusCode> {
|
||||
let leads = sqlx::query_as::<_, Lead>(
|
||||
"SELECT id, title, description, location, profession_key, status, created_at FROM leads ORDER BY created_at DESC"
|
||||
|
|
@ -94,6 +102,38 @@ async fn health() -> &'static str {
|
|||
"Leads Service OK"
|
||||
}
|
||||
|
||||
async fn update_lead(
|
||||
State(state): State<Arc<AppState>>,
|
||||
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||
Json(payload): Json<UpdateLead>,
|
||||
) -> Result<Json<Lead>, StatusCode> {
|
||||
let status = payload.status.as_deref().unwrap_or("OPEN");
|
||||
|
||||
let lead = sqlx::query_as::<_, Lead>(
|
||||
r#"
|
||||
UPDATE leads
|
||||
SET title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
location = COALESCE($3, location),
|
||||
status = $4,
|
||||
updated_at = NOW()
|
||||
WHERE id = $5
|
||||
RETURNING id, title, description, location, profession_key, status, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.title)
|
||||
.bind(&payload.description)
|
||||
.bind(&payload.location)
|
||||
.bind(status)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.ok_or(StatusCode::NOT_FOUND)?;
|
||||
|
||||
Ok(Json(lead))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::registry()
|
||||
|
|
@ -130,10 +170,13 @@ async fn main() {
|
|||
|
||||
let app = Router::new()
|
||||
.route("/health", get(health))
|
||||
.route("/leads", get(list_leads))
|
||||
.route("/leads", post(create_lead))
|
||||
.route("/leads/{id}", get(get_lead))
|
||||
.nest("/api/lead-requests", lead_requests::router())
|
||||
.nest("/api", Router::new()
|
||||
.route("/leads", get(list_leads))
|
||||
.route("/leads", post(create_lead))
|
||||
.route("/leads/{id}", get(get_lead))
|
||||
.route("/leads/{id}", patch(update_lead))
|
||||
.nest("/lead-requests", lead_requests::router())
|
||||
)
|
||||
.layer(cors)
|
||||
.with_state(state);
|
||||
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ async fn create_order(
|
|||
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO payments (user_id, package_id, razorpay_order_id, amount, tracecoins_credited, status)
|
||||
INSERT INTO payments (user_id, package_id, razorpay_order_id, amount_inr, tracecoins_credited, status)
|
||||
VALUES ($1, $2, $3, $4, $5, 'PENDING')
|
||||
"#,
|
||||
)
|
||||
|
|
@ -248,8 +248,8 @@ async fn verify_payment(
|
|||
{
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, balance_after, reference_type, reference_id, description)
|
||||
VALUES ($1, 'CREDIT', $2, $2, 'PAYMENT', $3, 'Package purchase')
|
||||
INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id)
|
||||
VALUES ($1, 'CREDIT', $2, 'PAYMENT', $3)
|
||||
"#,
|
||||
)
|
||||
.bind(wallet_id)
|
||||
|
|
@ -262,7 +262,7 @@ async fn verify_payment(
|
|||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO notifications (user_id, title, body, notification_type, reference_id)
|
||||
INSERT INTO notifications (user_id, title, body, type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
"#,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ use uuid::Uuid;
|
|||
pub struct PackageTypeQuery {
|
||||
pub package_type: Option<String>,
|
||||
pub applicable_role: Option<String>,
|
||||
pub role: Option<String>,
|
||||
pub active_only: Option<bool>,
|
||||
}
|
||||
|
||||
|
|
@ -29,48 +30,37 @@ pub struct CreatePackageRequest {
|
|||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub package_type: String,
|
||||
pub applicable_roles: Vec<String>,
|
||||
pub role_key: Option<String>,
|
||||
pub applicable_roles: Option<Vec<String>>,
|
||||
pub tracecoins_amount: i32,
|
||||
pub price: i32,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub is_promotional: Option<bool>,
|
||||
pub price: Option<i32>,
|
||||
pub price_inr: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
pub features: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdatePackageRequest {
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub role_key: Option<String>,
|
||||
pub applicable_roles: Option<Vec<String>>,
|
||||
pub tracecoins_amount: Option<i32>,
|
||||
pub price: Option<i32>,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub is_promotional: Option<bool>,
|
||||
pub price_inr: Option<i32>,
|
||||
pub is_active: Option<bool>,
|
||||
pub features: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, FromRow)]
|
||||
pub struct PricingPackageRow {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub role_key: String,
|
||||
pub package_type: String,
|
||||
pub applicable_roles: Vec<String>,
|
||||
pub tracecoins_amount: i32,
|
||||
pub price: i32,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub is_promotional: bool,
|
||||
pub price_inr: i32,
|
||||
pub description: Option<String>,
|
||||
pub is_active: bool,
|
||||
pub features: Option<serde_json::Value>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
|
@ -78,10 +68,12 @@ pub struct PricingPackageResponse {
|
|||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub package_type: String,
|
||||
pub role_key: String,
|
||||
pub applicable_roles: Vec<String>,
|
||||
pub package_type: String,
|
||||
pub tracecoins_amount: i32,
|
||||
pub price: i32,
|
||||
pub price_inr: i32,
|
||||
pub duration_days: Option<i32>,
|
||||
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
|
|
@ -96,33 +88,58 @@ pub struct PricingPackageResponse {
|
|||
|
||||
impl From<PricingPackageRow> for PricingPackageResponse {
|
||||
fn from(row: PricingPackageRow) -> Self {
|
||||
let now = chrono::Utc::now();
|
||||
let is_expired = row.valid_until.map(|v| v < now).unwrap_or(false);
|
||||
let is_not_started = row.valid_from.map(|v| v > now).unwrap_or(false);
|
||||
let is_available = row.is_active && !is_expired && !is_not_started;
|
||||
|
||||
PricingPackageResponse {
|
||||
Self {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
role_key: row.role_key.clone(),
|
||||
applicable_roles: vec![row.role_key],
|
||||
package_type: row.package_type,
|
||||
applicable_roles: row.applicable_roles,
|
||||
tracecoins_amount: row.tracecoins_amount,
|
||||
price: row.price,
|
||||
duration_days: row.duration_days,
|
||||
valid_from: row.valid_from,
|
||||
valid_until: row.valid_until,
|
||||
is_promotional: row.is_promotional,
|
||||
price: row.price_inr,
|
||||
price_inr: row.price_inr,
|
||||
duration_days: None,
|
||||
valid_from: None,
|
||||
valid_until: None,
|
||||
is_promotional: false,
|
||||
is_active: row.is_active,
|
||||
features: row.features,
|
||||
features: None,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
is_available,
|
||||
is_expired,
|
||||
updated_at: row.created_at,
|
||||
is_available: row.is_active,
|
||||
is_expired: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_role_key(role_key: Option<String>, applicable_roles: Option<Vec<String>>) -> Result<String, String> {
|
||||
if let Some(role) = role_key {
|
||||
let cleaned = role.trim().to_uppercase();
|
||||
if !cleaned.is_empty() {
|
||||
return Ok(cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(roles) = applicable_roles {
|
||||
if let Some(role) = roles.into_iter().map(|role| role.trim().to_uppercase()).find(|role| !role.is_empty()) {
|
||||
return Ok(role);
|
||||
}
|
||||
}
|
||||
|
||||
Err("role_key is required".to_string())
|
||||
}
|
||||
|
||||
fn package_query(base_where: &str, order_by: &str) -> String {
|
||||
format!(
|
||||
r#"
|
||||
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active, created_at
|
||||
FROM pricing_packages
|
||||
{base_where}
|
||||
{order_by}
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_packages))
|
||||
|
|
@ -138,83 +155,67 @@ async fn list_packages(
|
|||
State(state): State<AppState>,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let page = q.page.unwrap_or(1);
|
||||
let limit = q.limit.unwrap_or(20).min(100);
|
||||
let page = q.page.unwrap_or(1).max(1);
|
||||
let limit = q.limit.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * limit;
|
||||
let search = q.search.unwrap_or_default().trim().to_string();
|
||||
|
||||
let search_filter = q.search
|
||||
.as_ref()
|
||||
.map(|s| format!("AND (name ILIKE '%{}%' OR description ILIKE '%{}%')", s.replace('\'', "''"), s.replace('\'', "''")))
|
||||
.unwrap_or_default();
|
||||
|
||||
let packages = sqlx::query_as::<_, PricingPackageRow>(
|
||||
let rows = sqlx::query_as::<_, PricingPackageRow>(
|
||||
&format!(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages
|
||||
WHERE 1=1 {}
|
||||
ORDER BY created_at DESC
|
||||
LIMIT {} OFFSET {}
|
||||
"#,
|
||||
search_filter, limit, offset
|
||||
)
|
||||
"{} LIMIT $2 OFFSET $3",
|
||||
package_query(
|
||||
"WHERE ($1 = '' OR name ILIKE '%' || $1 || '%' OR COALESCE(description, '') ILIKE '%' || $1 || '%')",
|
||||
"ORDER BY created_at DESC"
|
||||
)
|
||||
),
|
||||
)
|
||||
.bind(&search)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
let packages = match packages {
|
||||
Ok(p) => p,
|
||||
let rows = match rows {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let total: (i64,) = match sqlx::query_as(
|
||||
&format!(
|
||||
"SELECT COUNT(*) FROM pricing_packages WHERE 1=1 {}",
|
||||
search_filter
|
||||
)
|
||||
let total: i64 = match sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM pricing_packages WHERE ($1 = '' OR name ILIKE '%' || $1 || '%' OR COALESCE(description, '') ILIKE '%' || $1 || '%')",
|
||||
)
|
||||
.bind(&search)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(t) => t,
|
||||
Ok(total) => total,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let packages: Vec<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
|
||||
let packages: Vec<PricingPackageResponse> = rows.into_iter().map(Into::into).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": packages,
|
||||
"packages": packages,
|
||||
"pagination": {
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"total": total.0,
|
||||
"pages": (total.0 as f64 / limit as f64).ceil() as i64
|
||||
"total": total,
|
||||
"pages": (total as f64 / limit as f64).ceil() as i64
|
||||
}
|
||||
}))).into_response()
|
||||
})))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn get_package(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
match sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages WHERE id = $1
|
||||
"#
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
match sqlx::query_as::<_, PricingPackageRow>(&package_query("WHERE id = $1", ""))
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(pkg)) => {
|
||||
let response: PricingPackageResponse = pkg.into();
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
Ok(Some(pkg)) => (StatusCode::OK, Json(PricingPackageResponse::from(pkg))).into_response(),
|
||||
Ok(None) => (StatusCode::NOT_FOUND, "Package not found").into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
@ -224,37 +225,31 @@ async fn create_package(
|
|||
State(state): State<AppState>,
|
||||
Json(payload): Json<CreatePackageRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let role_key = match normalize_role_key(payload.role_key, payload.applicable_roles) {
|
||||
Ok(role_key) => role_key,
|
||||
Err(message) => return (StatusCode::BAD_REQUEST, message).into_response(),
|
||||
};
|
||||
let price_inr = payload.price_inr.or(payload.price).unwrap_or(0);
|
||||
|
||||
let result = sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
INSERT INTO pricing_packages (name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
"#
|
||||
INSERT INTO pricing_packages (name, role_key, package_type, tracecoins_amount, price_inr, description, is_active)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(&payload.name)
|
||||
.bind(&payload.description)
|
||||
.bind(&role_key)
|
||||
.bind(&payload.package_type)
|
||||
.bind(&payload.applicable_roles)
|
||||
.bind(payload.tracecoins_amount)
|
||||
.bind(payload.price)
|
||||
.bind(payload.duration_days)
|
||||
.bind(payload.valid_from)
|
||||
.bind(payload.valid_until)
|
||||
.bind(payload.is_promotional.unwrap_or(false))
|
||||
.bind(price_inr)
|
||||
.bind(&payload.description)
|
||||
.bind(payload.is_active.unwrap_or(true))
|
||||
.bind(payload.features)
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(pkg) => {
|
||||
let response: PricingPackageResponse = pkg.into();
|
||||
(StatusCode::CREATED, Json(response)).into_response()
|
||||
}
|
||||
Ok(pkg) => (StatusCode::CREATED, Json(PricingPackageResponse::from(pkg))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
|
@ -264,58 +259,47 @@ async fn update_package(
|
|||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<UpdatePackageRequest>,
|
||||
) -> impl IntoResponse {
|
||||
let existing = sqlx::query_as::<_, PricingPackageRow>(
|
||||
"SELECT * FROM pricing_packages WHERE id = $1"
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
let _existing = match existing {
|
||||
Ok(Some(e)) => e,
|
||||
let current = match sqlx::query_as::<_, PricingPackageRow>(&package_query("WHERE id = $1", ""))
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(pkg)) => pkg,
|
||||
Ok(None) => return (StatusCode::NOT_FOUND, "Package not found").into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let role_key = match normalize_role_key(payload.role_key, payload.applicable_roles) {
|
||||
Ok(role_key) => role_key,
|
||||
Err(_) => current.role_key.clone(),
|
||||
};
|
||||
let price_inr = payload.price_inr.or(payload.price).unwrap_or(current.price_inr);
|
||||
|
||||
let updated = sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
UPDATE pricing_packages SET
|
||||
name = COALESCE($2, name),
|
||||
description = COALESCE($3, description),
|
||||
tracecoins_amount = COALESCE($4, tracecoins_amount),
|
||||
price = COALESCE($5, price),
|
||||
duration_days = COALESCE($6, duration_days),
|
||||
valid_from = COALESCE($7, valid_from),
|
||||
valid_until = COALESCE($8, valid_until),
|
||||
is_promotional = COALESCE($9, is_promotional),
|
||||
is_active = COALESCE($10, is_active),
|
||||
features = COALESCE($11, features),
|
||||
updated_at = NOW()
|
||||
role_key = $3,
|
||||
description = COALESCE($4, description),
|
||||
tracecoins_amount = COALESCE($5, tracecoins_amount),
|
||||
price_inr = $6,
|
||||
is_active = COALESCE($7, is_active)
|
||||
WHERE id = $1
|
||||
RETURNING id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
"#
|
||||
RETURNING id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(&payload.name)
|
||||
.bind(&role_key)
|
||||
.bind(&payload.description)
|
||||
.bind(payload.tracecoins_amount)
|
||||
.bind(payload.price)
|
||||
.bind(payload.duration_days)
|
||||
.bind(payload.valid_from)
|
||||
.bind(payload.valid_until)
|
||||
.bind(payload.is_promotional)
|
||||
.bind(price_inr)
|
||||
.bind(payload.is_active)
|
||||
.bind(payload.features)
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
match updated {
|
||||
Ok(pkg) => {
|
||||
let response: PricingPackageResponse = pkg.into();
|
||||
(StatusCode::OK, Json(response)).into_response()
|
||||
}
|
||||
Ok(pkg) => (StatusCode::OK, Json(PricingPackageResponse::from(pkg))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
|
@ -329,7 +313,7 @@ async fn delete_package(
|
|||
.execute(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(r) if r.rows_affected() > 0 => {
|
||||
Ok(result) if result.rows_affected() > 0 => {
|
||||
(StatusCode::OK, Json(serde_json::json!({"message": "Package deleted"}))).into_response()
|
||||
}
|
||||
Ok(_) => (StatusCode::NOT_FOUND, "Package not found").into_response(),
|
||||
|
|
@ -341,78 +325,65 @@ async fn get_packages_by_type(
|
|||
State(state): State<AppState>,
|
||||
Query(q): Query<PackageTypeQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let package_type = q.package_type.as_deref().unwrap_or("TRACECOIN_BUNDLE");
|
||||
let now = chrono::Utc::now();
|
||||
let package_type = q.package_type.unwrap_or_else(|| "TRACECOIN_BUNDLE".to_string());
|
||||
|
||||
let packages = sqlx::query_as::<_, PricingPackageRow>(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages
|
||||
WHERE package_type = $1
|
||||
AND is_active = true
|
||||
AND (valid_from IS NULL OR valid_from <= $2)
|
||||
AND (valid_until IS NULL OR valid_until > $2)
|
||||
ORDER BY is_promotional DESC, price ASC
|
||||
"#
|
||||
let rows = sqlx::query_as::<_, PricingPackageRow>(
|
||||
&package_query(
|
||||
"WHERE package_type = $1 AND is_active = true",
|
||||
"ORDER BY price_inr ASC, created_at DESC",
|
||||
),
|
||||
)
|
||||
.bind(package_type)
|
||||
.bind(now)
|
||||
.bind(&package_type)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
let packages = match packages {
|
||||
Ok(p) => p,
|
||||
let rows = match rows {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let packages: Vec<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
|
||||
let packages: Vec<PricingPackageResponse> = rows.into_iter().map(Into::into).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": packages,
|
||||
"package_type": package_type
|
||||
}))).into_response()
|
||||
})))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn get_packages_for_role(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<PackageTypeQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let applicable_role = q.applicable_role.as_deref().unwrap_or("");
|
||||
let role = q
|
||||
.applicable_role
|
||||
.or(q.role)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_uppercase();
|
||||
let active_only = q.active_only.unwrap_or(true);
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
let packages = sqlx::query_as::<_, PricingPackageRow>(
|
||||
&format!(
|
||||
r#"
|
||||
SELECT id, name, description, package_type, applicable_roles,
|
||||
tracecoins_amount, price, duration_days, valid_from, valid_until,
|
||||
is_promotional, is_active, features, created_at, updated_at
|
||||
FROM pricing_packages
|
||||
WHERE ($1 = '' OR $1 = ANY(applicable_roles))
|
||||
AND (is_active = true OR {} = false)
|
||||
AND (valid_from IS NULL OR valid_from <= $2)
|
||||
AND (valid_until IS NULL OR valid_until > $2)
|
||||
ORDER BY is_promotional DESC, price ASC
|
||||
"#,
|
||||
if active_only { "true" } else { "false" }
|
||||
)
|
||||
let rows = sqlx::query_as::<_, PricingPackageRow>(
|
||||
&package_query(
|
||||
"WHERE ($1 = '' OR role_key = $1) AND ($2 = false OR is_active = true)",
|
||||
"ORDER BY price_inr ASC, created_at DESC",
|
||||
),
|
||||
)
|
||||
.bind(applicable_role)
|
||||
.bind(now)
|
||||
.bind(&role)
|
||||
.bind(active_only)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
let packages = match packages {
|
||||
Ok(p) => p,
|
||||
let rows = match rows {
|
||||
Ok(rows) => rows,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
|
||||
let packages: Vec<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
|
||||
let packages: Vec<PricingPackageResponse> = rows.into_iter().map(Into::into).collect();
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"data": packages,
|
||||
"applicable_role": applicable_role
|
||||
}))).into_response()
|
||||
"applicable_role": role
|
||||
})))
|
||||
.into_response()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,13 @@ fn resolve_signup_role_candidates(intent: Option<&str>, profession: Option<&str>
|
|||
vec![]
|
||||
}
|
||||
|
||||
fn is_dummy_account_email(email: &str) -> bool {
|
||||
email.ends_with("@demo.com")
|
||||
|| email == "paymentgateway@demo.com"
|
||||
|| email.contains("+dummy@")
|
||||
|| email.starts_with("dummy+")
|
||||
}
|
||||
|
||||
fn role_display_name_from_code(code: &str) -> String {
|
||||
code
|
||||
.split('_')
|
||||
|
|
@ -296,6 +303,9 @@ async fn register(
|
|||
}
|
||||
})?;
|
||||
|
||||
// Check if this is a demo account (payment gateway integration)
|
||||
let is_demo_account = is_dummy_account_email(&email);
|
||||
|
||||
// Assign signup role immediately (intent-driven). Email verification is still required for login.
|
||||
let role_candidates = resolve_signup_role_candidates(
|
||||
payload.intent.as_deref(),
|
||||
|
|
@ -304,22 +314,25 @@ async fn register(
|
|||
for role_key in role_candidates {
|
||||
let role_id = ensure_role_exists(&state.pool, &role_key).await;
|
||||
if let Some(role_id) = role_id {
|
||||
// For demo accounts, auto-approve the role immediately
|
||||
let status = if is_demo_account { "APPROVED" } else { "PENDING" };
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
UPDATE user_role_assignments
|
||||
SET status = 'APPROVED'
|
||||
SET status = $3
|
||||
WHERE user_id = $1 AND role_id = $2
|
||||
"#,
|
||||
)
|
||||
.bind(user.id)
|
||||
.bind(role_id)
|
||||
.bind(status)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
let _ = sqlx::query(
|
||||
r#"
|
||||
INSERT INTO user_role_assignments (user_id, role_id, status)
|
||||
SELECT $1, $2, 'APPROVED'
|
||||
SELECT $1, $2, $3
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM user_role_assignments WHERE user_id = $1 AND role_id = $2
|
||||
)
|
||||
|
|
@ -327,12 +340,37 @@ async fn register(
|
|||
)
|
||||
.bind(user.id)
|
||||
.bind(role_id)
|
||||
.bind(status)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// For demo accounts: auto-verify email and skip OTP
|
||||
if is_demo_account {
|
||||
tracing::info!(email = %email, "Demo account auto-verified");
|
||||
let _ = sqlx::query(
|
||||
"UPDATE users SET email_verified = true, status = 'ACTIVE' WHERE id = $1"
|
||||
)
|
||||
.bind(user.id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
// Return success with demo flag
|
||||
let user_name = format!("{} {}", user.first_name.unwrap_or_default(), user.last_name.unwrap_or_default());
|
||||
return Ok((StatusCode::CREATED, Json(RegisterResponse {
|
||||
user_id: user.id.to_string(),
|
||||
email: user.email,
|
||||
phone: None,
|
||||
name: user_name,
|
||||
status: "ACTIVE".to_string(),
|
||||
email_verified: true,
|
||||
created_at: user.created_at.to_rfc3339(),
|
||||
otp: Some("DEMO".to_string()), // Return dummy OTP for demo
|
||||
})));
|
||||
}
|
||||
|
||||
// Store OTP in Redis (15-min TTL, keyed by code → user_id)
|
||||
let otp = format!("{:06}", rand::random::<u32>() % 1_000_000);
|
||||
tracing::info!(otp = %otp, email = %email, "OTP generated for registration");
|
||||
|
|
@ -384,7 +422,10 @@ async fn login(
|
|||
if user.status == "SUSPENDED" {
|
||||
return Err(err(StatusCode::FORBIDDEN, "Account suspended", "ACCOUNT_SUSPENDED"));
|
||||
}
|
||||
if !user.email_verified {
|
||||
|
||||
// Allow demo accounts to login without email verification
|
||||
let is_demo_account = is_dummy_account_email(&email);
|
||||
if !user.email_verified && !is_demo_account {
|
||||
return Err(err(StatusCode::UNAUTHORIZED, "Email not verified. Check your inbox.", "EMAIL_NOT_VERIFIED"));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use axum::{
|
|||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
routing::{get, patch},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -14,6 +14,8 @@ use contracts::auth_middleware::{AuthUser, require_admin};
|
|||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_external_roles).post(create_external_role))
|
||||
.route("/by-key/{role_key}", get(get_external_role_by_key))
|
||||
.route("/by-key/{role_key}", patch(update_external_role_by_key))
|
||||
.route("/{id}", get(get_external_role).put(update_external_role).delete(delete_external_role))
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +249,60 @@ async fn get_external_role(
|
|||
}))
|
||||
}
|
||||
|
||||
async fn get_external_role_by_key(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(role_key): Path<String>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
|
||||
r#"
|
||||
SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
|
||||
rc.updated_at as updated_at, rc.config_json as config_json
|
||||
FROM roles r
|
||||
LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
WHERE r.key = $1 AND r.audience = 'EXTERNAL'
|
||||
"#,
|
||||
)
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;
|
||||
|
||||
Ok(Json(ExternalRoleDetail {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
audience: row.audience,
|
||||
is_active: row.is_active,
|
||||
runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn update_external_role_by_key(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(role_key): Path<String>,
|
||||
Json(payload): Json<UpdateExternalRolePayload>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
let row: (Uuid,) = sqlx::query_as("SELECT id FROM roles WHERE key = $1 AND audience = 'EXTERNAL'")
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;
|
||||
|
||||
update_external_role_impl(&state, row.0, payload).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateExternalRolePayload {
|
||||
name: String,
|
||||
|
|
@ -340,6 +396,14 @@ async fn update_external_role(
|
|||
if let Err(_e) = require_admin(&auth) {
|
||||
return Err((StatusCode::FORBIDDEN, "Forbidden".to_string()));
|
||||
}
|
||||
update_external_role_impl(&state, id, payload).await
|
||||
}
|
||||
|
||||
async fn update_external_role_impl(
|
||||
state: &AppState,
|
||||
role_id: Uuid,
|
||||
payload: UpdateExternalRolePayload,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
if payload.name.is_some() || payload.is_active.is_some() {
|
||||
sqlx::query(
|
||||
r#"
|
||||
|
|
@ -351,7 +415,7 @@ async fn update_external_role(
|
|||
)
|
||||
.bind(payload.name)
|
||||
.bind(payload.is_active)
|
||||
.bind(id)
|
||||
.bind(role_id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
|
|
@ -364,7 +428,7 @@ async fn update_external_role(
|
|||
WHERE role_id = $1 AND is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(role_id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
|
|
@ -379,13 +443,38 @@ async fn update_external_role(
|
|||
)
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.bind(role_id)
|
||||
.bind(runtime)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
|
||||
}
|
||||
get_external_role(auth, State(state), Path(id)).await
|
||||
// Return the updated role detail
|
||||
let row = sqlx::query_as::<_, ExternalRoleDetailRow>(
|
||||
r#"
|
||||
SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at,
|
||||
rc.updated_at as updated_at, rc.config_json as config_json
|
||||
FROM roles r
|
||||
LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true
|
||||
WHERE r.id = $1 AND r.audience = 'EXTERNAL'
|
||||
"#,
|
||||
)
|
||||
.bind(role_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
|
||||
.ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?;
|
||||
|
||||
Ok(Json(ExternalRoleDetail {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
code: row.code,
|
||||
audience: row.audience,
|
||||
is_active: row.is_active,
|
||||
runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})),
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn delete_external_role(
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ pub fn public_router() -> Router<AppState> {
|
|||
.route("/categories", get(public_list_categories))
|
||||
.route("/articles", get(public_list_articles))
|
||||
.route("/articles/{slug}", get(public_get_article))
|
||||
.route("/articles/id/{id}", get(public_get_article_by_id))
|
||||
}
|
||||
|
||||
/// Admin CRUD routes
|
||||
|
|
@ -346,6 +347,68 @@ async fn public_get_article(
|
|||
}
|
||||
}
|
||||
|
||||
// ── Public: single article by ID ───────────────────────────────────────────────
|
||||
|
||||
async fn public_get_article_by_id(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let row = sqlx::query_as::<_, PublicArticleRow>(
|
||||
r#"
|
||||
SELECT
|
||||
a.id, a.title, a.slug, a.summary, a.body, a.target_roles, a.tags,
|
||||
a.updated_at,
|
||||
c.name AS category_name, c.slug AS category_slug
|
||||
FROM kb_articles a
|
||||
JOIN kb_categories c ON c.id = a.category_id
|
||||
WHERE a.id = $1 AND a.status = 'PUBLISHED' AND c.is_active = true
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
let pool = state.pool.clone();
|
||||
tokio::spawn(async move {
|
||||
let _ = sqlx::query("UPDATE kb_articles SET views = views + 1 WHERE id = $1")
|
||||
.bind(id)
|
||||
.execute(&pool)
|
||||
.await;
|
||||
});
|
||||
|
||||
match row {
|
||||
Ok(Some(r)) => {
|
||||
let role = derive_role(r.target_roles.as_deref().unwrap_or(&[]));
|
||||
let dto = PublicArticleDto {
|
||||
id: r.id,
|
||||
slug: r.slug,
|
||||
title: r.title,
|
||||
summary: r.summary,
|
||||
category_key: r.category_slug,
|
||||
category: r.category_name,
|
||||
role,
|
||||
tags: r.tags,
|
||||
updated_at: r.updated_at.to_rfc3339(),
|
||||
content: r.body,
|
||||
};
|
||||
(StatusCode::OK, Json(dto)).into_response()
|
||||
}
|
||||
Ok(None) => (
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({ "error": "Article not found" })),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to fetch KB article by id {}: {}", id, e);
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({ "error": "Failed to fetch article" })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Admin: categories ─────────────────────────────────────────────────────────
|
||||
|
||||
async fn admin_list_categories(
|
||||
|
|
|
|||
|
|
@ -173,8 +173,8 @@ async fn submit(
|
|||
|
||||
let query = format!(
|
||||
r#"
|
||||
INSERT INTO {} (id, custom_data, status, updated_at)
|
||||
VALUES ($1, $2, 'PENDING', NOW())
|
||||
INSERT INTO {} (id, user_id, custom_data, status, updated_at)
|
||||
VALUES ($1, $2, $3, 'PENDING', NOW())
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
custom_data = EXCLUDED.custom_data,
|
||||
status = 'PENDING',
|
||||
|
|
@ -185,6 +185,7 @@ async fn submit(
|
|||
|
||||
sqlx::query(&query)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(auth.user_id)
|
||||
.bind(&progress)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use axum::{
|
|||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::AuthUser;
|
||||
use db::models::{role::RoleRepository, verification::VerificationRepository};
|
||||
use db::models::{role::RoleRepository, user::UserRepository, verification::VerificationRepository};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
@ -19,8 +19,9 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/submit-for-verification", post(submit_for_verification))
|
||||
}
|
||||
|
||||
pub fn me_verification_router() -> Router<AppState> {
|
||||
pub fn me_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(get_me))
|
||||
.route("/verification-status", get(verification_status))
|
||||
}
|
||||
|
||||
|
|
@ -68,6 +69,13 @@ fn role_to_table(role_key: &str) -> Option<&'static str> {
|
|||
}
|
||||
}
|
||||
|
||||
fn is_dummy_account_email(email: &str) -> bool {
|
||||
email.ends_with("@demo.com")
|
||||
|| email == "paymentgateway@demo.com"
|
||||
|| email.contains("+dummy@")
|
||||
|| email.starts_with("dummy+")
|
||||
}
|
||||
|
||||
fn extract_documents(profile_data: &serde_json::Value) -> serde_json::Value {
|
||||
let doc_keys = [
|
||||
"aadhar_doc",
|
||||
|
|
@ -304,75 +312,137 @@ async fn submit_for_verification(
|
|||
) -> impl IntoResponse {
|
||||
let role_key = input.role_key.to_uppercase();
|
||||
|
||||
// Guard: reject if an active verification already exists
|
||||
let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT id FROM verifications
|
||||
WHERE user_id = $1 AND role_key = $2
|
||||
AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
// Check if user is a demo account
|
||||
let is_demo = sqlx::query_scalar::<_, String>("SELECT email FROM users WHERE id = $1")
|
||||
.bind(auth.user_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.map(|email| is_dummy_account_email(&email))
|
||||
.unwrap_or(false);
|
||||
|
||||
if existing.unwrap_or(None).is_some() {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(serde_json::json!({
|
||||
"error": "A verification is already in progress for this role. Please wait for it to be reviewed."
|
||||
})),
|
||||
// For demo accounts: auto-approve verification
|
||||
if is_demo {
|
||||
tracing::info!(user_id = %auth.user_id, role_key = %role_key, "Demo account auto-approved for verification");
|
||||
|
||||
// Update role assignment to APPROVED
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"UPDATE user_role_assignments SET status = 'APPROVED' WHERE user_id = $1 AND role_id = $2",
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Mark profile as VERIFIED
|
||||
set_profile_status(&state, auth.user_id, &role_key, "VERIFIED").await;
|
||||
|
||||
// Create a verification record with APPROVED status
|
||||
let profile_data = input.profile_data.unwrap_or_else(|| {
|
||||
serde_json::json!({
|
||||
"company_name": "Payment Gateway Demo Company",
|
||||
"company_description": "Demo account for reviewing packages",
|
||||
"industry": "Technology",
|
||||
"location": "India"
|
||||
})
|
||||
});
|
||||
let documents = extract_documents(&profile_data);
|
||||
|
||||
match VerificationRepository::create_approved(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
&role_key,
|
||||
"PROFILE_VERIFICATION",
|
||||
profile_data,
|
||||
documents,
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Fetch saved profile data or use submitted data
|
||||
let profile_data = match input.profile_data {
|
||||
Some(data) => data,
|
||||
None => fetch_saved_profile(&state, auth.user_id, &role_key).await,
|
||||
};
|
||||
|
||||
let documents = extract_documents(&profile_data);
|
||||
|
||||
// Mark profile as PENDING in role-specific table
|
||||
set_profile_status(&state, auth.user_id, &role_key, "PENDING").await;
|
||||
|
||||
// Mark user_role as PENDING
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
.await
|
||||
{
|
||||
Ok(v) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"verification_id": v.id,
|
||||
"status": "APPROVED",
|
||||
"message": "Your profile has been auto-approved for demo access."
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
} else {
|
||||
// Regular verification flow for non-demo accounts
|
||||
// Guard: reject if an active verification already exists
|
||||
let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT id FROM verifications
|
||||
WHERE user_id = $1 AND role_key = $2
|
||||
AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
// Create verification record — appears in admin Verification Management
|
||||
match VerificationRepository::create(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
&role_key,
|
||||
"PROFILE_VERIFICATION",
|
||||
"MEDIUM",
|
||||
profile_data,
|
||||
documents,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"verification_id": v.id,
|
||||
"status": v.status,
|
||||
"message": "Your profile has been submitted for verification. We will notify you once it has been reviewed."
|
||||
})),
|
||||
if existing.unwrap_or(None).is_some() {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(serde_json::json!({
|
||||
"error": "A verification is already in progress for this role. Please wait for it to be reviewed."
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Fetch saved profile data or use submitted data
|
||||
let profile_data = match input.profile_data {
|
||||
Some(data) => data,
|
||||
None => fetch_saved_profile(&state, auth.user_id, &role_key).await,
|
||||
};
|
||||
|
||||
let documents = extract_documents(&profile_data);
|
||||
|
||||
// Mark profile as PENDING in role-specific table
|
||||
set_profile_status(&state, auth.user_id, &role_key, "PENDING").await;
|
||||
|
||||
// Mark user_role as PENDING
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query(
|
||||
"UPDATE user_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Create verification record — appears in admin Verification Management
|
||||
match VerificationRepository::create(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
&role_key,
|
||||
"PROFILE_VERIFICATION",
|
||||
"MEDIUM",
|
||||
profile_data,
|
||||
documents,
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
.await
|
||||
{
|
||||
Ok(v) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"verification_id": v.id,
|
||||
"status": v.status,
|
||||
"message": "Your profile has been submitted for verification. We will notify you once it has been reviewed."
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -557,3 +627,22 @@ async fn fetch_saved_profile_by_urp_id(
|
|||
}
|
||||
serde_json::Value::Object(Default::default())
|
||||
}
|
||||
|
||||
/// GET /api/me — returns the authenticated user's basic info
|
||||
pub async fn get_me(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let user = UserRepository::get_by_id(&state.pool, auth.user_id)
|
||||
.await
|
||||
.map_err(|_| (StatusCode::UNAUTHORIZED, "User not found".to_string()))?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"id": user.id,
|
||||
"email": user.email,
|
||||
"firstName": user.first_name,
|
||||
"lastName": user.last_name,
|
||||
"activeRole": auth.claims.active_role,
|
||||
"emailVerified": user.email_verified,
|
||||
})))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::AuthUser;
|
||||
|
|
@ -18,35 +18,50 @@ pub fn admin_router() -> Router<AppState> {
|
|||
.route("/{id}", axum::routing::patch(admin_update_review).delete(admin_delete_review))
|
||||
}
|
||||
|
||||
pub fn public_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(list_reviews))
|
||||
.route("/professional/{professional_id}", get(list_reviews_by_professional))
|
||||
}
|
||||
|
||||
// ── DTOs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ReviewDto {
|
||||
id: Uuid,
|
||||
subject_type: String,
|
||||
subject_id: Option<String>,
|
||||
reviewer_name: Option<String>,
|
||||
reviewer_id: Option<Uuid>,
|
||||
professional_id: Uuid,
|
||||
customer_id: Uuid,
|
||||
rating: i16,
|
||||
title: Option<String>,
|
||||
comment: Option<String>,
|
||||
status: String,
|
||||
is_published: bool,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct PublicReviewDto {
|
||||
id: Uuid,
|
||||
professional_id: Uuid,
|
||||
rating: i16,
|
||||
comment: Option<String>,
|
||||
created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateReviewBody {
|
||||
subject_type: Option<String>,
|
||||
subject_id: Option<String>,
|
||||
reviewer_name: Option<String>,
|
||||
lead_request_id: Uuid,
|
||||
rating: i16,
|
||||
title: Option<String>,
|
||||
comment: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PatchReviewBody {
|
||||
status: Option<String>,
|
||||
is_published: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct PublicListQuery {
|
||||
page: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
}
|
||||
|
||||
// ── FromRow structs ──────────────────────────────────────────────────────────
|
||||
|
|
@ -54,14 +69,12 @@ struct PatchReviewBody {
|
|||
#[derive(sqlx::FromRow)]
|
||||
struct ReviewRow {
|
||||
id: Uuid,
|
||||
subject_type: String,
|
||||
subject_id: Option<String>,
|
||||
reviewer_name: Option<String>,
|
||||
reviewer_id: Option<Uuid>,
|
||||
lead_request_id: Uuid,
|
||||
customer_id: Uuid,
|
||||
professional_id: Uuid,
|
||||
rating: i16,
|
||||
title: Option<String>,
|
||||
comment: Option<String>,
|
||||
status: String,
|
||||
is_published: bool,
|
||||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
|
|
@ -75,14 +88,12 @@ async fn admin_list_reviews(
|
|||
r#"
|
||||
SELECT
|
||||
r.id,
|
||||
r.subject_type,
|
||||
r.subject_id,
|
||||
r.reviewer_name,
|
||||
r.reviewer_user_id AS reviewer_id,
|
||||
r.lead_request_id,
|
||||
r.customer_id,
|
||||
r.professional_id,
|
||||
r.rating,
|
||||
r.title,
|
||||
r.comment,
|
||||
r.status,
|
||||
r.is_published,
|
||||
r.created_at
|
||||
FROM reviews r
|
||||
ORDER BY r.created_at DESC
|
||||
|
|
@ -97,14 +108,11 @@ async fn admin_list_reviews(
|
|||
.into_iter()
|
||||
.map(|r| ReviewDto {
|
||||
id: r.id,
|
||||
subject_type: r.subject_type,
|
||||
subject_id: r.subject_id,
|
||||
reviewer_name: r.reviewer_name,
|
||||
reviewer_id: r.reviewer_id,
|
||||
professional_id: r.professional_id,
|
||||
customer_id: r.customer_id,
|
||||
rating: r.rating,
|
||||
title: r.title,
|
||||
comment: r.comment,
|
||||
status: r.status,
|
||||
is_published: r.is_published,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -126,24 +134,19 @@ async fn admin_create_review(
|
|||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Rating must be 1-5" }))).into_response();
|
||||
}
|
||||
|
||||
let subject_type = body.subject_type.unwrap_or_else(|| "PLATFORM".to_string());
|
||||
let status = "PUBLISHED".to_string();
|
||||
|
||||
let row = sqlx::query_as::<_, ReviewRow>(
|
||||
r#"
|
||||
INSERT INTO reviews (subject_type, subject_id, reviewer_name, rating, title, comment, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING id, subject_type, subject_id, reviewer_name, reviewer_user_id AS reviewer_id,
|
||||
rating, title, comment, status, created_at
|
||||
INSERT INTO reviews (lead_request_id, customer_id, professional_id, rating, comment, is_published)
|
||||
SELECT $1,
|
||||
(SELECT id FROM customer_profiles WHERE user_id = (SELECT user_id FROM lead_requests WHERE id = $1)),
|
||||
(SELECT user_role_profile_id FROM lead_requests WHERE id = $1),
|
||||
$2, $3, true
|
||||
RETURNING id, lead_request_id, customer_id, professional_id, rating, comment, is_published, created_at
|
||||
"#,
|
||||
)
|
||||
.bind(&subject_type)
|
||||
.bind(&body.subject_id)
|
||||
.bind(&body.reviewer_name)
|
||||
.bind(body.lead_request_id)
|
||||
.bind(body.rating)
|
||||
.bind(&body.title)
|
||||
.bind(&body.comment)
|
||||
.bind(&status)
|
||||
.fetch_one(&state.pool)
|
||||
.await;
|
||||
|
||||
|
|
@ -151,14 +154,11 @@ async fn admin_create_review(
|
|||
Ok(r) => {
|
||||
let dto = ReviewDto {
|
||||
id: r.id,
|
||||
subject_type: r.subject_type,
|
||||
subject_id: r.subject_id,
|
||||
reviewer_name: r.reviewer_name,
|
||||
reviewer_id: r.reviewer_id,
|
||||
professional_id: r.professional_id,
|
||||
customer_id: r.customer_id,
|
||||
rating: r.rating,
|
||||
title: r.title,
|
||||
comment: r.comment,
|
||||
status: r.status,
|
||||
is_published: r.is_published,
|
||||
created_at: r.created_at,
|
||||
};
|
||||
(StatusCode::CREATED, Json(serde_json::json!(dto))).into_response()
|
||||
|
|
@ -176,13 +176,12 @@ async fn admin_update_review(
|
|||
Path(id): Path<Uuid>,
|
||||
Json(body): Json<PatchReviewBody>,
|
||||
) -> impl IntoResponse {
|
||||
let status = body.status.as_deref().unwrap_or("PUBLISHED").to_string();
|
||||
let is_published = body.is_published.unwrap_or(true);
|
||||
|
||||
let result = sqlx::query(
|
||||
"UPDATE reviews SET status = $1, updated_at = NOW() WHERE id = $2",
|
||||
"UPDATE reviews SET is_published = $1, updated_at = NOW() WHERE id = $2",
|
||||
)
|
||||
.bind(&status)
|
||||
.bind(id)
|
||||
.bind(is_published)
|
||||
.bind(id)
|
||||
.execute(&state.pool)
|
||||
.await;
|
||||
|
|
@ -220,3 +219,107 @@ async fn admin_delete_review(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Public handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
async fn list_reviews(
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<PublicListQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let page = q.page.unwrap_or(1).max(1);
|
||||
let limit = q.limit.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let rows = sqlx::query_as::<_, ReviewRow>(
|
||||
r#"
|
||||
SELECT id, lead_request_id, customer_id, professional_id, rating, comment, is_published, created_at
|
||||
FROM reviews
|
||||
WHERE is_published = true
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
"#,
|
||||
)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
match rows {
|
||||
Ok(rows) => {
|
||||
let dtos: Vec<PublicReviewDto> = rows
|
||||
.into_iter()
|
||||
.map(|r| PublicReviewDto {
|
||||
id: r.id,
|
||||
professional_id: r.professional_id,
|
||||
rating: r.rating,
|
||||
comment: r.comment,
|
||||
created_at: r.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
(StatusCode::OK, Json(serde_json::json!({ "reviews": dtos }))).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to list public reviews: {e}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to load reviews" }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_reviews_by_professional(
|
||||
State(state): State<AppState>,
|
||||
Path(professional_id): Path<Uuid>,
|
||||
Query(q): Query<PublicListQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let page = q.page.unwrap_or(1).max(1);
|
||||
let limit = q.limit.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
let rows = sqlx::query_as::<_, ReviewRow>(
|
||||
r#"
|
||||
SELECT id, lead_request_id, customer_id, professional_id, rating, comment, is_published, created_at
|
||||
FROM reviews
|
||||
WHERE professional_id = $1 AND is_published = true
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(professional_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
match rows {
|
||||
Ok(rows) => {
|
||||
let avg: (f64,) = sqlx::query_as("SELECT COALESCE(AVG(rating), 0)::float FROM reviews WHERE professional_id = $1 AND is_published = true")
|
||||
.bind(professional_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap_or((0.0,));
|
||||
let count: (i64,) = sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE professional_id = $1 AND is_published = true")
|
||||
.bind(professional_id)
|
||||
.fetch_one(&state.pool)
|
||||
.await
|
||||
.unwrap_or((0,));
|
||||
let dtos: Vec<PublicReviewDto> = rows
|
||||
.into_iter()
|
||||
.map(|r| PublicReviewDto {
|
||||
id: r.id,
|
||||
professional_id: r.professional_id,
|
||||
rating: r.rating,
|
||||
comment: r.comment,
|
||||
created_at: r.created_at.to_rfc3339(),
|
||||
})
|
||||
.collect();
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"reviews": dtos,
|
||||
"averageRating": avg.0,
|
||||
"totalCount": count.0
|
||||
}))).into_response()
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to list reviews for professional {professional_id}: {e}");
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to load reviews" }))).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -479,7 +479,7 @@ struct AdminTicketRow {
|
|||
created_at: chrono::DateTime<chrono::Utc>,
|
||||
updated_at: chrono::DateTime<chrono::Utc>,
|
||||
user_name: Option<String>,
|
||||
user_email: String,
|
||||
user_email: Option<String>,
|
||||
}
|
||||
|
||||
async fn admin_list_cases(
|
||||
|
|
@ -503,7 +503,11 @@ async fn admin_list_cases(
|
|||
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
||||
FROM support_tickets t
|
||||
LEFT JOIN users u ON u.id = t.user_id
|
||||
WHERE t.id = $1
|
||||
WHERE ($1 = '' OR t.status = $1)
|
||||
AND ($2 = '' OR t.priority = $2)
|
||||
AND ($3 = '' OR t.category = $3)
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT $4 OFFSET $5
|
||||
"#,
|
||||
)
|
||||
.bind(&status_filter)
|
||||
|
|
@ -526,7 +530,7 @@ async fn admin_list_cases(
|
|||
.map(|r| {
|
||||
// Use user info if available, fall back to requester fields
|
||||
let requester_name = r.requester_name.or(r.user_name);
|
||||
let requester_email = r.requester_email.or(Some(r.user_email));
|
||||
let requester_email = r.requester_email.or(r.user_email);
|
||||
serde_json::json!({
|
||||
"id": r.id,
|
||||
"title": r.subject,
|
||||
|
|
@ -642,11 +646,7 @@ async fn admin_get_case(
|
|||
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
||||
FROM support_tickets t
|
||||
LEFT JOIN users u ON u.id = t.user_id
|
||||
WHERE ($1 = '' OR t.status = $1)
|
||||
AND ($2 = '' OR t.priority = $2)
|
||||
AND ($3 = '' OR t.category = $3)
|
||||
ORDER BY t.updated_at DESC
|
||||
LIMIT $4 OFFSET $5
|
||||
WHERE t.id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
@ -681,7 +681,7 @@ async fn admin_get_case(
|
|||
.collect();
|
||||
|
||||
let requester_name = t.requester_name.or(t.user_name);
|
||||
let requester_email = t.requester_email.or(Some(t.user_email));
|
||||
let requester_email = t.requester_email.or(t.user_email);
|
||||
|
||||
(StatusCode::OK, Json(serde_json::json!({
|
||||
"ticket": {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use axum::{
|
|||
};
|
||||
use contracts::auth_middleware::AuthUser;
|
||||
use db::models::role::RoleRepository;
|
||||
use db::models::user_role_profile::UserRoleProfileRepository;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
|
|
@ -42,6 +43,7 @@ fn is_professional_role(role_key: &str) -> bool {
|
|||
| "SOCIAL_MEDIA_MANAGER"
|
||||
| "FITNESS_TRAINER"
|
||||
| "CATERING_SERVICES"
|
||||
| "UGC_CONTENT_CREATOR"
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +114,14 @@ async fn register_role(
|
|||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// Note: Professional profile creation is now handled upon successful submission of onboarding data.
|
||||
// For professional/external roles, also create the user_role_profiles entry so
|
||||
// downstream services (e.g. leads) can find the professional's profile.
|
||||
if is_professional_role(&role_key) {
|
||||
if let Err(e) = UserRoleProfileRepository::create(&state.pool, auth.user_id, &role_key).await {
|
||||
tracing::warn!("Failed to create user_role_profiles entry for {}: {}", role_key, e);
|
||||
// Non-fatal — the assignment is created; the profile row can be backfilled later.
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
StatusCode::OK,
|
||||
|
|
|
|||
|
|
@ -72,8 +72,7 @@ async fn main() {
|
|||
.nest("/api/admin/approvals", handlers::approvals::router())
|
||||
.nest("/api/admin/verifications", handlers::verifications::router())
|
||||
// ── Me: Profile Status + Verification Status ──────────────────────
|
||||
.nest("/api/me", handlers::onboarding::me_router())
|
||||
.nest("/api/me", handlers::profile::me_verification_router())
|
||||
.nest("/api/me", handlers::profile::me_router())
|
||||
// ── Profile (save + submit-for-verification) ──────────────────────
|
||||
.nest("/api/profile", handlers::profile::router())
|
||||
// ── Onboarding State (legacy, kept for compatibility) ────────────
|
||||
|
|
@ -97,7 +96,8 @@ async fn main() {
|
|||
.nest("/api/support/tickets", handlers::support::user_router())
|
||||
// ── Support Tickets (admin) ───────────────────────────────────────
|
||||
.nest("/api/admin/support-cases", handlers::support::admin_router())
|
||||
// ── Reviews (admin) ───────────────────────────────────────────────
|
||||
// ── Reviews (public + admin) ─────────────────────────────────────
|
||||
.nest("/api/reviews", handlers::reviews::public_router())
|
||||
.nest("/api/admin/reviews", handlers::reviews::admin_router())
|
||||
// ── Coupons & Discounts (admin) ───────────────────────────────────
|
||||
.nest("/api/admin/coupons", handlers::coupons::coupons_router())
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
jsonwebtoken = "10.3"
|
||||
jsonwebtoken = { version = "10.3", features = ["rust_crypto"] }
|
||||
argon2 = "0.5"
|
||||
rand_core = { version = "0.6", features = ["std"] }
|
||||
serde = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ chrono = { workspace = true }
|
|||
anyhow = { workspace = true }
|
||||
sqlx = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
jsonwebtoken = "10.3"
|
||||
jsonwebtoken = { version = "10.3", features = ["rust_crypto"] }
|
||||
db = { path = "../db" }
|
||||
cache = { path = "../cache" }
|
||||
storage = { path = "../storage" }
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ pub struct PaginationQuery {
|
|||
#[derive(Deserialize)]
|
||||
pub struct LeadRequestPayload {
|
||||
pub requirement_id: Uuid,
|
||||
#[serde(default)]
|
||||
pub message: Option<String>,
|
||||
}
|
||||
|
||||
/// Build the shared Router that every profession service merges into its own Router.
|
||||
|
|
@ -189,10 +191,12 @@ async fn send_lead_request(
|
|||
return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response();
|
||||
}
|
||||
|
||||
let db_payload = CreateLeadRequestPayload {
|
||||
user_role_profile_id: user_role_profile.id,
|
||||
expires_at: Utc::now() + chrono::Duration::hours(24),
|
||||
};
|
||||
let db_payload = CreateLeadRequestPayload::new(
|
||||
req.id,
|
||||
user_role_profile.id,
|
||||
auth.user_id,
|
||||
payload.message.clone(),
|
||||
);
|
||||
|
||||
match LeadRequestRepository::create(&state.pool, db_payload).await {
|
||||
Ok(lead) => {
|
||||
|
|
@ -456,7 +460,7 @@ async fn cancel_request(
|
|||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
|
||||
};
|
||||
|
||||
if lead.user_role_profile_id != user_role_profile.id {
|
||||
if lead.user_role_profile_id != Some(user_role_profile.id) {
|
||||
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Access denied" }))).into_response();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS leads;
|
||||
27
crates/db/migrations/20260610003321_create_leads.up.sql
Normal file
27
crates/db/migrations/20260610003321_create_leads.up.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
-- Create the leads table (also called "requirements" in some contexts)
|
||||
-- Required by RequirementRepository in crates/db/src/models/requirement.rs
|
||||
CREATE TABLE IF NOT EXISTS leads (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
profession_key VARCHAR(100) NOT NULL,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
location VARCHAR(255) NOT NULL DEFAULT '',
|
||||
budget_inr INT,
|
||||
required_date DATE,
|
||||
extra_data_json JSONB,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||
rejection_reason TEXT,
|
||||
request_count INT NOT NULL DEFAULT 0,
|
||||
accepted_count INT NOT NULL DEFAULT 0,
|
||||
expires_at TIMESTAMPTZ,
|
||||
approved_at TIMESTAMPTZ,
|
||||
approved_by UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_by_user_id UUID,
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_status ON leads(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_profession_key ON leads(profession_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_created_by_user_id ON leads(created_by_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_leads_status_profession ON leads(status, profession_key);
|
||||
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS lead_requests;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
-- Create the lead_requests table for professional responses to leads
|
||||
CREATE TABLE IF NOT EXISTS lead_requests (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
lead_id UUID NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
|
||||
user_role_profile_id UUID NOT NULL,
|
||||
customer_user_id UUID NOT NULL,
|
||||
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||
tracecoins_reserved INT NOT NULL DEFAULT 0,
|
||||
message TEXT,
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
accepted_at TIMESTAMPTZ,
|
||||
rejected_at TIMESTAMPTZ,
|
||||
rejected_reason TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_lead_id ON lead_requests(lead_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_user_role_profile_id ON lead_requests(user_role_profile_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_customer_user_id ON lead_requests(customer_user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_lead_requests_status ON lead_requests(status);
|
||||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS niche_tags;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
-- Fix UGC Content Creator schema: rename content_niches→niche_tags (matches Rust model)
|
||||
ALTER TABLE ugc_content_creator_profiles
|
||||
ADD COLUMN IF NOT EXISTS niche_tags TEXT[] DEFAULT '{}';
|
||||
|
||||
UPDATE ugc_content_creator_profiles
|
||||
SET niche_tags = COALESCE(content_niches, '{}')
|
||||
WHERE niche_tags IS NULL;
|
||||
|
|
@ -52,6 +52,20 @@ pub struct UpsertCompanyProfilePayload {
|
|||
pub struct CompanyRepository;
|
||||
|
||||
impl CompanyRepository {
|
||||
async fn is_dummy_account(pool: &PgPool, user_id: Uuid) -> Result<bool, sqlx::Error> {
|
||||
let email = sqlx::query_scalar::<_, String>("SELECT email FROM users WHERE id = $1")
|
||||
.bind(user_id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(
|
||||
email.ends_with("@demo.com")
|
||||
|| email == "paymentgateway@demo.com"
|
||||
|| email.contains("+dummy@")
|
||||
|| email.starts_with("dummy+"),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn get_by_user_id(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
|
|
@ -81,6 +95,9 @@ impl CompanyRepository {
|
|||
user_id: Uuid,
|
||||
payload: UpsertCompanyProfilePayload,
|
||||
) -> Result<CompanyProfile, sqlx::Error> {
|
||||
let is_dummy_account = Self::is_dummy_account(pool, user_id).await?;
|
||||
let default_status = if is_dummy_account { "APPROVED" } else { "PENDING" };
|
||||
|
||||
let profile = sqlx::query_as::<_, CompanyProfile>(
|
||||
r#"
|
||||
INSERT INTO company_profiles (
|
||||
|
|
@ -88,7 +105,7 @@ impl CompanyRepository {
|
|||
employee_count, business_type, gst_number, contact_name,
|
||||
contact_email, contact_phone, address_line1, city, state, postal_code, status
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, 'PENDING')
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
company_name = EXCLUDED.company_name,
|
||||
registration_number = EXCLUDED.registration_number,
|
||||
|
|
@ -105,6 +122,7 @@ impl CompanyRepository {
|
|||
state = EXCLUDED.state,
|
||||
postal_code = EXCLUDED.postal_code,
|
||||
status = CASE
|
||||
WHEN $17 THEN 'APPROVED'
|
||||
WHEN company_profiles.status = 'APPROVED' THEN 'APPROVED'
|
||||
ELSE 'PENDING'
|
||||
END,
|
||||
|
|
@ -133,6 +151,8 @@ impl CompanyRepository {
|
|||
.bind(payload.city)
|
||||
.bind(payload.state)
|
||||
.bind(payload.postal_code)
|
||||
.bind(default_status)
|
||||
.bind(is_dummy_account)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
|
|
@ -143,10 +163,13 @@ impl CompanyRepository {
|
|||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
) -> Result<CompanyProfile, sqlx::Error> {
|
||||
let is_dummy_account = Self::is_dummy_account(pool, user_id).await?;
|
||||
let next_status = if is_dummy_account { "APPROVED" } else { "PENDING_REVIEW" };
|
||||
|
||||
let profile = sqlx::query_as::<_, CompanyProfile>(
|
||||
r#"
|
||||
UPDATE company_profiles
|
||||
SET status = 'PENDING_REVIEW', updated_at = NOW()
|
||||
SET status = $2, updated_at = NOW()
|
||||
WHERE user_id = $1
|
||||
RETURNING
|
||||
id, user_id, company_name, registration_number, industry,
|
||||
|
|
@ -158,6 +181,7 @@ impl CompanyRepository {
|
|||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(next_status)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,20 +6,41 @@ use uuid::Uuid;
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct LeadRequest {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub lead_id: Option<Uuid>,
|
||||
pub user_role_profile_id: Option<Uuid>,
|
||||
pub professional_user_id: Option<Uuid>,
|
||||
pub status: String,
|
||||
pub tracecoins_reserved: i32,
|
||||
pub remarks: Option<String>,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub requested_at: DateTime<Utc>,
|
||||
pub resolved_at: Option<DateTime<Utc>>,
|
||||
pub remarks: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateLeadRequestPayload {
|
||||
pub lead_id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub professional_user_id: Uuid,
|
||||
pub remarks: Option<String>,
|
||||
}
|
||||
|
||||
impl CreateLeadRequestPayload {
|
||||
pub fn new(
|
||||
lead_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
professional_user_id: Uuid,
|
||||
remarks: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
lead_id,
|
||||
user_role_profile_id,
|
||||
professional_user_id,
|
||||
remarks,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LeadRequestRepository;
|
||||
|
|
@ -31,13 +52,15 @@ impl LeadRequestRepository {
|
|||
) -> Result<LeadRequest, sqlx::Error> {
|
||||
let req = sqlx::query_as::<_, LeadRequest>(
|
||||
r#"
|
||||
INSERT INTO lead_requests (user_role_profile_id, expires_at)
|
||||
VALUES ($1, $2)
|
||||
INSERT INTO lead_requests (lead_id, user_role_profile_id, professional_user_id, remarks)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(payload.lead_id)
|
||||
.bind(payload.user_role_profile_id)
|
||||
.bind(payload.expires_at)
|
||||
.bind(payload.professional_user_id)
|
||||
.bind(&payload.remarks)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
|
|
@ -92,6 +115,7 @@ impl LeadRequestRepository {
|
|||
.bind(id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
Ok(req)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,30 @@ impl VerificationRepository {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn create_approved(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
role_key: &str,
|
||||
case_type: &str,
|
||||
payload: serde_json::Value,
|
||||
documents: serde_json::Value,
|
||||
) -> Result<Verification, sqlx::Error> {
|
||||
sqlx::query_as::<_, Verification>(
|
||||
r#"
|
||||
INSERT INTO verifications (user_id, role_key, case_type, priority, status, payload, documents, reviewed_at, reviewer_notes)
|
||||
VALUES ($1, $2, $3, 'MEDIUM', 'APPROVED', $4, $5, NOW(), 'Auto-approved for demo account')
|
||||
RETURNING *
|
||||
"#
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(role_key)
|
||||
.bind(case_type)
|
||||
.bind(payload)
|
||||
.bind(documents)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Verification>, sqlx::Error> {
|
||||
sqlx::query_as::<_, Verification>("SELECT * FROM verifications WHERE id = $1")
|
||||
.bind(id)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue