Spaces:
Runtime error
Runtime error
Upload 100 files
#1
by harshbhatiyaa - opened
This view is limited to 50 files because it contains too many changes. See the raw diff here.
- Dockerfile +19 -0
- alembic.ini +149 -0
- app/__init__.py +1 -0
- app/__pycache__/__init__.cpython-314.pyc +0 -0
- app/__pycache__/main.cpython-314.pyc +0 -0
- app/core/__init__.py +1 -0
- app/core/__pycache__/__init__.cpython-314.pyc +0 -0
- app/core/__pycache__/config.cpython-314.pyc +0 -0
- app/core/__pycache__/database.cpython-314.pyc +0 -0
- app/core/__pycache__/security.cpython-314.pyc +0 -0
- app/core/__pycache__/seeding.cpython-314.pyc +0 -0
- app/core/config.py +20 -0
- app/core/database.py +29 -0
- app/core/security.py +35 -0
- app/core/seeding.py +191 -0
- app/main.py +81 -0
- app/models/__init__.py +24 -0
- app/models/__pycache__/__init__.cpython-314.pyc +0 -0
- app/models/__pycache__/alerts.cpython-314.pyc +0 -0
- app/models/__pycache__/base.cpython-314.pyc +0 -0
- app/models/__pycache__/bookings.cpython-314.pyc +0 -0
- app/models/__pycache__/jobs.cpython-314.pyc +0 -0
- app/models/__pycache__/notifications.cpython-314.pyc +0 -0
- app/models/__pycache__/reviews.cpython-314.pyc +0 -0
- app/models/__pycache__/roster.cpython-314.pyc +0 -0
- app/models/__pycache__/users.cpython-314.pyc +0 -0
- app/models/__pycache__/wallet.cpython-314.pyc +0 -0
- app/models/alerts.py +10 -0
- app/models/base.py +14 -0
- app/models/bookings.py +20 -0
- app/models/jobs.py +17 -0
- app/models/notifications.py +15 -0
- app/models/reviews.py +20 -0
- app/models/roster.py +17 -0
- app/models/users.py +48 -0
- app/models/wallet.py +17 -0
- app/repositories/__init__.py +15 -0
- app/repositories/__pycache__/__init__.cpython-314.pyc +0 -0
- app/repositories/__pycache__/alerts.cpython-314.pyc +0 -0
- app/repositories/__pycache__/bookings.cpython-314.pyc +0 -0
- app/repositories/__pycache__/jobs.cpython-314.pyc +0 -0
- app/repositories/__pycache__/roster.cpython-314.pyc +0 -0
- app/repositories/__pycache__/users.cpython-314.pyc +0 -0
- app/repositories/__pycache__/wallet.cpython-314.pyc +0 -0
- app/repositories/alerts.py +34 -0
- app/repositories/bookings.py +63 -0
- app/repositories/jobs.py +42 -0
- app/repositories/roster.py +44 -0
- app/repositories/users.py +94 -0
- app/repositories/wallet.py +48 -0
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /code
|
| 4 |
+
|
| 5 |
+
# Install system libraries needed for database drivers
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
libpq-dev \
|
| 9 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
+
|
| 11 |
+
# Copy requirements and install dependencies in binary mode to avoid compiler blocks
|
| 12 |
+
COPY ./requirements.txt /code/requirements.txt
|
| 13 |
+
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt --only-binary :all:
|
| 14 |
+
|
| 15 |
+
# Copy the rest of the application files
|
| 16 |
+
COPY . /code
|
| 17 |
+
|
| 18 |
+
# Hugging Face Spaces routes internal traffic to port 7860
|
| 19 |
+
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "7860"]
|
alembic.ini
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# A generic, single database configuration.
|
| 2 |
+
|
| 3 |
+
[alembic]
|
| 4 |
+
# path to migration scripts.
|
| 5 |
+
# this is typically a path given in POSIX (e.g. forward slashes)
|
| 6 |
+
# format, relative to the token %(here)s which refers to the location of this
|
| 7 |
+
# ini file
|
| 8 |
+
script_location = %(here)s/migrations
|
| 9 |
+
|
| 10 |
+
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
|
| 11 |
+
# Uncomment the line below if you want the files to be prepended with date and time
|
| 12 |
+
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
|
| 13 |
+
# for all available tokens
|
| 14 |
+
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
|
| 15 |
+
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
|
| 16 |
+
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
|
| 17 |
+
|
| 18 |
+
# sys.path path, will be prepended to sys.path if present.
|
| 19 |
+
# defaults to the current working directory. for multiple paths, the path separator
|
| 20 |
+
# is defined by "path_separator" below.
|
| 21 |
+
prepend_sys_path = .
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
# timezone to use when rendering the date within the migration file
|
| 25 |
+
# as well as the filename.
|
| 26 |
+
# If specified, requires the tzdata library which can be installed by adding
|
| 27 |
+
# `alembic[tz]` to the pip requirements.
|
| 28 |
+
# string value is passed to ZoneInfo()
|
| 29 |
+
# leave blank for localtime
|
| 30 |
+
# timezone =
|
| 31 |
+
|
| 32 |
+
# max length of characters to apply to the "slug" field
|
| 33 |
+
# truncate_slug_length = 40
|
| 34 |
+
|
| 35 |
+
# set to 'true' to run the environment during
|
| 36 |
+
# the 'revision' command, regardless of autogenerate
|
| 37 |
+
# revision_environment = false
|
| 38 |
+
|
| 39 |
+
# set to 'true' to allow .pyc and .pyo files without
|
| 40 |
+
# a source .py file to be detected as revisions in the
|
| 41 |
+
# versions/ directory
|
| 42 |
+
# sourceless = false
|
| 43 |
+
|
| 44 |
+
# version location specification; This defaults
|
| 45 |
+
# to <script_location>/versions. When using multiple version
|
| 46 |
+
# directories, initial revisions must be specified with --version-path.
|
| 47 |
+
# The path separator used here should be the separator specified by "path_separator"
|
| 48 |
+
# below.
|
| 49 |
+
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
|
| 50 |
+
|
| 51 |
+
# path_separator; This indicates what character is used to split lists of file
|
| 52 |
+
# paths, including version_locations and prepend_sys_path within configparser
|
| 53 |
+
# files such as alembic.ini.
|
| 54 |
+
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
|
| 55 |
+
# to provide os-dependent path splitting.
|
| 56 |
+
#
|
| 57 |
+
# Note that in order to support legacy alembic.ini files, this default does NOT
|
| 58 |
+
# take place if path_separator is not present in alembic.ini. If this
|
| 59 |
+
# option is omitted entirely, fallback logic is as follows:
|
| 60 |
+
#
|
| 61 |
+
# 1. Parsing of the version_locations option falls back to using the legacy
|
| 62 |
+
# "version_path_separator" key, which if absent then falls back to the legacy
|
| 63 |
+
# behavior of splitting on spaces and/or commas.
|
| 64 |
+
# 2. Parsing of the prepend_sys_path option falls back to the legacy
|
| 65 |
+
# behavior of splitting on spaces, commas, or colons.
|
| 66 |
+
#
|
| 67 |
+
# Valid values for path_separator are:
|
| 68 |
+
#
|
| 69 |
+
# path_separator = :
|
| 70 |
+
# path_separator = ;
|
| 71 |
+
# path_separator = space
|
| 72 |
+
# path_separator = newline
|
| 73 |
+
#
|
| 74 |
+
# Use os.pathsep. Default configuration used for new projects.
|
| 75 |
+
path_separator = os
|
| 76 |
+
|
| 77 |
+
# set to 'true' to search source files recursively
|
| 78 |
+
# in each "version_locations" directory
|
| 79 |
+
# new in Alembic version 1.10
|
| 80 |
+
# recursive_version_locations = false
|
| 81 |
+
|
| 82 |
+
# the output encoding used when revision files
|
| 83 |
+
# are written from script.py.mako
|
| 84 |
+
# output_encoding = utf-8
|
| 85 |
+
|
| 86 |
+
# database URL. This is consumed by the user-maintained env.py script only.
|
| 87 |
+
# other means of configuring database URLs may be customized within the env.py
|
| 88 |
+
# file.
|
| 89 |
+
sqlalchemy.url = driver://user:pass@localhost/dbname
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
[post_write_hooks]
|
| 93 |
+
# post_write_hooks defines scripts or Python functions that are run
|
| 94 |
+
# on newly generated revision scripts. See the documentation for further
|
| 95 |
+
# detail and examples
|
| 96 |
+
|
| 97 |
+
# format using "black" - use the console_scripts runner, against the "black" entrypoint
|
| 98 |
+
# hooks = black
|
| 99 |
+
# black.type = console_scripts
|
| 100 |
+
# black.entrypoint = black
|
| 101 |
+
# black.options = -l 79 REVISION_SCRIPT_FILENAME
|
| 102 |
+
|
| 103 |
+
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
|
| 104 |
+
# hooks = ruff
|
| 105 |
+
# ruff.type = module
|
| 106 |
+
# ruff.module = ruff
|
| 107 |
+
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
| 108 |
+
|
| 109 |
+
# Alternatively, use the exec runner to execute a binary found on your PATH
|
| 110 |
+
# hooks = ruff
|
| 111 |
+
# ruff.type = exec
|
| 112 |
+
# ruff.executable = ruff
|
| 113 |
+
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
|
| 114 |
+
|
| 115 |
+
# Logging configuration. This is also consumed by the user-maintained
|
| 116 |
+
# env.py script only.
|
| 117 |
+
[loggers]
|
| 118 |
+
keys = root,sqlalchemy,alembic
|
| 119 |
+
|
| 120 |
+
[handlers]
|
| 121 |
+
keys = console
|
| 122 |
+
|
| 123 |
+
[formatters]
|
| 124 |
+
keys = generic
|
| 125 |
+
|
| 126 |
+
[logger_root]
|
| 127 |
+
level = WARNING
|
| 128 |
+
handlers = console
|
| 129 |
+
qualname =
|
| 130 |
+
|
| 131 |
+
[logger_sqlalchemy]
|
| 132 |
+
level = WARNING
|
| 133 |
+
handlers =
|
| 134 |
+
qualname = sqlalchemy.engine
|
| 135 |
+
|
| 136 |
+
[logger_alembic]
|
| 137 |
+
level = INFO
|
| 138 |
+
handlers =
|
| 139 |
+
qualname = alembic
|
| 140 |
+
|
| 141 |
+
[handler_console]
|
| 142 |
+
class = StreamHandler
|
| 143 |
+
args = (sys.stderr,)
|
| 144 |
+
level = NOTSET
|
| 145 |
+
formatter = generic
|
| 146 |
+
|
| 147 |
+
[formatter_generic]
|
| 148 |
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
| 149 |
+
datefmt = %H:%M:%S
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# ConstructHire app package
|
app/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (171 Bytes). View file
|
|
|
app/__pycache__/main.cpython-314.pyc
ADDED
|
Binary file (3.18 kB). View file
|
|
|
app/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Core configuration and security
|
app/core/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (176 Bytes). View file
|
|
|
app/core/__pycache__/config.cpython-314.pyc
ADDED
|
Binary file (1.56 kB). View file
|
|
|
app/core/__pycache__/database.cpython-314.pyc
ADDED
|
Binary file (1.15 kB). View file
|
|
|
app/core/__pycache__/security.cpython-314.pyc
ADDED
|
Binary file (2.74 kB). View file
|
|
|
app/core/__pycache__/seeding.cpython-314.pyc
ADDED
|
Binary file (6.35 kB). View file
|
|
|
app/core/config.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pydantic_settings import BaseSettings
|
| 3 |
+
|
| 4 |
+
class Settings(BaseSettings):
|
| 5 |
+
PROJECT_NAME: str = "ConstructHire API"
|
| 6 |
+
API_V1_STR: str = "/api"
|
| 7 |
+
|
| 8 |
+
# Security config
|
| 9 |
+
# In production, this must be a secure random hex string read from environment
|
| 10 |
+
SECRET_KEY: str = os.getenv("SECRET_KEY", "09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7")
|
| 11 |
+
ALGORITHM: str = "HS256"
|
| 12 |
+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 7 # 7 days for BCA viva convenience
|
| 13 |
+
|
| 14 |
+
# Database settings
|
| 15 |
+
DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/constructhire")
|
| 16 |
+
|
| 17 |
+
class Config:
|
| 18 |
+
case_sensitive = True
|
| 19 |
+
|
| 20 |
+
settings = Settings()
|
app/core/database.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from sqlalchemy import create_engine
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
|
| 5 |
+
# Database URL configuration
|
| 6 |
+
# For local development, default to SQLite fallback.
|
| 7 |
+
# For production/staging, it will read PostgreSQL from environment variables.
|
| 8 |
+
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./constructhire.db")
|
| 9 |
+
|
| 10 |
+
# Support SQLite configuration parameters
|
| 11 |
+
if DATABASE_URL.startswith("sqlite"):
|
| 12 |
+
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
| 13 |
+
else:
|
| 14 |
+
# Use pool_pre_ping=True to prevent stale connection errors with Supabase/Render
|
| 15 |
+
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 19 |
+
|
| 20 |
+
def get_db():
|
| 21 |
+
"""
|
| 22 |
+
Database session generator to be used as a FastAPI dependency.
|
| 23 |
+
Yields a database session and ensures it is closed after the request is finished.
|
| 24 |
+
"""
|
| 25 |
+
db = SessionLocal()
|
| 26 |
+
try:
|
| 27 |
+
yield db
|
| 28 |
+
finally:
|
| 29 |
+
db.close()
|
app/core/security.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import bcrypt
|
| 2 |
+
from datetime import datetime, timedelta
|
| 3 |
+
from typing import Any, Union
|
| 4 |
+
from jose import jwt
|
| 5 |
+
from backend.app.core.config import settings
|
| 6 |
+
|
| 7 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 8 |
+
"""Verify if a plain text password matches its hashed bcrypt value."""
|
| 9 |
+
if not hashed_password:
|
| 10 |
+
return False
|
| 11 |
+
try:
|
| 12 |
+
return bcrypt.checkpw(
|
| 13 |
+
plain_password.encode("utf-8"),
|
| 14 |
+
hashed_password.encode("utf-8")
|
| 15 |
+
)
|
| 16 |
+
except Exception:
|
| 17 |
+
return False
|
| 18 |
+
|
| 19 |
+
def get_password_hash(password: str) -> str:
|
| 20 |
+
"""Generate a secure bcrypt hash of a plain text password."""
|
| 21 |
+
# Generate salt and hash the password
|
| 22 |
+
salt = bcrypt.gensalt()
|
| 23 |
+
hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
|
| 24 |
+
return hashed.decode("utf-8")
|
| 25 |
+
|
| 26 |
+
def create_access_token(subject: Union[str, Any], expires_delta: timedelta = None) -> str:
|
| 27 |
+
"""Generate a JWT access token for a subject (usually user phone)."""
|
| 28 |
+
if expires_delta:
|
| 29 |
+
expire = datetime.utcnow() + expires_delta
|
| 30 |
+
else:
|
| 31 |
+
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 32 |
+
|
| 33 |
+
to_encode = {"exp": expire, "sub": str(subject)}
|
| 34 |
+
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
| 35 |
+
return encoded_jwt
|
app/core/seeding.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from backend.app.models.users import User, WorkerProfile, CustomerProfile
|
| 3 |
+
from backend.app.models.jobs import Job
|
| 4 |
+
from backend.app.models.bookings import Booking
|
| 5 |
+
from backend.app.models.wallet import WalletTransaction
|
| 6 |
+
from backend.app.models.roster import RosterWorker
|
| 7 |
+
from backend.app.models.alerts import AdminAlert
|
| 8 |
+
from backend.app.core.security import get_password_hash
|
| 9 |
+
|
| 10 |
+
def seed_db(db: Session):
|
| 11 |
+
"""Pre-populates the database with initial viva seed data if empty."""
|
| 12 |
+
# Check if DB is already seeded
|
| 13 |
+
if db.query(User).first() is not None:
|
| 14 |
+
print("Database already seeded.")
|
| 15 |
+
return False
|
| 16 |
+
|
| 17 |
+
print("Seeding database...")
|
| 18 |
+
|
| 19 |
+
# 1. Create Default Admin
|
| 20 |
+
admin = User(
|
| 21 |
+
name="Chandan Admin",
|
| 22 |
+
phone="9999999999",
|
| 23 |
+
role="admin",
|
| 24 |
+
password_hash=get_password_hash("admin123")
|
| 25 |
+
)
|
| 26 |
+
db.add(admin)
|
| 27 |
+
|
| 28 |
+
# 2. Create Default Customer
|
| 29 |
+
customer = User(
|
| 30 |
+
name="Harsh",
|
| 31 |
+
phone="9876543210",
|
| 32 |
+
role="customer",
|
| 33 |
+
password_hash=get_password_hash("pass123")
|
| 34 |
+
)
|
| 35 |
+
db.add(customer)
|
| 36 |
+
db.flush()
|
| 37 |
+
|
| 38 |
+
cust_profile = CustomerProfile(user_id=customer.id, wallet_balance=8200)
|
| 39 |
+
db.add(cust_profile)
|
| 40 |
+
|
| 41 |
+
# 3. Create Default Mediator
|
| 42 |
+
mediator = User(
|
| 43 |
+
name="Rafiq Thekedar",
|
| 44 |
+
phone="9876599999",
|
| 45 |
+
role="mediator",
|
| 46 |
+
password_hash=get_password_hash("pass123")
|
| 47 |
+
)
|
| 48 |
+
db.add(mediator)
|
| 49 |
+
db.flush()
|
| 50 |
+
|
| 51 |
+
med_cust_profile = CustomerProfile(user_id=mediator.id, wallet_balance=5000)
|
| 52 |
+
db.add(med_cust_profile)
|
| 53 |
+
|
| 54 |
+
# 4. Create Seed Workers
|
| 55 |
+
workers_data = [
|
| 56 |
+
("Ramesh Kumar", "9876500001", "Mason", 650, 4.8, 126, 97, 67, 34, True, True),
|
| 57 |
+
("Imran Ali", "9876500002", "Electrician", 800, 4.7, 88, 95, 35, 45, True, True),
|
| 58 |
+
("Sita Devi", "9876500003", "Painter", 700, 4.9, 112, 99, 58, 72, True, True),
|
| 59 |
+
("Babulal Meena", "9876500004", "Helper", 500, 4.4, 46, 91, 26, 67, True, False),
|
| 60 |
+
("Karan Singh", "9876500005", "Carpenter", 750, 4.6, 73, 92, 74, 58, False, True),
|
| 61 |
+
("Mohan Lal", "9876500006", "Plumber", 720, 4.5, 65, 93, 46, 28, True, True),
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
workers = []
|
| 65 |
+
for name, phone, skill, rate, rating, completed, completion, x, y, online, verified in workers_data:
|
| 66 |
+
user = User(
|
| 67 |
+
name=name,
|
| 68 |
+
phone=phone,
|
| 69 |
+
role="worker",
|
| 70 |
+
password_hash=get_password_hash("pass123")
|
| 71 |
+
)
|
| 72 |
+
db.add(user)
|
| 73 |
+
db.flush()
|
| 74 |
+
|
| 75 |
+
# Calculate distance
|
| 76 |
+
import math
|
| 77 |
+
dist = round(math.sqrt((x - 50)**2 + (y - 50)**2) / 15, 1) or 1.2
|
| 78 |
+
|
| 79 |
+
prof = WorkerProfile(
|
| 80 |
+
user_id=user.id,
|
| 81 |
+
skill=skill,
|
| 82 |
+
rate=rate,
|
| 83 |
+
rating=rating,
|
| 84 |
+
distance=dist,
|
| 85 |
+
online=online,
|
| 86 |
+
verified=verified,
|
| 87 |
+
completed_jobs=completed,
|
| 88 |
+
completion_rate=completion,
|
| 89 |
+
map_x=x,
|
| 90 |
+
map_y=y
|
| 91 |
+
)
|
| 92 |
+
db.add(prof)
|
| 93 |
+
workers.append(user)
|
| 94 |
+
|
| 95 |
+
db.flush()
|
| 96 |
+
|
| 97 |
+
# 5. Create Seed Jobs
|
| 98 |
+
jobs_data = [
|
| 99 |
+
("Mason", "Mansarovar, Jaipur", 1300, "POSTED", "Boundary wall repair and cement finishing."),
|
| 100 |
+
("Painter", "Vaishali Nagar", 2800, "ACCEPTED", "Two rooms wall putty and primer."),
|
| 101 |
+
("Electrician", "Malviya Nagar", 900, "IN_PROGRESS", "Switch board repair and fan installation."),
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
jobs = []
|
| 105 |
+
for skill, loc, budget, status, desc in jobs_data:
|
| 106 |
+
job = Job(
|
| 107 |
+
customer_id=customer.id,
|
| 108 |
+
skill_required=skill,
|
| 109 |
+
location=loc,
|
| 110 |
+
budget=budget,
|
| 111 |
+
description=desc,
|
| 112 |
+
status=status
|
| 113 |
+
)
|
| 114 |
+
db.add(job)
|
| 115 |
+
jobs.append(job)
|
| 116 |
+
|
| 117 |
+
db.flush()
|
| 118 |
+
|
| 119 |
+
# 6. Create Seed Bookings
|
| 120 |
+
bookings_data = [
|
| 121 |
+
("B-2401", "Boundary wall repair", workers[0].id, 1300, "ACCEPTED", jobs[0].id),
|
| 122 |
+
("B-2402", "Switch board repair", workers[1].id, 900, "IN_PROGRESS", jobs[2].id),
|
| 123 |
+
]
|
| 124 |
+
|
| 125 |
+
for code, title, worker_id, amount, status, job_id in bookings_data:
|
| 126 |
+
booking = Booking(
|
| 127 |
+
code=code,
|
| 128 |
+
customer_id=customer.id,
|
| 129 |
+
worker_id=worker_id,
|
| 130 |
+
amount=amount,
|
| 131 |
+
status=status,
|
| 132 |
+
job_id=job_id
|
| 133 |
+
)
|
| 134 |
+
db.add(booking)
|
| 135 |
+
|
| 136 |
+
db.flush()
|
| 137 |
+
|
| 138 |
+
# 7. Create Seed Wallet Transactions
|
| 139 |
+
tx_data = [
|
| 140 |
+
("Wallet top-up via UPI", 5000, "credit"),
|
| 141 |
+
("Escrow hold for B-2401", -1300, "hold"),
|
| 142 |
+
("Escrow hold for B-2402", -900, "hold"),
|
| 143 |
+
("Refund from cancelled booking", 600, "credit"),
|
| 144 |
+
]
|
| 145 |
+
|
| 146 |
+
for label, amount, tx_type in tx_data:
|
| 147 |
+
tx = WalletTransaction(
|
| 148 |
+
user_id=customer.id,
|
| 149 |
+
label=label,
|
| 150 |
+
amount=amount,
|
| 151 |
+
type=tx_type
|
| 152 |
+
)
|
| 153 |
+
db.add(tx)
|
| 154 |
+
|
| 155 |
+
# 8. Create Seed Roster Workers
|
| 156 |
+
roster_data = [
|
| 157 |
+
("Rafiq", "Helper", "IVR only", "Available", 1200),
|
| 158 |
+
("Sunita", "Painter", "Verified", "On job", 820),
|
| 159 |
+
("Dinesh", "Mason", "Verified", "Available", 1440),
|
| 160 |
+
]
|
| 161 |
+
|
| 162 |
+
for name, skill, phone_status, status, commission in roster_data:
|
| 163 |
+
roster_worker = RosterWorker(
|
| 164 |
+
mediator_id=mediator.id,
|
| 165 |
+
name=name,
|
| 166 |
+
skill=skill,
|
| 167 |
+
phone_status=phone_status,
|
| 168 |
+
status=status,
|
| 169 |
+
commission=commission
|
| 170 |
+
)
|
| 171 |
+
db.add(roster_worker)
|
| 172 |
+
|
| 173 |
+
# 9. Create Seed Admin Alerts
|
| 174 |
+
alerts_data = [
|
| 175 |
+
("Dispute", "B-2397 quality issue pending admin review", "red"),
|
| 176 |
+
("Fraud", "Duplicate Aadhaar hash detected for two accounts", "orange"),
|
| 177 |
+
("IVR", "23 non-smartphone workers received daily job alerts", "gray"),
|
| 178 |
+
]
|
| 179 |
+
|
| 180 |
+
for type_, text, severity in alerts_data:
|
| 181 |
+
alert = AdminAlert(
|
| 182 |
+
type=type_,
|
| 183 |
+
text=text,
|
| 184 |
+
severity=severity,
|
| 185 |
+
status="active"
|
| 186 |
+
)
|
| 187 |
+
db.add(alert)
|
| 188 |
+
|
| 189 |
+
db.commit()
|
| 190 |
+
print("Database seeded successfully!")
|
| 191 |
+
return True
|
app/main.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from fastapi import FastAPI, Depends, APIRouter
|
| 3 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
+
from sqlalchemy.orm import Session
|
| 5 |
+
|
| 6 |
+
from backend.app.core.config import settings
|
| 7 |
+
from backend.app.core.database import SessionLocal, engine, get_db
|
| 8 |
+
from backend.app.core.seeding import seed_db
|
| 9 |
+
from backend.app.models.base import Base
|
| 10 |
+
|
| 11 |
+
# Import Routers
|
| 12 |
+
from backend.app.routers import (
|
| 13 |
+
auth_router,
|
| 14 |
+
workers_router,
|
| 15 |
+
jobs_router,
|
| 16 |
+
bookings_router,
|
| 17 |
+
wallet_router,
|
| 18 |
+
roster_router,
|
| 19 |
+
admin_router
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Initialize database tables
|
| 23 |
+
# This is a safe fallback; in production migrations are preferred,
|
| 24 |
+
# but for the BCA viva, ensuring tables exist automatically is a lifesaver.
|
| 25 |
+
Base.metadata.create_all(bind=engine)
|
| 26 |
+
|
| 27 |
+
# Trigger Database Seeding on startup
|
| 28 |
+
db = SessionLocal()
|
| 29 |
+
try:
|
| 30 |
+
seed_db(db)
|
| 31 |
+
finally:
|
| 32 |
+
db.close()
|
| 33 |
+
|
| 34 |
+
app = FastAPI(
|
| 35 |
+
title=settings.PROJECT_NAME,
|
| 36 |
+
description="ConstructHire location-aware labor marketplace backend",
|
| 37 |
+
version="1.0.0",
|
| 38 |
+
docs_url="/docs",
|
| 39 |
+
redoc_url="/redoc"
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# CORS configuration
|
| 43 |
+
# Allow local Vite dev server and production netlify deployments
|
| 44 |
+
app.add_middleware(
|
| 45 |
+
CORSMiddleware,
|
| 46 |
+
allow_origins=["*"], # For BCA viva simplicity; in strict prod specify origins
|
| 47 |
+
allow_credentials=True,
|
| 48 |
+
allow_methods=["*"],
|
| 49 |
+
allow_headers=["*"],
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
# Register API Routers
|
| 53 |
+
api_router = APIRouter(prefix="/api")
|
| 54 |
+
api_router.include_router(auth_router)
|
| 55 |
+
api_router.include_router(workers_router)
|
| 56 |
+
api_router.include_router(jobs_router)
|
| 57 |
+
api_router.include_router(bookings_router)
|
| 58 |
+
api_router.include_router(wallet_router)
|
| 59 |
+
api_router.include_router(roster_router)
|
| 60 |
+
api_router.include_router(admin_router)
|
| 61 |
+
|
| 62 |
+
app.include_router(api_router)
|
| 63 |
+
|
| 64 |
+
@app.get("/")
|
| 65 |
+
def read_root():
|
| 66 |
+
"""Root endpoint to check backend status."""
|
| 67 |
+
return {
|
| 68 |
+
"status": "online",
|
| 69 |
+
"service": settings.PROJECT_NAME,
|
| 70 |
+
"docs": "/docs",
|
| 71 |
+
"api_prefix": settings.API_V1_STR
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
@app.post("/api/admin/reset-db")
|
| 75 |
+
def reset_database(db: Session = Depends(get_db)):
|
| 76 |
+
"""Convenience endpoint to clear and re-seed the database with demo values."""
|
| 77 |
+
# Drop all tables and recreate them
|
| 78 |
+
Base.metadata.drop_all(bind=engine)
|
| 79 |
+
Base.metadata.create_all(bind=engine)
|
| 80 |
+
seed_db(db)
|
| 81 |
+
return {"message": "Database successfully reset and re-seeded."}
|
app/models/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend.app.models.base import Base, BaseDBModel
|
| 2 |
+
from backend.app.models.users import User, WorkerProfile, CustomerProfile
|
| 3 |
+
from backend.app.models.jobs import Job
|
| 4 |
+
from backend.app.models.bookings import Booking
|
| 5 |
+
from backend.app.models.wallet import WalletTransaction
|
| 6 |
+
from backend.app.models.reviews import Review
|
| 7 |
+
from backend.app.models.notifications import Notification
|
| 8 |
+
from backend.app.models.roster import RosterWorker
|
| 9 |
+
from backend.app.models.alerts import AdminAlert
|
| 10 |
+
|
| 11 |
+
__all__ = [
|
| 12 |
+
"Base",
|
| 13 |
+
"BaseDBModel",
|
| 14 |
+
"User",
|
| 15 |
+
"WorkerProfile",
|
| 16 |
+
"CustomerProfile",
|
| 17 |
+
"Job",
|
| 18 |
+
"Booking",
|
| 19 |
+
"WalletTransaction",
|
| 20 |
+
"Review",
|
| 21 |
+
"Notification",
|
| 22 |
+
"RosterWorker",
|
| 23 |
+
"AdminAlert",
|
| 24 |
+
]
|
app/models/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (880 Bytes). View file
|
|
|
app/models/__pycache__/alerts.cpython-314.pyc
ADDED
|
Binary file (805 Bytes). View file
|
|
|
app/models/__pycache__/base.cpython-314.pyc
ADDED
|
Binary file (1.09 kB). View file
|
|
|
app/models/__pycache__/bookings.cpython-314.pyc
ADDED
|
Binary file (1.57 kB). View file
|
|
|
app/models/__pycache__/jobs.cpython-314.pyc
ADDED
|
Binary file (1.27 kB). View file
|
|
|
app/models/__pycache__/notifications.cpython-314.pyc
ADDED
|
Binary file (1.14 kB). View file
|
|
|
app/models/__pycache__/reviews.cpython-314.pyc
ADDED
|
Binary file (1.57 kB). View file
|
|
|
app/models/__pycache__/roster.cpython-314.pyc
ADDED
|
Binary file (1.22 kB). View file
|
|
|
app/models/__pycache__/users.cpython-314.pyc
ADDED
|
Binary file (2.95 kB). View file
|
|
|
app/models/__pycache__/wallet.cpython-314.pyc
ADDED
|
Binary file (1.28 kB). View file
|
|
|
app/models/alerts.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String
|
| 2 |
+
from backend.app.models.base import BaseDBModel
|
| 3 |
+
|
| 4 |
+
class AdminAlert(BaseDBModel):
|
| 5 |
+
__tablename__ = "admin_alerts"
|
| 6 |
+
|
| 7 |
+
type = Column(String, nullable=False, index=True) # Dispute, Fraud, IVR
|
| 8 |
+
text = Column(String, nullable=False)
|
| 9 |
+
severity = Column(String, nullable=False) # red, orange, gray
|
| 10 |
+
status = Column(String, nullable=False, default="active", index=True) # active, resolved
|
app/models/base.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from sqlalchemy import Column, DateTime
|
| 4 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 5 |
+
from sqlalchemy.orm import declarative_base
|
| 6 |
+
|
| 7 |
+
Base = declarative_base()
|
| 8 |
+
|
| 9 |
+
class BaseDBModel(Base):
|
| 10 |
+
__abstract__ = True
|
| 11 |
+
|
| 12 |
+
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4, index=True, nullable=False)
|
| 13 |
+
created_at = Column(DateTime, default=datetime.utcnow, nullable=False, index=True)
|
| 14 |
+
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
app/models/bookings.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Integer, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from backend.app.models.base import BaseDBModel
|
| 5 |
+
|
| 6 |
+
class Booking(BaseDBModel):
|
| 7 |
+
__tablename__ = "bookings"
|
| 8 |
+
|
| 9 |
+
code = Column(String, unique=True, index=True, nullable=False) # e.g. B-2401
|
| 10 |
+
job_id = Column(UUID(as_uuid=True), ForeignKey("jobs.id", ondelete="SET NULL"), nullable=True)
|
| 11 |
+
customer_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 12 |
+
worker_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 13 |
+
amount = Column(Integer, nullable=False)
|
| 14 |
+
status = Column(String, nullable=False, default="ACCEPTED", index=True) # ACCEPTED, IN_PROGRESS, COMPLETED, RATED, DISPUTED
|
| 15 |
+
|
| 16 |
+
# Relationships
|
| 17 |
+
job = relationship("Job", back_populates="bookings")
|
| 18 |
+
customer = relationship("User", foreign_keys=[customer_id])
|
| 19 |
+
worker = relationship("User", foreign_keys=[worker_id])
|
| 20 |
+
reviews = relationship("Review", back_populates="booking", cascade="all, delete-orphan")
|
app/models/jobs.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Integer, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from backend.app.models.base import BaseDBModel
|
| 5 |
+
|
| 6 |
+
class Job(BaseDBModel):
|
| 7 |
+
__tablename__ = "jobs"
|
| 8 |
+
|
| 9 |
+
customer_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 10 |
+
skill_required = Column(String, index=True, nullable=False)
|
| 11 |
+
location = Column(String, nullable=False)
|
| 12 |
+
budget = Column(Integer, nullable=False)
|
| 13 |
+
description = Column(String, nullable=False)
|
| 14 |
+
status = Column(String, nullable=False, default="POSTED", index=True) # POSTED, ACCEPTED, IN_PROGRESS, COMPLETED, CANCELLED
|
| 15 |
+
|
| 16 |
+
# Relationships
|
| 17 |
+
bookings = relationship("Booking", back_populates="job", cascade="all, delete-orphan")
|
app/models/notifications.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Boolean, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from backend.app.models.base import BaseDBModel
|
| 5 |
+
|
| 6 |
+
class Notification(BaseDBModel):
|
| 7 |
+
__tablename__ = "notifications"
|
| 8 |
+
|
| 9 |
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 10 |
+
title = Column(String, nullable=False)
|
| 11 |
+
message = Column(String, nullable=False)
|
| 12 |
+
read = Column(Boolean, nullable=False, default=False, index=True)
|
| 13 |
+
|
| 14 |
+
# Relationships
|
| 15 |
+
user = relationship("User", back_populates="notifications")
|
app/models/reviews.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Integer, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from backend.app.models.base import BaseDBModel
|
| 5 |
+
|
| 6 |
+
class Review(BaseDBModel):
|
| 7 |
+
__tablename__ = "reviews"
|
| 8 |
+
|
| 9 |
+
booking_id = Column(UUID(as_uuid=True), ForeignKey("bookings.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 10 |
+
reviewer_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 11 |
+
reviewee_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 12 |
+
rating = Column(Integer, nullable=False) # 1 to 5
|
| 13 |
+
comment = Column(String, nullable=True)
|
| 14 |
+
|
| 15 |
+
# Relationships
|
| 16 |
+
booking = relationship("Booking", back_populates="reviews")
|
| 17 |
+
reviewer = relationship("User", foreign_keys=[reviewer_id])
|
| 18 |
+
reviewee = relationship("User", foreign_keys=[reviewee_id])
|
| 19 |
+
class Review_Table_Init:
|
| 20 |
+
pass
|
app/models/roster.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Integer, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from backend.app.models.base import BaseDBModel
|
| 5 |
+
|
| 6 |
+
class RosterWorker(BaseDBModel):
|
| 7 |
+
__tablename__ = "roster_workers"
|
| 8 |
+
|
| 9 |
+
mediator_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 10 |
+
name = Column(String, nullable=False)
|
| 11 |
+
skill = Column(String, nullable=False)
|
| 12 |
+
phone_status = Column(String, nullable=False) # e.g. "IVR only", "Verified"
|
| 13 |
+
status = Column(String, nullable=False) # e.g. "Available", "On job"
|
| 14 |
+
commission = Column(Integer, nullable=False, default=0)
|
| 15 |
+
|
| 16 |
+
# Relationships
|
| 17 |
+
mediator = relationship("User", back_populates="roster_workers")
|
app/models/users.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Integer, Float, Boolean, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from backend.app.models.base import BaseDBModel
|
| 5 |
+
|
| 6 |
+
class User(BaseDBModel):
|
| 7 |
+
__tablename__ = "users"
|
| 8 |
+
|
| 9 |
+
name = Column(String, nullable=False)
|
| 10 |
+
phone = Column(String, unique=True, index=True, nullable=False)
|
| 11 |
+
role = Column(String, index=True, nullable=False) # admin, customer, worker, mediator
|
| 12 |
+
password_hash = Column(String, nullable=True) # can be null for demo OTP bypass
|
| 13 |
+
city = Column(String, nullable=False, default="Jaipur")
|
| 14 |
+
is_active = Column(Boolean, nullable=False, default=True)
|
| 15 |
+
|
| 16 |
+
# Relationships
|
| 17 |
+
worker_profile = relationship("WorkerProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
| 18 |
+
customer_profile = relationship("CustomerProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
|
| 19 |
+
notifications = relationship("Notification", back_populates="user", cascade="all, delete-orphan")
|
| 20 |
+
wallet_transactions = relationship("WalletTransaction", back_populates="user", cascade="all, delete-orphan")
|
| 21 |
+
roster_workers = relationship("RosterWorker", back_populates="mediator", cascade="all, delete-orphan")
|
| 22 |
+
|
| 23 |
+
class WorkerProfile(BaseDBModel):
|
| 24 |
+
__tablename__ = "worker_profiles"
|
| 25 |
+
|
| 26 |
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), unique=True, index=True, nullable=False)
|
| 27 |
+
skill = Column(String, index=True, nullable=False) # Mason, Electrician, Painter, etc.
|
| 28 |
+
rate = Column(Integer, nullable=False) # Daily wage in INR
|
| 29 |
+
rating = Column(Float, nullable=False, default=4.5)
|
| 30 |
+
distance = Column(Float, nullable=False, default=1.5) # Simulated distance in km
|
| 31 |
+
online = Column(Boolean, nullable=False, default=True)
|
| 32 |
+
verified = Column(Boolean, nullable=False, default=False) # Aadhaar verified
|
| 33 |
+
completed_jobs = Column(Integer, nullable=False, default=0)
|
| 34 |
+
completion_rate = Column(Integer, nullable=False, default=90) # percentage
|
| 35 |
+
map_x = Column(Integer, nullable=False, default=50) # Coordinate x on simulated map
|
| 36 |
+
map_y = Column(Integer, nullable=False, default=50) # Coordinate y on simulated map
|
| 37 |
+
|
| 38 |
+
# Relationships
|
| 39 |
+
user = relationship("User", back_populates="worker_profile")
|
| 40 |
+
|
| 41 |
+
class CustomerProfile(BaseDBModel):
|
| 42 |
+
__tablename__ = "customer_profiles"
|
| 43 |
+
|
| 44 |
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), unique=True, index=True, nullable=False)
|
| 45 |
+
wallet_balance = Column(Integer, nullable=False, default=8200)
|
| 46 |
+
|
| 47 |
+
# Relationships
|
| 48 |
+
user = relationship("User", back_populates="customer_profile")
|
app/models/wallet.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, String, Integer, ForeignKey
|
| 2 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 3 |
+
from sqlalchemy.orm import relationship
|
| 4 |
+
from backend.app.models.base import BaseDBModel
|
| 5 |
+
|
| 6 |
+
class WalletTransaction(BaseDBModel):
|
| 7 |
+
__tablename__ = "wallet_transactions"
|
| 8 |
+
|
| 9 |
+
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), index=True, nullable=False)
|
| 10 |
+
booking_id = Column(UUID(as_uuid=True), ForeignKey("bookings.id", ondelete="SET NULL"), nullable=True)
|
| 11 |
+
label = Column(String, nullable=False) # e.g. "Wallet top-up via UPI", "Escrow hold for B-2401"
|
| 12 |
+
amount = Column(Integer, nullable=False) # positive for credits/refunds, negative for debits/holds
|
| 13 |
+
type = Column(String, nullable=False, index=True) # credit, hold, release, refund
|
| 14 |
+
|
| 15 |
+
# Relationships
|
| 16 |
+
user = relationship("User", back_populates="wallet_transactions")
|
| 17 |
+
booking = relationship("Booking")
|
app/repositories/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from backend.app.repositories.users import UserRepository
|
| 2 |
+
from backend.app.repositories.jobs import JobRepository
|
| 3 |
+
from backend.app.repositories.bookings import BookingRepository
|
| 4 |
+
from backend.app.repositories.wallet import WalletRepository
|
| 5 |
+
from backend.app.repositories.roster import RosterRepository
|
| 6 |
+
from backend.app.repositories.alerts import AlertRepository
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"UserRepository",
|
| 10 |
+
"JobRepository",
|
| 11 |
+
"BookingRepository",
|
| 12 |
+
"WalletRepository",
|
| 13 |
+
"RosterRepository",
|
| 14 |
+
"AlertRepository",
|
| 15 |
+
]
|
app/repositories/__pycache__/__init__.cpython-314.pyc
ADDED
|
Binary file (674 Bytes). View file
|
|
|
app/repositories/__pycache__/alerts.cpython-314.pyc
ADDED
|
Binary file (3.63 kB). View file
|
|
|
app/repositories/__pycache__/bookings.cpython-314.pyc
ADDED
|
Binary file (6.99 kB). View file
|
|
|
app/repositories/__pycache__/jobs.cpython-314.pyc
ADDED
|
Binary file (4.63 kB). View file
|
|
|
app/repositories/__pycache__/roster.cpython-314.pyc
ADDED
|
Binary file (4.69 kB). View file
|
|
|
app/repositories/__pycache__/users.cpython-314.pyc
ADDED
|
Binary file (7.48 kB). View file
|
|
|
app/repositories/__pycache__/wallet.cpython-314.pyc
ADDED
|
Binary file (4.34 kB). View file
|
|
|
app/repositories/alerts.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from backend.app.models.alerts import AdminAlert
|
| 5 |
+
|
| 6 |
+
class AlertRepository:
|
| 7 |
+
def __init__(self, db: Session):
|
| 8 |
+
self.db = db
|
| 9 |
+
|
| 10 |
+
def create(self, type_: str, text: str, severity: str = "gray") -> AdminAlert:
|
| 11 |
+
alert = AdminAlert(
|
| 12 |
+
type=type_,
|
| 13 |
+
text=text,
|
| 14 |
+
severity=severity,
|
| 15 |
+
status="active"
|
| 16 |
+
)
|
| 17 |
+
self.db.add(alert)
|
| 18 |
+
self.db.commit()
|
| 19 |
+
self.db.refresh(alert)
|
| 20 |
+
return alert
|
| 21 |
+
|
| 22 |
+
def get_active(self) -> List[AdminAlert]:
|
| 23 |
+
return self.db.query(AdminAlert).filter(AdminAlert.status == "active").order_by(AdminAlert.created_at.asc()).all()
|
| 24 |
+
|
| 25 |
+
def get_by_id(self, alert_id: uuid.UUID) -> Optional[AdminAlert]:
|
| 26 |
+
return self.db.query(AdminAlert).filter(AdminAlert.id == alert_id).first()
|
| 27 |
+
|
| 28 |
+
def resolve(self, alert_id: uuid.UUID) -> Optional[AdminAlert]:
|
| 29 |
+
alert = self.get_by_id(alert_id)
|
| 30 |
+
if alert:
|
| 31 |
+
alert.status = "resolved"
|
| 32 |
+
self.db.commit()
|
| 33 |
+
self.db.refresh(alert)
|
| 34 |
+
return alert
|
app/repositories/bookings.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from backend.app.models.bookings import Booking
|
| 5 |
+
from backend.app.models.reviews import Review
|
| 6 |
+
|
| 7 |
+
class BookingRepository:
|
| 8 |
+
def __init__(self, db: Session):
|
| 9 |
+
self.db = db
|
| 10 |
+
|
| 11 |
+
def get_by_id(self, booking_id: uuid.UUID) -> Optional[Booking]:
|
| 12 |
+
return self.db.query(Booking).filter(Booking.id == booking_id).first()
|
| 13 |
+
|
| 14 |
+
def get_by_code(self, code: str) -> Optional[Booking]:
|
| 15 |
+
return self.db.query(Booking).filter(Booking.code == code).first()
|
| 16 |
+
|
| 17 |
+
def create(self, customer_id: uuid.UUID, worker_id: uuid.UUID, amount: int, job_id: Optional[uuid.UUID] = None) -> Booking:
|
| 18 |
+
# Generate code (B-24XX)
|
| 19 |
+
count = self.db.query(Booking).count()
|
| 20 |
+
code = f"B-{2401 + count}"
|
| 21 |
+
|
| 22 |
+
booking = Booking(
|
| 23 |
+
code=code,
|
| 24 |
+
customer_id=customer_id,
|
| 25 |
+
worker_id=worker_id,
|
| 26 |
+
job_id=job_id,
|
| 27 |
+
amount=amount,
|
| 28 |
+
status="ACCEPTED"
|
| 29 |
+
)
|
| 30 |
+
self.db.add(booking)
|
| 31 |
+
self.db.commit()
|
| 32 |
+
self.db.refresh(booking)
|
| 33 |
+
return booking
|
| 34 |
+
|
| 35 |
+
def get_by_customer(self, customer_id: uuid.UUID) -> List[Booking]:
|
| 36 |
+
return self.db.query(Booking).filter(Booking.customer_id == customer_id).order_by(Booking.created_at.desc()).all()
|
| 37 |
+
|
| 38 |
+
def get_by_worker(self, worker_id: uuid.UUID) -> List[Booking]:
|
| 39 |
+
return self.db.query(Booking).filter(Booking.worker_id == worker_id).order_by(Booking.created_at.desc()).all()
|
| 40 |
+
|
| 41 |
+
def get_all(self) -> List[Booking]:
|
| 42 |
+
return self.db.query(Booking).order_by(Booking.created_at.desc()).all()
|
| 43 |
+
|
| 44 |
+
def update_status(self, booking_id: uuid.UUID, status: str) -> Optional[Booking]:
|
| 45 |
+
booking = self.get_by_id(booking_id)
|
| 46 |
+
if booking:
|
| 47 |
+
booking.status = status
|
| 48 |
+
self.db.commit()
|
| 49 |
+
self.db.refresh(booking)
|
| 50 |
+
return booking
|
| 51 |
+
|
| 52 |
+
def add_review(self, booking_id: uuid.UUID, reviewer_id: uuid.UUID, reviewee_id: uuid.UUID, rating: int, comment: Optional[str] = None) -> Review:
|
| 53 |
+
review = Review(
|
| 54 |
+
booking_id=booking_id,
|
| 55 |
+
reviewer_id=reviewer_id,
|
| 56 |
+
reviewee_id=reviewee_id,
|
| 57 |
+
rating=rating,
|
| 58 |
+
comment=comment
|
| 59 |
+
)
|
| 60 |
+
self.db.add(review)
|
| 61 |
+
self.db.commit()
|
| 62 |
+
self.db.refresh(review)
|
| 63 |
+
return review
|
app/repositories/jobs.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from backend.app.models.jobs import Job
|
| 5 |
+
|
| 6 |
+
class JobRepository:
|
| 7 |
+
def __init__(self, db: Session):
|
| 8 |
+
self.db = db
|
| 9 |
+
|
| 10 |
+
def create(self, customer_id: uuid.UUID, skill_required: str, location: str, budget: int, description: str) -> Job:
|
| 11 |
+
job = Job(
|
| 12 |
+
customer_id=customer_id,
|
| 13 |
+
skill_required=skill_required,
|
| 14 |
+
location=location,
|
| 15 |
+
budget=budget,
|
| 16 |
+
description=description,
|
| 17 |
+
status="POSTED"
|
| 18 |
+
)
|
| 19 |
+
self.db.add(job)
|
| 20 |
+
self.db.commit()
|
| 21 |
+
self.db.refresh(job)
|
| 22 |
+
return job
|
| 23 |
+
|
| 24 |
+
def get_by_id(self, job_id: uuid.UUID) -> Optional[Job]:
|
| 25 |
+
return self.db.query(Job).filter(Job.id == job_id).first()
|
| 26 |
+
|
| 27 |
+
def get_all(self, skill: Optional[str] = None) -> List[Job]:
|
| 28 |
+
query = self.db.query(Job)
|
| 29 |
+
if skill and skill != "All":
|
| 30 |
+
query = query.filter(Job.skill_required == skill)
|
| 31 |
+
return query.order_by(Job.created_at.desc()).all()
|
| 32 |
+
|
| 33 |
+
def get_by_customer(self, customer_id: uuid.UUID) -> List[Job]:
|
| 34 |
+
return self.db.query(Job).filter(Job.customer_id == customer_id).order_by(Job.created_at.desc()).all()
|
| 35 |
+
|
| 36 |
+
def update_status(self, job_id: uuid.UUID, status: str) -> Optional[Job]:
|
| 37 |
+
job = self.get_by_id(job_id)
|
| 38 |
+
if job:
|
| 39 |
+
job.status = status
|
| 40 |
+
self.db.commit()
|
| 41 |
+
self.db.refresh(job)
|
| 42 |
+
return job
|
app/repositories/roster.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from backend.app.models.roster import RosterWorker
|
| 5 |
+
|
| 6 |
+
class RosterRepository:
|
| 7 |
+
def __init__(self, db: Session):
|
| 8 |
+
self.db = db
|
| 9 |
+
|
| 10 |
+
def create(self, mediator_id: uuid.UUID, name: str, skill: str, phone_status: str = "IVR only", status: str = "Available", commission: int = 0) -> RosterWorker:
|
| 11 |
+
worker = RosterWorker(
|
| 12 |
+
mediator_id=mediator_id,
|
| 13 |
+
name=name,
|
| 14 |
+
skill=skill,
|
| 15 |
+
phone_status=phone_status,
|
| 16 |
+
status=status,
|
| 17 |
+
commission=commission
|
| 18 |
+
)
|
| 19 |
+
self.db.add(worker)
|
| 20 |
+
self.db.commit()
|
| 21 |
+
self.db.refresh(worker)
|
| 22 |
+
return worker
|
| 23 |
+
|
| 24 |
+
def get_by_mediator(self, mediator_id: uuid.UUID) -> List[RosterWorker]:
|
| 25 |
+
return self.db.query(RosterWorker).filter(RosterWorker.mediator_id == mediator_id).order_by(RosterWorker.created_at.desc()).all()
|
| 26 |
+
|
| 27 |
+
def get_by_id(self, worker_id: uuid.UUID) -> Optional[RosterWorker]:
|
| 28 |
+
return self.db.query(RosterWorker).filter(RosterWorker.id == worker_id).first()
|
| 29 |
+
|
| 30 |
+
def update_status(self, worker_id: uuid.UUID, status: str) -> Optional[RosterWorker]:
|
| 31 |
+
worker = self.get_by_id(worker_id)
|
| 32 |
+
if worker:
|
| 33 |
+
worker.status = status
|
| 34 |
+
self.db.commit()
|
| 35 |
+
self.db.refresh(worker)
|
| 36 |
+
return worker
|
| 37 |
+
|
| 38 |
+
def add_commission(self, worker_id: uuid.UUID, amount: int) -> Optional[RosterWorker]:
|
| 39 |
+
worker = self.get_by_id(worker_id)
|
| 40 |
+
if worker:
|
| 41 |
+
worker.commission += amount
|
| 42 |
+
self.db.commit()
|
| 43 |
+
self.db.refresh(worker)
|
| 44 |
+
return worker
|
app/repositories/users.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from backend.app.models.users import User, WorkerProfile, CustomerProfile
|
| 5 |
+
|
| 6 |
+
class UserRepository:
|
| 7 |
+
def __init__(self, db: Session):
|
| 8 |
+
self.db = db
|
| 9 |
+
|
| 10 |
+
def get_by_id(self, user_id: uuid.UUID) -> Optional[User]:
|
| 11 |
+
return self.db.query(User).filter(User.id == user_id).first()
|
| 12 |
+
|
| 13 |
+
def get_by_phone(self, phone: str) -> Optional[User]:
|
| 14 |
+
return self.db.query(User).filter(User.phone == phone).first()
|
| 15 |
+
|
| 16 |
+
def create_customer(self, name: str, phone: str, city: str = "Jaipur") -> User:
|
| 17 |
+
user = User(name=name, phone=phone, role="customer", city=city)
|
| 18 |
+
self.db.add(user)
|
| 19 |
+
self.db.flush() # get user id
|
| 20 |
+
|
| 21 |
+
customer_profile = CustomerProfile(user_id=user.id, wallet_balance=8200)
|
| 22 |
+
self.db.add(customer_profile)
|
| 23 |
+
self.db.commit()
|
| 24 |
+
self.db.refresh(user)
|
| 25 |
+
return user
|
| 26 |
+
|
| 27 |
+
def create_worker(
|
| 28 |
+
self,
|
| 29 |
+
name: str,
|
| 30 |
+
phone: str,
|
| 31 |
+
skill: str,
|
| 32 |
+
rate: int,
|
| 33 |
+
city: str = "Jaipur",
|
| 34 |
+
map_x: int = 50,
|
| 35 |
+
map_y: int = 50
|
| 36 |
+
) -> User:
|
| 37 |
+
user = User(name=name, phone=phone, role="worker", city=city)
|
| 38 |
+
self.db.add(user)
|
| 39 |
+
self.db.flush()
|
| 40 |
+
|
| 41 |
+
# Calculate simulated distance from customer (center 50, 50)
|
| 42 |
+
import math
|
| 43 |
+
dist = round(math.sqrt((map_x - 50)**2 + (map_y - 50)**2) / 15, 1) or 1.2
|
| 44 |
+
|
| 45 |
+
worker_profile = WorkerProfile(
|
| 46 |
+
user_id=user.id,
|
| 47 |
+
skill=skill,
|
| 48 |
+
rate=rate,
|
| 49 |
+
distance=dist,
|
| 50 |
+
map_x=map_x,
|
| 51 |
+
map_y=map_y
|
| 52 |
+
)
|
| 53 |
+
self.db.add(worker_profile)
|
| 54 |
+
self.db.commit()
|
| 55 |
+
self.db.refresh(user)
|
| 56 |
+
return user
|
| 57 |
+
|
| 58 |
+
def get_all_users(self) -> List[User]:
|
| 59 |
+
return self.db.query(User).all()
|
| 60 |
+
|
| 61 |
+
def get_all_workers(
|
| 62 |
+
self,
|
| 63 |
+
skill: Optional[str] = None,
|
| 64 |
+
radius: Optional[float] = None,
|
| 65 |
+
online_only: bool = False
|
| 66 |
+
) -> List[User]:
|
| 67 |
+
query = self.db.query(User).join(WorkerProfile).filter(User.role == "worker")
|
| 68 |
+
|
| 69 |
+
if skill and skill != "All":
|
| 70 |
+
query = query.filter(WorkerProfile.skill == skill)
|
| 71 |
+
|
| 72 |
+
if online_only:
|
| 73 |
+
query = query.filter(WorkerProfile.online == True)
|
| 74 |
+
|
| 75 |
+
if radius:
|
| 76 |
+
query = query.filter(WorkerProfile.distance <= radius)
|
| 77 |
+
|
| 78 |
+
return query.all()
|
| 79 |
+
|
| 80 |
+
def update_user_status(self, user_id: uuid.UUID, is_active: bool) -> Optional[User]:
|
| 81 |
+
user = self.get_by_id(user_id)
|
| 82 |
+
if user:
|
| 83 |
+
user.is_active = is_active
|
| 84 |
+
self.db.commit()
|
| 85 |
+
self.db.refresh(user)
|
| 86 |
+
return user
|
| 87 |
+
|
| 88 |
+
def delete_user(self, user_id: uuid.UUID) -> bool:
|
| 89 |
+
user = self.get_by_id(user_id)
|
| 90 |
+
if user:
|
| 91 |
+
self.db.delete(user)
|
| 92 |
+
self.db.commit()
|
| 93 |
+
return True
|
| 94 |
+
return False
|
app/repositories/wallet.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from backend.app.models.wallet import WalletTransaction
|
| 5 |
+
from backend.app.models.users import CustomerProfile, User
|
| 6 |
+
|
| 7 |
+
class WalletRepository:
|
| 8 |
+
def __init__(self, db: Session):
|
| 9 |
+
self.db = db
|
| 10 |
+
|
| 11 |
+
def get_customer_profile(self, user_id: uuid.UUID) -> Optional[CustomerProfile]:
|
| 12 |
+
return self.db.query(CustomerProfile).filter(CustomerProfile.user_id == user_id).first()
|
| 13 |
+
|
| 14 |
+
def update_balance(self, user_id: uuid.UUID, amount_change: int) -> int:
|
| 15 |
+
"""Update customer wallet balance and return the new balance."""
|
| 16 |
+
profile = self.get_customer_profile(user_id)
|
| 17 |
+
if not profile:
|
| 18 |
+
# Create a customer profile dynamically if it doesn't exist
|
| 19 |
+
profile = CustomerProfile(user_id=user_id, wallet_balance=0)
|
| 20 |
+
self.db.add(profile)
|
| 21 |
+
self.db.flush()
|
| 22 |
+
|
| 23 |
+
profile.wallet_balance += amount_change
|
| 24 |
+
self.db.commit()
|
| 25 |
+
return profile.wallet_balance
|
| 26 |
+
|
| 27 |
+
def add_transaction(
|
| 28 |
+
self,
|
| 29 |
+
user_id: uuid.UUID,
|
| 30 |
+
label: str,
|
| 31 |
+
amount: int,
|
| 32 |
+
type_: str,
|
| 33 |
+
booking_id: Optional[uuid.UUID] = None
|
| 34 |
+
) -> WalletTransaction:
|
| 35 |
+
tx = WalletTransaction(
|
| 36 |
+
user_id=user_id,
|
| 37 |
+
booking_id=booking_id,
|
| 38 |
+
label=label,
|
| 39 |
+
amount=amount,
|
| 40 |
+
type=type_
|
| 41 |
+
)
|
| 42 |
+
self.db.add(tx)
|
| 43 |
+
self.db.commit()
|
| 44 |
+
self.db.refresh(tx)
|
| 45 |
+
return tx
|
| 46 |
+
|
| 47 |
+
def get_user_transactions(self, user_id: uuid.UUID) -> List[WalletTransaction]:
|
| 48 |
+
return self.db.query(WalletTransaction).filter(WalletTransaction.user_id == user_id).order_by(WalletTransaction.created_at.desc()).all()
|