Coverage for app/venv/lib/python3.14/site-packages/weblate/billing/tasks.py: 0%
74 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 07:15 +0000
1# Copyright © Michal Čihař <michal@weblate.org>
2#
3# SPDX-License-Identifier: GPL-3.0-or-later
5from datetime import timedelta
7from celery.schedules import crontab
8from django.conf import settings
9from django.db import transaction
10from django.db.models import Count, Q
11from django.utils import timezone
12from django.utils.translation import gettext
14from weblate.accounts.notifications import send_notification_email
15from weblate.billing.models import Billing, BillingEvent
16from weblate.trans.tasks import project_removal
17from weblate.utils.celery import app
20@app.task(trail=False)
21def billing_check(billing_id: int | None = None) -> None:
22 if billing_id is None:
23 Billing.objects.check_limits()
24 else:
25 Billing.objects.get(pk=billing_id).check_limits()
28@app.task(trail=False)
29def billing_notify() -> None:
30 billing_check()
32 limit = Billing.objects.get_out_of_limits()
33 due = Billing.objects.get_unpaid()
35 with_project = Billing.objects.annotate(Count("projects")).filter(
36 projects__count__gt=0
37 )
38 toremove = with_project.exclude(removal=None).order_by("removal")
39 trial = with_project.filter(removal=None, state=Billing.STATE_TRIAL).order_by(
40 "expiry"
41 )
43 if limit or due or toremove or trial:
44 send_notification_email(
45 "en",
46 [a[1] for a in settings.ADMINS] + settings.ADMINS_BILLING,
47 "billing_check",
48 context={
49 "limit": limit,
50 "due": due,
51 "toremove": toremove,
52 "trial": trial,
53 },
54 )
57@app.task(trail=False)
58def notify_expired() -> None:
59 # Notify about expired billings
60 possible_billings = Billing.objects.filter(
61 # Active without payment (checked later)
62 Q(state=Billing.STATE_ACTIVE)
63 # Scheduled removal
64 | Q(removal__isnull=False)
65 # Trials expiring soon
66 | Q(state=Billing.STATE_TRIAL, expiry__lte=timezone.now() + timedelta(days=7))
67 ).exclude(projects__isnull=True)
68 for bill in possible_billings:
69 if bill.state == Billing.STATE_ACTIVE and bill.check_payment_status(now=True):
70 continue
71 if bill.plan.price:
72 note = gettext(
73 "You will stop receiving this notification once "
74 "you pay the bills or the project is removed."
75 )
76 else:
77 note = gettext(
78 "You will stop receiving this notification once "
79 "you change to regular subscription or the project is removed."
80 )
82 for user in bill.get_notify_users():
83 bill.billinglog_set.create(
84 event=BillingEvent.EMAIL, summary="Billing expired", user=user
85 )
86 send_notification_email(
87 user.profile.language,
88 [user.email],
89 "billing_expired",
90 context={
91 "billing": bill,
92 "payment_enabled": getattr(settings, "PAYMENT_ENABLED", False),
93 "unsubscribe_note": note,
94 },
95 info=bill,
96 )
99@app.task(trail=False)
100@transaction.atomic
101def schedule_removal() -> None:
102 removal = timezone.now() + timedelta(days=settings.BILLING_REMOVAL_PERIOD)
103 for bill in Billing.objects.filter(
104 state=Billing.STATE_ACTIVE, removal=None
105 ).select_for_update():
106 if bill.check_payment_status():
107 continue
108 bill.billinglog_set.create(
109 event=BillingEvent.UNPAID,
110 summary=f"Scheduled removal at {removal.isoformat()}",
111 )
112 bill.removal = removal
113 bill.save(update_fields=["removal"])
116@app.task(trail=False)
117@transaction.atomic
118def remove_single_billing(billing_id: int) -> None:
119 bill = Billing.objects.select_for_update().get(pk=billing_id)
120 for user in bill.get_notify_users():
121 bill.billinglog_set.create(
122 event=BillingEvent.EMAIL, summary="Billing removed", user=user
123 )
124 send_notification_email(
125 user.profile.language,
126 [user.email],
127 "billing_expired",
128 context={"billing": bill, "final_removal": True},
129 info=bill,
130 )
131 for prj in bill.projects.iterator():
132 bill.billinglog_set.create(
133 event=BillingEvent.REMOVED, summary=f"Removed project {prj}"
134 )
135 prj.log_warning("removing due to unpaid billing")
136 project_removal(prj.id, None)
137 bill.removal = None
138 bill.state = Billing.STATE_TERMINATED
139 bill.save()
140 bill.billinglog_set.create(event=BillingEvent.REMOVED, summary="Terminated billing")
143@app.task(trail=False)
144def perform_removal() -> None:
145 for bill in Billing.objects.filter(removal__lte=timezone.now()).iterator():
146 remove_single_billing.delay(bill.pk)
149@app.on_after_finalize.connect
150def setup_periodic_tasks(sender, **kwargs) -> None:
151 sender.add_periodic_task(3600, billing_check.s(), name="billing-check")
152 sender.add_periodic_task(
153 crontab(hour=3, minute=0, day_of_week="mon,thu"),
154 billing_notify.s(),
155 name="billing-notify",
156 )
157 sender.add_periodic_task(
158 crontab(hour=1, minute=0),
159 perform_removal.s(),
160 name="perform-removal",
161 )
162 sender.add_periodic_task(
163 crontab(hour=2, minute=0, day_of_week="mon,thu"),
164 schedule_removal.s(),
165 name="schedule-removal",
166 )
167 sender.add_periodic_task(
168 crontab(hour=2, minute=30, day_of_week="mon,thu"),
169 notify_expired.s(),
170 name="notify-expired",
171 )