This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. Dockerfile +19 -0
  2. alembic.ini +149 -0
  3. app/__init__.py +1 -0
  4. app/__pycache__/__init__.cpython-314.pyc +0 -0
  5. app/__pycache__/main.cpython-314.pyc +0 -0
  6. app/core/__init__.py +1 -0
  7. app/core/__pycache__/__init__.cpython-314.pyc +0 -0
  8. app/core/__pycache__/config.cpython-314.pyc +0 -0
  9. app/core/__pycache__/database.cpython-314.pyc +0 -0
  10. app/core/__pycache__/security.cpython-314.pyc +0 -0
  11. app/core/__pycache__/seeding.cpython-314.pyc +0 -0
  12. app/core/config.py +20 -0
  13. app/core/database.py +29 -0
  14. app/core/security.py +35 -0
  15. app/core/seeding.py +191 -0
  16. app/main.py +81 -0
  17. app/models/__init__.py +24 -0
  18. app/models/__pycache__/__init__.cpython-314.pyc +0 -0
  19. app/models/__pycache__/alerts.cpython-314.pyc +0 -0
  20. app/models/__pycache__/base.cpython-314.pyc +0 -0
  21. app/models/__pycache__/bookings.cpython-314.pyc +0 -0
  22. app/models/__pycache__/jobs.cpython-314.pyc +0 -0
  23. app/models/__pycache__/notifications.cpython-314.pyc +0 -0
  24. app/models/__pycache__/reviews.cpython-314.pyc +0 -0
  25. app/models/__pycache__/roster.cpython-314.pyc +0 -0
  26. app/models/__pycache__/users.cpython-314.pyc +0 -0
  27. app/models/__pycache__/wallet.cpython-314.pyc +0 -0
  28. app/models/alerts.py +10 -0
  29. app/models/base.py +14 -0
  30. app/models/bookings.py +20 -0
  31. app/models/jobs.py +17 -0
  32. app/models/notifications.py +15 -0
  33. app/models/reviews.py +20 -0
  34. app/models/roster.py +17 -0
  35. app/models/users.py +48 -0
  36. app/models/wallet.py +17 -0
  37. app/repositories/__init__.py +15 -0
  38. app/repositories/__pycache__/__init__.cpython-314.pyc +0 -0
  39. app/repositories/__pycache__/alerts.cpython-314.pyc +0 -0
  40. app/repositories/__pycache__/bookings.cpython-314.pyc +0 -0
  41. app/repositories/__pycache__/jobs.cpython-314.pyc +0 -0
  42. app/repositories/__pycache__/roster.cpython-314.pyc +0 -0
  43. app/repositories/__pycache__/users.cpython-314.pyc +0 -0
  44. app/repositories/__pycache__/wallet.cpython-314.pyc +0 -0
  45. app/repositories/alerts.py +34 -0
  46. app/repositories/bookings.py +63 -0
  47. app/repositories/jobs.py +42 -0
  48. app/repositories/roster.py +44 -0
  49. app/repositories/users.py +94 -0
  50. 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()