""" Norda Biznes - Database Models =============================== SQLAlchemy models for PostgreSQL database. Models: - User: User accounts with authentication - Company: Company information - Category, Service, Competency: Company classifications - AIChatConversation, AIChatMessage: Chat history - AIAPICostLog: API cost tracking - CompanyDigitalMaturity: Digital maturity scores and benchmarking - CompanyWebsiteAnalysis: Website analysis and SEO metrics - MaturityAssessment: Historical tracking of maturity scores - GBPAudit: Google Business Profile audit results - ITAudit: IT infrastructure audit results - ITCollaborationMatch: IT collaboration matches between companies Author: Norda Biznes Development Team Created: 2025-11-23 Updated: 2026-01-09 (IT Audit Tool, IT Collaboration Matching) """ import os import json from datetime import datetime from sqlalchemy import create_engine, Column, Integer, String, Text, Boolean, DateTime, ForeignKey, Table, Numeric, Date, Time, TypeDecorator, UniqueConstraint from sqlalchemy.dialects.postgresql import ARRAY as PG_ARRAY, JSONB as PG_JSONB from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship from flask_login import UserMixin # Database configuration # WARNING: The fallback DATABASE_URL uses a placeholder password. # Production credentials MUST be set via the DATABASE_URL environment variable. # NEVER commit real credentials to version control (CWE-798). DATABASE_URL = os.getenv( 'DATABASE_URL', 'postgresql://nordabiz_app:CHANGE_ME@localhost:5432/nordabiz' ) # Determine if we're using SQLite IS_SQLITE = DATABASE_URL.startswith('sqlite') class StringArray(TypeDecorator): """ Platform-agnostic array type. Uses PostgreSQL ARRAY for PostgreSQL, stores as JSON string for SQLite. """ impl = Text cache_ok = True def load_dialect_impl(self, dialect): if dialect.name == 'postgresql': return dialect.type_descriptor(PG_ARRAY(String)) return dialect.type_descriptor(Text()) def process_bind_param(self, value, dialect): if value is None: return None if dialect.name == 'postgresql': return value return json.dumps(value) def process_result_value(self, value, dialect): if value is None: return None if dialect.name == 'postgresql': return value if isinstance(value, list): return value return json.loads(value) class JSONBType(TypeDecorator): """ Platform-agnostic JSONB type. Uses PostgreSQL JSONB for PostgreSQL, stores as JSON string for SQLite. """ impl = Text cache_ok = True def load_dialect_impl(self, dialect): if dialect.name == 'postgresql': return dialect.type_descriptor(PG_JSONB()) return dialect.type_descriptor(Text()) def process_bind_param(self, value, dialect): if value is None: return None if dialect.name == 'postgresql': return value return json.dumps(value) def process_result_value(self, value, dialect): if value is None: return None if dialect.name == 'postgresql': return value if isinstance(value, dict): return value return json.loads(value) # Aliases for backwards compatibility ARRAY = StringArray JSONB = JSONBType # Create engine engine = create_engine(DATABASE_URL, echo=False) SessionLocal = sessionmaker(bind=engine) Base = declarative_base() # ============================================================ # USER MANAGEMENT # ============================================================ class User(Base, UserMixin): """User accounts""" __tablename__ = 'users' id = Column(Integer, primary_key=True) email = Column(String(255), unique=True, nullable=False, index=True) password_hash = Column(String(255), nullable=False) name = Column(String(255)) company_nip = Column(String(10)) company_id = Column(Integer, ForeignKey('companies.id'), nullable=True) company = relationship('Company', backref='users', lazy='joined') # eager load to avoid DetachedInstanceError phone = Column(String(50)) # Status is_active = Column(Boolean, default=True) is_verified = Column(Boolean, default=False) is_admin = Column(Boolean, default=False) is_norda_member = Column(Boolean, default=False) # Timestamps created_at = Column(DateTime, default=datetime.now) last_login = Column(DateTime) verified_at = Column(DateTime) # Verification token verification_token = Column(String(255)) verification_token_expires = Column(DateTime) # Password reset token reset_token = Column(String(255)) reset_token_expires = Column(DateTime) # Relationships conversations = relationship('AIChatConversation', back_populates='user', cascade='all, delete-orphan') forum_topics = relationship('ForumTopic', back_populates='author', cascade='all, delete-orphan') forum_replies = relationship('ForumReply', back_populates='author', cascade='all, delete-orphan') def __repr__(self): return f'' # ============================================================ # COMPANY DIRECTORY (existing schema from SQL) # ============================================================ class Category(Base): """Company categories""" __tablename__ = 'categories' id = Column(Integer, primary_key=True) name = Column(String(100), nullable=False, unique=True) slug = Column(String(100), nullable=False, unique=True) description = Column(Text) icon = Column(String(50)) sort_order = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.now) companies = relationship('Company', back_populates='category') class Company(Base): """Companies""" __tablename__ = 'companies' id = Column(Integer, primary_key=True) name = Column(String(255), nullable=False) legal_name = Column(String(255)) slug = Column(String(255), nullable=False, unique=True, index=True) category_id = Column(Integer, ForeignKey('categories.id')) # Descriptions description_short = Column(Text) description_full = Column(Text) # Legal nip = Column(String(10), unique=True) regon = Column(String(14)) krs = Column(String(10)) # External registry slugs aleo_slug = Column(String(255)) # ALEO.com company slug for direct links # Contact website = Column(String(500)) email = Column(String(255)) phone = Column(String(50)) # Address address_street = Column(String(255)) address_city = Column(String(100)) address_postal = Column(String(10)) address_full = Column(Text) # Business data year_established = Column(Integer) employees_count = Column(Integer) capital_amount = Column(Numeric(15, 2)) # Status (PostgreSQL uses ENUM types, no default here) status = Column(String(20)) data_quality = Column(String(20)) # Extended company info legal_form = Column(String(100)) parent_organization = Column(String(255)) industry_sector = Column(String(255)) services_offered = Column(Text) operational_area = Column(String(500)) languages_offered = Column(String(200)) technologies_used = Column(Text) founding_history = Column(Text) # Historia firmy + właściciele core_values = Column(Text) # Wartości firmy branch_count = Column(Integer) employee_count_range = Column(String(50)) # Data source tracking data_source = Column(String(100)) data_quality_score = Column(Integer) last_verified_at = Column(DateTime) norda_biznes_url = Column(String(500)) norda_biznes_member_id = Column(String(50)) # Metadata last_updated = Column(DateTime, default=datetime.now) created_at = Column(DateTime, default=datetime.now) # === DIGITAL MATURITY (added 2025-11-26) === digital_maturity_last_assessed = Column(DateTime) digital_maturity_score = Column(Integer) # 0-100 composite score digital_maturity_rank_category = Column(Integer) digital_maturity_rank_overall = Column(Integer) # AI Readiness ai_enabled = Column(Boolean, default=False) ai_tools_used = Column(ARRAY(String)) # PostgreSQL array (will be Text for SQLite) data_structured = Column(Boolean, default=False) # IT Management it_manager_exists = Column(Boolean, default=False) it_outsourced = Column(Boolean, default=False) it_provider_company_id = Column(Integer, ForeignKey('companies.id')) # Website tracking website_last_analyzed = Column(DateTime) website_status = Column(String(20)) # 'active', 'broken', 'no_website' website_quality_score = Column(Integer) # 0-100 # Relationships category = relationship('Category', back_populates='companies') services = relationship('CompanyService', back_populates='company', cascade='all, delete-orphan') competencies = relationship('CompanyCompetency', back_populates='company', cascade='all, delete-orphan') certifications = relationship('Certification', back_populates='company', cascade='all, delete-orphan') awards = relationship('Award', back_populates='company', cascade='all, delete-orphan') events = relationship('CompanyEvent', back_populates='company', cascade='all, delete-orphan') # Digital Maturity relationships digital_maturity = relationship('CompanyDigitalMaturity', back_populates='company', uselist=False) website_analyses = relationship('CompanyWebsiteAnalysis', back_populates='company', cascade='all, delete-orphan') maturity_history = relationship('MaturityAssessment', back_populates='company', cascade='all, delete-orphan') # Quality tracking quality_tracking = relationship('CompanyQualityTracking', back_populates='company', uselist=False) # Website scraping and AI analysis website_content = relationship('CompanyWebsiteContent', back_populates='company', cascade='all, delete-orphan') ai_insights = relationship('CompanyAIInsights', back_populates='company', uselist=False) class Service(Base): """Services offered by companies""" __tablename__ = 'services' id = Column(Integer, primary_key=True) name = Column(String(255), nullable=False, unique=True) slug = Column(String(255), nullable=False, unique=True) description = Column(Text) created_at = Column(DateTime, default=datetime.now) companies = relationship('CompanyService', back_populates='service') class CompanyService(Base): """Many-to-many: Companies <-> Services""" __tablename__ = 'company_services' company_id = Column(Integer, ForeignKey('companies.id'), primary_key=True) service_id = Column(Integer, ForeignKey('services.id'), primary_key=True) is_primary = Column(Boolean, default=False) added_at = Column(DateTime, default=datetime.now) company = relationship('Company', back_populates='services') service = relationship('Service', back_populates='companies') class Competency(Base): """Competencies/skills of companies""" __tablename__ = 'competencies' id = Column(Integer, primary_key=True) name = Column(String(255), nullable=False, unique=True) slug = Column(String(255), nullable=False, unique=True) category = Column(String(100)) description = Column(Text) created_at = Column(DateTime, default=datetime.now) companies = relationship('CompanyCompetency', back_populates='competency') class CompanyCompetency(Base): """Many-to-many: Companies <-> Competencies""" __tablename__ = 'company_competencies' company_id = Column(Integer, ForeignKey('companies.id'), primary_key=True) competency_id = Column(Integer, ForeignKey('competencies.id'), primary_key=True) level = Column(String(50)) added_at = Column(DateTime, default=datetime.now) company = relationship('Company', back_populates='competencies') competency = relationship('Competency', back_populates='companies') class Certification(Base): """Company certifications""" __tablename__ = 'certifications' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id')) name = Column(String(255), nullable=False) issuer = Column(String(255)) certificate_number = Column(String(100)) issue_date = Column(Date) expiry_date = Column(Date) is_active = Column(Boolean, default=True) created_at = Column(DateTime, default=datetime.now) company = relationship('Company', back_populates='certifications') class Award(Base): """Company awards and achievements""" __tablename__ = 'awards' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id')) name = Column(String(255), nullable=False) issuer = Column(String(255)) year = Column(Integer) description = Column(Text) created_at = Column(DateTime, default=datetime.now) company = relationship('Company', back_populates='awards') class CompanyEvent(Base): """Company events and news""" __tablename__ = 'company_events' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id')) event_type = Column(String(50), nullable=False) title = Column(String(500), nullable=False) description = Column(Text) event_date = Column(Date) source_url = Column(String(1000)) created_at = Column(DateTime, default=datetime.now) company = relationship('Company', back_populates='events') # ============================================================ # DIGITAL MATURITY ASSESSMENT PLATFORM # ============================================================ class CompanyDigitalMaturity(Base): """Central dashboard for company digital maturity - composite scores and benchmarking""" __tablename__ = 'company_digital_maturity' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id'), nullable=False, unique=True, index=True) last_updated = Column(DateTime, default=datetime.now) # === COMPOSITE SCORES (0-100 each) === overall_score = Column(Integer) online_presence_score = Column(Integer) social_media_score = Column(Integer) it_infrastructure_score = Column(Integer) business_applications_score = Column(Integer) backup_disaster_recovery_score = Column(Integer) cybersecurity_score = Column(Integer) ai_readiness_score = Column(Integer) digital_marketing_score = Column(Integer) # === GAPS & OPPORTUNITIES === critical_gaps = Column(ARRAY(String)) # ['no_backup', 'no_firewall', etc.] improvement_priority = Column(String(20)) # 'critical', 'high', 'medium', 'low' estimated_investment_needed = Column(Numeric(10, 2)) # PLN # === BENCHMARKING === rank_in_category = Column(Integer) # position in category rank_overall = Column(Integer) # overall position percentile = Column(Integer) # top X% of companies # === SALES INTELLIGENCE === total_opportunity_value = Column(Numeric(10, 2)) # potential sales value (PLN) sales_readiness = Column(String(20)) # 'hot', 'warm', 'cold', 'not_ready' # Relationship company = relationship('Company', back_populates='digital_maturity') class CompanyWebsiteAnalysis(Base): """Detailed website and online presence analysis""" __tablename__ = 'company_website_analysis' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id'), nullable=False, index=True) analyzed_at = Column(DateTime, default=datetime.now, index=True) # === BASIC INFO === website_url = Column(String(500)) final_url = Column(String(500)) # After redirects http_status_code = Column(Integer) load_time_ms = Column(Integer) # === TECHNICAL === has_ssl = Column(Boolean, default=False) ssl_expires_at = Column(Date) ssl_issuer = Column(String(100)) # Certificate Authority (Let's Encrypt, DigiCert, etc.) is_responsive = Column(Boolean, default=False) # mobile-friendly cms_detected = Column(String(100)) frameworks_detected = Column(ARRAY(String)) # ['WordPress', 'Bootstrap', etc.] # === HOSTING & SERVER (from audit) === last_modified_at = Column(DateTime) hosting_provider = Column(String(100)) hosting_ip = Column(String(45)) server_software = Column(String(100)) site_author = Column(String(255)) # Website creator/agency site_generator = Column(String(100)) domain_registrar = Column(String(100)) is_mobile_friendly = Column(Boolean, default=False) has_viewport_meta = Column(Boolean, default=False) # === GOOGLE BUSINESS (from audit) === google_rating = Column(Numeric(2, 1)) google_reviews_count = Column(Integer) google_place_id = Column(String(100)) google_business_status = Column(String(50)) google_opening_hours = Column(JSONB) # Opening hours from GBP google_photos_count = Column(Integer) # Number of photos on GBP google_name = Column(String(255)) # Business name from Google google_address = Column(String(500)) # Formatted address from Google google_phone = Column(String(50)) # Phone from Google google_website = Column(String(500)) # Website from Google google_types = Column(ARRAY(Text)) # Business types/categories google_maps_url = Column(String(500)) # Google Maps URL # === AUDIT METADATA === audit_source = Column(String(50), default='automated') audit_version = Column(String(20), default='1.0') audit_errors = Column(Text) # === CONTENT RICHNESS === content_richness_score = Column(Integer) # 1-10 page_count_estimate = Column(Integer) word_count_homepage = Column(Integer) has_blog = Column(Boolean, default=False) has_portfolio = Column(Boolean, default=False) has_contact_form = Column(Boolean, default=False) has_live_chat = Column(Boolean, default=False) # === EXTRACTED CONTENT === content_summary = Column(Text) # AI-generated summary from website services_extracted = Column(ARRAY(String)) # Services mentioned on website main_keywords = Column(ARRAY(String)) # Top keywords # === SEO === seo_title = Column(String(500)) seo_description = Column(Text) has_sitemap = Column(Boolean, default=False) has_robots_txt = Column(Boolean, default=False) google_indexed_pages = Column(Integer) # === PAGESPEED INSIGHTS SCORES (0-100) === pagespeed_seo_score = Column(Integer) # Google PageSpeed SEO score 0-100 pagespeed_performance_score = Column(Integer) # Google PageSpeed Performance score 0-100 pagespeed_accessibility_score = Column(Integer) # Google PageSpeed Accessibility score 0-100 pagespeed_best_practices_score = Column(Integer) # Google PageSpeed Best Practices score 0-100 pagespeed_audits = Column(JSONB) # Full PageSpeed audit results as JSON # === ON-PAGE SEO DETAILS === meta_title = Column(String(500)) # Full meta title from tag meta_description = Column(Text) # Full meta description from <meta name="description"> meta_keywords = Column(Text) # Meta keywords (legacy, rarely used) # Heading structure h1_count = Column(Integer) # Number of H1 tags on homepage (should be 1) h2_count = Column(Integer) # Number of H2 tags on homepage h3_count = Column(Integer) # Number of H3 tags on homepage h1_text = Column(String(500)) # Text content of first H1 tag # Image analysis total_images = Column(Integer) # Total number of images images_without_alt = Column(Integer) # Images missing alt attribute - accessibility issue images_with_alt = Column(Integer) # Images with proper alt text # Link analysis internal_links_count = Column(Integer) # Links to same domain external_links_count = Column(Integer) # Links to external domains broken_links_count = Column(Integer) # Links returning 4xx/5xx # Structured data (Schema.org, JSON-LD, Microdata) has_structured_data = Column(Boolean, default=False) # Whether page contains JSON-LD, Microdata, or RDFa structured_data_types = Column(ARRAY(String)) # Schema.org types found: Organization, LocalBusiness, etc. structured_data_json = Column(JSONB) # Full structured data as JSON # === TECHNICAL SEO === # Canonical URL handling has_canonical = Column(Boolean, default=False) # Whether page has canonical URL defined canonical_url = Column(String(500)) # The canonical URL value # Indexability is_indexable = Column(Boolean, default=True) # Whether page can be indexed (no noindex directive) noindex_reason = Column(String(200)) # Reason if page is not indexable: meta tag, robots.txt, etc. # Core Web Vitals viewport_configured = Column(Boolean) # Whether viewport meta tag is properly configured largest_contentful_paint_ms = Column(Integer) # Core Web Vital: LCP in milliseconds first_input_delay_ms = Column(Integer) # Core Web Vital: FID in milliseconds cumulative_layout_shift = Column(Numeric(5, 3)) # Core Web Vital: CLS score # Open Graph & Social Meta has_og_tags = Column(Boolean, default=False) # Whether page has Open Graph tags og_title = Column(String(500)) # Open Graph title og_description = Column(Text) # Open Graph description og_image = Column(String(500)) # Open Graph image URL has_twitter_cards = Column(Boolean, default=False) # Whether page has Twitter Card meta tags # Language & International html_lang = Column(String(10)) # Language attribute from <html lang="..."> has_hreflang = Column(Boolean, default=False) # Whether page has hreflang tags # === SEO AUDIT METADATA === seo_audit_version = Column(String(20)) # Version of SEO audit script used seo_audited_at = Column(DateTime) # Timestamp of last SEO audit seo_audit_errors = Column(ARRAY(String)) # Errors encountered during SEO audit seo_overall_score = Column(Integer) # Calculated overall SEO score 0-100 seo_health_score = Column(Integer) # On-page SEO health score 0-100 seo_issues = Column(JSONB) # List of SEO issues found with severity levels # === DOMAIN === domain_registered_at = Column(Date) domain_expires_at = Column(Date) domain_age_years = Column(Integer) # === ANALYTICS === has_google_analytics = Column(Boolean, default=False) has_google_tag_manager = Column(Boolean, default=False) has_facebook_pixel = Column(Boolean, default=False) # === OPPORTUNITY SCORING === needs_redesign = Column(Boolean, default=False) missing_features = Column(ARRAY(String)) # ['blog', 'portfolio', 'ssl', etc.] opportunity_score = Column(Integer) # 0-100 estimated_project_value = Column(Numeric(10, 2)) # PLN opportunity_notes = Column(Text) # Relationship company = relationship('Company', back_populates='website_analyses') class CompanyQualityTracking(Base): """Quality tracking for company data - verification counter and quality score""" __tablename__ = 'company_quality_tracking' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id'), nullable=False, unique=True, index=True) verification_count = Column(Integer, default=0) last_verified_at = Column(DateTime) verified_by = Column(String(100)) verification_notes = Column(Text) quality_score = Column(Integer) # 0-100% issues_found = Column(Integer, default=0) issues_fixed = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.utcnow) updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Relationship company = relationship('Company', back_populates='quality_tracking') class CompanyWebsiteContent(Base): """Scraped website content for companies""" __tablename__ = 'company_website_content' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id'), nullable=False, index=True) scraped_at = Column(DateTime, default=datetime.utcnow) url = Column(String(500)) http_status = Column(Integer) raw_html = Column(Text) raw_text = Column(Text) page_title = Column(String(500)) meta_description = Column(Text) main_content = Column(Text) email_addresses = Column(ARRAY(String)) phone_numbers = Column(ARRAY(String)) social_media = Column(JSONB) word_count = Column(Integer) # Relationship company = relationship('Company', back_populates='website_content') class CompanyAIInsights(Base): """AI-generated insights from website analysis""" __tablename__ = 'company_ai_insights' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id'), nullable=False, unique=True, index=True) content_id = Column(Integer, ForeignKey('company_website_content.id')) business_summary = Column(Text) services_list = Column(ARRAY(String)) target_market = Column(Text) unique_selling_points = Column(ARRAY(String)) company_values = Column(ARRAY(String)) certifications = Column(ARRAY(String)) suggested_category = Column(String(100)) category_confidence = Column(Numeric(3, 2)) industry_tags = Column(ARRAY(String)) ai_confidence_score = Column(Numeric(3, 2)) processing_time_ms = Column(Integer) analyzed_at = Column(DateTime, default=datetime.utcnow) # Relationship company = relationship('Company', back_populates='ai_insights') class MaturityAssessment(Base): """Historical tracking of digital maturity scores over time""" __tablename__ = 'maturity_assessments' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id'), nullable=False, index=True) assessed_at = Column(DateTime, default=datetime.now, index=True) assessed_by_user_id = Column(Integer, ForeignKey('users.id')) assessment_type = Column(String(50)) # 'full', 'quick', 'self_reported', 'audit' # === SNAPSHOT OF SCORES === overall_score = Column(Integer) online_presence_score = Column(Integer) social_media_score = Column(Integer) it_infrastructure_score = Column(Integer) business_applications_score = Column(Integer) backup_dr_score = Column(Integer) cybersecurity_score = Column(Integer) ai_readiness_score = Column(Integer) # === CHANGES SINCE LAST ASSESSMENT === score_change = Column(Integer) # +5, -3, etc. areas_improved = Column(ARRAY(String)) # ['cybersecurity', 'backup'] areas_declined = Column(ARRAY(String)) # ['social_media'] notes = Column(Text) # Relationship company = relationship('Company', back_populates='maturity_history') # ============================================================ # AI CHAT # ============================================================ class AIChatConversation(Base): """Chat conversations""" __tablename__ = 'ai_chat_conversations' id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey('users.id'), nullable=False, index=True) title = Column(String(255)) conversation_type = Column(String(50), default='general') # Timestamps started_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) is_active = Column(Boolean, default=True) # Metrics message_count = Column(Integer, default=0) model_name = Column(String(100)) # Relationships user = relationship('User', back_populates='conversations') messages = relationship('AIChatMessage', back_populates='conversation', cascade='all, delete-orphan', order_by='AIChatMessage.created_at') class AIChatMessage(Base): """Chat messages""" __tablename__ = 'ai_chat_messages' id = Column(Integer, primary_key=True) conversation_id = Column(Integer, ForeignKey('ai_chat_conversations.id'), nullable=False, index=True) created_at = Column(DateTime, default=datetime.now) # Message role = Column(String(20), nullable=False) # 'user' or 'assistant' content = Column(Text, nullable=False) # Metrics tokens_input = Column(Integer) tokens_output = Column(Integer) cost_usd = Column(Numeric(10, 6)) latency_ms = Column(Integer) # Flags edited = Column(Boolean, default=False) regenerated = Column(Boolean, default=False) # Feedback (for assistant messages) feedback_rating = Column(Integer) # 1 = thumbs down, 2 = thumbs up feedback_comment = Column(Text) # Optional user comment feedback_at = Column(DateTime) # Quality metrics (for analytics) companies_mentioned = Column(Integer) # Number of companies in response query_intent = Column(String(100)) # Detected intent: 'find_company', 'get_info', 'compare', etc. # Relationship conversation = relationship('AIChatConversation', back_populates='messages') feedback = relationship('AIChatFeedback', back_populates='message', uselist=False) class AIChatFeedback(Base): """Detailed feedback for AI responses - for learning and improvement""" __tablename__ = 'ai_chat_feedback' id = Column(Integer, primary_key=True) message_id = Column(Integer, ForeignKey('ai_chat_messages.id'), nullable=False, unique=True) user_id = Column(Integer, ForeignKey('users.id'), nullable=False) created_at = Column(DateTime, default=datetime.now) # Rating rating = Column(Integer, nullable=False) # 1-5 stars or 1=bad, 2=good is_helpful = Column(Boolean) # Was the answer helpful? is_accurate = Column(Boolean) # Was the information accurate? found_company = Column(Boolean) # Did user find what they were looking for? # Feedback text comment = Column(Text) suggested_answer = Column(Text) # What should have been the answer? # Context for learning original_query = Column(Text) # The user's question expected_companies = Column(Text) # JSON list of company names user expected # Relationship message = relationship('AIChatMessage', back_populates='feedback') # ============================================================ # FORUM # ============================================================ class ForumTopic(Base): """Forum topics/threads""" __tablename__ = 'forum_topics' id = Column(Integer, primary_key=True) title = Column(String(255), nullable=False) content = Column(Text, nullable=False) author_id = Column(Integer, ForeignKey('users.id'), nullable=False) # Status is_pinned = Column(Boolean, default=False) is_locked = Column(Boolean, default=False) views_count = Column(Integer, default=0) # Timestamps created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationships author = relationship('User', back_populates='forum_topics') replies = relationship('ForumReply', back_populates='topic', cascade='all, delete-orphan', order_by='ForumReply.created_at') @property def reply_count(self): return len(self.replies) @property def last_activity(self): if self.replies: return max(r.created_at for r in self.replies) return self.created_at class ForumReply(Base): """Forum replies to topics""" __tablename__ = 'forum_replies' id = Column(Integer, primary_key=True) topic_id = Column(Integer, ForeignKey('forum_topics.id'), nullable=False) author_id = Column(Integer, ForeignKey('users.id'), nullable=False) content = Column(Text, nullable=False) # Timestamps created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationships topic = relationship('ForumTopic', back_populates='replies') author = relationship('User', back_populates='forum_replies') class AIAPICostLog(Base): """API cost tracking""" __tablename__ = 'ai_api_costs' id = Column(Integer, primary_key=True) timestamp = Column(DateTime, default=datetime.now, index=True) # API details api_provider = Column(String(50)) # 'gemini' model_name = Column(String(100)) feature = Column(String(100)) # 'ai_chat', 'general', etc. # User context user_id = Column(Integer, ForeignKey('users.id'), index=True) # Token usage input_tokens = Column(Integer) output_tokens = Column(Integer) total_tokens = Column(Integer) # Costs input_cost = Column(Numeric(10, 6)) output_cost = Column(Numeric(10, 6)) total_cost = Column(Numeric(10, 6)) # Status success = Column(Boolean, default=True) error_message = Column(Text) latency_ms = Column(Integer) # Privacy prompt_hash = Column(String(64)) # SHA256 hash, not storing actual prompts # ============================================================ # CALENDAR / EVENTS # ============================================================ class NordaEvent(Base): """Spotkania i wydarzenia Norda Biznes""" __tablename__ = 'norda_events' id = Column(Integer, primary_key=True) title = Column(String(255), nullable=False) description = Column(Text) event_type = Column(String(50), default='meeting') # meeting, webinar, networking, other # Data i czas event_date = Column(Date, nullable=False) time_start = Column(Time) time_end = Column(Time) # Lokalizacja location = Column(String(500)) # Adres lub "Online" location_url = Column(String(1000)) # Link do Google Maps lub Zoom # Prelegent (opcjonalnie) speaker_name = Column(String(255)) speaker_company_id = Column(Integer, ForeignKey('companies.id')) # Metadane is_featured = Column(Boolean, default=False) max_attendees = Column(Integer) created_by = Column(Integer, ForeignKey('users.id')) created_at = Column(DateTime, default=datetime.now) # Relationships speaker_company = relationship('Company') creator = relationship('User', foreign_keys=[created_by]) attendees = relationship('EventAttendee', back_populates='event', cascade='all, delete-orphan') @property def attendee_count(self): return len(self.attendees) @property def is_past(self): from datetime import date return self.event_date < date.today() class EventAttendee(Base): """RSVP na wydarzenia""" __tablename__ = 'event_attendees' id = Column(Integer, primary_key=True) event_id = Column(Integer, ForeignKey('norda_events.id'), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False) status = Column(String(20), default='confirmed') # confirmed, maybe, declined registered_at = Column(DateTime, default=datetime.now) event = relationship('NordaEvent', back_populates='attendees') user = relationship('User') # ============================================================ # PRIVATE MESSAGES # ============================================================ class PrivateMessage(Base): """Wiadomości prywatne między członkami""" __tablename__ = 'private_messages' id = Column(Integer, primary_key=True) sender_id = Column(Integer, ForeignKey('users.id'), nullable=False) recipient_id = Column(Integer, ForeignKey('users.id'), nullable=False) subject = Column(String(255)) content = Column(Text, nullable=False) is_read = Column(Boolean, default=False) read_at = Column(DateTime) # Dla wątków konwersacji parent_id = Column(Integer, ForeignKey('private_messages.id')) created_at = Column(DateTime, default=datetime.now) sender = relationship('User', foreign_keys=[sender_id], backref='sent_messages') recipient = relationship('User', foreign_keys=[recipient_id], backref='received_messages') parent = relationship('PrivateMessage', remote_side=[id]) # ============================================================ # B2B CLASSIFIEDS # ============================================================ class Classified(Base): """Ogłoszenia B2B - Szukam/Oferuję""" __tablename__ = 'classifieds' id = Column(Integer, primary_key=True) author_id = Column(Integer, ForeignKey('users.id'), nullable=False) company_id = Column(Integer, ForeignKey('companies.id')) # Typ ogłoszenia listing_type = Column(String(20), nullable=False) # 'szukam', 'oferuje' category = Column(String(50), nullable=False) # uslugi, produkty, wspolpraca, praca, inne title = Column(String(255), nullable=False) description = Column(Text, nullable=False) # Opcjonalne szczegóły budget_info = Column(String(255)) # "do negocjacji", "5000-10000 PLN" location_info = Column(String(255)) # Wejherowo, Cała Polska, Online # Status is_active = Column(Boolean, default=True) expires_at = Column(DateTime) # Auto-wygaśnięcie po 30 dniach views_count = Column(Integer, default=0) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) author = relationship('User', backref='classifieds') company = relationship('Company') @property def is_expired(self): if self.expires_at: return datetime.now() > self.expires_at return False class CompanyContact(Base): """Multiple contacts (phones, emails) per company with source tracking""" __tablename__ = 'company_contacts' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) # Contact type: 'phone', 'email', 'fax', 'mobile' contact_type = Column(String(20), nullable=False, index=True) # Contact value (phone number or email address) value = Column(String(255), nullable=False) # Purpose/description: 'Biuro', 'Sprzedaż', 'Właściciel', 'Transport', 'Serwis', etc. purpose = Column(String(100)) # Is this the primary contact of this type? is_primary = Column(Boolean, default=False) # Source of this contact data source = Column(String(100)) # 'website', 'krs', 'google_business', 'facebook', 'manual', 'brave_search' source_url = Column(String(500)) # URL where the contact was found source_date = Column(Date) # When the contact was found/verified # Validation is_verified = Column(Boolean, default=False) verified_at = Column(DateTime) verified_by = Column(String(100)) # Metadata created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationship company = relationship('Company', backref='contacts') __table_args__ = ( UniqueConstraint('company_id', 'contact_type', 'value', name='uq_company_contact_type_value'), ) class CompanySocialMedia(Base): """Social media profiles for companies with verification tracking""" __tablename__ = 'company_social_media' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) platform = Column(String(50), nullable=False, index=True) # facebook, linkedin, instagram, youtube, twitter url = Column(String(500), nullable=False) # Tracking freshness verified_at = Column(DateTime, nullable=False, default=datetime.now, index=True) source = Column(String(100)) # website_scrape, brave_search, manual, facebook_api # Validation is_valid = Column(Boolean, default=True) last_checked_at = Column(DateTime) check_status = Column(String(50)) # ok, 404, redirect, blocked # Metadata from platform page_name = Column(String(255)) followers_count = Column(Integer) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationship company = relationship('Company', backref='social_media_profiles') __table_args__ = ( UniqueConstraint('company_id', 'platform', 'url', name='uq_company_platform_url'), ) class CompanyRecommendation(Base): """Peer recommendations between NORDA BIZNES members""" __tablename__ = 'company_recommendations' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) # Recommendation content recommendation_text = Column(Text, nullable=False) service_category = Column(String(200)) # Optional: specific service recommended for # Privacy settings show_contact = Column(Boolean, default=True) # Show recommender's contact info # Moderation status = Column(String(20), default='pending', index=True) # pending, approved, rejected moderated_by = Column(Integer, ForeignKey('users.id'), nullable=True) moderated_at = Column(DateTime) rejection_reason = Column(Text) # Timestamps created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationships company = relationship('Company', backref='recommendations') user = relationship('User', foreign_keys=[user_id], backref='recommendations_given') moderator = relationship('User', foreign_keys=[moderated_by], backref='recommendations_moderated') __table_args__ = ( UniqueConstraint('user_id', 'company_id', name='uq_user_company_recommendation'), ) class UserNotification(Base): """ In-app notifications for users. Supports badges and notification center. """ __tablename__ = 'user_notifications' id = Column(Integer, primary_key=True) user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'), nullable=False, index=True) # Notification content title = Column(String(255), nullable=False) message = Column(Text) notification_type = Column(String(50), default='info', index=True) # Types: news, system, message, event, alert # Related entity (optional) related_type = Column(String(50)) # company_news, event, message related_id = Column(Integer) # Status is_read = Column(Boolean, default=False, index=True) read_at = Column(DateTime) # Link action_url = Column(String(500)) # Timestamps created_at = Column(DateTime, default=datetime.now, index=True) # Relationship user = relationship('User', backref='notifications') def mark_as_read(self): self.is_read = True self.read_at = datetime.now() # ============================================================ # GOOGLE BUSINESS PROFILE AUDIT # ============================================================ class GBPAudit(Base): """ Google Business Profile audit results for companies. Tracks completeness scores and provides improvement recommendations. """ __tablename__ = 'gbp_audits' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) # Audit timestamp audit_date = Column(DateTime, default=datetime.now, nullable=False, index=True) # Completeness scoring (0-100) completeness_score = Column(Integer) # Field-by-field status tracking # Example: {"name": {"status": "complete", "value": "Company Name"}, "phone": {"status": "missing"}, ...} fields_status = Column(JSONB) # AI-generated recommendations # Example: [{"priority": "high", "field": "description", "recommendation": "Add a detailed business description..."}, ...] recommendations = Column(JSONB) # Individual field scores (for detailed breakdown) has_name = Column(Boolean, default=False) has_address = Column(Boolean, default=False) has_phone = Column(Boolean, default=False) has_website = Column(Boolean, default=False) has_hours = Column(Boolean, default=False) has_categories = Column(Boolean, default=False) has_photos = Column(Boolean, default=False) has_description = Column(Boolean, default=False) has_services = Column(Boolean, default=False) has_reviews = Column(Boolean, default=False) # Photo counts photo_count = Column(Integer, default=0) logo_present = Column(Boolean, default=False) cover_photo_present = Column(Boolean, default=False) # Review metrics review_count = Column(Integer, default=0) average_rating = Column(Numeric(2, 1)) # Google Place data google_place_id = Column(String(100)) google_maps_url = Column(String(500)) # Audit metadata audit_source = Column(String(50), default='manual') # manual, automated, api audit_version = Column(String(20), default='1.0') audit_errors = Column(Text) # Timestamps created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationship company = relationship('Company', backref='gbp_audits') def __repr__(self): return f'<GBPAudit company_id={self.company_id} score={self.completeness_score}>' @property def score_category(self): """Return score category: excellent, good, needs_work, poor""" if self.completeness_score is None: return 'unknown' if self.completeness_score >= 90: return 'excellent' elif self.completeness_score >= 70: return 'good' elif self.completeness_score >= 50: return 'needs_work' else: return 'poor' # ============================================================ # IT INFRASTRUCTURE AUDIT # ============================================================ class ITAudit(Base): """ IT infrastructure audit for companies. Tracks IT infrastructure, security posture, and collaboration readiness. Used for cross-company collaboration matching. """ __tablename__ = 'it_audits' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) # Audit timestamp and metadata audit_date = Column(DateTime, default=datetime.now, nullable=False, index=True) audit_source = Column(String(50), default='form') # form, api_sync audited_by = Column(Integer, ForeignKey('users.id')) # === SCORES (0-100) === overall_score = Column(Integer) completeness_score = Column(Integer) security_score = Column(Integer) collaboration_score = Column(Integer) maturity_level = Column(String(20)) # basic, developing, established, advanced # === SECTION 1: IT CONTACT === has_it_manager = Column(Boolean, default=False) it_outsourced = Column(Boolean, default=False) it_provider_name = Column(String(255)) it_contact_name = Column(String(255)) it_contact_email = Column(String(255)) # === SECTION 2: CLOUD & IDENTITY === has_azure_ad = Column(Boolean, default=False) azure_tenant_name = Column(String(255)) azure_user_count = Column(String(20)) # Range: 1-10, 11-50, 51-100, 100+ has_m365 = Column(Boolean, default=False) m365_plans = Column(ARRAY(String)) # Business Basic, Business Standard, E3, E5, etc. teams_usage = Column(ARRAY(String)) # chat, meetings, files, phone has_google_workspace = Column(Boolean, default=False) # === SECTION 3: SERVER INFRASTRUCTURE === server_count = Column(String(20)) # Range: 0, 1-3, 4-10, 10+ server_types = Column(ARRAY(String)) # physical, vm_onprem, cloud_iaas virtualization_platform = Column(String(50)) # none, vmware, hyperv, proxmox, kvm server_os = Column(ARRAY(String)) # windows_server, linux_ubuntu, linux_debian, linux_rhel network_firewall_brand = Column(String(100)) # === SECTION 4: ENDPOINTS === employee_count = Column(String(20)) # Range: 1-10, 11-50, 51-100, 100+ computer_count = Column(String(20)) # Range: 1-10, 11-50, 51-100, 100+ desktop_os = Column(ARRAY(String)) # windows_10, windows_11, macos, linux has_mdm = Column(Boolean, default=False) mdm_solution = Column(String(50)) # intune, jamf, other # === SECTION 5: SECURITY === antivirus_solution = Column(String(50)) # none, windows_defender, eset, kaspersky, other has_edr = Column(Boolean, default=False) edr_solution = Column(String(100)) # microsoft_defender_atp, crowdstrike, sentinelone, other has_vpn = Column(Boolean, default=False) vpn_solution = Column(String(50)) # ipsec, wireguard, openvpn, fortinet, other has_mfa = Column(Boolean, default=False) mfa_scope = Column(ARRAY(String)) # email, vpn, erp, all_apps # === SECTION 6: BACKUP & DISASTER RECOVERY === backup_solution = Column(String(50)) # none, veeam, acronis, pbs, azure_backup, other backup_targets = Column(ARRAY(String)) # local_nas, offsite, cloud, tape backup_frequency = Column(String(20)) # daily, weekly, monthly, continuous has_proxmox_pbs = Column(Boolean, default=False) has_dr_plan = Column(Boolean, default=False) # === SECTION 7: MONITORING === monitoring_solution = Column(String(50)) # none, zabbix, prtg, nagios, datadog, other zabbix_integration = Column(JSONB) # {hostname: '', agent_installed: bool, templates: []} # === SECTION 8: BUSINESS APPS === ticketing_system = Column(String(50)) # none, freshdesk, zendesk, jira_service, other erp_system = Column(String(50)) # none, sap, microsoft_dynamics, enova, optima, other crm_system = Column(String(50)) # none, salesforce, hubspot, pipedrive, other document_management = Column(String(50)) # none, sharepoint, google_drive, dropbox, other # === SECTION 9: ACTIVE DIRECTORY === has_local_ad = Column(Boolean, default=False) ad_domain_name = Column(String(255)) has_ad_azure_sync = Column(Boolean, default=False) # Azure AD Connect / Cloud Sync # === COLLABORATION FLAGS (for matching algorithm) === open_to_shared_licensing = Column(Boolean, default=False) open_to_backup_replication = Column(Boolean, default=False) open_to_teams_federation = Column(Boolean, default=False) open_to_shared_monitoring = Column(Boolean, default=False) open_to_collective_purchasing = Column(Boolean, default=False) open_to_knowledge_sharing = Column(Boolean, default=False) # === RAW DATA & METADATA === form_data = Column(JSONB) # Full form submission for reference recommendations = Column(JSONB) # AI-generated recommendations audit_errors = Column(Text) # Any errors during audit processing # Timestamps created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationships company = relationship('Company', backref='it_audits') auditor = relationship('User', foreign_keys=[audited_by]) def __repr__(self): return f'<ITAudit company_id={self.company_id} score={self.overall_score}>' @property def maturity_label(self): """Return Polish label for maturity level""" labels = { 'basic': 'Podstawowy', 'developing': 'Rozwijający się', 'established': 'Ugruntowany', 'advanced': 'Zaawansowany' } return labels.get(self.maturity_level, 'Nieznany') @property def score_category(self): """Return score category: excellent, good, needs_work, poor""" if self.overall_score is None: return 'unknown' if self.overall_score >= 80: return 'excellent' elif self.overall_score >= 60: return 'good' elif self.overall_score >= 40: return 'needs_work' else: return 'poor' @property def collaboration_flags_count(self): """Count how many collaboration flags are enabled""" flags = [ self.open_to_shared_licensing, self.open_to_backup_replication, self.open_to_teams_federation, self.open_to_shared_monitoring, self.open_to_collective_purchasing, self.open_to_knowledge_sharing ] return sum(1 for f in flags if f) class ITCollaborationMatch(Base): """ IT collaboration matches between companies. Stores potential collaboration opportunities discovered by the matching algorithm. Match types: shared_licensing, backup_replication, teams_federation, shared_monitoring, collective_purchasing, knowledge_sharing """ __tablename__ = 'it_collaboration_matches' id = Column(Integer, primary_key=True) company_a_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) company_b_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) # Match details match_type = Column(String(50), nullable=False, index=True) # Types: shared_licensing, backup_replication, teams_federation, # shared_monitoring, collective_purchasing, knowledge_sharing match_reason = Column(Text) # Human-readable explanation of why this is a match match_score = Column(Integer) # 0-100 strength of the match # Status: suggested, contacted, in_progress, completed, declined status = Column(String(20), default='suggested', index=True) # Shared attributes that led to this match (JSONB for flexibility) # Example: {"m365_plans": ["E3", "E5"], "has_proxmox_pbs": true} shared_attributes = Column(JSONB) # Timestamps created_at = Column(DateTime, default=datetime.now, index=True) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationships company_a = relationship('Company', foreign_keys=[company_a_id], backref='collaboration_matches_as_a') company_b = relationship('Company', foreign_keys=[company_b_id], backref='collaboration_matches_as_b') __table_args__ = ( UniqueConstraint('company_a_id', 'company_b_id', 'match_type', name='uq_it_collab_match_pair_type'), ) def __repr__(self): return f'<ITCollaborationMatch {self.company_a_id}<->{self.company_b_id} type={self.match_type}>' @property def match_type_label(self): """Return Polish label for match type""" labels = { 'shared_licensing': 'Współdzielone licencje', 'backup_replication': 'Replikacja backupów', 'teams_federation': 'Federacja Teams', 'shared_monitoring': 'Wspólny monitoring', 'collective_purchasing': 'Zakupy grupowe', 'knowledge_sharing': 'Wymiana wiedzy' } return labels.get(self.match_type, self.match_type) @property def status_label(self): """Return Polish label for status""" labels = { 'suggested': 'Sugerowane', 'contacted': 'Skontaktowano', 'in_progress': 'W trakcie', 'completed': 'Zakończone', 'declined': 'Odrzucone' } return labels.get(self.status, self.status) # ============================================================ # MEMBERSHIP FEES # ============================================================ class MembershipFee(Base): """ Membership fee records for companies. Tracks monthly payments from Norda Biznes members. """ __tablename__ = 'membership_fees' id = Column(Integer, primary_key=True) company_id = Column(Integer, ForeignKey('companies.id', ondelete='CASCADE'), nullable=False, index=True) # Period identification fee_year = Column(Integer, nullable=False) # e.g., 2026 fee_month = Column(Integer, nullable=False) # 1-12 # Fee details amount = Column(Numeric(10, 2), nullable=False) # Amount due in PLN amount_paid = Column(Numeric(10, 2), default=0) # Amount actually paid # Payment status: pending, paid, partial, overdue, waived status = Column(String(20), default='pending', index=True) # Payment tracking payment_date = Column(Date) payment_method = Column(String(50)) # transfer, cash, card, other payment_reference = Column(String(100)) # Bank transfer reference # Admin tracking recorded_by = Column(Integer, ForeignKey('users.id')) recorded_at = Column(DateTime) notes = Column(Text) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationships company = relationship('Company', backref='membership_fees') recorded_by_user = relationship('User', foreign_keys=[recorded_by]) __table_args__ = ( UniqueConstraint('company_id', 'fee_year', 'fee_month', name='uq_company_fee_period'), ) @property def is_fully_paid(self): return (self.amount_paid or 0) >= self.amount @property def outstanding_amount(self): return max(0, float(self.amount) - float(self.amount_paid or 0)) class MembershipFeeConfig(Base): """ Configuration for membership fees. Allows variable amounts per company or category. """ __tablename__ = 'membership_fee_config' id = Column(Integer, primary_key=True) # Scope: global, category, or company scope = Column(String(20), nullable=False) # 'global', 'category', 'company' category_id = Column(Integer, ForeignKey('categories.id'), nullable=True) company_id = Column(Integer, ForeignKey('companies.id'), nullable=True) monthly_amount = Column(Numeric(10, 2), nullable=False) valid_from = Column(Date, nullable=False) valid_until = Column(Date) # NULL = currently active created_by = Column(Integer, ForeignKey('users.id')) created_at = Column(DateTime, default=datetime.now) notes = Column(Text) # Relationships category = relationship('Category') company = relationship('Company') # ============================================================ # DATABASE INITIALIZATION # ============================================================ def init_db(): """Initialize database - create all tables""" # Import all models to ensure they're registered # (already done at module level) # Create tables (only creates if they don't exist) Base.metadata.create_all(bind=engine) print("Database tables created successfully!") def drop_all_tables(): """Drop all tables - USE WITH CAUTION!""" Base.metadata.drop_all(bind=engine) print("All tables dropped!") if __name__ == '__main__': # Test database connection try: init_db() print("✅ Database initialized successfully") # Test query db = SessionLocal() try: count = db.query(Company).count() print(f"✅ Database connected. Found {count} companies.") finally: db.close() except Exception as e: print(f"❌ Database error: {e}")