mirror of
				https://github.com/ae-utbm/sith.git
				synced 2025-11-03 18:43:04 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			157 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			157 lines
		
	
	
		
			6.1 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from collections.abc import Iterable
 | 
						|
from operator import attrgetter
 | 
						|
 | 
						|
from django.conf import settings
 | 
						|
from django.core.mail import send_mass_mail
 | 
						|
from django.core.management.base import BaseCommand
 | 
						|
from django.db import transaction
 | 
						|
from django.db.models import Exists, OuterRef, QuerySet
 | 
						|
from django.template.loader import render_to_string
 | 
						|
from django.utils.timezone import now
 | 
						|
from django.utils.translation import gettext as _
 | 
						|
 | 
						|
from core.models import User, UserQuerySet
 | 
						|
from counter.models import AccountDump, Counter, Customer, Product, Selling
 | 
						|
 | 
						|
 | 
						|
class Command(BaseCommand):
 | 
						|
    """Effectively dump the inactive users.
 | 
						|
 | 
						|
    Users who received a warning mail enough time ago will
 | 
						|
    have their account emptied, unless they reactivated their
 | 
						|
    account in the meantime (e.g. by resubscribing).
 | 
						|
 | 
						|
    This command should be automated with a cron task.
 | 
						|
    """
 | 
						|
 | 
						|
    def add_arguments(self, parser):
 | 
						|
        parser.add_argument(
 | 
						|
            "--dry-run",
 | 
						|
            action="store_true",
 | 
						|
            help="Don't do anything, just display the number of users concerned",
 | 
						|
        )
 | 
						|
 | 
						|
    def handle(self, *args, **options):
 | 
						|
        users = self._get_users()
 | 
						|
        # some users may have resubscribed or performed a purchase
 | 
						|
        # (which reactivates the account).
 | 
						|
        # Those reactivated users are not to be mailed about their account dump.
 | 
						|
        # Instead, the related AccountDump row will be dropped,
 | 
						|
        # as if nothing ever happened.
 | 
						|
        # Refunding a user implies a transaction, so refunded users
 | 
						|
        # count as reactivated users
 | 
						|
        users_to_dump_qs = users.filter_inactive()
 | 
						|
        reactivated_users = list(users.difference(users_to_dump_qs))
 | 
						|
        users_to_dump = list(users_to_dump_qs)
 | 
						|
        self.stdout.write(
 | 
						|
            f"{len(reactivated_users)} users have reactivated their account"
 | 
						|
        )
 | 
						|
        self.stdout.write(f"{len(users_to_dump)} users will see their account dumped")
 | 
						|
 | 
						|
        if options["dry_run"]:
 | 
						|
            return
 | 
						|
 | 
						|
        AccountDump.objects.ongoing().filter(
 | 
						|
            customer__user__in=reactivated_users
 | 
						|
        ).delete()
 | 
						|
        self._dump_accounts({u.customer for u in users_to_dump})
 | 
						|
        self.stdout.write("Accounts dumped")
 | 
						|
        nb_successful_mails = self._send_mails(users_to_dump)
 | 
						|
        self.stdout.write(f"{nb_successful_mails} were successfuly sent.")
 | 
						|
        self.stdout.write("Finished !")
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _get_users() -> UserQuerySet:
 | 
						|
        """Fetch the users which have a pending account dump."""
 | 
						|
        threshold = now() - settings.SITH_ACCOUNT_DUMP_DELTA
 | 
						|
        ongoing_dump_operations: QuerySet[AccountDump] = (
 | 
						|
            AccountDump.objects.ongoing()
 | 
						|
            .filter(customer__user=OuterRef("pk"), warning_mail_sent_at__lt=threshold)
 | 
						|
        )  # fmt: off
 | 
						|
        # cf. https://github.com/astral-sh/ruff/issues/14103
 | 
						|
        return (
 | 
						|
            User.objects.filter(Exists(ongoing_dump_operations))
 | 
						|
            .annotate(
 | 
						|
                warning_date=ongoing_dump_operations.values("warning_mail_sent_at")
 | 
						|
            )
 | 
						|
            .select_related("customer")
 | 
						|
        )
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    @transaction.atomic
 | 
						|
    def _dump_accounts(accounts: set[Customer]):
 | 
						|
        """Perform the actual db operations to dump the accounts.
 | 
						|
 | 
						|
        An account dump completion is a two steps process:
 | 
						|
            - create a special sale which price is equal
 | 
						|
              to the money in the account
 | 
						|
            - update the pending account dump operation
 | 
						|
              by linking it to the aforementioned sale
 | 
						|
 | 
						|
        Args:
 | 
						|
            accounts: the customer accounts which must be emptied
 | 
						|
        """
 | 
						|
        # Dump operations are special sales,
 | 
						|
        # which price is equal to the money the user has.
 | 
						|
        # They are made in a special counter (which should belong to the AE).
 | 
						|
        # However, they are not linked to a product, because it would
 | 
						|
        # make no sense to have a product without price.
 | 
						|
        customer_ids = [account.pk for account in accounts]
 | 
						|
        pending_dumps: list[AccountDump] = list(
 | 
						|
            AccountDump.objects.ongoing()
 | 
						|
            .filter(customer_id__in=customer_ids)
 | 
						|
            .order_by("customer_id")
 | 
						|
        )
 | 
						|
        if len(pending_dumps) != len(customer_ids):
 | 
						|
            raise ValueError("One or more accounts were not engaged in a dump process")
 | 
						|
        counter = Counter.objects.get(pk=settings.SITH_COUNTER_ACCOUNT_DUMP_ID)
 | 
						|
        seller = User.objects.get(pk=settings.SITH_ROOT_USER_ID)
 | 
						|
        product = Product.objects.get(id=settings.SITH_PRODUCT_REFOUND_ID)
 | 
						|
        sales = Selling.objects.bulk_create(
 | 
						|
            [
 | 
						|
                Selling(
 | 
						|
                    label="Vidange compte inactif",
 | 
						|
                    club=counter.club,
 | 
						|
                    counter=counter,
 | 
						|
                    seller=seller,
 | 
						|
                    product=product,
 | 
						|
                    customer=account,
 | 
						|
                    quantity=1,
 | 
						|
                    unit_price=account.amount,
 | 
						|
                    date=now(),
 | 
						|
                    is_validated=True,
 | 
						|
                )
 | 
						|
                for account in accounts
 | 
						|
            ]
 | 
						|
        )
 | 
						|
        sales.sort(key=attrgetter("customer_id"))
 | 
						|
 | 
						|
        # dumps and sales are linked to the same customers
 | 
						|
        # and both ordered with the same key, so zipping them is valid
 | 
						|
        for dump, sale in zip(pending_dumps, sales, strict=False):
 | 
						|
            dump.dump_operation = sale
 | 
						|
        AccountDump.objects.bulk_update(pending_dumps, ["dump_operation"])
 | 
						|
 | 
						|
        # Because the sales were created with a bull_create,
 | 
						|
        # the account amounts haven't been updated,
 | 
						|
        # which mean we must do it explicitly
 | 
						|
        Customer.objects.filter(pk__in=customer_ids).update(amount=0)
 | 
						|
 | 
						|
    @staticmethod
 | 
						|
    def _send_mails(users: Iterable[User]) -> int:
 | 
						|
        """Send the mails informing users that their account has been dumped.
 | 
						|
 | 
						|
        Returns:
 | 
						|
            The number of emails successfully sent.
 | 
						|
        """
 | 
						|
        mails = [
 | 
						|
            (
 | 
						|
                _("Your AE account has been emptied"),
 | 
						|
                render_to_string("counter/mails/account_dump.jinja", {"user": user}),
 | 
						|
                settings.DEFAULT_FROM_EMAIL,
 | 
						|
                [user.email],
 | 
						|
            )
 | 
						|
            for user in users
 | 
						|
        ]
 | 
						|
        return send_mass_mail(mails, fail_silently=True)
 |