superadmin / app /models.py
suhailshafi007's picture
fix(hadith/hijri): support dict/list hadith grades, timezone variance in daily content rotation, and comprehensive Hijri calendar system
fb8b215
Raw History Blame Contribute Delete
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")
@field_validator('end_date')
@classmethod
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")
@field_validator('compare_at_price')
@classmethod
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)
@field_validator('event_type')
@classmethod
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
@field_validator('metadata')
@classmethod
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
@field_validator('ad_format')
@classmethod
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
@field_validator('ad_unit_id')
@classmethod
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
@field_validator('platform')
@classmethod
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