import hashlib import os from sqlalchemy import Boolean, Column, Date, DateTime, ForeignKey, Integer, String from sqlalchemy.orm import declarative_base, relationship Base = declarative_base() # Async SQLAlchemy setup def get_database_url(config=None): db_url = None if config and "database" in config and "url" in config["database"]: db_url = config["database"]["url"] if not db_url: db_url = os.environ.get("DATABASE_URL") if not db_url: db_url = "sqlite+aiosqlite:///alpinebits.db" return db_url class Customer(Base): __tablename__ = "customers" id = Column(Integer, primary_key=True) given_name = Column(String) contact_id = Column(String, unique=True) surname = Column(String) name_prefix = Column(String) email_address = Column(String) phone = Column(String) email_newsletter = Column(Boolean) address_line = Column(String) city_name = Column(String) postal_code = Column(String) country_code = Column(String) gender = Column(String) birth_date = Column(String) language = Column(String) address_catalog = Column(Boolean) # Added for XML name_title = Column(String) # Added for XML reservations = relationship("Reservation", back_populates="customer") @staticmethod def _normalize_and_hash(value): """Normalize and hash a value according to Meta Conversion API requirements.""" if not value: return None # Normalize: lowercase, strip whitespace normalized = str(value).lower().strip() # Remove spaces for phone numbers is_phone = normalized.startswith("+") or normalized.replace( "-", "" ).replace(" ", "").isdigit() if is_phone: chars_to_remove = [" ", "-", "(", ")"] for char in chars_to_remove: normalized = normalized.replace(char, "") # SHA256 hash return hashlib.sha256(normalized.encode("utf-8")).hexdigest() def create_hashed_customer(self): """Create a HashedCustomer instance from this Customer.""" return HashedCustomer( customer_id=self.id, contact_id=self.contact_id, hashed_email=self._normalize_and_hash(self.email_address), hashed_phone=self._normalize_and_hash(self.phone), hashed_given_name=self._normalize_and_hash(self.given_name), hashed_surname=self._normalize_and_hash(self.surname), hashed_city=self._normalize_and_hash(self.city_name), hashed_postal_code=self._normalize_and_hash(self.postal_code), hashed_country_code=self._normalize_and_hash(self.country_code), hashed_gender=self._normalize_and_hash(self.gender), hashed_birth_date=self._normalize_and_hash(self.birth_date), ) class HashedCustomer(Base): """Hashed customer data for Meta Conversion API. Stores SHA256 hashed versions of customer PII according to Meta's requirements. This allows sending conversion events without exposing raw customer data. """ __tablename__ = "hashed_customers" id = Column(Integer, primary_key=True) customer_id = Column( Integer, ForeignKey("customers.id"), unique=True, nullable=False ) contact_id = Column(String, unique=True) # Keep unhashed for reference hashed_email = Column(String(64)) # SHA256 produces 64 hex chars hashed_phone = Column(String(64)) hashed_given_name = Column(String(64)) hashed_surname = Column(String(64)) hashed_city = Column(String(64)) hashed_postal_code = Column(String(64)) hashed_country_code = Column(String(64)) hashed_gender = Column(String(64)) hashed_birth_date = Column(String(64)) created_at = Column(DateTime) customer = relationship("Customer", backref="hashed_version") class Reservation(Base): __tablename__ = "reservations" id = Column(Integer, primary_key=True) customer_id = Column(Integer, ForeignKey("customers.id")) unique_id = Column(String, unique=True) md5_unique_id = Column(String(32), unique=True) # max length 32 guaranteed start_date = Column(Date) end_date = Column(Date) num_adults = Column(Integer) num_children = Column(Integer) children_ages = Column(String) # comma-separated offer = Column(String) created_at = Column(DateTime) # Add all UTM fields and user comment for XML utm_source = Column(String) utm_medium = Column(String) utm_campaign = Column(String) utm_term = Column(String) utm_content = Column(String) user_comment = Column(String) fbclid = Column(String) gclid = Column(String) # Add hotel_code and hotel_name for XML hotel_code = Column(String) hotel_name = Column(String) customer = relationship("Customer", back_populates="reservations") # Table for tracking acknowledged requests by client class AckedRequest(Base): __tablename__ = "acked_requests" id = Column(Integer, primary_key=True) client_id = Column(String, index=True) unique_id = Column( String, index=True ) # Should match Reservation.form_id or another unique field timestamp = Column(DateTime)