feat(loyalty): Phase 6 — admin GDPR, bulk ops, point restore, cascade
Some checks failed
CI / ruff (push) Successful in 15s
CI / validate (push) Has been cancelled
CI / dependency-scanning (push) Has been cancelled
CI / docs (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / pytest (push) Has started running

Admin operations for production management:

- GDPR anonymization: DELETE /admin/loyalty/cards/customer/{id}
  Nulls customer_id, deactivates cards, scrubs PII from transaction
  notes. Keeps aggregate data for reporting.
- Bulk deactivate: POST /admin/loyalty/merchants/{id}/cards/bulk/deactivate
  and POST /store/loyalty/cards/bulk/deactivate (merchant_owner only).
  Deactivates multiple cards with audit trail.
- Point restore: POST /admin/loyalty/cards/{id}/restore-points
  Creates ADMIN_ADJUSTMENT transaction with positive delta. Reuses
  existing adjust_points service method.
- Cascade restore: POST /admin/loyalty/merchants/{id}/restore-deleted
  Restores all soft-deleted programs and cards for a merchant.

Service methods: anonymize_cards_for_customer, bulk_deactivate_cards,
restore_deleted_cards, restore_deleted_programs.

342 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 22:40:34 +02:00
parent 4c1608f78a
commit 8cd09f3f89
4 changed files with 354 additions and 0 deletions

View File

@@ -1127,5 +1127,30 @@ class ProgramService:
return stats
def restore_deleted_programs(self, db: Session, merchant_id: int) -> int:
"""Restore all soft-deleted programs for a merchant.
Returns number of programs restored.
"""
from sqlalchemy import update
result = db.execute(
update(LoyaltyProgram)
.where(
LoyaltyProgram.merchant_id == merchant_id,
LoyaltyProgram.deleted_at.isnot(None),
)
.values(deleted_at=None, deleted_by_id=None)
.execution_options(include_deleted=True)
)
db.commit()
count = result.rowcount
if count:
logger.info(
f"Restored {count} soft-deleted programs for merchant {merchant_id}"
)
return count
# Singleton instance
program_service = ProgramService()