• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / Iznik / 12176

10 May 2026 09:29AM UTC coverage: 69.155%. First build
12176

Pull #431

circleci

edwh
feat(group-customisation): convert email to ModTools-branded MJML

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull Request #431: feat(batch): migrate group_customisation.php to groups:remind-customisation

9132 of 10565 branches covered (86.44%)

Branch coverage included in aggregate %.

75 of 97 new or added lines in 3 files covered. (77.32%)

102491 of 150845 relevant lines covered (67.94%)

19.62 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.39
/iznik-batch/app/Services/GroupCustomisationService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Mail\Group\CustomisationReminderMail;
6
use App\Models\Group;
7
use App\Models\Membership;
8
use Illuminate\Support\Facades\DB;
9
use Illuminate\Support\Facades\Log;
10
use Illuminate\Support\Facades\Mail;
11

12
/**
13
 * Sends monthly reminders to group moderators about missing customisation attributes.
14
 *
15
 * Migrated from iznik-server/scripts/cron/group_customisation.php
16
 */
17
class GroupCustomisationService
18
{
19
    private const SYSTEM_MOD_EMAIL = 'modtools@modtools.org';
20

21
    private const CUST_ATTRS = [
22
        'profile'     => '* Profile picture - this helps people recognise your group.',
23
        'tagline'     => '* Tagline - something short and snappy, ideally with a local reference.',
24
        'welcomemail' => '* Welcome Mail - this is sent out to people when they join - your chance to welcome them, and let them know anything they need to (e.g. whether pets are allowed).',
25
        'description' => '* Description - this is longer than the tagline, and is visible on the site - often containing similar information to the welcome mail.',
26
    ];
27

28
    public function sendReminders(bool $dryRun = false): array
10✔
29
    {
30
        $noreplyAddr = config('freegle.mail.noreply_addr', 'noreply@ilovefreegle.org');
10✔
31
        $mentorsAddr = config('freegle.mail.mentors_addr', 'mentors@ilovefreegle.org');
10✔
32
        $modSite     = config('freegle.sites.mod', 'https://modtools.org');
10✔
33

34
        $groups = DB::table('groups')
10✔
35
            ->where('type', Group::TYPE_FREEGLE)
10✔
36
            ->where('publish', 1)
10✔
37
            ->where('onhere', 1)
10✔
38
            ->orderByRaw('RAND()')
10✔
39
            ->select(['id', 'nameshort', 'profile', 'tagline', 'welcomemail', 'description'])
10✔
40
            ->get();
10✔
41

42
        $sent    = 0;
10✔
43
        $skipped = 0;
10✔
44

45
        foreach ($groups as $group) {
10✔
46
            $missing = $this->buildMissingList($group);
10✔
47

48
            if ($missing === '') {
10✔
49
                $skipped++;
1✔
50
                continue;
1✔
51
            }
52

53
            $modEmails = $this->getModEmails($group->id);
10✔
54

55
            if (empty($modEmails)) {
10✔
56
                Log::info("GroupCustomisation: no mod emails for group {$group->id} {$group->nameshort}");
10✔
57
                $skipped++;
10✔
58
                continue;
10✔
59
            }
60

61
            $body = "Just to remind you, you can make your Freegle group look more local and friendly for your members by customising how it appears on Freegle Direct.\n\n"
10✔
62
                . "Here are some things you could add:\n\n"
10✔
63
                . $missing . "\n"
10✔
64
                . "You can change these from ModTools ({$modSite}), in Settings - Group Settings - Group Appearance. ModTools helps you run your group, and is free to use for all Freegle groups.\n\n"
10✔
65
                . "If you need help with this, then please contact {$mentorsAddr}\n\n"
10✔
66
                . "P.S. This is an automated message sent once a month. We send it regularly because sometimes the moderators on a group change, and they might not know about this stuff.";
10✔
67

68
            Log::info("GroupCustomisation: sending reminder for {$group->nameshort}", [
10✔
69
                'group_id' => $group->id,
10✔
70
                'mod_count' => count($modEmails),
10✔
71
                'dry_run'   => $dryRun,
10✔
72
            ]);
10✔
73

74
            if (!$dryRun) {
10✔
75
                foreach ($modEmails as $modEmail) {
9✔
76
                    Mail::send(new CustomisationReminderMail($modEmail, $group->nameshort, $missing));
9✔
77
                }
78
            }
79

80
            $sent++;
10✔
81
        }
82

83
        return [
10✔
84
            'groups_checked' => count($groups),
10✔
85
            'sent'           => $sent,
10✔
86
            'skipped'        => $skipped,
10✔
87
        ];
10✔
88
    }
89

90
    private function buildMissingList(object $group): string
10✔
91
    {
92
        $missing = '';
10✔
93

94
        foreach (self::CUST_ATTRS as $attr => $description) {
10✔
95
            if (empty($group->$attr)) {
10✔
96
                $missing .= $description . "\n";
10✔
97
            }
98
        }
99

100
        return $missing;
10✔
101
    }
102

103
    /**
104
     * @return string[]  Preferred email addresses for all active mods/owners of this group.
105
     */
106
    private function getModEmails(int $groupId): array
10✔
107
    {
108
        $systemUserId = DB::table('users_emails')
10✔
109
            ->where('email', self::SYSTEM_MOD_EMAIL)
10✔
110
            ->value('userid');
10✔
111

112
        $query = DB::table('memberships')
10✔
113
            ->join('users_emails', 'users_emails.userid', '=', 'memberships.userid')
10✔
114
            ->where('memberships.groupid', $groupId)
10✔
115
            ->whereIn('memberships.role', [Membership::ROLE_OWNER, Membership::ROLE_MODERATOR])
10✔
116
            ->where('memberships.collection', Membership::COLLECTION_APPROVED)
10✔
117
            ->where('users_emails.preferred', 1);
10✔
118

119
        if ($systemUserId) {
10✔
NEW
120
            $query->where('memberships.userid', '!=', $systemUserId);
×
121
        }
122

123
        return $query->pluck('users_emails.email')->all();
10✔
124
    }
125
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc