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

Freegle / Iznik / 11307

08 May 2026 12:02PM UTC coverage: 72.829% (+0.07%) from 72.761%
11307

Pull #401

circleci

edwh
feat(batch): migrate chat_process.php to chats:process-incoming

Process pending chat messages (processingrequired=1) in Laravel batch.
Mirrors V1 cron/chat_process.php + ChatMessage::process().

IncomingMailService creates messages with processingrequired=1 (with comment
'Background chat_process.php cron handles visibility, roster, push
notifications'). Without this migration those incoming mail replies were
invisible to ChatNotificationService (which filters on processingrequired=0
AND processingsuccessful=1).

Key operations per message:
- Spam/ban checks for User2User chats (spam_users, users_banned, chatmodstatus)
- Review cascade: hold new message if previous message in chat is under review
- Mark processingrequired=0, processingsuccessful=1 (or 0 on failure)
- Update sender's chat_roster (lastmsgseen/lastmsgemailed)
- Reopen CLOSED roster entries (not BLOCKED) after new activity

Scheduler entry commented out in routes/console.php pending sign-off.
10 tests covering all transitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull Request #401: feat(batch): migrate chat_process.php to chats:process-incoming

13699 of 20568 branches covered (66.6%)

Branch coverage included in aggregate %.

230 of 279 new or added lines in 4 files covered. (82.44%)

19 existing lines in 4 files now uncovered.

101296 of 137329 relevant lines covered (73.76%)

22.56 hits per line

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

84.62
/iznik-batch/app/Services/ChatProcessService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Models\ChatMessage;
6
use App\Models\ChatRoom;
7
use App\Models\ChatRoster;
8
use Illuminate\Support\Facades\DB;
9
use Illuminate\Support\Facades\Log;
10

11
/**
12
 * Processes chat messages that arrive via incoming email (processingrequired = 1).
13
 *
14
 * Mirrors V1 cron/chat_process.php + ChatMessage::process().
15
 *
16
 * Key responsibilities:
17
 * - Spam/ban checks for User2User chats
18
 * - Setting processingrequired = 0, processingsuccessful = 1 (or 0 on failure)
19
 * - Updating chat_roster for the sender
20
 * - Reopening closed chats after new activity
21
 */
22
class ChatProcessService
23
{
24
    // V1: ChatMessage::REVIEW_SPAM, REVIEW_FORCE, etc.
25
    private const REVIEW_SPAM = 'Spam';
26
    private const REVIEW_LAST = 'Last';
27

28
    /**
29
     * Process all pending chat messages (processingrequired = 1).
30
     *
31
     * @return int Number of messages processed.
32
     */
33
    public function processIncoming(): int
10✔
34
    {
35
        $messages = DB::table('chat_messages')
10✔
36
            ->join('chat_rooms', 'chat_messages.chatid', '=', 'chat_rooms.id')
10✔
37
            ->where('chat_messages.processingrequired', 1)
10✔
38
            ->orderBy('chat_messages.id', 'asc')
10✔
39
            ->select('chat_messages.*', 'chat_rooms.chattype', 'chat_rooms.user1', 'chat_rooms.user2')
10✔
40
            ->get();
10✔
41

42
        $count = 0;
10✔
43

44
        foreach ($messages as $message) {
10✔
45
            if ($this->processMessage($message)) {
9✔
46
                $count++;
9✔
47
            }
48
        }
49

50
        Log::info("ChatProcess: processed {$count} messages");
10✔
51

52
        return $count;
10✔
53
    }
54

55
    /**
56
     * Process a single pending message.
57
     *
58
     * @return bool True if the message was processed (success or failure), false if skipped.
59
     */
60
    private function processMessage(object $message): bool
9✔
61
    {
62
        $id = $message->id;
9✔
63
        $chatid = $message->chatid;
9✔
64
        $userid = $message->userid;
9✔
65
        $chattype = $message->chattype;
9✔
66
        $platform = (bool) $message->platform;
9✔
67

68
        // --- Ban check for messages with a refmsgid ---
69
        if (!empty($message->refmsgid)) {
9✔
NEW
70
            $banned = DB::table('messages_groups')
×
NEW
71
                ->join('users_banned', function ($join) use ($userid) {
×
NEW
72
                    $join->on('messages_groups.groupid', '=', 'users_banned.groupid')
×
NEW
73
                        ->where('users_banned.userid', '=', $userid);
×
NEW
74
                })
×
NEW
75
                ->where('messages_groups.msgid', $message->refmsgid)
×
NEW
76
                ->exists();
×
77

NEW
78
            if ($banned) {
×
NEW
79
                $this->processFailed($id);
×
NEW
80
                return true;
×
81
            }
82
        }
83

84
        // --- User2User spam and review checks ---
85
        $review = 0;
9✔
86
        $reviewreason = null;
9✔
87
        $spam = 0;
9✔
88

89
        if ($chattype === ChatRoom::TYPE_USER2USER) {
9✔
90
            // Check if sender is a confirmed or pending spammer.
91
            $isSpammer = DB::table('spam_users')
9✔
92
                ->where('userid', $userid)
9✔
93
                ->whereIn('collection', ['Spammer', 'PendingAdd'])
9✔
94
                ->exists();
9✔
95

96
            if ($isSpammer) {
9✔
97
                $this->processFailed($id);
2✔
98
                return true;
2✔
99
            }
100

101
            // Check if sender is banned on all common groups with the other user.
102
            $otherId = $message->user1 == $userid ? $message->user2 : $message->user1;
7✔
103

104
            $bannedInCommon = $this->isBannedInCommonGroups($userid, $otherId);
7✔
105

106
            if ($bannedInCommon) {
7✔
NEW
107
                $this->processFailed($id);
×
NEW
108
                return true;
×
109
            }
110

111
            // Check if sender's messages should be held for review.
112
            $user = DB::table('users')->where('id', $userid)->first();
7✔
113
            $chatmodstatus = $user?->chatmodstatus ?? 'Moderated';
7✔
114

115
            if ($chatmodstatus === 'Fully') {
7✔
NEW
116
                $review = 1;
×
NEW
117
                $reviewreason = self::REVIEW_SPAM;
×
118
            }
119

120
            // If the previous message in this chat is held for review, hold this one too.
121
            if (!$review) {
7✔
122
                $lastReview = DB::table('chat_messages')
7✔
123
                    ->where('chatid', $chatid)
7✔
124
                    ->where('id', '!=', $id)
7✔
125
                    ->orderByDesc('id')
7✔
126
                    ->value('reviewrequired');
7✔
127

128
                if ($lastReview) {
7✔
129
                    $review = 1;
1✔
130
                    $reviewreason = self::REVIEW_LAST;
1✔
131
                }
132
            }
133
        }
134

135
        // Mark the message as processed.
136
        DB::table('chat_messages')
7✔
137
            ->where('id', $id)
7✔
138
            ->update([
7✔
139
                'reviewrequired' => $review,
7✔
140
                'reportreason' => $reviewreason,
7✔
141
                'reviewrejected' => $spam,
7✔
142
                'processingrequired' => 0,
7✔
143
                'processingsuccessful' => 1,
7✔
144
            ]);
7✔
145

146
        // Update the sender's roster position.
147
        $this->updateSenderRoster($id, $chatid, $userid, $platform);
7✔
148

149
        // Reopen any CLOSED roster entries for this chat (not BLOCKED).
150
        DB::table('chat_roster')
7✔
151
            ->where('chatid', $chatid)
7✔
152
            ->where('status', ChatRoster::STATUS_CLOSED)
7✔
153
            ->update(['status' => ChatRoster::STATUS_OFFLINE]);
7✔
154

155
        return true;
7✔
156
    }
157

158
    /**
159
     * Mark a message as failed processing.
160
     */
161
    private function processFailed(int $messageId): void
2✔
162
    {
163
        DB::table('chat_messages')
2✔
164
            ->where('id', $messageId)
2✔
165
            ->update([
2✔
166
                'processingrequired' => 0,
2✔
167
                'processingsuccessful' => 0,
2✔
168
            ]);
2✔
169
    }
170

171
    /**
172
     * Check if $userId is banned on all groups they have in common with $otherId.
173
     */
174
    private function isBannedInCommonGroups(int $userId, int $otherId): bool
7✔
175
    {
176
        // Get groups both users are members of.
177
        $commonGroups = DB::table('memberships as m1')
7✔
178
            ->join('memberships as m2', function ($join) use ($otherId) {
7✔
179
                $join->on('m1.groupid', '=', 'm2.groupid')
7✔
180
                    ->where('m2.userid', '=', $otherId);
7✔
181
            })
7✔
182
            ->where('m1.userid', $userId)
7✔
183
            ->pluck('m1.groupid');
7✔
184

185
        if ($commonGroups->isEmpty()) {
7✔
186
            return false;
7✔
187
        }
188

189
        // Check if $userId is banned on ALL common groups.
NEW
190
        $bannedCount = DB::table('users_banned')
×
NEW
191
            ->where('userid', $userId)
×
NEW
192
            ->whereIn('groupid', $commonGroups)
×
NEW
193
            ->count();
×
194

NEW
195
        return $bannedCount >= $commonGroups->count();
×
196
    }
197

198
    /**
199
     * Update the sender's roster entry after they sent a message.
200
     *
201
     * V1: If the message came by email (!platform), mark it seen/emailed (since the sender
202
     * wrote it, they've "seen" it). For platform messages, same unless they have email-mine on.
203
     */
204
    private function updateSenderRoster(int $messageId, int $chatid, int $userid, bool $platform): void
7✔
205
    {
206
        if (!$platform) {
7✔
207
            // Incoming email reply: only update if there are no unseen messages from the other user.
208
            $hasUnseen = DB::table('chat_messages as cm')
1✔
209
                ->leftJoin('chat_roster as cr', function ($join) use ($userid) {
1✔
210
                    $join->on('cr.chatid', '=', 'cm.chatid')
1✔
211
                        ->where('cr.userid', '=', $userid);
1✔
212
                })
1✔
213
                ->where('cm.chatid', $chatid)
1✔
214
                ->where('cm.userid', '!=', $userid)
1✔
215
                ->where('cm.seenbyall', 0)
1✔
216
                ->where('cm.mailedtoall', 0)
1✔
217
                ->where(function ($q) {
1✔
218
                    $q->whereNull('cr.lastmsgseen')
1✔
219
                        ->orWhereColumn('cr.lastmsgseen', '<', 'cm.id');
1✔
220
                })
1✔
221
                ->where(function ($q) {
1✔
222
                    $q->whereNull('cr.lastmsgemailed')
1✔
223
                        ->orWhereColumn('cr.lastmsgemailed', '<', 'cm.id');
1✔
224
                })
1✔
225
                ->exists();
1✔
226

227
            if ($hasUnseen) {
1✔
NEW
228
                return;
×
229
            }
230
        }
231

232
        DB::table('chat_roster')
7✔
233
            ->where('chatid', $chatid)
7✔
234
            ->where('userid', $userid)
7✔
235
            ->where(function ($q) use ($messageId) {
7✔
236
                $q->whereNull('lastmsgseen')
7✔
237
                    ->orWhere('lastmsgseen', '<', $messageId);
7✔
238
            })
7✔
239
            ->update([
7✔
240
                'lastmsgseen' => $messageId,
7✔
241
                'lastmsgemailed' => $messageId,
7✔
242
            ]);
7✔
243
    }
244
}
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