From 9754f2db6e3412c7a4cbcec811f4e02f098181df Mon Sep 17 00:00:00 2001 From: Koncept Kit <63216427+konceptkit@users.noreply.github.com> Date: Sat, 31 Jan 2026 01:03:17 +0700 Subject: [PATCH] 1. Models (backend/models.py)- Added PaymentMethodType enum (card, cash, bank_transfer, check)- Added stripe_customer_id column to User model- Created new PaymentMethod model with all fields specified in the plan2. Alembic Migration (backend/alembic/versions/add_payment_methods.py)- Creates payment_methods table- Adds stripe_customer_id to users table- Creates appropriate indexes3. API Endpoints (backend/server.py)Added 12 new endpoints:Member Endpoints:- GET /api/payment-methods - List user's payment methods- POST /api/payment-methods/setup-intent - Create Stripe SetupIntent- POST /api/payment-methods - Save payment method after setup- PUT /api/payment-methods/{id}/default - Set as default- DELETE /api/payment-methods/{id} - Remove payment methodAdmin Endpoints:- GET /api/admin/users/{user_id}/payment-methods - List user's methods (masked)- POST /api/admin/users/{user_id}/payment-methods/reveal - Reveal sensitive details (requires password)- POST /api/admin/users/{user_id}/payment-methods/setup-intent - Create SetupIntent for user- POST /api/admin/users/{user_id}/payment-methods - Save method on behalf- POST /api/admin/users/{user_id}/payment-methods/manual - Record manual method (cash/check)- PUT /api/admin/users/{user_id}/payment-methods/{id}/default - Set default- DELETE /api/admin/users/{user_id}/payment-methods/{id} - Delete method4. Permissions (backend/permissions_seed.py)Added 5 new permissions:- payment_methods.view- payment_methods.view_sensitive- payment_methods.create- payment_methods.delete- payment_methods.set_default --- __pycache__/models.cpython-312.pyc | Bin 33150 -> 36353 bytes __pycache__/server.cpython-312.pyc | Bin 330986 -> 361724 bytes alembic/versions/add_payment_methods.py | 86 +++ models.py | 56 ++ permissions_seed.py | 66 +++ server.py | 689 +++++++++++++++++++++++- 6 files changed, 896 insertions(+), 1 deletion(-) create mode 100644 alembic/versions/add_payment_methods.py diff --git a/__pycache__/models.cpython-312.pyc b/__pycache__/models.cpython-312.pyc index 5c0f75bf9f3e828c63c50d81b6b460a09fa6aa73..9b570f464fcc36a4ead9a47515330b67ccb28144 100644 GIT binary patch delta 7970 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 Y_YNi51@jBd_YClJZ=VqIOjv~f1!>R=*8l(j delta 5758 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 sIgQ%E-b`O7{A}W{Fa%GthLg|DH+u*u97 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 RPNGxH`R`9^t8?R4#p=^E0VQ{xa=`xSh5 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 delta 51448 zcmce<2Y3`!7dM`n+1d2o8|gsk9TY)8KtK#Apb~`;vI_*#m`x}_4fSDlm9wl?NYUWjJM3Y)RWzr7!wmJs>FB) zEPcyaP(Ckx+dN+#p5N@PTKaC3+z&*Vp=O%jsYzXJ-ZoF+vfeQ52lb(zm+uJtYuW}iBe#-g|pM!zBI&;meY5QrQr`nso z$nEK~&Q*J|8gpcfr|-HLPd{(=ZJI<^dx|;8mECnJ=MAL1L5{pGP903CLma7VIdv$d z4s)cQ#;L<8b%Z1JbWR;fsiPdJ|Kij`DD_a8S_EQh*KzV_NGP>Uc_>;7C22Q;(q3iH_9uoH~h8Cp%Kl;nX83b&4bPTuz-zsncYtB=9^=K8lj3 zI|~2X+@NQ7ozHnQC~u}C?*dLehEk7pq;BBUS(G~4k$NGg&Y{%f9H|#^>hY9%f+N-B z)VY*eD^n$b7jyE7lzft-@FnIrcXroC&a0!m1&%zysr8h)(2;s6r#4Wk$B}v&r+O)M zkt6kTPF+l?OB|`2IJJ>dm&#Q3Ftt~BJ2oidu~Ie5{J~wEe+rwY%hSA0xBA0X-j0wZ zkB%+MzdBf?%~9kU3rm`3d8i9er_#JN=Gx4Pbf2T_wN_b|r`_9S?usqWKQ)MRg`?bc zRyngzT(S9QY+U*(NA~r0c6(fL{%JuBosJ?maFJ_I*Rst=<5H9wb6jG6Nt5?iTqSZY@kR@XW(-eMIspN-GaH`}kj#uw?g zTCbUBL}_nqLa~0E#X;&B5p5AKT9fw-^O}TQeT#+3d^sV_d}2agtmawoE%aV(?oTMx zZ@03QbItoE^eQ>ed$mIb|8`J(hgH-(H!+1URhSPYChK=vbOe(NsO>Y&l%!mJtF4Ez zNyTW}e11}Dmgiyz*1J$MN={d1nPV%el&j6H$+@xjI?$(I?UWO#jtdq$`Mt(L*?krP=8-9qu2m_?y5CN| zl9LNct_+je4GtXl+m!v5Ql`!F++@zoFG+QIP{s7ddTwS-OiL|{-R_{&>`F}|Ha4VI zg;BS~f%yT(%mz$_3G?lQxvb(g!h5rWoH)-Nj!F+&v?t$5GCemWQ}g}_1I+W%lJ$qI z0sc$; zd^Rgbf6jjWH4BP+5sEWgQxei&a@2a>E;ctiyx1#_VlUXm9?UK!n|U`ozwB!06ufr0#qU{3Ta#VcDt|)TysHLqry$+WZ-nOa+=;>uf z!Y(EZU6SV=2f9~mbk(^%2;CjIxzuMD*u%Wh zua4sH*u`fTmJq|c3d?jBqIr2?VeGre4mtUk3*p;{fG})-IIz8EV;fNf@og=tQiR!3 zl&imQXI&M_D!J5R!o5N9#5gd0U>A=o&SXsGJ*kXJ+O^D_-Ze*^U6NRH=Gbg=bunn& zT2x*crIPNb^dW4Pt9)8qR2UowQJq*vnU8E@2K6XWHkmVfWa}T>iKq4$sLZ;ktTF+S zf$Aoa`MujCU;o6yomyh`Sc>A@W6fzL#rmhA66t9NX?lKnuaafE?R6JRZQlE$Ln~kM;WDM*x2D^yHbnn#!^Su8dnD70MCDa6ZFK}S^!fxk_(p
q~$fd0Gnw!WvN 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 None: + # Create PaymentMethodType enum + paymentmethodtype = postgresql.ENUM( + 'card', 'cash', 'bank_transfer', 'check', + name='paymentmethodtype', + create_type=False + ) + paymentmethodtype.create(op.get_bind(), 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']) + + # 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']) + + +def downgrade() -> None: + # Drop indexes + op.drop_index('idx_payment_method_active', table_name='payment_methods') + op.drop_index('idx_payment_method_user_default', table_name='payment_methods') + op.drop_index('ix_payment_methods_stripe_pm_id', table_name='payment_methods') + op.drop_index('ix_payment_methods_user_id', table_name='payment_methods') + + # Drop payment_methods table + op.drop_table('payment_methods') + + # Drop stripe_customer_id from users + op.drop_index('ix_users_stripe_customer_id', table_name='users') + op.drop_column('users', 'stripe_customer_id') + + # Drop PaymentMethodType enum + paymentmethodtype = postgresql.ENUM( + 'card', 'cash', 'bank_transfer', 'check', + name='paymentmethodtype' + ) + paymentmethodtype.drop(op.get_bind(), checkfirst=True) diff --git a/models.py b/models.py index 930ce27..4e3a4c6 100644 --- a/models.py +++ b/models.py @@ -44,6 +44,13 @@ class DonationStatus(enum.Enum): completed = "completed" failed = "failed" + +class PaymentMethodType(enum.Enum): + card = "card" + cash = "cash" + bank_transfer = "bank_transfer" + check = "check" + class User(Base): __tablename__ = "users" @@ -141,6 +148,9 @@ class User(Base): role_changed_at = Column(DateTime(timezone=True), nullable=True, comment="Timestamp when role was last changed") role_changed_by = Column(UUID(as_uuid=True), ForeignKey('users.id', ondelete='SET NULL'), nullable=True, comment="Admin who changed the role") + # Stripe Customer ID - Centralized for payment method management + stripe_customer_id = Column(String, nullable=True, index=True, comment="Stripe Customer ID for payment method management") + created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) @@ -150,6 +160,52 @@ class User(Base): rsvps = relationship("EventRSVP", back_populates="user") subscriptions = relationship("Subscription", back_populates="user", foreign_keys="Subscription.user_id") role_changer = relationship("User", foreign_keys=[role_changed_by], remote_side="User.id", post_update=True) + payment_methods = relationship("PaymentMethod", back_populates="user", foreign_keys="PaymentMethod.user_id") + + +class PaymentMethod(Base): + """Stored payment methods for users (Stripe or manual records)""" + __tablename__ = "payment_methods" + + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True) + + # Stripe payment method reference + stripe_payment_method_id = Column(String, nullable=True, unique=True, index=True, comment="Stripe pm_xxx reference") + + # Card details (stored for display purposes - PCI compliant) + card_brand = Column(String(20), nullable=True, comment="Card brand: visa, mastercard, amex, etc.") + card_last4 = Column(String(4), nullable=True, comment="Last 4 digits of card") + card_exp_month = Column(Integer, nullable=True, comment="Card expiration month") + card_exp_year = Column(Integer, nullable=True, comment="Card expiration year") + card_funding = Column(String(20), nullable=True, comment="Card funding type: credit, debit, prepaid") + + # Payment type classification + payment_type = Column(SQLEnum(PaymentMethodType), default=PaymentMethodType.card, nullable=False) + + # Status flags + is_default = Column(Boolean, default=False, nullable=False, comment="Whether this is the default payment method for auto-renewals") + is_active = Column(Boolean, default=True, nullable=False, comment="Soft delete flag - False means removed") + is_manual = Column(Boolean, default=False, nullable=False, comment="True for manually recorded methods (cash/check)") + + # Manual payment notes (for cash/check records) + manual_notes = Column(Text, nullable=True, comment="Admin notes for manual payment methods") + + # Audit trail + created_by = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True, comment="Admin who added this on behalf of user") + created_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) + updated_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False) + + # Relationships + user = relationship("User", back_populates="payment_methods", foreign_keys=[user_id]) + creator = relationship("User", foreign_keys=[created_by]) + + # Composite index for efficient queries + __table_args__ = ( + Index('idx_payment_method_user_default', 'user_id', 'is_default'), + Index('idx_payment_method_active', 'user_id', 'is_active'), + ) + class Event(Base): __tablename__ = "events" diff --git a/permissions_seed.py b/permissions_seed.py index 13e42f6..4206238 100644 --- a/permissions_seed.py +++ b/permissions_seed.py @@ -327,6 +327,38 @@ PERMISSIONS = [ "module": "gallery" }, + # ========== PAYMENT METHODS MODULE ========== + { + "code": "payment_methods.view", + "name": "View Payment Methods", + "description": "View user payment methods (masked)", + "module": "payment_methods" + }, + { + "code": "payment_methods.view_sensitive", + "name": "View Sensitive Payment Details", + "description": "View full payment method details including Stripe IDs (requires password)", + "module": "payment_methods" + }, + { + "code": "payment_methods.create", + "name": "Create Payment Methods", + "description": "Add payment methods on behalf of users", + "module": "payment_methods" + }, + { + "code": "payment_methods.delete", + "name": "Delete Payment Methods", + "description": "Delete user payment methods", + "module": "payment_methods" + }, + { + "code": "payment_methods.set_default", + "name": "Set Default Payment Method", + "description": "Set a user's default payment method", + "module": "payment_methods" + }, + # ========== SETTINGS MODULE ========== { "code": "settings.view", @@ -453,6 +485,10 @@ DEFAULT_ROLE_PERMISSIONS = { "gallery.edit", "gallery.delete", "gallery.moderate", + "payment_methods.view", + "payment_methods.create", + "payment_methods.delete", + "payment_methods.set_default", "settings.view", "settings.edit", "settings.email_templates", @@ -460,6 +496,36 @@ DEFAULT_ROLE_PERMISSIONS = { "settings.logs", ], + UserRole.finance: [ + # Finance role has all admin permissions plus sensitive payment access + "users.view", + "users.export", + "events.view", + "events.rsvps", + "events.calendar_export", + "subscriptions.view", + "subscriptions.create", + "subscriptions.edit", + "subscriptions.cancel", + "subscriptions.activate", + "subscriptions.plans", + "financials.view", + "financials.create", + "financials.edit", + "financials.delete", + "financials.export", + "financials.payments", + "newsletters.view", + "bylaws.view", + "gallery.view", + "payment_methods.view", + "payment_methods.view_sensitive", # Finance can view sensitive payment details + "payment_methods.create", + "payment_methods.delete", + "payment_methods.set_default", + "settings.view", + ], + # Superadmin gets all permissions automatically in code, # so we don't need to explicitly assign them UserRole.superadmin: [] diff --git a/server.py b/server.py index 3706a38..ff70028 100644 --- a/server.py +++ b/server.py @@ -17,7 +17,7 @@ import csv import io from database import engine, get_db, Base -from models import User, Event, EventRSVP, UserStatus, UserRole, RSVPStatus, SubscriptionPlan, Subscription, SubscriptionStatus, StorageUsage, EventGallery, NewsletterArchive, FinancialReport, BylawsDocument, Permission, RolePermission, Role, UserInvitation, InvitationStatus, ImportJob, ImportJobStatus, ImportRollbackAudit, Donation, DonationType, DonationStatus, SystemSettings +from models import User, Event, EventRSVP, UserStatus, UserRole, RSVPStatus, SubscriptionPlan, Subscription, SubscriptionStatus, StorageUsage, EventGallery, NewsletterArchive, FinancialReport, BylawsDocument, Permission, RolePermission, Role, UserInvitation, InvitationStatus, ImportJob, ImportJobStatus, ImportRollbackAudit, Donation, DonationType, DonationStatus, SystemSettings, PaymentMethod, PaymentMethodType from auth import ( get_password_hash, verify_password, @@ -6448,6 +6448,693 @@ async def create_donation_checkout( logger.error(f"Error creating donation checkout: {str(e)}") raise HTTPException(status_code=500, detail="Failed to create donation checkout") +# ============================================================ +# Payment Method Management API Endpoints +# ============================================================ + +class PaymentMethodResponse(BaseModel): + id: str + card_brand: Optional[str] = None + card_last4: Optional[str] = None + card_exp_month: Optional[int] = None + card_exp_year: Optional[int] = None + card_funding: Optional[str] = None + payment_type: str + is_default: bool + is_manual: bool + manual_notes: Optional[str] = None + created_at: datetime + + model_config = {"from_attributes": True} + +class PaymentMethodSaveRequest(BaseModel): + stripe_payment_method_id: str + set_as_default: bool = False + +class AdminManualPaymentMethodRequest(BaseModel): + payment_type: Literal["cash", "bank_transfer", "check"] + manual_notes: Optional[str] = None + set_as_default: bool = False + +class AdminRevealRequest(BaseModel): + password: str + + +def get_or_create_stripe_customer(user: User, db: Session) -> str: + """Get existing or create new Stripe customer for user.""" + import stripe + + # Get Stripe API key + stripe_key = get_setting(db, 'stripe_secret_key', decrypt=True) + if not stripe_key: + stripe_key = os.getenv("STRIPE_SECRET_KEY") + if not stripe_key: + raise HTTPException(status_code=500, detail="Stripe API key not configured") + stripe.api_key = stripe_key + + if user.stripe_customer_id: + # Verify customer still exists in Stripe + try: + customer = stripe.Customer.retrieve(user.stripe_customer_id) + # Check if customer was deleted using getattr (Stripe SDK doesn't expose 'deleted' directly) + if getattr(customer, 'deleted', False) or customer.get('deleted', False): + # Customer was deleted, create a new one + user.stripe_customer_id = None + else: + return user.stripe_customer_id + except stripe.error.InvalidRequestError: + # Customer doesn't exist, create a new one + user.stripe_customer_id = None + except Exception as e: + logger.warning(f"Error retrieving Stripe customer {user.stripe_customer_id}: {str(e)}") + user.stripe_customer_id = None + + # Create new Stripe customer + customer = stripe.Customer.create( + email=user.email, + name=f"{user.first_name} {user.last_name}", + metadata={"user_id": str(user.id)} + ) + + user.stripe_customer_id = customer.id + db.commit() + logger.info(f"Created Stripe customer {customer.id} for user {user.id}") + + return customer.id + + +@api_router.get("/payment-methods") +async def list_payment_methods( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """List current user's saved payment methods.""" + methods = db.query(PaymentMethod).filter( + PaymentMethod.user_id == current_user.id, + PaymentMethod.is_active == True + ).order_by(PaymentMethod.is_default.desc(), PaymentMethod.created_at.desc()).all() + + return [{ + "id": str(m.id), + "card_brand": m.card_brand, + "card_last4": m.card_last4, + "card_exp_month": m.card_exp_month, + "card_exp_year": m.card_exp_year, + "card_funding": m.card_funding, + "payment_type": m.payment_type.value, + "is_default": m.is_default, + "is_manual": m.is_manual, + "manual_notes": m.manual_notes if m.is_manual else None, + "created_at": m.created_at.isoformat() + } for m in methods] + + +@api_router.post("/payment-methods/setup-intent") +async def create_setup_intent( + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Create a Stripe SetupIntent for adding a new payment method.""" + import stripe + + # Get or create Stripe customer + customer_id = get_or_create_stripe_customer(current_user, db) + + # Get Stripe API key + stripe_key = get_setting(db, 'stripe_secret_key', decrypt=True) + if not stripe_key: + stripe_key = os.getenv("STRIPE_SECRET_KEY") + stripe.api_key = stripe_key + + # Create SetupIntent + setup_intent = stripe.SetupIntent.create( + customer=customer_id, + payment_method_types=["card"], + metadata={"user_id": str(current_user.id)} + ) + + logger.info(f"Created SetupIntent for user {current_user.id}") + + return { + "client_secret": setup_intent.client_secret, + "setup_intent_id": setup_intent.id + } + + +@api_router.post("/payment-methods") +async def save_payment_method( + request: PaymentMethodSaveRequest, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Save a payment method after successful SetupIntent confirmation.""" + import stripe + + # Get Stripe API key + stripe_key = get_setting(db, 'stripe_secret_key', decrypt=True) + if not stripe_key: + stripe_key = os.getenv("STRIPE_SECRET_KEY") + stripe.api_key = stripe_key + + # Refresh user from DB to get latest stripe_customer_id + db.refresh(current_user) + + # Retrieve payment method from Stripe + try: + pm = stripe.PaymentMethod.retrieve(request.stripe_payment_method_id) + except stripe.error.InvalidRequestError as e: + logger.error(f"Invalid payment method ID: {request.stripe_payment_method_id}, error: {str(e)}") + raise HTTPException(status_code=400, detail="Invalid payment method ID") + + # Verify ownership - payment method must be attached to user's customer + pm_customer = pm.customer if hasattr(pm, 'customer') else None + logger.info(f"Verifying PM ownership: pm.customer={pm_customer}, user.stripe_customer_id={current_user.stripe_customer_id}") + + if not current_user.stripe_customer_id: + raise HTTPException(status_code=403, detail="User does not have a Stripe customer ID") + + if not pm_customer: + raise HTTPException(status_code=403, detail="Payment method is not attached to any customer") + + if pm_customer != current_user.stripe_customer_id: + raise HTTPException(status_code=403, detail="Payment method not owned by user") + + # Check for duplicate + existing = db.query(PaymentMethod).filter( + PaymentMethod.stripe_payment_method_id == request.stripe_payment_method_id, + PaymentMethod.is_active == True + ).first() + + if existing: + raise HTTPException(status_code=400, detail="Payment method already saved") + + # Handle default setting - unset others if setting this as default + if request.set_as_default: + db.query(PaymentMethod).filter( + PaymentMethod.user_id == current_user.id, + PaymentMethod.is_active == True + ).update({"is_default": False}) + + # Extract card details + card = pm.card if pm.type == "card" else None + + # Create payment method record + payment_method = PaymentMethod( + user_id=current_user.id, + stripe_payment_method_id=request.stripe_payment_method_id, + card_brand=card.brand if card else None, + card_last4=card.last4 if card else None, + card_exp_month=card.exp_month if card else None, + card_exp_year=card.exp_year if card else None, + card_funding=card.funding if card else None, + payment_type=PaymentMethodType.card, + is_default=request.set_as_default, + is_active=True, + is_manual=False + ) + + db.add(payment_method) + db.commit() + db.refresh(payment_method) + + logger.info(f"Saved payment method {payment_method.id} for user {current_user.id}") + + return { + "id": str(payment_method.id), + "card_brand": payment_method.card_brand, + "card_last4": payment_method.card_last4, + "card_exp_month": payment_method.card_exp_month, + "card_exp_year": payment_method.card_exp_year, + "is_default": payment_method.is_default, + "message": "Payment method saved successfully" + } + + +@api_router.put("/payment-methods/{payment_method_id}/default") +async def set_default_payment_method( + payment_method_id: str, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Set a payment method as the default for auto-renewals.""" + try: + pm_uuid = uuid.UUID(payment_method_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid payment method ID") + + payment_method = db.query(PaymentMethod).filter( + PaymentMethod.id == pm_uuid, + PaymentMethod.user_id == current_user.id, + PaymentMethod.is_active == True + ).first() + + if not payment_method: + raise HTTPException(status_code=404, detail="Payment method not found") + + # Unset all other defaults + db.query(PaymentMethod).filter( + PaymentMethod.user_id == current_user.id, + PaymentMethod.is_active == True + ).update({"is_default": False}) + + # Set this one as default + payment_method.is_default = True + db.commit() + + logger.info(f"Set default payment method {payment_method_id} for user {current_user.id}") + + return {"message": "Default payment method updated", "id": str(payment_method.id)} + + +@api_router.delete("/payment-methods/{payment_method_id}") +async def delete_payment_method( + payment_method_id: str, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db) +): + """Delete (soft-delete) a saved payment method.""" + import stripe + + try: + pm_uuid = uuid.UUID(payment_method_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid payment method ID") + + payment_method = db.query(PaymentMethod).filter( + PaymentMethod.id == pm_uuid, + PaymentMethod.user_id == current_user.id, + PaymentMethod.is_active == True + ).first() + + if not payment_method: + raise HTTPException(status_code=404, detail="Payment method not found") + + # Detach from Stripe if it's a Stripe payment method + if payment_method.stripe_payment_method_id: + try: + stripe_key = get_setting(db, 'stripe_secret_key', decrypt=True) + if not stripe_key: + stripe_key = os.getenv("STRIPE_SECRET_KEY") + stripe.api_key = stripe_key + + stripe.PaymentMethod.detach(payment_method.stripe_payment_method_id) + logger.info(f"Detached Stripe payment method {payment_method.stripe_payment_method_id}") + except stripe.error.StripeError as e: + logger.warning(f"Failed to detach Stripe payment method: {str(e)}") + + # Soft delete + payment_method.is_active = False + payment_method.is_default = False + db.commit() + + logger.info(f"Deleted payment method {payment_method_id} for user {current_user.id}") + + return {"message": "Payment method deleted"} + + +# ============================================================ +# Admin Payment Method Management Endpoints +# ============================================================ + +@api_router.get("/admin/users/{user_id}/payment-methods") +async def admin_list_user_payment_methods( + user_id: str, + current_user: User = Depends(require_permission("payment_methods.view")), + db: Session = Depends(get_db) +): + """Admin: List a user's payment methods (masked).""" + try: + user_uuid = uuid.UUID(user_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid user ID") + + user = db.query(User).filter(User.id == user_uuid).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + methods = db.query(PaymentMethod).filter( + PaymentMethod.user_id == user_uuid, + PaymentMethod.is_active == True + ).order_by(PaymentMethod.is_default.desc(), PaymentMethod.created_at.desc()).all() + + return [{ + "id": str(m.id), + "card_brand": m.card_brand, + "card_last4": m.card_last4, + "card_exp_month": m.card_exp_month, + "card_exp_year": m.card_exp_year, + "card_funding": m.card_funding, + "payment_type": m.payment_type.value, + "is_default": m.is_default, + "is_manual": m.is_manual, + "manual_notes": m.manual_notes if m.is_manual else None, + "created_at": m.created_at.isoformat(), + # Sensitive data masked + "stripe_payment_method_id": None + } for m in methods] + + +@api_router.post("/admin/users/{user_id}/payment-methods/reveal") +async def admin_reveal_payment_details( + user_id: str, + request: AdminRevealRequest, + current_user: User = Depends(require_permission("payment_methods.view_sensitive")), + db: Session = Depends(get_db) +): + """Admin: Reveal full payment method details (requires password confirmation).""" + try: + user_uuid = uuid.UUID(user_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid user ID") + + # Verify admin's password + if not verify_password(request.password, current_user.password_hash): + logger.warning(f"Admin {current_user.email} failed password verification for payment reveal") + raise HTTPException(status_code=401, detail="Invalid password") + + user = db.query(User).filter(User.id == user_uuid).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + methods = db.query(PaymentMethod).filter( + PaymentMethod.user_id == user_uuid, + PaymentMethod.is_active == True + ).order_by(PaymentMethod.is_default.desc(), PaymentMethod.created_at.desc()).all() + + # Log sensitive access + logger.info(f"Admin {current_user.email} revealed payment details for user {user_id}") + + return [{ + "id": str(m.id), + "card_brand": m.card_brand, + "card_last4": m.card_last4, + "card_exp_month": m.card_exp_month, + "card_exp_year": m.card_exp_year, + "card_funding": m.card_funding, + "payment_type": m.payment_type.value, + "is_default": m.is_default, + "is_manual": m.is_manual, + "manual_notes": m.manual_notes, + "created_at": m.created_at.isoformat(), + "stripe_payment_method_id": m.stripe_payment_method_id + } for m in methods] + + +@api_router.post("/admin/users/{user_id}/payment-methods/setup-intent") +async def admin_create_setup_intent_for_user( + user_id: str, + current_user: User = Depends(require_permission("payment_methods.create")), + db: Session = Depends(get_db) +): + """Admin: Create a SetupIntent for adding a card on behalf of a user.""" + import stripe + + try: + user_uuid = uuid.UUID(user_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid user ID") + + user = db.query(User).filter(User.id == user_uuid).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Get or create Stripe customer for the target user + customer_id = get_or_create_stripe_customer(user, db) + + # Get Stripe API key + stripe_key = get_setting(db, 'stripe_secret_key', decrypt=True) + if not stripe_key: + stripe_key = os.getenv("STRIPE_SECRET_KEY") + stripe.api_key = stripe_key + + # Create SetupIntent + setup_intent = stripe.SetupIntent.create( + customer=customer_id, + payment_method_types=["card"], + metadata={ + "user_id": str(user.id), + "created_by_admin": str(current_user.id) + } + ) + + logger.info(f"Admin {current_user.email} created SetupIntent for user {user_id}") + + return { + "client_secret": setup_intent.client_secret, + "setup_intent_id": setup_intent.id + } + + +@api_router.post("/admin/users/{user_id}/payment-methods") +async def admin_save_payment_method_for_user( + user_id: str, + request: PaymentMethodSaveRequest, + current_user: User = Depends(require_permission("payment_methods.create")), + db: Session = Depends(get_db) +): + """Admin: Save a payment method on behalf of a user.""" + import stripe + + try: + user_uuid = uuid.UUID(user_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid user ID") + + user = db.query(User).filter(User.id == user_uuid).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Refresh user to get latest data + db.refresh(user) + + # Get Stripe API key + stripe_key = get_setting(db, 'stripe_secret_key', decrypt=True) + if not stripe_key: + stripe_key = os.getenv("STRIPE_SECRET_KEY") + stripe.api_key = stripe_key + + # Retrieve payment method from Stripe + try: + pm = stripe.PaymentMethod.retrieve(request.stripe_payment_method_id) + except stripe.error.InvalidRequestError as e: + logger.error(f"Invalid payment method ID: {request.stripe_payment_method_id}, error: {str(e)}") + raise HTTPException(status_code=400, detail="Invalid payment method ID") + + # Verify ownership - payment method must be attached to user's customer + pm_customer = pm.customer if hasattr(pm, 'customer') else None + logger.info(f"Admin verifying PM ownership: pm.customer={pm_customer}, user.stripe_customer_id={user.stripe_customer_id}") + + if not user.stripe_customer_id: + raise HTTPException(status_code=403, detail="User does not have a Stripe customer ID") + + if not pm_customer: + raise HTTPException(status_code=403, detail="Payment method is not attached to any customer") + + if pm_customer != user.stripe_customer_id: + raise HTTPException(status_code=403, detail="Payment method not attached to user's Stripe customer") + + # Check for duplicate + existing = db.query(PaymentMethod).filter( + PaymentMethod.stripe_payment_method_id == request.stripe_payment_method_id, + PaymentMethod.is_active == True + ).first() + + if existing: + raise HTTPException(status_code=400, detail="Payment method already saved") + + # Handle default setting + if request.set_as_default: + db.query(PaymentMethod).filter( + PaymentMethod.user_id == user.id, + PaymentMethod.is_active == True + ).update({"is_default": False}) + + # Extract card details + card = pm.card if pm.type == "card" else None + + # Create payment method record + payment_method = PaymentMethod( + user_id=user.id, + stripe_payment_method_id=request.stripe_payment_method_id, + card_brand=card.brand if card else None, + card_last4=card.last4 if card else None, + card_exp_month=card.exp_month if card else None, + card_exp_year=card.exp_year if card else None, + card_funding=card.funding if card else None, + payment_type=PaymentMethodType.card, + is_default=request.set_as_default, + is_active=True, + is_manual=False, + created_by=current_user.id + ) + + db.add(payment_method) + db.commit() + db.refresh(payment_method) + + logger.info(f"Admin {current_user.email} saved payment method {payment_method.id} for user {user_id}") + + return { + "id": str(payment_method.id), + "card_brand": payment_method.card_brand, + "card_last4": payment_method.card_last4, + "message": "Payment method saved successfully" + } + + +@api_router.post("/admin/users/{user_id}/payment-methods/manual") +async def admin_record_manual_payment_method( + user_id: str, + request: AdminManualPaymentMethodRequest, + current_user: User = Depends(require_permission("payment_methods.create")), + db: Session = Depends(get_db) +): + """Admin: Record a manual payment method (cash, check, bank transfer).""" + try: + user_uuid = uuid.UUID(user_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid user ID") + + user = db.query(User).filter(User.id == user_uuid).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + # Map payment type string to enum + payment_type_map = { + "cash": PaymentMethodType.cash, + "bank_transfer": PaymentMethodType.bank_transfer, + "check": PaymentMethodType.check + } + payment_type = payment_type_map.get(request.payment_type) + if not payment_type: + raise HTTPException(status_code=400, detail="Invalid payment type") + + # Handle default setting + if request.set_as_default: + db.query(PaymentMethod).filter( + PaymentMethod.user_id == user.id, + PaymentMethod.is_active == True + ).update({"is_default": False}) + + # Create manual payment method record + payment_method = PaymentMethod( + user_id=user.id, + stripe_payment_method_id=None, + payment_type=payment_type, + is_default=request.set_as_default, + is_active=True, + is_manual=True, + manual_notes=request.manual_notes, + created_by=current_user.id + ) + + db.add(payment_method) + db.commit() + db.refresh(payment_method) + + logger.info(f"Admin {current_user.email} recorded manual payment method {payment_method.id} ({payment_type.value}) for user {user_id}") + + return { + "id": str(payment_method.id), + "payment_type": payment_method.payment_type.value, + "message": "Manual payment method recorded successfully" + } + + +@api_router.put("/admin/users/{user_id}/payment-methods/{payment_method_id}/default") +async def admin_set_default_payment_method( + user_id: str, + payment_method_id: str, + current_user: User = Depends(require_permission("payment_methods.set_default")), + db: Session = Depends(get_db) +): + """Admin: Set a user's payment method as default.""" + try: + user_uuid = uuid.UUID(user_id) + pm_uuid = uuid.UUID(payment_method_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid ID format") + + user = db.query(User).filter(User.id == user_uuid).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + payment_method = db.query(PaymentMethod).filter( + PaymentMethod.id == pm_uuid, + PaymentMethod.user_id == user_uuid, + PaymentMethod.is_active == True + ).first() + + if not payment_method: + raise HTTPException(status_code=404, detail="Payment method not found") + + # Unset all other defaults + db.query(PaymentMethod).filter( + PaymentMethod.user_id == user_uuid, + PaymentMethod.is_active == True + ).update({"is_default": False}) + + # Set this one as default + payment_method.is_default = True + db.commit() + + logger.info(f"Admin {current_user.email} set default payment method {payment_method_id} for user {user_id}") + + return {"message": "Default payment method updated", "id": str(payment_method.id)} + + +@api_router.delete("/admin/users/{user_id}/payment-methods/{payment_method_id}") +async def admin_delete_payment_method( + user_id: str, + payment_method_id: str, + current_user: User = Depends(require_permission("payment_methods.delete")), + db: Session = Depends(get_db) +): + """Admin: Delete a user's payment method.""" + import stripe + + try: + user_uuid = uuid.UUID(user_id) + pm_uuid = uuid.UUID(payment_method_id) + except ValueError: + raise HTTPException(status_code=400, detail="Invalid ID format") + + user = db.query(User).filter(User.id == user_uuid).first() + if not user: + raise HTTPException(status_code=404, detail="User not found") + + payment_method = db.query(PaymentMethod).filter( + PaymentMethod.id == pm_uuid, + PaymentMethod.user_id == user_uuid, + PaymentMethod.is_active == True + ).first() + + if not payment_method: + raise HTTPException(status_code=404, detail="Payment method not found") + + # Detach from Stripe if it's a Stripe payment method + if payment_method.stripe_payment_method_id: + try: + stripe_key = get_setting(db, 'stripe_secret_key', decrypt=True) + if not stripe_key: + stripe_key = os.getenv("STRIPE_SECRET_KEY") + stripe.api_key = stripe_key + + stripe.PaymentMethod.detach(payment_method.stripe_payment_method_id) + logger.info(f"Detached Stripe payment method {payment_method.stripe_payment_method_id}") + except stripe.error.StripeError as e: + logger.warning(f"Failed to detach Stripe payment method: {str(e)}") + + # Soft delete + payment_method.is_active = False + payment_method.is_default = False + db.commit() + + logger.info(f"Admin {current_user.email} deleted payment method {payment_method_id} for user {user_id}") + + return {"message": "Payment method deleted"} + + @api_router.post("/contact") async def submit_contact_form( request: ContactFormRequest,