feat(arch): implement soft delete for business-critical models
Adds SoftDeleteMixin (deleted_at + deleted_by_id) with automatic query
filtering via do_orm_execute event. Soft-deleted records are invisible
by default; bypass with execution_options={"include_deleted": True}.
Models: User, Merchant, Store, StoreUser, Customer, Order, Product,
LoyaltyProgram, LoyaltyCard.
Infrastructure:
- SoftDeleteMixin in models/database/base.py
- Auto query filter registered on SessionLocal and test sessions
- soft_delete(), restore(), soft_delete_cascade() in app/core/soft_delete.py
- Alembic migration adding columns to 9 tables
- Partial unique indexes on users.email/username, stores.store_code/subdomain
Service changes:
- admin_service: delete_user, delete_store → soft_delete/soft_delete_cascade
- merchant_service: delete_merchant → soft_delete_cascade (stores→children)
- store_team_service: remove_team_member → soft_delete (fixes is_active bug)
- product_service: delete_product → soft_delete
- program_service: delete_program → soft_delete_cascade
Admin API:
- include_deleted/only_deleted query params on admin list endpoints
- PUT restore endpoints for users, merchants, stores
Tests: 9 unit tests for soft-delete infrastructure.
Docs: docs/backend/soft-delete.md + follow-up proposals.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,10 +26,10 @@ from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from app.utils.money import cents_to_euros, euros_to_cents
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class Product(Base, TimestampMixin):
|
||||
class Product(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""Store-specific product.
|
||||
|
||||
Products can be created from marketplace imports or directly by stores.
|
||||
|
||||
@@ -192,9 +192,11 @@ class ProductService:
|
||||
True if deleted
|
||||
"""
|
||||
try:
|
||||
from app.core.soft_delete import soft_delete
|
||||
|
||||
product = self.get_product(db, store_id, product_id)
|
||||
|
||||
db.delete(product)
|
||||
soft_delete(db, product, deleted_by_id=None)
|
||||
|
||||
logger.info(f"Deleted product {product_id} from store {store_id} catalog")
|
||||
return True
|
||||
|
||||
@@ -17,10 +17,10 @@ from sqlalchemy import (
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class Customer(Base, TimestampMixin):
|
||||
class Customer(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""Customer model with store isolation."""
|
||||
|
||||
__tablename__ = "customers"
|
||||
|
||||
@@ -29,7 +29,7 @@ from sqlalchemy import (
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
def generate_card_number() -> str:
|
||||
@@ -48,7 +48,7 @@ def generate_apple_auth_token() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
class LoyaltyCard(Base, TimestampMixin):
|
||||
class LoyaltyCard(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""
|
||||
Customer's loyalty card (PassObject).
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ from sqlalchemy.dialects.sqlite import JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class LoyaltyType(str, enum.Enum):
|
||||
@@ -44,7 +44,7 @@ class LoyaltyType(str, enum.Enum):
|
||||
HYBRID = "hybrid" # Both stamps and points
|
||||
|
||||
|
||||
class LoyaltyProgram(Base, TimestampMixin):
|
||||
class LoyaltyProgram(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""
|
||||
Merchant's loyalty program configuration.
|
||||
|
||||
|
||||
@@ -568,19 +568,23 @@ class ProgramService:
|
||||
return program
|
||||
|
||||
def delete_program(self, db: Session, program_id: int) -> None:
|
||||
"""Delete a loyalty program and all associated data."""
|
||||
"""Soft-delete a loyalty program and associated cards."""
|
||||
from app.core.soft_delete import soft_delete_cascade
|
||||
|
||||
program = self.require_program(db, program_id)
|
||||
merchant_id = program.merchant_id
|
||||
|
||||
# Also delete merchant settings
|
||||
# Hard delete merchant settings (config data, not business records)
|
||||
db.query(MerchantLoyaltySettings).filter(
|
||||
MerchantLoyaltySettings.merchant_id == merchant_id
|
||||
).delete()
|
||||
|
||||
db.delete(program)
|
||||
soft_delete_cascade(db, program, deleted_by_id=None, cascade_rels=[
|
||||
("cards", []),
|
||||
])
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Deleted loyalty program {program_id} for merchant {merchant_id}")
|
||||
logger.info(f"Soft-deleted loyalty program {program_id} for merchant {merchant_id}")
|
||||
|
||||
# =========================================================================
|
||||
# Merchant Settings
|
||||
|
||||
@@ -37,10 +37,10 @@ from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from app.utils.money import cents_to_euros, euros_to_cents
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class Order(Base, TimestampMixin):
|
||||
class Order(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""
|
||||
Unified order model for all sales channels.
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ from sqlalchemy import Boolean, Column, ForeignKey, Integer, String, Text
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class Merchant(Base, TimestampMixin):
|
||||
class Merchant(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""
|
||||
Represents a merchant (business entity) in the system.
|
||||
|
||||
@@ -74,7 +74,7 @@ class Merchant(Base, TimestampMixin):
|
||||
# ========================================================================
|
||||
# Relationships
|
||||
# ========================================================================
|
||||
owner = relationship("User", back_populates="owned_merchants")
|
||||
owner = relationship("User", foreign_keys="[Merchant.owner_user_id]", back_populates="owned_merchants")
|
||||
"""The user who owns this merchant."""
|
||||
|
||||
stores = relationship(
|
||||
|
||||
@@ -14,6 +14,7 @@ from sqlalchemy import (
|
||||
Column,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
String,
|
||||
Text,
|
||||
@@ -24,13 +25,17 @@ from app.core.config import settings
|
||||
|
||||
# Import Base from the central database module instead of creating a new one
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class Store(Base, TimestampMixin):
|
||||
class Store(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""Represents a store in the system."""
|
||||
|
||||
__tablename__ = "stores" # Name of the table in the database
|
||||
__table_args__ = (
|
||||
Index("uq_stores_store_code_active", "store_code", unique=True, postgresql_where="deleted_at IS NULL"),
|
||||
Index("uq_stores_subdomain_active", "subdomain", unique=True, postgresql_where="deleted_at IS NULL"),
|
||||
)
|
||||
|
||||
id = Column(
|
||||
Integer, primary_key=True, index=True
|
||||
@@ -42,11 +47,11 @@ class Store(Base, TimestampMixin):
|
||||
) # Foreign key to the parent merchant
|
||||
|
||||
store_code = Column(
|
||||
String, unique=True, index=True, nullable=False
|
||||
) # Unique, indexed, non-nullable store code column
|
||||
String, index=True, nullable=False
|
||||
) # Indexed, non-nullable store code column (unique among non-deleted)
|
||||
subdomain = Column(
|
||||
String(100), unique=True, nullable=False, index=True
|
||||
) # Unique, non-nullable subdomain column with indexing
|
||||
String(100), nullable=False, index=True
|
||||
) # Non-nullable subdomain column (unique among non-deleted)
|
||||
name = Column(
|
||||
String, nullable=False
|
||||
) # Non-nullable name column for the store (brand name)
|
||||
@@ -418,7 +423,7 @@ class Store(Base, TimestampMixin):
|
||||
}
|
||||
|
||||
|
||||
class StoreUser(Base, TimestampMixin):
|
||||
class StoreUser(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""
|
||||
Represents a user's team membership in a store.
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@ ROLE SYSTEM (Phase 1 — Consolidated 4-value enum):
|
||||
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Boolean, Column, DateTime, Integer, String
|
||||
from sqlalchemy import Boolean, Column, DateTime, Index, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from app.core.database import Base
|
||||
from models.database.base import TimestampMixin
|
||||
from models.database.base import SoftDeleteMixin, TimestampMixin
|
||||
|
||||
|
||||
class UserRole(str, enum.Enum):
|
||||
@@ -31,14 +31,18 @@ class UserRole(str, enum.Enum):
|
||||
STORE_MEMBER = "store_member" # Team member on specific store(s)
|
||||
|
||||
|
||||
class User(Base, TimestampMixin):
|
||||
class User(Base, TimestampMixin, SoftDeleteMixin):
|
||||
"""Represents a platform user (admins, merchant owners, and store team)."""
|
||||
|
||||
__tablename__ = "users"
|
||||
__table_args__ = (
|
||||
Index("uq_users_email_active", "email", unique=True, postgresql_where="deleted_at IS NULL"),
|
||||
Index("uq_users_username_active", "username", unique=True, postgresql_where="deleted_at IS NULL"),
|
||||
)
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
username = Column(String, unique=True, index=True, nullable=False)
|
||||
email = Column(String, index=True, nullable=False)
|
||||
username = Column(String, index=True, nullable=False)
|
||||
first_name = Column(String)
|
||||
last_name = Column(String)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
@@ -57,7 +61,7 @@ class User(Base, TimestampMixin):
|
||||
# Relationships
|
||||
# NOTE: marketplace_import_jobs relationship removed - owned by marketplace module
|
||||
# Use: MarketplaceImportJob.query.filter_by(user_id=user.id) instead
|
||||
owned_merchants = relationship("Merchant", back_populates="owner")
|
||||
owned_merchants = relationship("Merchant", foreign_keys="[Merchant.owner_user_id]", back_populates="owner")
|
||||
store_memberships = relationship(
|
||||
"StoreUser", foreign_keys="[StoreUser.user_id]", back_populates="user"
|
||||
)
|
||||
|
||||
@@ -124,6 +124,8 @@ def get_all_merchants(
|
||||
search: str | None = Query(None, description="Search by merchant name"),
|
||||
is_active: bool | None = Query(None),
|
||||
is_verified: bool | None = Query(None),
|
||||
include_deleted: bool = Query(False, description="Include soft-deleted merchants"),
|
||||
only_deleted: bool = Query(False, description="Show only soft-deleted merchants (trash view)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
@@ -135,6 +137,8 @@ def get_all_merchants(
|
||||
search=search,
|
||||
is_active=is_active,
|
||||
is_verified=is_verified,
|
||||
include_deleted=include_deleted,
|
||||
only_deleted=only_deleted,
|
||||
)
|
||||
|
||||
return MerchantListResponse(
|
||||
@@ -403,3 +407,24 @@ def delete_merchant(
|
||||
db.commit() # ✅ ARCH: Commit at API level for transaction control
|
||||
|
||||
return {"message": f"Merchant {merchant_id} deleted successfully"}
|
||||
|
||||
|
||||
@admin_merchants_router.put("/{merchant_id}/restore")
|
||||
def restore_merchant(
|
||||
merchant_id: int = Path(..., description="Merchant ID"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Restore a soft-deleted merchant (Admin only).
|
||||
|
||||
This only restores the merchant record itself.
|
||||
Stores and their children must be restored separately.
|
||||
"""
|
||||
from app.core.soft_delete import restore
|
||||
from app.modules.tenancy.models import Merchant
|
||||
|
||||
restored = restore(db, Merchant, merchant_id, restored_by_id=current_admin.id)
|
||||
db.commit()
|
||||
logger.info(f"Merchant {merchant_id} restored by admin {current_admin.username}")
|
||||
return {"message": f"Merchant '{restored.name}' restored successfully", "merchant_id": merchant_id}
|
||||
|
||||
@@ -87,6 +87,8 @@ def get_all_stores_admin(
|
||||
is_active: bool | None = Query(None),
|
||||
is_verified: bool | None = Query(None),
|
||||
merchant_id: int | None = Query(None, description="Filter by merchant ID"),
|
||||
include_deleted: bool = Query(False, description="Include soft-deleted stores"),
|
||||
only_deleted: bool = Query(False, description="Show only soft-deleted stores (trash view)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
@@ -99,6 +101,8 @@ def get_all_stores_admin(
|
||||
is_active=is_active,
|
||||
is_verified=is_verified,
|
||||
merchant_id=merchant_id,
|
||||
include_deleted=include_deleted,
|
||||
only_deleted=only_deleted,
|
||||
)
|
||||
return StoreListResponse(stores=stores, total=total, skip=skip, limit=limit)
|
||||
|
||||
@@ -309,3 +313,24 @@ def delete_store(
|
||||
message = admin_service.delete_store(db, store.id)
|
||||
db.commit()
|
||||
return {"message": message}
|
||||
|
||||
|
||||
@admin_stores_router.put("/{store_id}/restore")
|
||||
def restore_store(
|
||||
store_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_admin_api),
|
||||
):
|
||||
"""
|
||||
Restore a soft-deleted store (Admin only).
|
||||
|
||||
This only restores the store record itself.
|
||||
Child records (products, customers, etc.) must be restored separately.
|
||||
"""
|
||||
from app.core.soft_delete import restore
|
||||
from app.modules.tenancy.models import Store
|
||||
|
||||
restored = restore(db, Store, store_id, restored_by_id=current_admin.id)
|
||||
db.commit()
|
||||
logger.info(f"Store {store_id} restored by admin {current_admin.username}")
|
||||
return {"message": f"Store '{restored.name}' restored successfully", "store_id": store_id}
|
||||
|
||||
@@ -143,6 +143,8 @@ def list_admin_users(
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(100, ge=1, le=500),
|
||||
include_super_admins: bool = Query(True),
|
||||
include_deleted: bool = Query(False, description="Include soft-deleted users"),
|
||||
only_deleted: bool = Query(False, description="Show only soft-deleted users (trash view)"),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_super_admin),
|
||||
):
|
||||
@@ -156,6 +158,8 @@ def list_admin_users(
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
include_super_admins=include_super_admins,
|
||||
include_deleted=include_deleted,
|
||||
only_deleted=only_deleted,
|
||||
)
|
||||
|
||||
admin_responses = [_build_admin_response(admin) for admin in admins]
|
||||
@@ -395,3 +399,26 @@ def delete_admin_user(
|
||||
"message": "Admin user deleted successfully",
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
|
||||
@admin_users_router.put("/{user_id}/restore")
|
||||
def restore_admin_user(
|
||||
user_id: int = Path(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: UserContext = Depends(get_current_super_admin_api),
|
||||
):
|
||||
"""
|
||||
Restore a soft-deleted admin user.
|
||||
|
||||
Super admin only.
|
||||
"""
|
||||
from app.core.soft_delete import restore
|
||||
from app.modules.tenancy.models import User
|
||||
|
||||
restored = restore(db, User, user_id, restored_by_id=current_admin.id)
|
||||
db.commit()
|
||||
|
||||
return {
|
||||
"message": f"User '{restored.username}' restored successfully",
|
||||
"user_id": user_id,
|
||||
}
|
||||
|
||||
@@ -443,6 +443,8 @@ class AdminPlatformService:
|
||||
include_super_admins: bool = True,
|
||||
is_active: bool | None = None,
|
||||
search: str | None = None,
|
||||
include_deleted: bool = False,
|
||||
only_deleted: bool = False,
|
||||
) -> tuple[list[User], int]:
|
||||
"""
|
||||
List all admin users with optional filtering.
|
||||
@@ -454,6 +456,8 @@ class AdminPlatformService:
|
||||
include_super_admins: Whether to include super admins
|
||||
is_active: Filter by active status
|
||||
search: Search term for username/email/name
|
||||
include_deleted: Include soft-deleted users
|
||||
only_deleted: Show only soft-deleted users
|
||||
|
||||
Returns:
|
||||
Tuple of (list of User objects, total count)
|
||||
@@ -462,6 +466,12 @@ class AdminPlatformService:
|
||||
User.role.in_(["super_admin", "platform_admin"])
|
||||
)
|
||||
|
||||
# Soft-delete visibility
|
||||
if include_deleted or only_deleted:
|
||||
query = query.execution_options(include_deleted=True)
|
||||
if only_deleted:
|
||||
query = query.filter(User.deleted_at.isnot(None))
|
||||
|
||||
if not include_super_admins:
|
||||
query = query.filter(User.role == "platform_admin")
|
||||
|
||||
|
||||
@@ -322,8 +322,10 @@ class AdminService:
|
||||
owned_count=len(user.owned_merchants),
|
||||
)
|
||||
|
||||
from app.core.soft_delete import soft_delete
|
||||
|
||||
username = user.username
|
||||
db.delete(user)
|
||||
soft_delete(db, user, deleted_by_id=current_admin_id)
|
||||
|
||||
logger.info(f"Admin {current_admin_id} deleted user {username}")
|
||||
return f"User {username} deleted successfully"
|
||||
@@ -477,12 +479,20 @@ class AdminService:
|
||||
is_active: bool | None = None,
|
||||
is_verified: bool | None = None,
|
||||
merchant_id: int | None = None,
|
||||
include_deleted: bool = False,
|
||||
only_deleted: bool = False,
|
||||
) -> tuple[list[Store], int]:
|
||||
"""Get paginated list of all stores with filtering."""
|
||||
try:
|
||||
# Eagerly load merchant relationship to avoid N+1 queries
|
||||
query = db.query(Store).options(joinedload(Store.merchant))
|
||||
|
||||
# Soft-delete visibility
|
||||
if include_deleted or only_deleted:
|
||||
query = query.execution_options(include_deleted=True)
|
||||
if only_deleted:
|
||||
query = query.filter(Store.deleted_at.isnot(None))
|
||||
|
||||
# Filter by merchant
|
||||
if merchant_id is not None:
|
||||
query = query.filter(Store.merchant_id == merchant_id)
|
||||
@@ -506,6 +516,10 @@ class AdminService:
|
||||
|
||||
# Get total count (without joinedload for performance)
|
||||
count_query = db.query(Store)
|
||||
if include_deleted or only_deleted:
|
||||
count_query = count_query.execution_options(include_deleted=True)
|
||||
if only_deleted:
|
||||
count_query = count_query.filter(Store.deleted_at.isnot(None))
|
||||
if merchant_id is not None:
|
||||
count_query = count_query.filter(Store.merchant_id == merchant_id)
|
||||
if search:
|
||||
@@ -596,17 +610,16 @@ class AdminService:
|
||||
store = self._get_store_by_id_or_raise(db, store_id)
|
||||
|
||||
try:
|
||||
from app.core.soft_delete import soft_delete_cascade
|
||||
|
||||
store_code = store.store_code
|
||||
|
||||
# TODO: Delete associated data in correct order
|
||||
# - Delete orders
|
||||
# - Delete customers
|
||||
# - Delete products
|
||||
# - Delete team members
|
||||
# - Delete roles
|
||||
# - Delete import jobs
|
||||
|
||||
db.delete(store)
|
||||
soft_delete_cascade(db, store, deleted_by_id=None, cascade_rels=[
|
||||
("products", []),
|
||||
("customers", []),
|
||||
("orders", []),
|
||||
("store_users", []),
|
||||
])
|
||||
|
||||
logger.warning(f"Store {store_code} and all associated data deleted")
|
||||
return f"Store {store_code} successfully deleted"
|
||||
|
||||
@@ -148,6 +148,8 @@ class MerchantService:
|
||||
search: str | None = None,
|
||||
is_active: bool | None = None,
|
||||
is_verified: bool | None = None,
|
||||
include_deleted: bool = False,
|
||||
only_deleted: bool = False,
|
||||
) -> tuple[list[Merchant], int]:
|
||||
"""
|
||||
Get paginated list of merchants with optional filters.
|
||||
@@ -159,15 +161,25 @@ class MerchantService:
|
||||
search: Search term for merchant name
|
||||
is_active: Filter by active status
|
||||
is_verified: Filter by verified status
|
||||
include_deleted: Include soft-deleted merchants
|
||||
only_deleted: Show only soft-deleted merchants (trash view)
|
||||
|
||||
Returns:
|
||||
Tuple of (merchants list, total count)
|
||||
"""
|
||||
exec_opts = {}
|
||||
if include_deleted or only_deleted:
|
||||
exec_opts["include_deleted"] = True
|
||||
|
||||
query = select(Merchant).options(
|
||||
joinedload(Merchant.stores),
|
||||
joinedload(Merchant.owner),
|
||||
)
|
||||
|
||||
# Soft-delete filter
|
||||
if only_deleted:
|
||||
query = query.where(Merchant.deleted_at.isnot(None))
|
||||
|
||||
# Apply filters
|
||||
if search:
|
||||
query = query.where(Merchant.name.ilike(f"%{search}%"))
|
||||
@@ -178,13 +190,13 @@ class MerchantService:
|
||||
|
||||
# Get total count
|
||||
count_query = select(func.count()).select_from(query.subquery())
|
||||
total = db.execute(count_query).scalar()
|
||||
total = db.execute(count_query, execution_options=exec_opts).scalar()
|
||||
|
||||
# Apply pagination and order
|
||||
query = query.order_by(Merchant.name).offset(skip).limit(limit)
|
||||
|
||||
# Use unique() when using joinedload with collections to avoid duplicate rows
|
||||
merchants = list(db.execute(query).scalars().unique().all())
|
||||
merchants = list(db.execute(query, execution_options=exec_opts).scalars().unique().all())
|
||||
|
||||
return merchants, total
|
||||
|
||||
@@ -228,11 +240,19 @@ class MerchantService:
|
||||
Raises:
|
||||
MerchantNotFoundException: If merchant not found
|
||||
"""
|
||||
from app.core.soft_delete import soft_delete_cascade
|
||||
|
||||
merchant = self.get_merchant_by_id(db, merchant_id)
|
||||
|
||||
# Due to cascade="all, delete-orphan", associated stores will be deleted
|
||||
db.delete(merchant)
|
||||
db.flush()
|
||||
MERCHANT_CASCADE = [
|
||||
("stores", [
|
||||
("products", []),
|
||||
("customers", []),
|
||||
("orders", []),
|
||||
("store_users", []),
|
||||
]),
|
||||
]
|
||||
soft_delete_cascade(db, merchant, deleted_by_id=None, cascade_rels=MERCHANT_CASCADE)
|
||||
logger.info(f"Deleted merchant ID {merchant_id} and associated stores")
|
||||
|
||||
def toggle_verification(
|
||||
|
||||
Reference in New Issue
Block a user