From 6f8ec1d254d2f0866daa2c19a6bd14c5c038d177 Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:16:02 +0700 Subject: [PATCH] make the migration idempotetnt --- alembic/versions/add_payment_methods.py | 82 +++++++++++++++---------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/alembic/versions/add_payment_methods.py b/alembic/versions/add_payment_methods.py index b9984d1..9723d1c 100644 --- a/alembic/versions/add_payment_methods.py +++ b/alembic/versions/add_payment_methods.py @@ -19,49 +19,63 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: + conn = op.get_bind() + # Create PaymentMethodType enum paymentmethodtype = postgresql.ENUM( 'card', 'cash', 'bank_transfer', 'check', name='paymentmethodtype', create_type=False ) - paymentmethodtype.create(op.get_bind(), checkfirst=True) + paymentmethodtype.create(conn, checkfirst=True) - # Add stripe_customer_id to users table - op.add_column('users', sa.Column( - 'stripe_customer_id', - sa.String(), - nullable=True, - comment='Stripe Customer ID for payment method management' - )) - op.create_index('ix_users_stripe_customer_id', 'users', ['stripe_customer_id']) + # Check if stripe_customer_id column exists on users table + result = conn.execute(sa.text(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'stripe_customer_id' + """)) + if result.fetchone() is None: + # Add stripe_customer_id to users table + op.add_column('users', sa.Column( + 'stripe_customer_id', + sa.String(), + nullable=True, + comment='Stripe Customer ID for payment method management' + )) + op.create_index('ix_users_stripe_customer_id', 'users', ['stripe_customer_id']) - # Create payment_methods table - op.create_table( - 'payment_methods', - sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), - sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), - sa.Column('stripe_payment_method_id', sa.String(), nullable=True, unique=True, comment='Stripe pm_xxx reference'), - sa.Column('card_brand', sa.String(20), nullable=True, comment='Card brand: visa, mastercard, amex, etc.'), - sa.Column('card_last4', sa.String(4), nullable=True, comment='Last 4 digits of card'), - sa.Column('card_exp_month', sa.Integer(), nullable=True, comment='Card expiration month'), - sa.Column('card_exp_year', sa.Integer(), nullable=True, comment='Card expiration year'), - sa.Column('card_funding', sa.String(20), nullable=True, comment='Card funding type: credit, debit, prepaid'), - sa.Column('payment_type', paymentmethodtype, nullable=False, server_default='card'), - sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false', comment='Whether this is the default payment method for auto-renewals'), - sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true', comment='Soft delete flag - False means removed'), - sa.Column('is_manual', sa.Boolean(), nullable=False, server_default='false', comment='True for manually recorded methods (cash/check)'), - sa.Column('manual_notes', sa.Text(), nullable=True, comment='Admin notes for manual payment methods'), - sa.Column('created_by', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True, comment='Admin who added this on behalf of user'), - sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), - sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()), - ) + # Check if payment_methods table exists + result = conn.execute(sa.text(""" + SELECT table_name FROM information_schema.tables + WHERE table_name = 'payment_methods' + """)) + if result.fetchone() is None: + # Create payment_methods table + op.create_table( + 'payment_methods', + sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='CASCADE'), nullable=False), + sa.Column('stripe_payment_method_id', sa.String(), nullable=True, unique=True, comment='Stripe pm_xxx reference'), + sa.Column('card_brand', sa.String(20), nullable=True, comment='Card brand: visa, mastercard, amex, etc.'), + sa.Column('card_last4', sa.String(4), nullable=True, comment='Last 4 digits of card'), + sa.Column('card_exp_month', sa.Integer(), nullable=True, comment='Card expiration month'), + sa.Column('card_exp_year', sa.Integer(), nullable=True, comment='Card expiration year'), + sa.Column('card_funding', sa.String(20), nullable=True, comment='Card funding type: credit, debit, prepaid'), + sa.Column('payment_type', paymentmethodtype, nullable=False, server_default='card'), + sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false', comment='Whether this is the default payment method for auto-renewals'), + sa.Column('is_active', sa.Boolean(), nullable=False, server_default='true', comment='Soft delete flag - False means removed'), + sa.Column('is_manual', sa.Boolean(), nullable=False, server_default='false', comment='True for manually recorded methods (cash/check)'), + sa.Column('manual_notes', sa.Text(), nullable=True, comment='Admin notes for manual payment methods'), + sa.Column('created_by', postgresql.UUID(as_uuid=True), sa.ForeignKey('users.id', ondelete='SET NULL'), nullable=True, comment='Admin who added this on behalf of user'), + sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now(), onupdate=sa.func.now()), + ) - # Create indexes - op.create_index('ix_payment_methods_user_id', 'payment_methods', ['user_id']) - op.create_index('ix_payment_methods_stripe_pm_id', 'payment_methods', ['stripe_payment_method_id']) - op.create_index('idx_payment_method_user_default', 'payment_methods', ['user_id', 'is_default']) - op.create_index('idx_payment_method_active', 'payment_methods', ['user_id', 'is_active']) + # Create indexes + op.create_index('ix_payment_methods_user_id', 'payment_methods', ['user_id']) + op.create_index('ix_payment_methods_stripe_pm_id', 'payment_methods', ['stripe_payment_method_id']) + op.create_index('idx_payment_method_user_default', 'payment_methods', ['user_id', 'is_default']) + op.create_index('idx_payment_method_active', 'payment_methods', ['user_id', 'is_active']) def downgrade() -> None: