Spaces:
Sleeping
Sleeping
Upload 24 files
Browse files- .env +11 -0
- app/__init__.py +0 -0
- app/config/__init__.py +0 -0
- app/config/nosql.py +51 -0
- app/controllers/__init__.py +0 -0
- app/controllers/customer_controller.py +100 -0
- app/controllers/merchant_controller.py +97 -0
- app/main.py +31 -0
- app/models/__init__.py +0 -0
- app/models/customer.py +13 -0
- app/models/merchant.py +18 -0
- app/repositories/__init__.py +0 -0
- app/repositories/cache_repository.py +55 -0
- app/repositories/db_repository.py +101 -0
- app/routers/__init__.py +0 -0
- app/routers/router.py +7 -0
- app/services/__init__.py +0 -0
- app/services/customer_services.py +165 -0
- app/services/merchant_services.py +144 -0
- app/utils/__init__.py +0 -0
- app/utils/auth_utils.py +27 -0
- app/utils/merchant_utils.py +33 -0
- app/utils/security.py +0 -0
- requirements.txt +12 -2
.env
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MONGO_URI=mongodb+srv://appliedcommerce:xaqtoj-gikdaw-6giQdy@cluster0.qpc5d.mongodb.net/test??ssl=true&ssl_cert_reqs=CERT_NONE
|
| 2 |
+
DB_NAME=ac-user-auth
|
| 3 |
+
|
| 4 |
+
DATABASE_URI=postgresql+asyncpg://trans_owner:BookMyService7@ep-sweet-surf-a1qeduoy.ap-southeast-1.aws.neon.tech/bookmyservice?options=-csearch_path%3Dtrans
|
| 5 |
+
|
| 6 |
+
CACHE_URI=redis-13036.c84.us-east-1-2.ec2.redns.redis-cloud.com:13036
|
| 7 |
+
CACHE_K=vLiMNdXeJZtvRUKUbk0Ck4HeGchzeHP6
|
| 8 |
+
|
| 9 |
+
#JWT related configurations
|
| 10 |
+
JWT_SECRET=book-my-service
|
| 11 |
+
JWT_EXPIRY_MINUTES=60
|
app/__init__.py
ADDED
|
File without changes
|
app/config/__init__.py
ADDED
|
File without changes
|
app/config/nosql.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module for NoSQL database connections and configurations.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
| 7 |
+
from redis.asyncio import Redis
|
| 8 |
+
from redis.exceptions import RedisError
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
load_dotenv()
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
# MongoDB connection
|
| 17 |
+
MONGO_URI = os.getenv("MONGO_URI")
|
| 18 |
+
DB_NAME = os.getenv("DB_NAME")
|
| 19 |
+
mongo_client = AsyncIOMotorClient(MONGO_URI)
|
| 20 |
+
db = mongo_client[DB_NAME]
|
| 21 |
+
|
| 22 |
+
# Redis connection
|
| 23 |
+
CACHE_URI = os.getenv("CACHE_URI")
|
| 24 |
+
CACHE_K = os.getenv("CACHE_K")
|
| 25 |
+
|
| 26 |
+
if not MONGO_URI or not DB_NAME:
|
| 27 |
+
raise ValueError("MongoDB URI or Database Name is not set in the environment variables.")
|
| 28 |
+
|
| 29 |
+
if not CACHE_URI or not CACHE_K:
|
| 30 |
+
raise ValueError("Redis URI or Database Name is not set in the environment variables.")
|
| 31 |
+
|
| 32 |
+
# Parse Redis host and port
|
| 33 |
+
CACHE_HOST, CACHE_PORT = CACHE_URI.split(":")
|
| 34 |
+
CACHE_PORT = int(CACHE_PORT)
|
| 35 |
+
|
| 36 |
+
# Initialize Redis client
|
| 37 |
+
try:
|
| 38 |
+
redis_client = Redis(
|
| 39 |
+
host=CACHE_HOST,
|
| 40 |
+
port=CACHE_PORT,
|
| 41 |
+
username="default",
|
| 42 |
+
password=CACHE_K,
|
| 43 |
+
decode_responses=True
|
| 44 |
+
)
|
| 45 |
+
logger.info("Connected to Redis.")
|
| 46 |
+
except RedisError as e:
|
| 47 |
+
logger.error(f"Failed to connect to Redis: {e}")
|
| 48 |
+
raise
|
| 49 |
+
|
| 50 |
+
async def get_mongo_client() -> AsyncIOMotorClient:
|
| 51 |
+
return mongo_client
|
app/controllers/__init__.py
ADDED
|
File without changes
|
app/controllers/customer_controller.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from fastapi import APIRouter, HTTPException, Query, Depends, status, Body
|
| 5 |
+
from fastapi.responses import JSONResponse
|
| 6 |
+
#from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
|
| 7 |
+
#from jose import JWTError, jwt
|
| 8 |
+
from app.services.customer_services import get_otp_from_cache, validate_email_mobile, verify_otp_and_save_customer
|
| 9 |
+
from app.models.customer import CustomerRegister
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
# Initialize router and logger
|
| 13 |
+
router = APIRouter(prefix="/customer")
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@router.post("/register")
|
| 19 |
+
async def register_customer(user: CustomerRegister = Body(...)):
|
| 20 |
+
"""
|
| 21 |
+
API endpoint to register a new customer.
|
| 22 |
+
|
| 23 |
+
Args:
|
| 24 |
+
user (CustomerRegister): The details of the customer to register.
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
dict: Confirmation message indicating email and SMS verification pending.
|
| 28 |
+
"""
|
| 29 |
+
try:
|
| 30 |
+
logger.info("Registering a new customer with email: %s and mobile: %s", user.email, user.mobile)
|
| 31 |
+
|
| 32 |
+
# Validate email and mobile format
|
| 33 |
+
validation_result = await validate_email_mobile(user.email, user.mobile)
|
| 34 |
+
if validation_result["status"] == "error":
|
| 35 |
+
logger.error("Validation failed: %s", validation_result["errors"])
|
| 36 |
+
raise HTTPException(status_code=400, detail=validation_result["errors"])
|
| 37 |
+
|
| 38 |
+
return user.dict()
|
| 39 |
+
|
| 40 |
+
except HTTPException as e:
|
| 41 |
+
logger.error("Failed to register customer: %s", e.detail)
|
| 42 |
+
raise e
|
| 43 |
+
except Exception as e:
|
| 44 |
+
logger.error("Unexpected error while registering customer: %s", e)
|
| 45 |
+
raise HTTPException(status_code=500, detail="Failed to register customer") from e
|
| 46 |
+
|
| 47 |
+
@router.post("/otp/verify")
|
| 48 |
+
async def customer_otp_verification(user: CustomerRegister = Body(...)):
|
| 49 |
+
"""
|
| 50 |
+
API endpoint to validate OTPs and save the complete customer object.
|
| 51 |
+
|
| 52 |
+
Args:
|
| 53 |
+
user (CustomerRegister): The complete customer object including email, mobile, and OTPs.
|
| 54 |
+
|
| 55 |
+
Returns:
|
| 56 |
+
dict: Confirmation message indicating success or failure.
|
| 57 |
+
"""
|
| 58 |
+
try:
|
| 59 |
+
logger.info("Verifying OTPs and registering customer with email: %s and mobile: %s", user.email, user.mobile)
|
| 60 |
+
# Verify OTPs and save customer
|
| 61 |
+
result = await verify_otp_and_save_customer(user)
|
| 62 |
+
if result["status"] == "error":
|
| 63 |
+
raise HTTPException(status_code=400, detail=result["errors"])
|
| 64 |
+
|
| 65 |
+
return {"message": "Customer successfully registered and verified."}
|
| 66 |
+
|
| 67 |
+
except HTTPException as e:
|
| 68 |
+
logger.error("Failed to verify and register customer: %s", e.detail)
|
| 69 |
+
raise e
|
| 70 |
+
except Exception as e:
|
| 71 |
+
logger.error("Unexpected error while verifying and registering customer: %s", e)
|
| 72 |
+
raise HTTPException(status_code=500, detail="Failed to verify and register customer") from e
|
| 73 |
+
|
| 74 |
+
@router.get("/otp/get")
|
| 75 |
+
async def get_otps(key: str = Query(..., description="The email or mobile number to retrieve the OTP for")):
|
| 76 |
+
"""
|
| 77 |
+
API endpoint to retrieve the email or SMS OTP from Redis cache.
|
| 78 |
+
|
| 79 |
+
Args:
|
| 80 |
+
key (str): The email or mobile number to retrieve the OTP for.
|
| 81 |
+
|
| 82 |
+
Returns:
|
| 83 |
+
dict: The OTP data if found, or an error message if not found.
|
| 84 |
+
"""
|
| 85 |
+
try:
|
| 86 |
+
logger.info("Retrieving OTP for key: %s", key)
|
| 87 |
+
|
| 88 |
+
# Retrieve OTP from Redis
|
| 89 |
+
otp_data = await get_otp_from_cache(key)
|
| 90 |
+
if not otp_data:
|
| 91 |
+
raise HTTPException(status_code=404, detail="OTP not found or expired")
|
| 92 |
+
|
| 93 |
+
return {"key": key, "otp_data": otp_data}
|
| 94 |
+
|
| 95 |
+
except HTTPException as e:
|
| 96 |
+
logger.error("Failed to retrieve OTP for key %s: %s", key, e.detail)
|
| 97 |
+
raise e
|
| 98 |
+
except Exception as e:
|
| 99 |
+
logger.error("Unexpected error while retrieving OTP for key %s: %s", key, e)
|
| 100 |
+
raise HTTPException(status_code=500, detail="Failed to retrieve OTP") from e
|
app/controllers/merchant_controller.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from datetime import datetime, timedelta
|
| 4 |
+
from fastapi import APIRouter, HTTPException, Query, Depends, status, Body
|
| 5 |
+
from app.models.merchant import MerchantRegister
|
| 6 |
+
from app.services.merchant_services import get_otp_from_cache, verify_otp_and_save_merchant, validate_merchant
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
# Initialize router and logger
|
| 10 |
+
router = APIRouter(prefix="/merchant")
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
@router.post("/register")
|
| 16 |
+
async def register_customer(user: MerchantRegister = Body(...)):
|
| 17 |
+
"""
|
| 18 |
+
API endpoint to register a new Merchant.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
user (MerchantRegister): The details of the Merchant to register.
|
| 22 |
+
|
| 23 |
+
Returns:
|
| 24 |
+
dict: Confirmation message indicating email and SMS verification pending.
|
| 25 |
+
"""
|
| 26 |
+
try:
|
| 27 |
+
logger.info("Registering a new Merchant with Merchant name : %s, email: %s and mobile: %s", user.merchantname, user.email, user.mobile)
|
| 28 |
+
|
| 29 |
+
# Validate email and mobile format
|
| 30 |
+
validation_result = await validate_merchant(user)
|
| 31 |
+
if validation_result["status"] == "error":
|
| 32 |
+
logger.error("Validation failed: %s", validation_result["errors"])
|
| 33 |
+
raise HTTPException(status_code=400, detail=validation_result["errors"])
|
| 34 |
+
|
| 35 |
+
return user.dict()
|
| 36 |
+
|
| 37 |
+
except HTTPException as e:
|
| 38 |
+
logger.error("Failed to register merchant: %s", e.detail)
|
| 39 |
+
raise e
|
| 40 |
+
except Exception as e:
|
| 41 |
+
logger.error("Unexpected error while registering merchant: %s", e)
|
| 42 |
+
raise HTTPException(status_code=500, detail="Failed to register Merchant") from e
|
| 43 |
+
|
| 44 |
+
@router.post("/otp/verify")
|
| 45 |
+
async def customer_otp_verification(user: MerchantRegister = Body(...)):
|
| 46 |
+
"""
|
| 47 |
+
API endpoint to validate OTPs and save the complete Merchant object.
|
| 48 |
+
|
| 49 |
+
Args:
|
| 50 |
+
user (MerchantRegister): The complete Merchant object including email, mobile, and OTPs.
|
| 51 |
+
|
| 52 |
+
Returns:
|
| 53 |
+
dict: Confirmation message indicating success or failure.
|
| 54 |
+
"""
|
| 55 |
+
try:
|
| 56 |
+
logger.info("Verifying OTPs and registering merchant with email: %s and mobile: %s", user.email, user.mobile)
|
| 57 |
+
# Verify OTPs and save customer
|
| 58 |
+
result = await verify_otp_and_save_merchant(user)
|
| 59 |
+
if result["status"] == "error":
|
| 60 |
+
raise HTTPException(status_code=400, detail=result["errors"])
|
| 61 |
+
|
| 62 |
+
return {"message": "Merchant successfully registered and verified."}
|
| 63 |
+
|
| 64 |
+
except HTTPException as e:
|
| 65 |
+
logger.error("Failed to verify and register merchant: %s", e.detail)
|
| 66 |
+
raise e
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.error("Unexpected error while verifying and registering merchant: %s", e)
|
| 69 |
+
raise HTTPException(status_code=500, detail="Failed to verify and register merchant") from e
|
| 70 |
+
|
| 71 |
+
@router.get("/otp/get")
|
| 72 |
+
async def get_otps(key: str = Query(..., description="The email or mobile number to retrieve the OTP for")):
|
| 73 |
+
"""
|
| 74 |
+
API endpoint to retrieve the email or SMS OTP from Redis cache.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
key (str): The email or mobile number to retrieve the OTP for.
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
dict: The OTP data if found, or an error message if not found.
|
| 81 |
+
"""
|
| 82 |
+
try:
|
| 83 |
+
logger.info("Retrieving OTP for key: %s", key)
|
| 84 |
+
|
| 85 |
+
# Retrieve OTP from Redis
|
| 86 |
+
otp_data = await get_otp_from_cache(key)
|
| 87 |
+
if not otp_data:
|
| 88 |
+
raise HTTPException(status_code=404, detail="OTP not found or expired")
|
| 89 |
+
|
| 90 |
+
return {"key": key, "otp_data": otp_data}
|
| 91 |
+
|
| 92 |
+
except HTTPException as e:
|
| 93 |
+
logger.error("Failed to retrieve OTP for key %s: %s", key, e.detail)
|
| 94 |
+
raise e
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logger.error("Unexpected error while retrieving OTP for key %s: %s", key, e)
|
| 97 |
+
raise HTTPException(status_code=500, detail="Failed to retrieve OTP") from e
|
app/main.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main application module for Customer Hub Service.
|
| 3 |
+
"""
|
| 4 |
+
import logging
|
| 5 |
+
from fastapi import FastAPI
|
| 6 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
+
from app.routers.router import router
|
| 8 |
+
|
| 9 |
+
# Configure logging
|
| 10 |
+
logging.basicConfig(level=logging.INFO)
|
| 11 |
+
#logger = logging.getLogger(__name__)
|
| 12 |
+
|
| 13 |
+
app = FastAPI(
|
| 14 |
+
title="Authentication API's",
|
| 15 |
+
description="API for managing registration and login related services",
|
| 16 |
+
version="1.0.0",
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
# CORS configuration
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["*"], # Restrict to specific domains in production
|
| 23 |
+
allow_credentials=True,
|
| 24 |
+
allow_methods=["*"],
|
| 25 |
+
allow_headers=["*"],
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
# Register routers
|
| 29 |
+
app.include_router(router, prefix="/api/v1", )
|
| 30 |
+
|
| 31 |
+
# Ensure there is no trailing newline
|
app/models/__init__.py
ADDED
|
File without changes
|
app/models/customer.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr, Field
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
class CustomerRegister(BaseModel):
|
| 5 |
+
"""
|
| 6 |
+
Pydantic model for customer registration request payload.
|
| 7 |
+
"""
|
| 8 |
+
email: EmailStr = Field(..., description="The email address of the customer")
|
| 9 |
+
mobile: str = Field(..., description="The 10-digit mobile number of the customer")
|
| 10 |
+
status: str = Field("pending-verification", description="The status of the customer account")
|
| 11 |
+
email_ver_code: str = Field(..., description="The email verification code")
|
| 12 |
+
mobile_ver_code: str = Field(..., description="The mobile verification code")
|
| 13 |
+
created_at: datetime = Field(default_factory=datetime.now, description="The timestamp when the customer was created")
|
app/models/merchant.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr, Field
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
|
| 4 |
+
class MerchantRegister(BaseModel):
|
| 5 |
+
"""
|
| 6 |
+
Pydantic model for Merchant registration request payload.
|
| 7 |
+
"""
|
| 8 |
+
merchant_name: str = Field(..., description="The name of the Merchant")
|
| 9 |
+
merchant_id: str = Field(..., description="The unique identifier for the Merchant")
|
| 10 |
+
country: str = Field(..., description="The country where the Merchant is located")
|
| 11 |
+
city: str = Field(..., description="The city where the Merchant is located")
|
| 12 |
+
area: str = Field(..., description="The area where the Merchant is located")
|
| 13 |
+
email: EmailStr = Field(..., description="The email address of the customer")
|
| 14 |
+
mobile: str = Field(..., description="The 10-digit mobile number of the customer")
|
| 15 |
+
status: str = Field("pending-verification", description="The status of the customer account")
|
| 16 |
+
email_ver_code: str = Field(..., description="The email verification code")
|
| 17 |
+
mobile_ver_code: str = Field(..., description="The mobile verification code")
|
| 18 |
+
created_at: datetime = Field(default_factory=datetime.now, description="The timestamp when the customer was created")
|
app/repositories/__init__.py
ADDED
|
File without changes
|
app/repositories/cache_repository.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
from app.config.nosql import redis_client
|
| 4 |
+
|
| 5 |
+
logger = logging.getLogger(__name__)
|
| 6 |
+
|
| 7 |
+
async def get_otp(key: str) -> dict:
|
| 8 |
+
"""
|
| 9 |
+
Retrieve OTP data from Redis.
|
| 10 |
+
|
| 11 |
+
Args:
|
| 12 |
+
key (str): The key to retrieve the OTP data (e.g., email or mobile).
|
| 13 |
+
|
| 14 |
+
Returns:
|
| 15 |
+
dict: The OTP data if found, None otherwise.
|
| 16 |
+
"""
|
| 17 |
+
try:
|
| 18 |
+
# Construct the Redis key
|
| 19 |
+
redis_key = f"otp:{key}"
|
| 20 |
+
|
| 21 |
+
# Retrieve the value from Redis
|
| 22 |
+
value = await redis_client.get(redis_key)
|
| 23 |
+
|
| 24 |
+
# If value exists, parse it as JSON and return
|
| 25 |
+
if value:
|
| 26 |
+
return json.loads(value)
|
| 27 |
+
|
| 28 |
+
# Return None if no value is found
|
| 29 |
+
return None
|
| 30 |
+
except Exception as e:
|
| 31 |
+
# Log the error and re-raise it
|
| 32 |
+
logger.error(f"Failed to retrieve OTP for key {key}: {e}")
|
| 33 |
+
raise
|
| 34 |
+
|
| 35 |
+
async def set_otp(key: str, otp_data: dict):
|
| 36 |
+
"""
|
| 37 |
+
Store OTP data in Redis with an expiry time.
|
| 38 |
+
|
| 39 |
+
Args:
|
| 40 |
+
key (str): The key to associate the OTP with (e.g., email or mobile).
|
| 41 |
+
otp_data (dict): The OTP data to store, including the OTP and expiry duration.
|
| 42 |
+
"""
|
| 43 |
+
try:
|
| 44 |
+
# Construct the Redis key
|
| 45 |
+
redis_key = f"otp:{key}"
|
| 46 |
+
|
| 47 |
+
# Extract expiry duration in seconds
|
| 48 |
+
expiry_duration = otp_data.get("expiry_duration", 15 * 60) # Default to 15 minutes if not provided
|
| 49 |
+
|
| 50 |
+
# Store the OTP data in Redis with the specified expiry
|
| 51 |
+
await redis_client.setex(redis_key, expiry_duration, json.dumps(otp_data))
|
| 52 |
+
except Exception as e:
|
| 53 |
+
# Log the error and re-raise it
|
| 54 |
+
logger.error(f"Failed to store OTP for key {key}: {e}")
|
| 55 |
+
raise
|
app/repositories/db_repository.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.config.nosql import db
|
| 2 |
+
|
| 3 |
+
async def save_customer(customer_data: dict):
|
| 4 |
+
"""
|
| 5 |
+
Save customer details to MongoDB.
|
| 6 |
+
|
| 7 |
+
Args:
|
| 8 |
+
customer_data (dict): The customer data to save.
|
| 9 |
+
"""
|
| 10 |
+
await db["customers"].insert_one(customer_data)
|
| 11 |
+
|
| 12 |
+
async def get_customer_by_email(email: str) -> dict:
|
| 13 |
+
"""
|
| 14 |
+
Retrieve a customer by email from the database.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
email (str): The email address to search for.
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
dict: The customer data if found, None otherwise.
|
| 21 |
+
"""
|
| 22 |
+
return await db["customers"].find_one({"email": email})
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
async def get_customer_by_mobile(mobile: str) -> dict:
|
| 26 |
+
"""
|
| 27 |
+
Retrieve a customer by mobile number from the database.
|
| 28 |
+
|
| 29 |
+
Args:
|
| 30 |
+
mobile (str): The mobile number to search for.
|
| 31 |
+
|
| 32 |
+
Returns:
|
| 33 |
+
dict: The customer data if found, None otherwise.
|
| 34 |
+
"""
|
| 35 |
+
return await db["customers"].find_one({"mobile": mobile})
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
async def get_merchant_by_name(name: str) -> dict:
|
| 39 |
+
"""
|
| 40 |
+
Retrieve a merchant by name from the database.
|
| 41 |
+
|
| 42 |
+
Args:
|
| 43 |
+
name (str): The name of the merchant.
|
| 44 |
+
|
| 45 |
+
Returns:
|
| 46 |
+
dict: The merchant data if found, None otherwise.
|
| 47 |
+
"""
|
| 48 |
+
return await db["merchants"].find_one({"merchantname": name})
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def get_merchant_by_id(merchant_id: str) -> dict:
|
| 52 |
+
"""
|
| 53 |
+
Retrieve a merchant by ID from the database.
|
| 54 |
+
|
| 55 |
+
Args:
|
| 56 |
+
merchant_id (str): The unique ID of the merchant.
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
dict: The merchant data if found, None otherwise.
|
| 60 |
+
"""
|
| 61 |
+
return await db["merchants"].find_one({"merchantId": merchant_id})
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
async def get_merchant_by_email(email: str) -> dict:
|
| 65 |
+
"""
|
| 66 |
+
Retrieve a merchant by email from the database.
|
| 67 |
+
|
| 68 |
+
Args:
|
| 69 |
+
email (str): The email address of the merchant.
|
| 70 |
+
|
| 71 |
+
Returns:
|
| 72 |
+
dict: The merchant data if found, None otherwise.
|
| 73 |
+
"""
|
| 74 |
+
return await db["merchants"].find_one({"email": email})
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
async def get_merchant_by_mobile(mobile: str) -> dict:
|
| 78 |
+
"""
|
| 79 |
+
Retrieve a merchant by mobile number from the database.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
mobile (str): The mobile number of the merchant.
|
| 83 |
+
|
| 84 |
+
Returns:
|
| 85 |
+
dict: The merchant data if found, None otherwise.
|
| 86 |
+
"""
|
| 87 |
+
return await db["merchants"].find_one({"mobile": mobile})
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
async def save_merchant(merchant_data: dict) -> dict:
|
| 91 |
+
"""
|
| 92 |
+
Save a new merchant to the database.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
merchant_data (dict): The merchant data to save.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
dict: The inserted merchant data.
|
| 99 |
+
"""
|
| 100 |
+
result = await db["merchants"].insert_one(merchant_data)
|
| 101 |
+
return {"id": str(result.inserted_id), **merchant_data}
|
app/routers/__init__.py
ADDED
|
File without changes
|
app/routers/router.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter
|
| 2 |
+
from app.controllers.customer_controller import router as customer
|
| 3 |
+
from app.controllers.merchant_controller import router as merchant
|
| 4 |
+
|
| 5 |
+
router = APIRouter()
|
| 6 |
+
router.include_router(customer, prefix="", tags=["Customer"])
|
| 7 |
+
router.include_router(merchant, prefix="", tags=["Merchant"])
|
app/services/__init__.py
ADDED
|
File without changes
|
app/services/customer_services.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import logging
|
| 3 |
+
from app.repositories.cache_repository import get_otp, set_otp
|
| 4 |
+
from app.repositories.db_repository import get_customer_by_email, get_customer_by_mobile, save_customer
|
| 5 |
+
from app.models.customer import CustomerRegister
|
| 6 |
+
from app.utils.auth_utils import validate_email, validate_mobile
|
| 7 |
+
import random
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
async def validate_email_mobile(email: str, mobile: str) -> dict:
|
| 12 |
+
"""
|
| 13 |
+
Validate email and mobile format and check if they already exist in the database.
|
| 14 |
+
|
| 15 |
+
Args:
|
| 16 |
+
email (str): The email address to validate.
|
| 17 |
+
mobile (str): The mobile number to validate.
|
| 18 |
+
|
| 19 |
+
Returns:
|
| 20 |
+
dict: Validation result with consolidated error messages or success message.
|
| 21 |
+
"""
|
| 22 |
+
errors = []
|
| 23 |
+
|
| 24 |
+
# Validate email format
|
| 25 |
+
if not validate_email(email):
|
| 26 |
+
errors.append("Invalid email format")
|
| 27 |
+
|
| 28 |
+
# Validate mobile format
|
| 29 |
+
if not validate_mobile(mobile):
|
| 30 |
+
errors.append("Invalid mobile number format")
|
| 31 |
+
|
| 32 |
+
# Check if email already exists in the database
|
| 33 |
+
existing_email = await get_customer_by_email(email)
|
| 34 |
+
if existing_email:
|
| 35 |
+
errors.append("Email is already registered")
|
| 36 |
+
|
| 37 |
+
# Check if mobile already exists in the database
|
| 38 |
+
existing_mobile = await get_customer_by_mobile(mobile)
|
| 39 |
+
if existing_mobile:
|
| 40 |
+
errors.append("Mobile number is already registered")
|
| 41 |
+
|
| 42 |
+
# Return errors if any
|
| 43 |
+
if errors:
|
| 44 |
+
return {"status": "error", "errors": errors}
|
| 45 |
+
else:
|
| 46 |
+
# Send verification emails and SMS if no errors
|
| 47 |
+
await send_email_verification(email)
|
| 48 |
+
await send_sms_verification(mobile)
|
| 49 |
+
|
| 50 |
+
# If all validations pass
|
| 51 |
+
return {"status": "success", "message": "Email and mobile are valid"}
|
| 52 |
+
|
| 53 |
+
async def save_customer_to_db(user: CustomerRegister) -> dict:
|
| 54 |
+
"""
|
| 55 |
+
Save customer details to MongoDB after checking for duplicates.
|
| 56 |
+
|
| 57 |
+
Args:
|
| 58 |
+
user (CustomerRegister): The customer details to save.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
dict: Success message or error message if email or mobile already exists.
|
| 62 |
+
"""
|
| 63 |
+
# Check if email already exists in the database
|
| 64 |
+
existing_email = await get_customer_by_email(user.email)
|
| 65 |
+
if existing_email:
|
| 66 |
+
return {"status": "error", "message": "Email is already registered"}
|
| 67 |
+
|
| 68 |
+
# Check if mobile already exists in the database
|
| 69 |
+
existing_mobile = await get_customer_by_mobile(user.mobile)
|
| 70 |
+
if existing_mobile:
|
| 71 |
+
return {"status": "error", "message": "Mobile number is already registered"}
|
| 72 |
+
|
| 73 |
+
# Save customer details to the database
|
| 74 |
+
await save_customer(user.dict())
|
| 75 |
+
return {"status": "success", "message": "Customer registered successfully"}
|
| 76 |
+
|
| 77 |
+
async def send_email_verification(email: str):
|
| 78 |
+
"""
|
| 79 |
+
Send an email verification link or code.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
email (str): The email address to send the verification to.
|
| 83 |
+
"""
|
| 84 |
+
# Implement your email sending logic here
|
| 85 |
+
# Generate a 6-digit OTP
|
| 86 |
+
otp = ''.join(random.choices("0123456789", k=6))
|
| 87 |
+
# Store OTP in cache with a 15-minute expiry
|
| 88 |
+
await set_otp(email, {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
|
| 89 |
+
# Send the OTP to the email address
|
| 90 |
+
# (You would typically use an email service here)
|
| 91 |
+
# For demonstration, we'll just log the OTP
|
| 92 |
+
logger.info("Sending email verification to: %s with OTP: %s", email, otp)
|
| 93 |
+
logger.info("Sending email verification to: %s", email)
|
| 94 |
+
|
| 95 |
+
async def send_sms_verification(mobile: str):
|
| 96 |
+
"""
|
| 97 |
+
Send an SMS verification code.
|
| 98 |
+
|
| 99 |
+
Args:
|
| 100 |
+
mobile (str): The mobile number to send the verification to.
|
| 101 |
+
"""
|
| 102 |
+
# Implement your SMS sending logic here
|
| 103 |
+
# Generate a 6-digit OTP
|
| 104 |
+
otp = ''.join(random.choices("0123456789", k=6))
|
| 105 |
+
# Store OTP in cache with a 15-minute expiry
|
| 106 |
+
await set_otp(mobile, {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
|
| 107 |
+
# Send the OTP to the mobile number
|
| 108 |
+
# (You would typically use an SMS service here)
|
| 109 |
+
# For demonstration, we'll just log the OTP
|
| 110 |
+
logger.info("Sending SMS verification to: %s with OTP: %s", mobile, otp)
|
| 111 |
+
logger.info("Sending SMS verification to: %s", mobile)
|
| 112 |
+
|
| 113 |
+
async def verify_otp_and_save_customer(user: CustomerRegister) -> dict:
|
| 114 |
+
"""
|
| 115 |
+
Verify SMS OTP and email OTP, and save the complete customer object in the database.
|
| 116 |
+
|
| 117 |
+
Args:
|
| 118 |
+
user (CustomerRegister): The complete customer object including email, mobile, and OTPs.
|
| 119 |
+
|
| 120 |
+
Returns:
|
| 121 |
+
dict: Result indicating success or failure.
|
| 122 |
+
"""
|
| 123 |
+
errors = []
|
| 124 |
+
|
| 125 |
+
# Retrieve OTPs from cache
|
| 126 |
+
email_otp_data = await get_otp(user.email)
|
| 127 |
+
mobile_otp_data = await get_otp(user.mobile)
|
| 128 |
+
|
| 129 |
+
# Verify email OTP
|
| 130 |
+
if not email_otp_data or email_otp_data["otp"] != user.email_ver_code:
|
| 131 |
+
errors.append("Invalid or expired email OTP")
|
| 132 |
+
|
| 133 |
+
# Verify mobile OTP
|
| 134 |
+
if not mobile_otp_data or mobile_otp_data["otp"] != user.mobile_ver_code:
|
| 135 |
+
errors.append("Invalid or expired mobile OTP")
|
| 136 |
+
|
| 137 |
+
# Return errors if any
|
| 138 |
+
if errors:
|
| 139 |
+
return {"status": "error", "errors": errors}
|
| 140 |
+
|
| 141 |
+
# Save the complete customer object to the database
|
| 142 |
+
customer_data = user.dict()
|
| 143 |
+
customer_data["status"] = "completed" # Update status to completed
|
| 144 |
+
await save_customer(customer_data)
|
| 145 |
+
|
| 146 |
+
return {"status": "success", "message": "Customer successfully registered and verified"}
|
| 147 |
+
|
| 148 |
+
async def get_otp_from_cache(key: str) -> dict:
|
| 149 |
+
"""
|
| 150 |
+
Retrieve OTP data for the given key (email or mobile) from the cache.
|
| 151 |
+
|
| 152 |
+
Args:
|
| 153 |
+
key (str): The email or mobile number to retrieve the OTP for.
|
| 154 |
+
|
| 155 |
+
Returns:
|
| 156 |
+
dict: The OTP data if found, or None if not found.
|
| 157 |
+
"""
|
| 158 |
+
try:
|
| 159 |
+
# Call the get_otp function from the cache repository
|
| 160 |
+
otp_data = await get_otp(key)
|
| 161 |
+
return otp_data
|
| 162 |
+
except Exception as e:
|
| 163 |
+
# Log the error and re-raise it
|
| 164 |
+
logger.error(f"Failed to retrieve OTP for key {key}: {e}")
|
| 165 |
+
raise
|
app/services/merchant_services.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from app.models.merchant import MerchantRegister
|
| 2 |
+
from app.repositories.db_repository import save_merchant, get_merchant_by_name, get_merchant_by_email, get_merchant_by_mobile
|
| 3 |
+
from app.utils.merchant_utils import generate_tenant_id
|
| 4 |
+
from app.utils.auth_utils import validate_email, validate_mobile
|
| 5 |
+
from app.repositories.cache_repository import get_otp, set_otp
|
| 6 |
+
import logging
|
| 7 |
+
import random
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
logger = logging.getLogger(__name__)
|
| 11 |
+
|
| 12 |
+
async def validate_merchant(merchant: MerchantRegister) -> dict:
|
| 13 |
+
"""
|
| 14 |
+
Validate if the merchant details are valid and not already existing.
|
| 15 |
+
|
| 16 |
+
Args:
|
| 17 |
+
name (str): The name of the merchant.
|
| 18 |
+
merchant_id (str): The unique ID of the merchant.
|
| 19 |
+
email (str): The email address of the merchant.
|
| 20 |
+
mobile (str): The mobile number of the merchant.
|
| 21 |
+
|
| 22 |
+
Returns:
|
| 23 |
+
dict: Validation result with success or error messages.
|
| 24 |
+
"""
|
| 25 |
+
errors = []
|
| 26 |
+
|
| 27 |
+
# Check if merchant name already exists
|
| 28 |
+
existing_merchant_name = await get_merchant_by_name(merchant.merchant_name)
|
| 29 |
+
if existing_merchant_name:
|
| 30 |
+
errors.append("Merchant name is already registered")
|
| 31 |
+
|
| 32 |
+
# Check if merchant email already exists
|
| 33 |
+
existing_merchant_email = await get_merchant_by_email(merchant.email)
|
| 34 |
+
if existing_merchant_email:
|
| 35 |
+
errors.append("Merchant email is already registered")
|
| 36 |
+
|
| 37 |
+
# Check if merchant mobile already exists
|
| 38 |
+
existing_merchant_mobile = await get_merchant_by_mobile(merchant.mobile)
|
| 39 |
+
if existing_merchant_mobile:
|
| 40 |
+
errors.append("Merchant mobile number is already registered")
|
| 41 |
+
|
| 42 |
+
# Return errors if any
|
| 43 |
+
if errors:
|
| 44 |
+
return {"status": "error", "errors": errors}
|
| 45 |
+
else:
|
| 46 |
+
#merchant_id = generate_tenant_id(merchant.merchantname, merchant.country, merchant.city, merchant.area) # Send verification emails and SMS if no errors
|
| 47 |
+
await send_email_verification(merchant.email)
|
| 48 |
+
await send_sms_verification(merchant.mobile)
|
| 49 |
+
#merchant.merchantId=merchant_id
|
| 50 |
+
|
| 51 |
+
# If all validations pass
|
| 52 |
+
return merchant.dict()
|
| 53 |
+
|
| 54 |
+
async def send_email_verification(email: str):
|
| 55 |
+
"""
|
| 56 |
+
Send an email verification link or code.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
email (str): The email address to send the verification to.
|
| 60 |
+
"""
|
| 61 |
+
# Implement your email sending logic here
|
| 62 |
+
# Generate a 6-digit OTP
|
| 63 |
+
otp = ''.join(random.choices("0123456789", k=6))
|
| 64 |
+
# Store OTP in cache with a 15-minute expiry
|
| 65 |
+
await set_otp(email, {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
|
| 66 |
+
# Send the OTP to the email address
|
| 67 |
+
# (You would typically use an email service here)
|
| 68 |
+
# For demonstration, we'll just log the OTP
|
| 69 |
+
logger.info("Sending email verification to: %s with OTP: %s", email, otp)
|
| 70 |
+
logger.info("Sending email verification to: %s", email)
|
| 71 |
+
|
| 72 |
+
async def send_sms_verification(mobile: str):
|
| 73 |
+
"""
|
| 74 |
+
Send an SMS verification code.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
mobile (str): The mobile number to send the verification to.
|
| 78 |
+
"""
|
| 79 |
+
# Implement your SMS sending logic here
|
| 80 |
+
# Generate a 6-digit OTP
|
| 81 |
+
otp = ''.join(random.choices("0123456789", k=6))
|
| 82 |
+
# Store OTP in cache with a 15-minute expiry
|
| 83 |
+
await set_otp(mobile, {"otp": otp, "expiry_duration": 15 * 60}) # 15 minutes in seconds
|
| 84 |
+
# Send the OTP to the mobile number
|
| 85 |
+
# (You would typically use an SMS service here)
|
| 86 |
+
# For demonstration, we'll just log the OTP
|
| 87 |
+
logger.info("Sending SMS verification to: %s with OTP: %s", mobile, otp)
|
| 88 |
+
logger.info("Sending SMS verification to: %s", mobile)
|
| 89 |
+
|
| 90 |
+
async def verify_otp_and_save_merchant(merchant: MerchantRegister) -> dict:
|
| 91 |
+
"""
|
| 92 |
+
Verify SMS OTP and email OTP, and save the complete merchant object in the database.
|
| 93 |
+
|
| 94 |
+
Args:
|
| 95 |
+
merchant (MerchantRegister): The complete merchant object including email, mobile, and OTPs.
|
| 96 |
+
|
| 97 |
+
Returns:
|
| 98 |
+
dict: Result indicating success or failure.
|
| 99 |
+
"""
|
| 100 |
+
errors = []
|
| 101 |
+
|
| 102 |
+
# Retrieve OTPs from cache
|
| 103 |
+
email_otp_data = await get_otp_from_cache(merchant.email)
|
| 104 |
+
mobile_otp_data = await get_otp_from_cache(merchant.mobile)
|
| 105 |
+
|
| 106 |
+
# Verify email OTP
|
| 107 |
+
if not email_otp_data or email_otp_data["otp"] != merchant.email_ver_code:
|
| 108 |
+
errors.append("Invalid or expired email OTP")
|
| 109 |
+
|
| 110 |
+
# Verify mobile OTP
|
| 111 |
+
if not mobile_otp_data or mobile_otp_data["otp"] != merchant.mobile_ver_code:
|
| 112 |
+
errors.append("Invalid or expired mobile OTP")
|
| 113 |
+
|
| 114 |
+
# Return errors if any
|
| 115 |
+
if errors:
|
| 116 |
+
return {"status": "error", "errors": errors}
|
| 117 |
+
|
| 118 |
+
# Save the complete merchant object to the database
|
| 119 |
+
merchant_data = merchant.dict()
|
| 120 |
+
merchant_data["merchant_id"] = generate_tenant_id(merchant.merchant_name, merchant.country, merchant.city, merchant.area)
|
| 121 |
+
merchant_data["status"] = "completed" # Update status to completed
|
| 122 |
+
|
| 123 |
+
await save_merchant(merchant_data)
|
| 124 |
+
|
| 125 |
+
return {"status": "success", "message": "Merchant successfully registered and verified"}
|
| 126 |
+
|
| 127 |
+
async def get_otp_from_cache(key: str) -> dict:
|
| 128 |
+
"""
|
| 129 |
+
Retrieve OTP data for the given key (email or mobile) from the cache.
|
| 130 |
+
|
| 131 |
+
Args:
|
| 132 |
+
key (str): The email or mobile number to retrieve the OTP for.
|
| 133 |
+
|
| 134 |
+
Returns:
|
| 135 |
+
dict: The OTP data if found, or None if not found.
|
| 136 |
+
"""
|
| 137 |
+
try:
|
| 138 |
+
# Call the get_otp function from the cache repository
|
| 139 |
+
otp_data = await get_otp(key)
|
| 140 |
+
return otp_data
|
| 141 |
+
except Exception as e:
|
| 142 |
+
# Log the error and re-raise it
|
| 143 |
+
logger.error(f"Failed to retrieve OTP for key {key}: {e}")
|
| 144 |
+
raise
|
app/utils/__init__.py
ADDED
|
File without changes
|
app/utils/auth_utils.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import logging
|
| 3 |
+
|
| 4 |
+
def validate_email(email: str) -> bool:
|
| 5 |
+
"""
|
| 6 |
+
Validate the email address format.
|
| 7 |
+
|
| 8 |
+
Args:
|
| 9 |
+
email (str): The email address to validate.
|
| 10 |
+
|
| 11 |
+
Returns:
|
| 12 |
+
bool: True if valid, False otherwise.
|
| 13 |
+
"""
|
| 14 |
+
email_regex = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
|
| 15 |
+
return re.match(email_regex, email) is not None
|
| 16 |
+
|
| 17 |
+
def validate_mobile(mobile: str) -> bool:
|
| 18 |
+
"""
|
| 19 |
+
Validate the mobile number format.
|
| 20 |
+
|
| 21 |
+
Args:
|
| 22 |
+
mobile (str): The mobile number to validate.
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
bool: True if valid, False otherwise.
|
| 26 |
+
"""
|
| 27 |
+
return mobile.isdigit() and len(mobile) == 10
|
app/utils/merchant_utils.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
from nanoid import generate
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
def slugify_company_name(name, max_length=6):
|
| 6 |
+
slug = re.sub(r'[^A-Z0-9]', '', name.upper())
|
| 7 |
+
return slug[:max_length]
|
| 8 |
+
|
| 9 |
+
def short_hash(text, length=4):
|
| 10 |
+
return hashlib.sha1(text.encode()).hexdigest().upper()[:length]
|
| 11 |
+
|
| 12 |
+
def alphanumeric_to_digits(text):
|
| 13 |
+
result = ''
|
| 14 |
+
for c in text.upper():
|
| 15 |
+
if c.isdigit():
|
| 16 |
+
result += c
|
| 17 |
+
elif c.isalpha():
|
| 18 |
+
result += str(ord(c) - 55) # A=10, B=11, ..., Z=35
|
| 19 |
+
return result
|
| 20 |
+
|
| 21 |
+
def compute_check_digit(base_id):
|
| 22 |
+
numeric_string = alphanumeric_to_digits(base_id)
|
| 23 |
+
total = sum(int(digit) for digit in numeric_string)
|
| 24 |
+
return str(total % 10)
|
| 25 |
+
|
| 26 |
+
def generate_tenant_id(company_name,country, city, area):
|
| 27 |
+
short_name = country + "-" + slugify_company_name(company_name, 5) + "-" + slugify_company_name(city,3) + slugify_company_name(area,3)
|
| 28 |
+
hash_part = short_hash(short_name)
|
| 29 |
+
nanoid_part = generate('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 5)
|
| 30 |
+
base_id = f"{short_name}-{hash_part}-{nanoid_part}"
|
| 31 |
+
return f"{base_id}"
|
| 32 |
+
|
| 33 |
+
|
app/utils/security.py
ADDED
|
File without changes
|
requirements.txt
CHANGED
|
@@ -1,2 +1,12 @@
|
|
| 1 |
-
fastapi
|
| 2 |
-
uvicorn[standard]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.95,<1.0
|
| 2 |
+
uvicorn[standard]>=0.22.0
|
| 3 |
+
motor>=3.0
|
| 4 |
+
pydantic
|
| 5 |
+
python-dotenv
|
| 6 |
+
pydantic[email]
|
| 7 |
+
redis
|
| 8 |
+
python-jose
|
| 9 |
+
passlib
|
| 10 |
+
python-multipart
|
| 11 |
+
bcrypt
|
| 12 |
+
nanoid
|