diff --git a/alembic/versions/2025_11_19_1457-08fe946414d8_add_hashed_customer_id_to_reservations_.py b/alembic/versions/2025_11_19_1457-08fe946414d8_add_hashed_customer_id_to_reservations_.py new file mode 100644 index 0000000..6622469 --- /dev/null +++ b/alembic/versions/2025_11_19_1457-08fe946414d8_add_hashed_customer_id_to_reservations_.py @@ -0,0 +1,65 @@ +"""add hashed_customer_id to reservations with cascade delete + +Revision ID: 08fe946414d8 +Revises: 70b2579d1d96 +Create Date: 2025-11-19 14:57:27.178924 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '08fe946414d8' +down_revision: Union[str, Sequence[str], None] = '70b2579d1d96' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('hashed_customers', 'customer_id', + existing_type=sa.INTEGER(), + nullable=True) + op.drop_constraint(op.f('hashed_customers_customer_id_fkey'), 'hashed_customers', type_='foreignkey') + op.create_foreign_key(None, 'hashed_customers', 'customers', ['customer_id'], ['id'], ondelete='SET NULL') + op.drop_constraint(op.f('reservations_customer_id_fkey'), 'reservations', type_='foreignkey') + op.create_foreign_key(None, 'reservations', 'customers', ['customer_id'], ['id'], ondelete='SET NULL') + + # Add hashed_customer_id column to reservations with cascade delete + op.add_column('reservations', sa.Column('hashed_customer_id', sa.Integer(), nullable=True)) + op.create_index(op.f('ix_reservations_hashed_customer_id'), 'reservations', ['hashed_customer_id'], unique=False) + op.create_foreign_key(None, 'reservations', 'hashed_customers', ['hashed_customer_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + # Data migration: Populate hashed_customer_id from customer relationship + connection = op.get_bind() + update_stmt = sa.text(""" + UPDATE reservations r + SET hashed_customer_id = hc.id + FROM hashed_customers hc + WHERE r.customer_id = hc.customer_id + AND hc.customer_id IS NOT NULL + """) + connection.execute(update_stmt) + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + # Drop the hashed_customer_id column and its constraints + op.drop_constraint(None, 'reservations', type_='foreignkey') + op.drop_index(op.f('ix_reservations_hashed_customer_id'), table_name='reservations') + op.drop_column('reservations', 'hashed_customer_id') + + op.drop_constraint(None, 'reservations', type_='foreignkey') + op.create_foreign_key(op.f('reservations_customer_id_fkey'), 'reservations', 'customers', ['customer_id'], ['id']) + op.drop_constraint(None, 'hashed_customers', type_='foreignkey') + op.create_foreign_key(op.f('hashed_customers_customer_id_fkey'), 'hashed_customers', 'customers', ['customer_id'], ['id']) + op.alter_column('hashed_customers', 'customer_id', + existing_type=sa.INTEGER(), + nullable=False) + # ### end Alembic commands ### diff --git a/src/alpine_bits_python/conversion_service.py b/src/alpine_bits_python/conversion_service.py index 1b4b49a..9e77579 100644 --- a/src/alpine_bits_python/conversion_service.py +++ b/src/alpine_bits_python/conversion_service.py @@ -253,7 +253,8 @@ class ConversionService: # Load all reservations with their hashed customers in one query from sqlalchemy.orm import selectinload query = select(Reservation).options( - selectinload(Reservation.customer).selectinload(Customer.hashed_version) + selectinload(Reservation.customer).selectinload(Customer.hashed_version), + selectinload(Reservation.hashed_customer) ) result = await session.execute(query) reservations = result.scalars().all() @@ -265,9 +266,11 @@ class ConversionService: hotel_code = reservation.hotel_code if hotel_code not in self._reservation_cache: self._reservation_cache[hotel_code] = [] - # Cache the hashed customer instead of raw customer + # Cache the hashed customer - prefer direct relationship, fall back to customer relationship hashed_customer = None - if reservation.customer and reservation.customer.hashed_version: + if reservation.hashed_customer: + hashed_customer = reservation.hashed_customer + elif reservation.customer and reservation.customer.hashed_version: hashed_customer = reservation.customer.hashed_version self._reservation_cache[hotel_code].append( (reservation, hashed_customer) diff --git a/src/alpine_bits_python/db.py b/src/alpine_bits_python/db.py index adb9651..9a5aefb 100644 --- a/src/alpine_bits_python/db.py +++ b/src/alpine_bits_python/db.py @@ -478,6 +478,7 @@ class Reservation(Base): __tablename__ = "reservations" id = Column(Integer, primary_key=True) customer_id = Column(Integer, ForeignKey("customers.id", ondelete="SET NULL")) + hashed_customer_id = Column(Integer, ForeignKey("hashed_customers.id", ondelete="CASCADE")) unique_id = Column(String, unique=True) md5_unique_id = Column(String(32), unique=True) # max length 32 guaranteed start_date = Column(Date) @@ -507,6 +508,7 @@ class Reservation(Base): room_classification_code = Column(String) room_type = Column(String) customer = relationship("Customer", back_populates="reservations") + hashed_customer = relationship("HashedCustomer", backref="reservations") # Table for tracking acknowledged requests by client diff --git a/src/alpine_bits_python/reservation_service.py b/src/alpine_bits_python/reservation_service.py index 0e48758..12c0a2a 100644 --- a/src/alpine_bits_python/reservation_service.py +++ b/src/alpine_bits_python/reservation_service.py @@ -7,7 +7,7 @@ from typing import Optional from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession -from .db import AckedRequest, Customer, Reservation +from .db import AckedRequest, Customer, HashedCustomer, Reservation from .schemas import ReservationData @@ -63,6 +63,19 @@ class ReservationService: reservation = self._convert_reservation_data_to_db( reservation_data, customer_id ) + + # Automatically populate hashed_customer_id from the customer + # Since hashed_customer is always created when a customer is created, + # we can get it by querying for the hashed_customer with matching customer_id + hashed_customer_result = await self.session.execute( + select(HashedCustomer).where( + HashedCustomer.customer_id == customer_id + ) + ) + hashed_customer = hashed_customer_result.scalar_one_or_none() + if hashed_customer: + reservation.hashed_customer_id = hashed_customer.id + self.session.add(reservation) if auto_commit: