""" 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 Author: Norda Biznes Development Team Created: 2025-11-23 Updated: 2025-11-26 (Digital Maturity Platform - ETAP 1) """ 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 DATABASE_URL = os.getenv( 'DATABASE_URL', 'postgresql://nordabiz_app:NordaBiz2025Secure@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) 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 copyright_year = Column(Integer) # Year from copyright notice (e.g., © 2015) 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)) # === 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) # === 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 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() # ============================================================ # 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') # ============================================================ # ANNOUNCEMENTS # ============================================================ class Announcement(Base): """ Board announcements visible to logged-in members. Used for organizational communications. """ __tablename__ = 'announcements' id = Column(Integer, primary_key=True) title = Column(String(255), nullable=False) content = Column(Text, nullable=False) # Types: general, fees, event, important, urgent announcement_type = Column(String(50), default='general') is_published = Column(Boolean, default=False) is_pinned = Column(Boolean, default=False) publish_date = Column(DateTime) expire_date = Column(DateTime) # Target audience: all, fee_pending, fee_overdue target_audience = Column(String(50), default='all') author_id = Column(Integer, ForeignKey('users.id'), nullable=False) created_at = Column(DateTime, default=datetime.now) updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now) # Relationships author = relationship('User', backref='announcements') @property def is_visible(self): now = datetime.now() if not self.is_published: return False if self.publish_date and now < self.publish_date: return False if self.expire_date and now > self.expire_date: return False return True # ============================================================ # 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}")