From 53dd30b74a0b805937dd3a8add59aeb8c5ffeee0 Mon Sep 17 00:00:00 2001
From: Radek Czajka <rczajka@rczajka.pl>
Date: Wed, 29 Jan 2020 09:12:17 +0100
Subject: [PATCH] Contact management.

---
 src/club/models.py                            |  17 ++-
 src/messaging/admin.py                        |  39 ++++-
 src/messaging/locale/pl/LC_MESSAGES/django.mo | Bin 1776 -> 2882 bytes
 src/messaging/locale/pl/LC_MESSAGES/django.po | 134 +++++++++++++++---
 .../migrations/0003_auto_20200128_2230.py     | 101 +++++++++++++
 src/messaging/models.py                       |  63 +++++++-
 src/messaging/states.py                       |  36 +++--
 7 files changed, 348 insertions(+), 42 deletions(-)
 create mode 100644 src/messaging/migrations/0003_auto_20200128_2230.py

diff --git a/src/club/models.py b/src/club/models.py
index 8c26d03a1..f36aada20 100644
--- a/src/club/models.py
+++ b/src/club/models.py
@@ -2,6 +2,7 @@
 # Copyright © Fundacja Nowoczesna Polska. See NOTICE for more information.
 #
 from datetime import datetime, timedelta
+from django.apps import apps
 from django.conf import settings
 from django.contrib.sites.models import Site
 from django.core.mail import send_mail
@@ -63,7 +64,8 @@ class Schedule(models.Model):
     def save(self, *args, **kwargs):
         if not self.key:
             self.key = get_random_hash(self.email)
-        return super(Schedule, self).save(*args, **kwargs)
+        super(Schedule, self).save(*args, **kwargs)
+        self.update_contact()
 
     def initiate_payment(self, request):
         return self.get_payment_method().initiate(request, self)
@@ -108,6 +110,19 @@ class Schedule(models.Model):
         self.email_sent = True
         self.save()
 
+    def update_contact(self):
+        Contact = apps.get_model('messaging', 'Contact')
+        if not self.payed_at:
+            level = Contact.TRIED
+            since = self.started_at
+        else:
+            since = self.payed_at
+            if self.is_recurring():
+                level = Contact.RECURRING
+            else:
+                level = Contact.SINGLE
+        Contact.update(self.email, level, since, self.expires_at)
+
 
 class Membership(models.Model):
     """ Represents a user being recognized as a member of the club. """
diff --git a/src/messaging/admin.py b/src/messaging/admin.py
index 1b2bac599..854079bee 100644
--- a/src/messaging/admin.py
+++ b/src/messaging/admin.py
@@ -1,9 +1,10 @@
 from django.contrib import admin
-from .models import EmailTemplate, EmailSent
+from django.utils.translation import ugettext_lazy as _
+from . import models
 
 
 class EmailSentInline(admin.TabularInline):
-    model = EmailSent
+    model = models.EmailSent
     fields = ['timestamp', 'email', 'subject']
     readonly_fields = ['timestamp', 'email', 'subject']
     extra = 0
@@ -15,11 +16,26 @@ class EmailSentInline(admin.TabularInline):
 
 
 class EmailTemplateAdmin(admin.ModelAdmin):
-    list_display = ['state', 'days', 'subject', 'hour']
+    list_display = ['state', 'min_days_since', 'subject', 'min_hour']
     inlines = [EmailSentInline]
-
-
-admin.site.register(EmailTemplate, EmailTemplateAdmin)
+    fieldsets = [
+        (None, {"fields": [
+            'state',
+            ('min_days_since', 'max_days_since'),
+            'is_active',
+            ]}),
+        (_('E-mail content'), {"fields": [
+            'subject', 'body'
+        ]}),
+        (_('Sending constraints'), {"fields": [
+            ('min_day_of_month', 'max_day_of_month'),
+            ('dow_1', 'dow_2', 'dow_3', 'dow_4', 'dow_5', 'dow_6', 'dow_7'),
+            ('min_hour', 'max_hour'),
+        ]}),
+    ]
+
+
+admin.site.register(models.EmailTemplate, EmailTemplateAdmin)
 
 
 class EmailSentAdmin(admin.ModelAdmin):
