Spaces:
Running on Zero
Running on Zero
fix(hadith/hijri): support dict/list hadith grades, timezone variance in daily content rotation, and comprehensive Hijri calendar system
fb8b215 Download app/models.py from sahihislam/superadmin: direct link, hf CLI and curl.
- Browser
- Download file 35.8 kB
-
https://huggingface.co/spaces/sahihislam/superadmin/resolve/main/app/models.py
- Command line
-
hf download hf://spaces/sahihislam/superadmin/app/models.py
-
curl -L -o models.py https://huggingface.co/spaces/sahihislam/superadmin/resolve/main/app/models.py
35.8 kB
| # ========================================================= | |
| # PYDANTIC MODELS - AADS & MARKETPLACE SYSTEM | |
| # ========================================================= | |
| # Complete data models for advertisements, products, and orders | |
| # ========================================================= | |
| from pydantic import BaseModel, Field, HttpUrl, field_validator | |
| from typing import Optional, List, Dict, Any | |
| from datetime import datetime | |
| from enum import Enum | |
| import re | |
| # ========================================================= | |
| # ENUMS - Status Types | |
| # ========================================================= | |
| class AdStatus(str, Enum): | |
| """Ad campaign status workflow""" | |
| DRAFT = "draft" # Created but not submitted | |
| PENDING = "pending" # Awaiting admin approval | |
| ACTIVE = "active" # Approved and running | |
| PAUSED = "paused" # Temporarily stopped by advertiser | |
| REJECTED = "rejected" # Rejected by admin | |
| COMPLETED = "completed" # End date reached | |
| CANCELLED = "cancelled" # Cancelled by advertiser | |
| class ProductStatus(str, Enum): | |
| """Product listing status""" | |
| DRAFT = "draft" | |
| ACTIVE = "active" | |
| OUT_OF_STOCK = "out_of_stock" | |
| INACTIVE = "inactive" | |
| class OrderStatus(str, Enum): | |
| """Order fulfillment workflow""" | |
| PENDING = "pending" | |
| CONFIRMED = "confirmed" | |
| PROCESSING = "processing" | |
| SHIPPED = "shipped" | |
| DELIVERED = "delivered" | |
| CANCELLED = "cancelled" | |
| REFUNDED = "refunded" | |
| class AdCategory(str, Enum): | |
| """Advertisement categories""" | |
| ISLAMIC_BOOKS = "Islamic Books & Quran" | |
| ATTAR_PERFUMES = "Attar, Oudh & Perfumes" | |
| ABAYAS_WEAR = "Abayas & Modest Wear" | |
| PRAYER_MATS = "Prayer Mats & Tasbih" | |
| HALAL_FOODS = "Halal Foods & Honey" | |
| DATES_DRY_FRUITS = "Dates & Dry Fruits" | |
| ISLAMIC_ART = "Islamic Art & Home Decor" | |
| HIJABS_SCARVES = "Hijabs & Scarves" | |
| THOBES_KURTAS = "Thobes & Kurtas" | |
| HALAL_COSMETICS = "Halal Cosmetics & Health" | |
| HAJJ_UMRAH = "Hajj & Umrah Essentials" | |
| ISLAMIC_ELECTRONICS = "Islamic Electronics & Clocks" | |
| KIDS_TOYS = "Kids Islamic Toys & Books" | |
| HALAL_MEAT = "Halal Meat & Groceries" | |
| CAPS_TURBANS = "Keffiyeh, Caps & Turbans" | |
| ISLAMIC_JEWELRY = "Islamic Jewelry & Rings" | |
| ISLAMIC_GIFTS = "Islamic Gifts & Hampers" | |
| HERBAL_MEDICINE = "Herbal Medicine & Tibb-e-Nabawi" | |
| CHARITY_SADAQAH = "Charity & Sadaqah Jariyah" | |
| ISLAMIC_COURSES = "Islamic Courses & Services" | |
| OTHER_HALAL = "Other Halal Products" | |
| # Legacy categories | |
| BOOKS = "Books" | |
| EDUCATION = "Education" | |
| DECOR = "Decor" | |
| FINANCE = "Finance" | |
| PRODUCTS = "Products" | |
| SERVICES = "Services" | |
| CHARITY = "Charity" | |
| EVENTS = "Events" | |
| PRAYER_ESSENTIALS = "Prayer Essentials" | |
| ISLAMIC_CLOTHING = "Islamic Clothing" | |
| HOME_DECOR = "Home Decor" | |
| JEWELRY = "Jewelry" | |
| ACCESSORIES = "Accessories" | |
| GIFTS = "Gifts" | |
| DIGITAL_PRODUCTS = "Digital Products" | |
| class ProductCategory(str, Enum): | |
| """Product categories""" | |
| ISLAMIC_BOOKS = "Islamic Books & Quran" | |
| ATTAR_PERFUMES = "Attar, Oudh & Perfumes" | |
| ABAYAS_WEAR = "Abayas & Modest Wear" | |
| PRAYER_MATS = "Prayer Mats & Tasbih" | |
| HALAL_FOODS = "Halal Foods & Honey" | |
| DATES_DRY_FRUITS = "Dates & Dry Fruits" | |
| ISLAMIC_ART = "Islamic Art & Home Decor" | |
| HIJABS_SCARVES = "Hijabs & Scarves" | |
| THOBES_KURTAS = "Thobes & Kurtas" | |
| HALAL_COSMETICS = "Halal Cosmetics & Health" | |
| HAJJ_UMRAH = "Hajj & Umrah Essentials" | |
| ISLAMIC_ELECTRONICS = "Islamic Electronics & Clocks" | |
| KIDS_TOYS = "Kids Islamic Toys & Books" | |
| HALAL_MEAT = "Halal Meat & Groceries" | |
| CAPS_TURBANS = "Keffiyeh, Caps & Turbans" | |
| ISLAMIC_JEWELRY = "Islamic Jewelry & Rings" | |
| ISLAMIC_GIFTS = "Islamic Gifts & Hampers" | |
| HERBAL_MEDICINE = "Herbal Medicine & Tibb-e-Nabawi" | |
| CHARITY_SADAQAH = "Charity & Sadaqah Jariyah" | |
| ISLAMIC_COURSES = "Islamic Courses & Services" | |
| OTHER_HALAL = "Other Halal Products" | |
| # Legacy categories | |
| BOOKS = "Books" | |
| PRAYER_ESSENTIALS = "Prayer Essentials" | |
| ISLAMIC_CLOTHING = "Islamic Clothing" | |
| HOME_DECOR = "Home Decor" | |
| JEWELRY = "Jewelry" | |
| ACCESSORIES = "Accessories" | |
| GIFTS = "Gifts" | |
| DIGITAL_PRODUCTS = "Digital Products" | |
| EDUCATION = "Education" | |
| DECOR = "Decor" | |
| FINANCE = "Finance" | |
| PRODUCTS = "Products" | |
| SERVICES = "Services" | |
| CHARITY = "Charity" | |
| EVENTS = "Events" | |
| # ========================================================= | |
| # AD CAMPAIGN MODELS | |
| # ========================================================= | |
| class AdCreate(BaseModel): | |
| """Model for creating a new ad campaign""" | |
| title: str = Field(..., min_length=3, max_length=100) | |
| description: str = Field(..., min_length=3, max_length=1000) | |
| image_url: str = Field(..., description="URL to ad banner image") | |
| destination_url: str = Field(..., description="Landing page URL") | |
| category: str = Field(..., min_length=2, max_length=100) | |
| budget: float = Field(0.0, ge=0, description="Total budget (0 = Free)") | |
| daily_budget: Optional[float] = Field(None, ge=0, description="Daily spending limit") | |
| start_date: datetime | |
| end_date: datetime | |
| targeting: Optional[dict] = Field(default_factory=dict, description="Geographic/demographic targeting") | |
| cta_text: Optional[str] = Field("Shop Now", description="Call to action button label") | |
| def validate_dates(cls, v, info): | |
| if 'start_date' in info.data and v <= info.data['start_date']: | |
| raise ValueError('end_date must be after start_date') | |
| return v | |
| class AdUpdate(BaseModel): | |
| """Model for updating an existing ad campaign""" | |
| title: Optional[str] = Field(None, min_length=3, max_length=100) | |
| description: Optional[str] = Field(None, min_length=3, max_length=1000) | |
| image_url: Optional[str] = None | |
| destination_url: Optional[str] = None | |
| category: Optional[str] = None | |
| budget: Optional[float] = Field(None, gt=0) | |
| daily_budget: Optional[float] = Field(None, gt=0) | |
| start_date: Optional[datetime] = None | |
| end_date: Optional[datetime] = None | |
| targeting: Optional[dict] = None | |
| cta_text: Optional[str] = Field(None, description="Call to action button label") | |
| class AdResponse(BaseModel): | |
| """Complete ad campaign response""" | |
| id: str = Field(..., alias="_id", description="MongoDB document ID") | |
| ad_id: str | |
| advertiser_uid: str | |
| advertiser_email: Optional[str] = None | |
| title: str | |
| description: str | |
| image_url: str | |
| destination_url: str | |
| category: str | |
| status: AdStatus | |
| budget: float | |
| daily_budget: Optional[float] = None | |
| cta_text: Optional[str] = "Shop Now" | |
| spent: float = 0.0 | |
| impressions: int = 0 | |
| clicks: int = 0 | |
| conversions: int = 0 | |
| start_date: datetime | |
| end_date: datetime | |
| targeting: dict = Field(default_factory=dict) | |
| created_at: datetime | |
| updated_at: datetime | |
| submitted_at: Optional[datetime] = None | |
| approved_at: Optional[datetime] = None | |
| rejected_at: Optional[datetime] = None | |
| rejection_reason: Optional[str] = None | |
| class Config: | |
| populate_by_name = True | |
| # ========================================================= | |
| # PRODUCT MODELS | |
| # ========================================================= | |
| class ProductVariant(BaseModel): | |
| """Product variant (size, color, etc.)""" | |
| variant_id: str | |
| name: str # e.g., "Large", "Blue", "Hardcover" | |
| price_adjustment: float = 0.0 # Price difference from base | |
| stock: int = 0 | |
| sku: Optional[str] = None | |
| class ProductCreate(BaseModel): | |
| """Model for creating a new product""" | |
| title: str = Field(..., min_length=3, max_length=200) | |
| description: str = Field(..., min_length=3, max_length=5000) | |
| price: float = Field(..., gt=0) | |
| compare_at_price: Optional[float] = Field(None, gt=0, description="Original price (for showing discounts)") | |
| category: str = Field(..., min_length=2, max_length=100) | |
| subcategory: Optional[str] = None | |
| tags: List[str] = Field(default_factory=list, description="Search tags") | |
| images: List[str] = Field(..., min_items=1, max_items=5, description="Product images (1 to 5 images)") | |
| stock: int = Field(..., ge=0) | |
| sku: Optional[str] = Field(None, description="Stock Keeping Unit") | |
| weight: Optional[float] = Field(None, gt=0, description="Weight in kg") | |
| dimensions: Optional[dict] = Field(None, description="Length, width, height in cm") | |
| variants: List[ProductVariant] = Field(default_factory=list) | |
| shipping_cost: float = Field(0.0, ge=0) | |
| shipping_regions: List[str] = Field(default_factory=lambda: ["Worldwide"]) | |
| is_digital: bool = False | |
| digital_file_url: Optional[str] = Field(None, description="Download URL for digital products") | |
| def validate_compare_price(cls, v, info): | |
| if v is not None and 'price' in info.data and v <= info.data['price']: | |
| raise ValueError('compare_at_price must be greater than price') | |
| return v | |
| class ProductUpdate(BaseModel): | |
| """Model for updating an existing product""" | |
| title: Optional[str] = Field(None, min_length=3, max_length=200) | |
| description: Optional[str] = Field(None, min_length=3, max_length=5000) | |
| price: Optional[float] = Field(None, gt=0) | |
| compare_at_price: Optional[float] = Field(None, gt=0) | |
| category: Optional[str] = None | |
| subcategory: Optional[str] = None | |
| tags: Optional[List[str]] = None | |
| images: Optional[List[str]] = None | |
| stock: Optional[int] = Field(None, ge=0) | |
| sku: Optional[str] = None | |
| weight: Optional[float] = Field(None, gt=0) | |
| dimensions: Optional[dict] = None | |
| variants: Optional[List[ProductVariant]] = None | |
| shipping_cost: Optional[float] = Field(None, ge=0) | |
| shipping_regions: Optional[List[str]] = None | |
| is_digital: Optional[bool] = None | |
| digital_file_url: Optional[str] = None | |
| status: Optional[ProductStatus] = None | |
| class ProductResponse(BaseModel): | |
| """Complete product response""" | |
| id: str = Field(..., alias="_id") | |
| product_id: str | |
| seller_uid: str | |
| seller_name: str | |
| seller_email: Optional[str] = None | |
| shop_name: Optional[str] = None | |
| title: str | |
| description: str | |
| price: float | |
| compare_at_price: Optional[float] = None | |
| category: str | |
| subcategory: Optional[str] = None | |
| tags: List[str] = Field(default_factory=list) | |
| images: List[str] | |
| stock: int | |
| sku: Optional[str] = None | |
| weight: Optional[float] = None | |
| dimensions: Optional[dict] = None | |
| variants: List[ProductVariant] = Field(default_factory=list) | |
| shipping_cost: float = 0.0 | |
| shipping_regions: List[str] = Field(default_factory=list) | |
| is_digital: bool = False | |
| digital_file_url: Optional[str] = None | |
| status: ProductStatus | |
| rating: float = 0.0 | |
| rating_count: int = 0 | |
| total_sales: int = 0 | |
| views: int = 0 | |
| created_at: datetime | |
| updated_at: datetime | |
| class Config: | |
| populate_by_name = True | |
| # ========================================================= | |
| # ORDER & PAYMENT MODELS | |
| # ========================================================= | |
| class PaymentMethod(str, Enum): | |
| WALLET = "wallet" | |
| CARD = "card" | |
| UPI = "upi" | |
| COD = "cod" | |
| SANDBOX = "sandbox" | |
| class PaymentStatus(str, Enum): | |
| PENDING = "pending" | |
| COMPLETED = "completed" | |
| FAILED = "failed" | |
| REFUNDED = "refunded" | |
| class OrderItem(BaseModel): | |
| """Single item in an order""" | |
| product_id: str | |
| product_title: Optional[str] = "Product" | |
| variant_id: Optional[str] = None | |
| variant_name: Optional[str] = None | |
| quantity: int = Field(default=1, gt=0) | |
| unit_price: Optional[float] = 0.0 | |
| total_price: Optional[float] = 0.0 | |
| seller_uid: Optional[str] = "" | |
| class ShippingAddress(BaseModel): | |
| """Shipping address information""" | |
| full_name: str = "Customer" | |
| phone: str = "" | |
| address_line1: str = "Address" | |
| address_line2: Optional[str] = None | |
| city: str = "City" | |
| state: Optional[str] = None | |
| postal_code: str = "000000" | |
| country: str = "India" | |
| class OrderCreate(BaseModel): | |
| """Model for creating a new order""" | |
| items: List[OrderItem] = Field(..., min_items=1) | |
| shipping_address: Optional[ShippingAddress] = Field(default_factory=ShippingAddress) | |
| billing_address: Optional[ShippingAddress] = None | |
| payment_method: str = "online" | |
| notes: Optional[str] = None | |
| class WalletTopUpRequest(BaseModel): | |
| """Model for topping up ad/user wallet""" | |
| amount: float = Field(..., gt=0, description="Amount in currency units (INR/USD)") | |
| payment_method: Optional[str] = "sandbox" | |
| notes: Optional[str] = None | |
| transaction_id: Optional[str] = Field(None, description="Client or caller specified transaction ID") | |
| idempotency_key: Optional[str] = Field(None, description="Idempotency key to guarantee single processing") | |
| class PaymentIntentCreate(BaseModel): | |
| """Model for initializing a payment intent""" | |
| amount: float = Field(..., gt=0) | |
| currency: str = "INR" | |
| purpose: str = "order" # "order" or "wallet_topup" | |
| order_id: Optional[str] = None | |
| payment_method: Optional[str] = "sandbox" | |
| class PaymentVerifyRequest(BaseModel): | |
| """Model for verifying a payment""" | |
| payment_id: str | |
| order_id: Optional[str] = None | |
| signature: Optional[str] = None | |
| class ShopSettingsUpdate(BaseModel): | |
| """Model for shopkeeper settings and shop creation""" | |
| shop_name: Optional[str] = None | |
| category: Optional[str] = None | |
| owner_name: Optional[str] = None | |
| description: Optional[str] = None | |
| contact: Optional[str] = None | |
| email: Optional[str] = None | |
| phone: Optional[str] = None | |
| address: Optional[str] = None | |
| city: Optional[str] = None | |
| banner_url: Optional[str] = None | |
| logo_url: Optional[str] = None | |
| shop_photo: Optional[str] = None # Base64 or image URL strictly under 30KB | |
| is_created: Optional[bool] = None | |
| class OrderResponse(BaseModel): | |
| """Complete order response""" | |
| id: str = Field(..., alias="_id") | |
| order_id: str | |
| buyer_uid: str | |
| buyer_email: str | |
| seller_uid: str # For single-seller orders | |
| items: List[OrderItem] | |
| subtotal: float | |
| shipping_cost: float | |
| tax: float | |
| total: float | |
| status: OrderStatus | |
| shipping_address: ShippingAddress | |
| billing_address: Optional[ShippingAddress] = None | |
| payment_method: str | |
| payment_status: str = "pending" | |
| tracking_number: Optional[str] = None | |
| notes: Optional[str] = None | |
| created_at: datetime | |
| updated_at: datetime | |
| shipped_at: Optional[datetime] = None | |
| delivered_at: Optional[datetime] = None | |
| class Config: | |
| populate_by_name = True | |
| # ========================================================= | |
| # ANALYTICS MODELS | |
| # ========================================================= | |
| class AdEvent(BaseModel): | |
| """Ad interaction event""" | |
| ad_id: str | |
| event_type: str # "impression", "click", "conversion" | |
| user_id: Optional[str] = None | |
| session_id: Optional[str] = None | |
| device_type: Optional[str] = None | |
| platform: Optional[str] = None | |
| location: Optional[dict] = None | |
| source: str = "app" | |
| timestamp: datetime = Field(default_factory=datetime.utcnow) | |
| class AdAnalytics(BaseModel): | |
| """Aggregated ad analytics""" | |
| ad_id: str | |
| date: str # YYYY-MM-DD | |
| impressions: int = 0 | |
| clicks: int = 0 | |
| conversions: int = 0 | |
| spent: float = 0.0 | |
| ctr: float = 0.0 # Click-through rate | |
| cpc: float = 0.0 # Cost per click | |
| conversion_rate: float = 0.0 | |
| ALLOWED_ADMOB_EVENT_TYPES = { | |
| "request", "loaded", "impression", "click", | |
| "failed_to_load", "failed_to_show", "dismissed" | |
| } | |
| class AdmobEventCreate(BaseModel): | |
| """Event payload emitted by Flutter Google Mobile Ads SDK with strict validation (F-06)""" | |
| event_type: str = Field(..., max_length=32, description="Lifecycle event type") | |
| ad_unit_id: Optional[str] = Field(None, max_length=128) | |
| ad_format: Optional[str] = Field("interstitial", max_length=32) | |
| platform: Optional[str] = Field("android", max_length=16) | |
| is_test_ad: Optional[bool] = False | |
| error_code: Optional[int] = Field(None, ge=0, le=100000) | |
| error_message: Optional[str] = Field(None, max_length=256) | |
| placement: Optional[str] = Field(None, max_length=64) | |
| duration_seconds: Optional[int] = Field(5, ge=0, le=3600) | |
| user_id: Optional[str] = Field(None, max_length=128) | |
| metadata: Optional[Dict[str, Any]] = None | |
| timestamp: datetime = Field(default_factory=datetime.utcnow) | |
| def validate_event_type(cls, v: str) -> str: | |
| clean = (v or "").strip().lower() | |
| if clean not in ALLOWED_ADMOB_EVENT_TYPES: | |
| raise ValueError(f"Invalid AdMob event_type '{v}'. Must be one of: {sorted(ALLOWED_ADMOB_EVENT_TYPES)}") | |
| return clean | |
| def validate_metadata(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: | |
| if v is None: | |
| return None | |
| if len(v) > 10: | |
| raise ValueError("AdMob event metadata cannot exceed 10 keys") | |
| for key, val in v.items(): | |
| if len(str(key)) > 64: | |
| raise ValueError(f"Metadata key '{key}' exceeds 64 characters") | |
| if len(str(val)) > 256: | |
| raise ValueError(f"Metadata value for key '{key}' exceeds 256 characters") | |
| return v | |
| def validate_ad_format(cls, v: Optional[str]) -> Optional[str]: | |
| if v is None: | |
| return v | |
| clean = v.strip().lower() | |
| if clean not in {"interstitial", "banner", "rewarded", "app_open"}: | |
| raise ValueError("Unsupported AdMob format") | |
| return clean | |
| def validate_ad_unit_id(cls, v: Optional[str]) -> Optional[str]: | |
| if v is not None: | |
| clean = v.strip() | |
| if not re.fullmatch(r"ca-app-pub-\d{16}/\d{10}", clean): | |
| raise ValueError("Invalid AdMob unit ID format") | |
| return clean | |
| return v | |
| def validate_platform(cls, v: Optional[str]) -> Optional[str]: | |
| if v is None: | |
| return v | |
| clean = v.strip().lower() | |
| if clean not in {"android", "ios"}: | |
| raise ValueError("Unsupported AdMob platform") | |
| return clean | |
| class AdmobConfigUpdate(BaseModel): | |
| """Dynamic AdMob Remote Configuration Update Payload""" | |
| ads_enabled: Optional[bool] = None | |
| real_ads_enabled: Optional[bool] = None | |
| min_seconds_between_ads: Optional[int] = None | |
| content_rating: Optional[str] = None | |
| banner_ads_enabled: Optional[bool] = None | |
| home_ads: Optional[bool] = None | |
| chat_ads: Optional[bool] = None | |
| hadith_ads: Optional[bool] = None | |
| tafsir_ads: Optional[bool] = None | |
| qaza_ads: Optional[bool] = None | |
| admob_app_id_android: Optional[str] = None | |
| admob_interstitial_id_android: Optional[str] = None | |
| admob_banner_id_android: Optional[str] = None | |
| admob_rewarded_id_android: Optional[str] = None | |
| admob_interstitial_id_ios: Optional[str] = None | |
| admob_banner_id_ios: Optional[str] = None | |
| # ========================================================= | |
| # BANK ACCOUNT & PAYOUT MODELS | |
| # ========================================================= | |
| class ShopkeeperBankDetails(BaseModel): | |
| """Bank account details for a shopkeeper to receive monthly settlements""" | |
| account_holder_name: str | |
| bank_name: str | |
| account_number: str | |
| ifsc_code: str | |
| upi_id: Optional[str] = None | |
| account_type: Optional[str] = "savings" # savings | current | |
| class PlatformBankSettings(BaseModel): | |
| """Platform owner bank and payment gateway configuration""" | |
| account_holder_name: Optional[str] = None | |
| bank_name: Optional[str] = None | |
| account_number: Optional[str] = None | |
| ifsc_code: Optional[str] = None | |
| upi_id: Optional[str] = None | |
| gateway_provider: Optional[str] = "razorpay" # razorpay | stripe | direct_upi | |
| razorpay_key_id: Optional[str] = None | |
| razorpay_key_secret: Optional[str] = None | |
| google_play_license_key: Optional[str] = None | |
| platform_commission_percent: Optional[float] = 7.0 | |
| auto_settlement: Optional[bool] = True | |
| class SubscriptionCreate(BaseModel): | |
| """Purchase a subscription tier""" | |
| plan_id: str | |
| billing_cycle: Optional[str] = "monthly" # monthly | annual | |
| payment_method: str = "google_play" # google_play | razorpay | upi | card | |
| purchase_token: Optional[str] = None # Google Play purchase token | |
| transaction_id: Optional[str] = None | |
| # ========================================================= | |
| # PROFESSIONAL ENTERPRISE USER ARCHITECTURE | |
| # ========================================================= | |
| class UserProfileModel(BaseModel): | |
| """User personal identity and localization""" | |
| display_name: str | |
| email: str | |
| phone: Optional[str] = None | |
| avatar_url: Optional[str] = None | |
| locale: str = "en" | |
| timezone: str = "Asia/Kolkata" | |
| class UserAuthModel(BaseModel): | |
| """Access control, credentials and lifecycle status""" | |
| role: str = "USER" # "USER" | "MASJID_ADMIN" | "MUFTI" | "APP_ADMIN" | "ADMIN" | |
| provider: str = "google" # "password" | "google" | "phone" | |
| is_email_verified: bool = False | |
| status: str = "ACTIVE" # "ACTIVE" | "SUSPENDED" | "BANNED" | |
| class UserMuftiAffiliation(BaseModel): | |
| """Mufti verification credentials and history""" | |
| is_mufti: bool = False | |
| status: str = "NONE" # "NONE" | "PENDING" | "APPROVED" | "REJECTED" | |
| applied_at: Optional[datetime] = None | |
| resolved_at: Optional[datetime] = None | |
| class UserMasjidAffiliation(BaseModel): | |
| """Masjid administrative links and followed masjids""" | |
| managed_masjid_id: str = "" | |
| status: str = "NONE" # "NONE" | "PENDING" | "APPROVED" | |
| co_admin_masjids: List[str] = Field(default_factory=list) | |
| followed_masjids: List[str] = Field(default_factory=list) | |
| class UserCommunityModel(BaseModel): | |
| """Community affiliations: Masjids & Mufti roles""" | |
| mufti: UserMuftiAffiliation = Field(default_factory=UserMuftiAffiliation) | |
| masjid: UserMasjidAffiliation = Field(default_factory=UserMasjidAffiliation) | |
| class UserBusinessModel(BaseModel): | |
| """Business Suite: shops, advertisements, activation status""" | |
| is_enabled: bool = False | |
| activated_at: Optional[datetime] = None | |
| expires_at: Optional[datetime] = None | |
| is_expired: bool = False | |
| shop_id: Optional[str] = None | |
| wallet_balance: float = 0.0 | |
| total_ads_count: int = 0 | |
| class UserMetricsModel(BaseModel): | |
| """High-speed engagement counters for dashboards & analysis""" | |
| prayers_tracked: int = 0 | |
| consecutive_streak: int = 0 | |
| total_orders_placed: int = 0 | |
| total_spent_inr: float = 0.0 | |
| login_count: int = 1 | |
| class UserAnalyticsModel(BaseModel): | |
| """Telemetry, audit logs, DAU/MAU cohorts""" | |
| signup_platform: str = "android" # "android" | "web" | "ios" | |
| app_version: str = "1.0.0" | |
| created_at: datetime = Field(default_factory=datetime.utcnow) | |
| updated_at: datetime = Field(default_factory=datetime.utcnow) | |
| last_active_at: datetime = Field(default_factory=datetime.utcnow) | |
| class UserFavoritesModel(BaseModel): | |
| """User favorites (Surahs & Ayahs) synchronized across devices""" | |
| surahs: List[dict] = Field(default_factory=list) | |
| ayahs: List[dict] = Field(default_factory=list) | |
| class UserSubscriptionModel(BaseModel): | |
| """Subscription entitlements for all 5 canonical tiers + ₹5 acquisition offer""" | |
| is_pro: bool = False | |
| is_progress_plus: bool = False | |
| is_ad_free_admob: bool = False | |
| has_historical_progress: bool = False | |
| has_premium_themes: bool = False | |
| show_sponsored_card: bool = True | |
| plan_id: Optional[str] = "free" # "free" | "sahih_ad_free_monthly" | "sahih_progress_plus_monthly" | "sahih_pro_monthly" | "sahih_pro_6month" | "sahih_pro_yearly" | "sahih_pro_intro_5" | |
| tier: Optional[str] = "FREE" # "FREE" | "AD_FREE_ONLY" | "PROGRESS_PLUS" | "PRO" | |
| status: str = "FREE" # "FREE" | "ACTIVE" | "EXPIRED" | "CANCELLED" | |
| activated_at: Optional[datetime] = None | |
| expires_at: Optional[datetime] = None | |
| payment_method: Optional[str] = None # "razorpay" | "google_play" | "manual_grant" | |
| transaction_id: Optional[str] = None | |
| daily_ai_queries_used: int = 0 | |
| last_query_date: Optional[str] = None # YYYY-MM-DD | |
| has_claimed_intro_offer: bool = False | |
| intro_offer_claimed_at: Optional[datetime] = None | |
| intro_offer_declined_count: int = 0 | |
| intro_offer_last_declined_at: Optional[datetime] = None | |
| auto_renew: bool = True | |
| class UserModel(BaseModel): | |
| """Root Enterprise User Document for MongoDB""" | |
| uid: str | |
| profile: UserProfileModel | |
| auth: UserAuthModel | |
| community: UserCommunityModel = Field(default_factory=UserCommunityModel) | |
| business: UserBusinessModel = Field(default_factory=UserBusinessModel) | |
| subscription: UserSubscriptionModel = Field(default_factory=UserSubscriptionModel) | |
| metrics: UserMetricsModel = Field(default_factory=UserMetricsModel) | |
| analytics: UserAnalyticsModel = Field(default_factory=UserAnalyticsModel) | |
| favorites: UserFavoritesModel = Field(default_factory=UserFavoritesModel) | |
| # Legacy flat mirrors for zero-downtime backward compatibility | |
| username: Optional[str] = None | |
| email: Optional[str] = None | |
| role: Optional[str] = "USER" | |
| is_pro: bool = False | |
| mufti_status: Optional[str] = "NONE" | |
| masjid_status: Optional[str] = "NONE" | |
| managed_masjid_id: Optional[str] = "" | |
| followed_masjids: List[str] = Field(default_factory=list) | |
| is_business_enabled: bool = False | |
| business_activated_at: Optional[datetime] = None | |
| business_expires_at: Optional[datetime] = None | |
| has_shop: bool = False | |
| created_at: Optional[datetime] = None | |
| updated_at: Optional[datetime] = None | |
| # ========================================================= | |
| # PROFESSIONAL ENTERPRISE MASJID ARCHITECTURE | |
| # ========================================================= | |
| class MasjidContactModel(BaseModel): | |
| """Public contact info for community inquiries""" | |
| phone: Optional[str] = None | |
| email: Optional[str] = None | |
| website: Optional[str] = None | |
| class MasjidIdentityModel(BaseModel): | |
| """Core public identity, fiqh, verification, and facilities""" | |
| name: str | |
| fiqh: str = "Sunni" # "Sunni" | "Shia" | "Ahmadi" | |
| description: Optional[str] = None | |
| is_verified: bool = False | |
| verified_at: Optional[datetime] = None | |
| facilities: List[str] = Field(default_factory=list) | |
| capacity: Optional[int] = None | |
| cover_image_url: Optional[str] = None | |
| gallery_urls: List[str] = Field(default_factory=list) | |
| contact: MasjidContactModel = Field(default_factory=MasjidContactModel) | |
| class GeoPointModel(BaseModel): | |
| """Standard GeoJSON Point for MongoDB 2dsphere index""" | |
| type: str = "Point" | |
| coordinates: List[float] = Field(default_factory=lambda: [39.8262, 21.4225]) # [longitude, latitude] | |
| class MasjidAddressModel(BaseModel): | |
| """Physical address hierarchy and localization""" | |
| country: str | |
| address_lines: List[str] = Field(default_factory=list) | |
| district: Optional[str] = None | |
| village: Optional[str] = None | |
| full_address: Optional[str] = None | |
| location_label: Optional[str] = None | |
| timezone_offset_hours: float = 5.5 | |
| class MasjidAdministrationModel(BaseModel): | |
| """Ownership, co-admin delegates, and status""" | |
| admin_uid: str | |
| co_admins: List[str] = Field(default_factory=list) | |
| approved_by: Optional[str] = None | |
| status: str = "APPROVED" # "PENDING" | "APPROVED" | "FLAGGED" | |
| class PrayerTimeSlot(BaseModel): | |
| """Azan and Jamaat slot""" | |
| azan: str = "-" | |
| jamaat: str = "-" | |
| class SunriseTimeSlot(BaseModel): | |
| time: str = "-" | |
| class MasjidDailyPrayerModel(BaseModel): | |
| """5 daily prayer times + sunrise""" | |
| fajr: PrayerTimeSlot = Field(default_factory=PrayerTimeSlot) | |
| sunrise: SunriseTimeSlot = Field(default_factory=SunriseTimeSlot) | |
| zuhr: PrayerTimeSlot = Field(default_factory=PrayerTimeSlot) | |
| asr: PrayerTimeSlot = Field(default_factory=PrayerTimeSlot) | |
| maghrib: PrayerTimeSlot = Field(default_factory=PrayerTimeSlot) | |
| isha: PrayerTimeSlot = Field(default_factory=PrayerTimeSlot) | |
| class MasjidJummaPrayerModel(BaseModel): | |
| """Friday congregation prayer schedules""" | |
| khutbah: str = "-" | |
| prayer: str = "-" | |
| shifts: List[str] = Field(default_factory=list) | |
| class MasjidEidPrayerModel(BaseModel): | |
| """Eid prayers""" | |
| fitr: str = "-" | |
| adha: str = "-" | |
| is_enabled: bool = False | |
| class MasjidHijriModel(BaseModel): | |
| """Local moon-sighting calendar calibration""" | |
| adjustment: int = 0 | |
| override: Optional[str] = None | |
| class MasjidPrayersModel(BaseModel): | |
| """Consolidated prayer timing and automation systems""" | |
| daily: MasjidDailyPrayerModel = Field(default_factory=MasjidDailyPrayerModel) | |
| jumma: MasjidJummaPrayerModel = Field(default_factory=MasjidJummaPrayerModel) | |
| eid: MasjidEidPrayerModel = Field(default_factory=MasjidEidPrayerModel) | |
| hijri: MasjidHijriModel = Field(default_factory=MasjidHijriModel) | |
| automation_config: dict = Field(default_factory=dict) | |
| class MasjidMetricsModel(BaseModel): | |
| """Pre-aggregated counters for fast dashboard ranking""" | |
| followers_count: int = 0 | |
| active_events_count: int = 0 | |
| reports_count: int = 0 | |
| class MasjidAuditModel(BaseModel): | |
| """Auditing and update timestamps""" | |
| created_at: datetime = Field(default_factory=datetime.utcnow) | |
| updated_at: datetime = Field(default_factory=datetime.utcnow) | |
| last_timings_updated_at: Optional[datetime] = None | |
| class MasjidModel(BaseModel): | |
| """Master Enterprise Masjid Document""" | |
| masjid_id: str | |
| identity: MasjidIdentityModel | |
| location: GeoPointModel # GeoJSON Point for 2dsphere index | |
| address: MasjidAddressModel | |
| administration: MasjidAdministrationModel | |
| prayers: MasjidPrayersModel = Field(default_factory=MasjidPrayersModel) | |
| metrics: MasjidMetricsModel = Field(default_factory=MasjidMetricsModel) | |
| audit: MasjidAuditModel = Field(default_factory=MasjidAuditModel) | |
| # Flat backward-compatibility fields (prevents breaking any existing Flutter screen) | |
| name: Optional[str] = None | |
| country: Optional[str] = None | |
| address_lines: List[str] = Field(default_factory=list) | |
| full_address: Optional[str] = None | |
| location_label: Optional[str] = None | |
| district: Optional[str] = None | |
| village: Optional[str] = None | |
| fiqh: Optional[str] = "Sunni" | |
| latitude: Optional[float] = None | |
| longitude: Optional[float] = None | |
| timezone_offset_hours: Optional[float] = 5.5 | |
| admin_uid: Optional[str] = None | |
| co_admins: List[str] = Field(default_factory=list) | |
| automation_config: Optional[dict] = None | |
| fajr_azan: Optional[str] = "-" | |
| fajr_jamaat: Optional[str] = "-" | |
| sunrise: Optional[str] = "-" | |
| zuhr_azan: Optional[str] = "-" | |
| zuhr_jamaat: Optional[str] = "-" | |
| asr_azan: Optional[str] = "-" | |
| asr_jamaat: Optional[str] = "-" | |
| maghrib_azan: Optional[str] = "-" | |
| maghrib_jamaat: Optional[str] = "-" | |
| isha_azan: Optional[str] = "-" | |
| isha_jamaat: Optional[str] = "-" | |
| jumma_khutbah: Optional[str] = "-" | |
| jumma_prayer: Optional[str] = "-" | |
| jumma_1st: Optional[str] = "-" | |
| jumma_2nd: Optional[str] = "-" | |
| jumma_3rd: Optional[str] = "-" | |
| eid_ul_fitr: Optional[str] = "-" | |
| eid_ul_adha: Optional[str] = "-" | |
| eid_enabled: Optional[bool] = False | |
| hijri_adjustment: Optional[int] = 0 | |
| hijri_override: Optional[str] = None | |
| clear_hijri_override: Optional[bool] = False | |
| region_id: Optional[str] = "kashmir" | |
| created_at: Optional[datetime] = None | |
| updated_at: Optional[datetime] = None | |
| # ========================================================= | |
| # ACCOUNT DELETION & REFUND REQUEST MODELS | |
| # ========================================================= | |
| class AccountDeleteRequest(BaseModel): | |
| """Payload for initiating a 7-day grace period account deletion""" | |
| password: Optional[str] = Field(None, description="Current password for email/password accounts") | |
| reauth_token: Optional[str] = Field(None, description="Fresh ID token or re-authentication proof for OAuth/Google accounts") | |
| reason: Optional[str] = Field(None, max_length=500, description="Optional user reason for deleting account") | |
| feedback: Optional[str] = Field(None, max_length=2000, description="Optional user feedback") | |
| class AccountCancelDeleteRequest(BaseModel): | |
| """Payload for cancelling a pending account deletion during 7-day grace period""" | |
| reason: Optional[str] = Field(None, max_length=500, description="Reason for cancelling deletion") | |
| class RefundReasonEnum(str, Enum): | |
| """Standardized user refund reasons""" | |
| ACCIDENTAL_PURCHASE = "ACCIDENTAL_PURCHASE" | |
| DUPLICATE_CHARGE = "DUPLICATE_CHARGE" | |
| SERVICE_DEFECT = "SERVICE_DEFECT" | |
| NOT_SATISFIED = "NOT_SATISFIED" | |
| OTHER = "OTHER" | |
| class RefundRequestCreate(BaseModel): | |
| """Payload for submitting a user-facing refund request""" | |
| transaction_id: str = Field(..., min_length=1, max_length=128, description="Reference transaction ID") | |
| reason: str = Field(..., min_length=2, max_length=100, description="Refund reason code or title") | |
| reason_details: Optional[str] = Field(None, max_length=2000, description="Detailed explanation of the issue") | |
| amount: Optional[float] = Field(None, gt=0, description="Requested refund amount (defaults to max refundable)") | |
| class RefundRequestReview(BaseModel): | |
| """SuperAdmin review action on a refund request""" | |
| status: str = Field(..., description="Target status: 'under_review', 'approved', or 'rejected'") | |
| admin_notes: Optional[str] = Field(None, max_length=2000, description="Internal administrator notes") | |
| rejection_reason: Optional[str] = Field(None, max_length=1000, description="Reason communicated if rejected") | |
| class RefundRequestProcess(BaseModel): | |
| """SuperAdmin execution payload for processing an approved refund""" | |
| amount: Optional[float] = Field(None, gt=0, description="Override amount if partial refund") | |
| reason: Optional[str] = Field("Customer refund request approved", max_length=500) | |
| admin_notes: Optional[str] = Field(None, max_length=2000) | |
| # ========================================================= | |
| # HIJRI CALENDAR MODELS | |
| # ========================================================= | |
| class HijriCorrectionRequest(BaseModel): | |
| '''Payload for submitting a bounded adjustment or time-scoped temporary correction''' | |
| masjid_id: str | |
| effective_from: Optional[str] = Field(None, description='ISO date YYYY-MM-DD from which correction is active') | |
| effective_to: Optional[str] = Field(None, description='ISO date YYYY-MM-DD until which correction is active') | |
| hijri_year: Optional[int] = Field(None, ge=1400, le=1600, description='Corrected Hijri year') | |
| hijri_month: Optional[int] = Field(None, ge=1, le=12, description='Corrected Hijri month (1-12)') | |
| hijri_day: Optional[int] = Field(None, ge=1, le=30, description='Corrected Hijri day (1-30)') | |
| hijri_adjustment: Optional[int] = Field(None, ge=-3, le=3, description='Persistent day adjustment (-3 to +3)') | |
| reason: Optional[str] = Field(None, max_length=200, description='Verification reason or moon sighting authority source') | |
| clear_correction: Optional[bool] = Field(False, description='Set true to clear any existing temporary correction') | |
| class MasjidIdPayload(BaseModel): | |
| '''Standard single masjid_id payload''' | |
| masjid_id: str | |