# ========================================================= # 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