@@ -30,5 +46,14 @@ class EmailSentAdmin(admin.ModelAdmin):
     change_links = ['template']
 
 
-admin.site.register(EmailSent, EmailSentAdmin)
+admin.site.register(models.EmailSent, EmailSentAdmin)
+
+
+class ContactAdmin(admin.ModelAdmin):
+    list_filter = ['level']
+    list_display = ['email', 'level', 'since', 'expires_at']
+    search_fields = ['email']
+    date_hierarchy = 'since'
+
 
+admin.site.register(models.Contact, ContactAdmin)
diff --git a/src/messaging/locale/pl/LC_MESSAGES/django.mo b/src/messaging/locale/pl/LC_MESSAGES/django.mo
index da52dab18c0b644eca0e03f94c567e556f85e3e6..a53c4b67d92ac691dfed4eb8eacb2b516e96fc41 100644
GIT binary patch
literal 2882
zcmbW1O^g&p6vqoi(Gf(!4-`KNWXY~<@6HSmahMqtVGWUW*WK|0FQlfscD8qVs>ZHr
zh8`fnjT?+tLo~sda4;s`jAA@+bH|$=Jm8IkCUP<{+>96hue*n3$dV|PuKrcMdi7q_
zt5<b()7tY4WeD{t)I)0++Y1JF;fM0l-HfdTUj{c|Yyx}`{ThV#f^NnIkmT=y>%g<%
zM({)MKJZg;Gk6iCd6zQzWsvlH;QioFAldm9B)vaCeC$vB9s#d|yTEn#F!m_88>Idb
zkj5i02Yv+7{PS7=Gmz|l3DUa010Mi?1Rnx_$;N*NX}#Ao`9GO_J&cijD+m$W0n)mk
z2fsyJirM%z2sfku7ZKxYu_@Bq0&V~~xE&nJ<Zpl+{bL|Zur5gZdLP8cKEaRtUIc0U
ztBhZRr1w2Y_J7WJ1tj}d!Eta6g2BN8=zudI#px_acF%)k_j8cq@<ql=AjPGZ@e24b
z`qw~w?C*?g5k!)21WCRv<0wey_5w)pDuW0Mdj@p}DivDibExZ4N%z)5^ZJEi$x(+;
zpFrJ&N`CugD;iItV%zMoOo&uX3hi?nDy1~7k<Ni)K>3bkCFN=SDUMVgPm#ohV)AU(
zr+84zsZcI$p~fpZ@bFVsp0uTMJ*?(<TFS2WWcAc3J~Z4yD>$x1OO6<R=1h4!8REa4
z{g?}07JNRGbCvBw!=84{@hA*N%=YTg@Cg~#wUODp^0=sSU0;x{9lP1hF&%MNDBg}N
z_wgzE?7ZZ2z6?CW%d%CSjgoogakRNmULriGy*QOb##asQY0H(i!unc`^1yFOnDNbj
z+0&Tst!!(I9d=s654cM~QI<`Fz9(WfsTE;LNyqvv34cn5Y^rU!jx3vzZWM;;I!Hb%
zY!s6ItW+L}Va?c3_{thK8zrV{^HFF>rxwYCZ%a>QEgc1(Q}25buJunyR>zi^8$@-k
z2UcNL74nX6=V_TqNVq)N_Gy8sZaBE!%)+hG)l5h)wa^(@A`Bprcq7zNo5>`AOeSG8
z6vMP>Wvd+sD{twVv~PH#+7c%@g2MG2Z{Y;ySth`+uUwgQ=XDga77}GpF%{fUL^n}1
zND9eJnnY|m)RctI5zl!`h6dSE;<*{w)}eJKO~dz`gHgjcH4X9flrtkw_@v|<f{_w0
z=8JhJUvP^1c(GX8TiBH^<ny_s!dRyk3T3c89b)EDp&F5BNT()6>!yB*w}af|k;%g=
z&I@DtTyg;wY{YFip}RPaNcn0%cjrZDq^-PJn{f86=#j5E89Ij*?jlaL#P`*Go0|@z
zPz26|4qK+gRXgdL%AWmjOCyzGRjCyC0iNe2KCFfc`D&%eckblWELZl7P&<+5%Y`C8
zeL5M)R|^<9zzdk9wqbr>sanaeOfHO&er9y<iu}bDIlRGY<_`W+vqhNQJ+eRdj`ONi
zGE5^lt)++epg2|>+mmBQwfDm}ou*)~(WNwZ@>1!#FPoOzxE=kUH*4Urn$*3^(sX&$
zyBsf{;|<L_ysZP_&L>Y&4QX!EH_1;0q3m5)K9`6_c~gho7|FP>yyWt3@>sbGK3dIC
zx0}mLqd2g!J9w|&6&(>;O`dpq^#p7C%S%=^Sz9Y#dR<@i7STH-Lbn_DK5zxRF5tD%
zq3G()@)GZ~dyB#@FVTbM(1SLh+8^a^+zfoTtDtCf9j`5$Y9JLMlFApn6PviV*z{TI
zR0e{%U0NJ`cW^O)-i1(mg1wGaY18d3#<;`%MKRH|aYx0>rg398hT_|<cR-~RH-t&z
zi!H6bzd`+M*U7%3bYFuCHyd0n3baw|Bk)(Y5woD5t=U6}upqiB&Q6D!uBZoE-8ihQ
z-yio`HbG_@l6jhY6yTOG4Nle-3|*w&DBgD(Y-pS@0;j#(>1mG!`+CSi#qb;p%l-u<
CyZ?>=

delta 700
zcmZ9|zi-n(6bJAZlBP)^v=GD(f#5(el!*XR5d%v%<}NIVi+!Z0#CMi`!8tFfDynV>
z$z>}11+cXn8%nm0tgs*?goNnK#P_8}NIm)abH01;e)!Y*!`Arg+1eXITS8nxyhU8#
zeg!YI5o{qmqK@zF6GS!m3SNW-<opltDx8$_-yrAzfD7<9JPrTAMcAwoog^C5C3JYf
zTW}8EhV$?a+{dO5U>p8I5|3A~D%*rN;2F3K@52s!3Q<C@As*Vni#OaYeGmDvA7_q<
z4$$ERzd%07cgP3%39;{N4$K;lb1^sG1UDdDXx8|4jk1qYYL$K7{1k%MZkA*GM`+a=
zS5+fdT(8|ME;km7jq2xTGI08ZqOJ`+1%qK%L@Mg4Bwg3BP@`BU+VqYlV(BAgoEVzt
z+URs$g-7Fw3Wf=%iGLY!V!HzsQ7C;{?94qXuGOD3IxC`c@8R8*y^VUWvUfOtt}>Q=
z=QHEUC2G1m*`^?$Zd%i~smrX$#G1`yl6!n<B{JWWsU)LSX!~~hGRUowBA#waXDnAA
uE1>caBLANh=*gA{e1D*W+)TFG$4f$^i+2l;+dUiR+DJ;BG~`qy9sLCrpObn3

diff --git a/src/messaging/locale/pl/LC_MESSAGES/django.po b/src/messaging/locale/pl/LC_MESSAGES/django.po
index b3d5a2d4b..e58763d06 100644
--- a/src/messaging/locale/pl/LC_MESSAGES/django.po
+++ b/src/messaging/locale/pl/LC_MESSAGES/django.po
@@ -7,8 +7,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-01-17 13:27+0100\n"
-"PO-Revision-Date: 2020-01-17 13:27+0100\n"
+"POT-Creation-Date: 2020-01-28 22:49+0100\n"
+"PO-Revision-Date: 2020-01-28 22:51+0100\n"
 "Last-Translator: \n"
 "Language-Team: \n"
 "Language: pl\n"
@@ -20,61 +20,145 @@ msgstr ""
 "%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
 "X-Generator: Poedit 2.2.4\n"
 
+#: admin.py:27
+msgid "E-mail content"
+msgstr "Zawartość e-maila"
+
+#: admin.py:30
+msgid "Sending constraints"
+msgstr "Ograniczenia wysyłki"
+
 #: models.py:11
 msgid "state"
 msgstr "stan"
 
-#: models.py:12 models.py:69
+#: models.py:12 models.py:79
 msgid "subject"
 msgstr "temat"
 
-#: models.py:13 models.py:70
+#: models.py:13 models.py:80
 msgid "body"
 msgstr "treść"
 
 #: models.py:14
-msgid "days"
-msgstr "dni"
+msgid "min days since"
+msgstr "dni po, od"
 
 #: models.py:15
-msgid "hour"
-msgstr "godzina"
+msgid "max days since"
+msgstr "dni po, do"
 
 #: models.py:16
+msgid "max hour"
+msgstr "do godziny"
+
+#: models.py:17
+msgid "min hour"
+msgstr "od godziny"
+
+#: models.py:18
+msgid "min day of month"
+msgstr "od dnia miesiąca"
+
+#: models.py:19
+msgid "max day of month"
+msgstr "do dnia miesiąca"
+
+#: models.py:20
+msgid "Monday"
+msgstr "poniedziałek"
+
+#: models.py:21
+msgid "Tuesday"
+msgstr "wtorek"
+
+#: models.py:22
+msgid "Wednesday"
+msgstr "środa"
+
+#: models.py:23
+msgid "Thursday"
+msgstr "czwartek"
+
+#: models.py:24
+msgid "Friday"
+msgstr "piątek"
+
+#: models.py:25
+msgid "Saturday"
+msgstr "sobota"
+
+#: models.py:26
+msgid "Sunday"
+msgstr "niedziela"
+
+#: models.py:27
 msgid "active"
 msgstr "aktywny"
 
-#: models.py:19
+#: models.py:30
 msgid "email template"
 msgstr "szablon e-maila"
 
-#: models.py:20
+#: models.py:31
 msgid "email templates"
 msgstr "szablony e-maili"
 
-#: models.py:68
+#: models.py:78
 msgid "e-mail"
 msgstr "e-mail"
 
-#: models.py:73
+#: models.py:83
 msgid "email sent"
 msgstr "wysłany e-mail"
 
-#: models.py:74
+#: models.py:84
 msgid "emails sent"
 msgstr "wysłane e-maile"
 
-#: states.py:47
-msgid "club membership expiring"
-msgstr "członkostwo w Towarzystwie wygasa"
+#: models.py:101
+msgid "Would-be donor"
+msgstr "Niedoszły darczyńca"
+
+#: models.py:102
+msgid "One-time donor"
+msgstr "Darczyńca z jednorazową wpłatą"
+
+#: models.py:103
+msgid "Recurring donor"
+msgstr "Darczyńca z wpłatą cykliczną"
+
+#: models.py:104
+msgid "Cold"
+msgstr "Lodówka"
+
+#: models.py:105
+msgid "Opt out"
+msgstr "Opt-out"
 
-#: states.py:62
-msgid "club payment unfinished"
-msgstr "niedokończona płatność w Towarzystwie"
+#: states.py:46
+msgid "club one-time donors"
+msgstr "darczyńcy z jednorazową wpłatą"
 
-#: states.py:74
-msgid "club recurring payment problem"
-msgstr "problem z płatnością cykliczną w Towarzystwie"
+#: states.py:52
+msgid "club one-time donors with donation expiring"
+msgstr "darczyńcy z wygasającą jednorazową wpłatą"
+
+#: states.py:67
+msgid "club would-be donors"
+msgstr "niedoszli darczyńcy"
+
+#: states.py:79
+msgid "club recurring donors"
+msgstr "darczyńcy z wpłatą cykluczną"
+
+#: states.py:84
+msgid "club recurring donors with donation expired"
+msgstr "darczyńcy z wygasającą wpłatą cykliczną"
+
+#: states.py:93
+msgid "cold group"
+msgstr "lodówka"
 
 #: views.py:19
 #, python-format
@@ -93,6 +177,12 @@ msgstr ""
 "z notacji z kropką, np.:<br>\n"
 "       <code>{{ %(model_name)s.id }}</code>."
 
+#~ msgid "days"
+#~ msgstr "dni"
+
+#~ msgid "club payment unfinished"
+#~ msgstr "niedokończona płatność w Towarzystwie"
+
 #~ msgid "New member"
 #~ msgstr "Nowy członek"
 
diff --git a/src/messaging/migrations/0003_auto_20200128_2230.py b/src/messaging/migrations/0003_auto_20200128_2230.py
new file mode 100644
index 000000000..431566911
--- /dev/null
+++ b/src/messaging/migrations/0003_auto_20200128_2230.py
@@ -0,0 +1,101 @@
+# Generated by Django 2.2.9 on 2020-01-28 21:30
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('messaging', '0002_auto_20200117_1326'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Contact',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('email', models.EmailField(max_length=254, unique=True)),
+                ('level', models.PositiveSmallIntegerField(choices=[(20, 'Tried'), (30, 'Single'), (40, 'Recurring'), (10, 'Cold'), (50, 'Opt out')])),
+                ('since', models.DateTimeField()),
+                ('expires_at', models.DateTimeField(blank=True, null=True)),
+            ],
+        ),
+        migrations.RemoveField(
+            model_name='emailtemplate',
+            name='days',
+        ),
+        migrations.RemoveField(
+            model_name='emailtemplate',
+            name='hour',
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='dow_1',
+            field=models.BooleanField(default=True, verbose_name='monday'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='dow_2',
+            field=models.BooleanField(default=True, verbose_name='tuesday'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='dow_3',
+            field=models.BooleanField(default=True, verbose_name='wednesday'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='dow_4',
+            field=models.BooleanField(default=True, verbose_name='thursday'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='dow_5',
+            field=models.BooleanField(default=True, verbose_name='friday'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='dow_6',
+            field=models.BooleanField(default=True, verbose_name='saturday'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='dow_7',
+            field=models.BooleanField(default=True, verbose_name='sunday'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='max_day_of_month',
+            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='max day of month'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='max_days_since',
+            field=models.SmallIntegerField(blank=True, null=True, verbose_name='max_days_since'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='max_hour',
+            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='max hour'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='min_day_of_month',
+            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='min day of month'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='min_days_since',
+            field=models.SmallIntegerField(blank=True, null=True, verbose_name='min_days_since'),
+        ),
+        migrations.AddField(
+            model_name='emailtemplate',
+            name='min_hour',
+            field=models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='min hour'),
+        ),
+        migrations.AlterField(
+            model_name='emailtemplate',
+            name='state',
+            field=models.CharField(choices=[('cold', 'cold group'), ('club-payment-unfinished', 'club would-be donor'), ('club-single', 'club one-time donors'), ('club-membership-expiring', 'club one-time donors with donation expiring'), ('club-recurring', 'club recurring donor'), ('club-recurring-payment-problem', 'club recurring donors with donation expired')], help_text='?', max_length=128, verbose_name='state'),
+        ),
+    ]
diff --git a/src/messaging/models.py b/src/messaging/models.py
index c40abbbd9..a8c481ff9 100644
--- a/src/messaging/models.py
+++ b/src/messaging/models.py
@@ -11,8 +11,19 @@ class EmailTemplate(models.Model):
     state = models.CharField(_('state'), max_length=128, choices=[(s.slug, s.name) for s in states], help_text='?')
     subject = models.CharField(_('subject'), max_length=1024)
     body = models.TextField(_('body'))
-    days = models.SmallIntegerField(_('days'), null=True, blank=True)
-    hour = models.IntegerField(_('hour'), null=True, blank=True)
+    min_days_since = models.SmallIntegerField(_('min days since'), null=True, blank=True)
+    max_days_since = models.SmallIntegerField(_('max days since'), null=True, blank=True)
+    min_hour = models.PositiveSmallIntegerField(_('min hour'), null=True, blank=True)
+    max_hour = models.PositiveSmallIntegerField(_('max hour'), null=True, blank=True)
+    min_day_of_month = models.PositiveSmallIntegerField(_('min day of month'), null=True, blank=True)
+    max_day_of_month = models.PositiveSmallIntegerField(_('max day of month'), null=True, blank=True)
+    dow_1 = models.BooleanField(_('Monday'), default=True)
+    dow_2 = models.BooleanField(_('Tuesday'), default=True)
+    dow_3 = models.BooleanField(_('Wednesday'), default=True)
+    dow_4 = models.BooleanField(_('Thursday'), default=True)
+    dow_5 = models.BooleanField(_('Friday'), default=True)
+    dow_6 = models.BooleanField(_('Saturday'), default=True)
+    dow_7 = models.BooleanField(_('Sunday'), default=True)
     is_active = models.BooleanField(_('active'), default=False)
 
     class Meta:
@@ -60,7 +71,6 @@ class EmailTemplate(models.Model):
                 )
 
 
-
 class EmailSent(models.Model):
     template = models.ForeignKey(EmailTemplate, models.CASCADE)
     hash_value = models.CharField(max_length=1024)
@@ -76,3 +86,50 @@ class EmailSent(models.Model):
 
     def __str__(self):
         return '%s %s' % (self.email, self.timestamp)
+
+
+class Contact(models.Model):
+    COLD = 10
+    TRIED = 20
+    SINGLE = 30
+    RECURRING = 40
+    OPT_OUT = 50
+
+    email = models.EmailField(unique=True)
+    level = models.PositiveSmallIntegerField(
+        choices=[
+            (TRIED, _('Would-be donor')),
+            (SINGLE, _('One-time donor')),
+            (RECURRING, _('Recurring donor')),
+            (COLD, _('Cold')),
+            (OPT_OUT, _('Opt out')),
+        ])
+    since = models.DateTimeField()
+    expires_at = models.DateTimeField(null=True, blank=True)
+
+    @classmethod
+    def update(cls, email, level, since, expires_at=None):
+        obj, created = cls.objects.get_or_create(email=email, defaults={
+                "level": level,
+                "since": since,
+                "expires_at": expires_at
+            })
+        if not created:
+            obj.ascend(level, since, expires_at)
+
+    def ascend(self, level, since, expires_at=None):
+        if level < self.level:
+            return
+        if level == self.level:
+            self.since = min(since, self.since)
+
+            if expires_at and self.expires_at:
+                self.expires_at = max(expires_at, self.expires_at)
+            else:
+                self.expires_at = expires_at
+        else:
+            self.level = level
+            self.since = since
+            self.expires_at = expires_at
+        self.save()
+
diff --git a/src/messaging/states.py b/src/messaging/states.py
index 0bb63b442..36b5d05f1 100644
--- a/src/messaging/states.py
+++ b/src/messaging/states.py
@@ -41,10 +41,15 @@ class State:
         return ctx
 
 
-class ClubMembershipExpiring(State):
+class ClubSingle(State):
+    slug = 'club-single'
+    name = _('club one-time donors')
+
+
+class ClubSingleExpired(State):
     slug = 'club-membership-expiring'
     allow_negative_offset = True
-    name = _('club membership expiring')
+    name = _('club one-time donors with donation expiring')
 
     def get_objects(self):
         from club.models import Schedule
@@ -57,9 +62,9 @@ class ClubMembershipExpiring(State):
         return '%s:%s' % (obj.pk, obj.expires_at.isoformat())
 
 
-class ClubPaymentUnfinished(State):
+class ClubTried(State):
     slug = 'club-payment-unfinished'
-    name = _('club payment unfinished')
+    name = _('club would-be donors')
 
     def get_objects(self):
         from club.models import Schedule
@@ -69,18 +74,31 @@ class ClubPaymentUnfinished(State):
             )
 
 
-class ClubRecurringPaymentProblem(State):
+class ClubRecurring(State):
+    slug = 'club-recurring'
+    name = _('club recurring donors')
+
+
+class ClubRecurringExpired(State):
     slug = 'club-recurring-payment-problem'
-    name = _('club recurring payment problem')
+    name = _('club recurring donors with donation expired')
 
     def get_objects(self):
         from club.models import Schedule
         return Schedule.objects.none()
 
 
+class Cold(State):
+    slug = 'cold'
+    name = _('cold group')
+
+
 states = [
-    ClubMembershipExpiring,
-    ClubPaymentUnfinished,
-    ClubRecurringPaymentProblem,
+    Cold,
+    ClubTried,
+    ClubSingle,
+    ClubSingleExpired,
+    ClubRecurring,
+    ClubRecurringExpired,
 ]
 
-- 
2.20.1