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

Freegle / Iznik / 11342

08 May 2026 02:47PM UTC coverage: 68.97% (-3.8%) from 72.761%
11342

push

circleci

web-flow
Merge pull request #403 from Freegle/feature/exports-migration

feat(batch): migrate exports.php to users:process-exports

9127 of 10554 branches covered (86.48%)

Branch coverage included in aggregate %.

400 of 452 new or added lines in 2 files covered. (88.5%)

12172 existing lines in 167 files now uncovered.

100855 of 148909 relevant lines covered (67.73%)

19.62 hits per line

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

91.18
/iznik-batch/app/Services/MessageSpatialService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Models\Message;
6
use App\Models\MessageGroup;
7
use Illuminate\Support\Facades\DB;
8
use Illuminate\Support\Facades\Log;
9

10
class MessageSpatialService
11
{
12
    // V1: MessageCollection::RECENTPOSTS = "Midnight 31 days ago"
13
    private const RECENT_DAYS = 31;
14
    private const SRID = 3857;
15

16
    public function updateSpatialIndex(bool $dryRun = false): array
5✔
17
    {
18
        $stats = [
5✔
19
            'upserted_recent' => $this->upsertRecentMessages($dryRun),
5✔
20
            'outcomes_updated' => $this->updateOutcomesAndPromises($dryRun),
5✔
21
            'removed_deleted' => $this->removeDeletedMessages($dryRun),
5✔
22
            'removed_old' => $this->removeOldMessages($dryRun),
5✔
23
            'removed_non_approved' => $this->removeNonApprovedMessages($dryRun),
5✔
24
        ];
5✔
25

26
        $total = array_sum($stats);
5✔
27
        $stats['total'] = $total;
5✔
28

29
        Log::info("MessageSpatialIndex: " . ($dryRun ? 'would update ' : 'updated ') . "{$total} entries", $stats);
5✔
30

31
        return $stats;
5✔
32
    }
33

34
    private function upsertRecentMessages(bool $dryRun = false): int
5✔
35
    {
36
        $cutoff = date('Y-m-d', strtotime('Midnight ' . self::RECENT_DAYS . ' days ago'));
5✔
37

38
        $msgs = DB::table('messages')
5✔
39
            ->join('messages_groups', 'messages_groups.msgid', '=', 'messages.id')
5✔
40
            ->join('users', 'users.id', '=', 'messages.fromuser')
5✔
41
            ->leftJoin('messages_spatial', 'messages_spatial.msgid', '=', 'messages_groups.msgid')
5✔
42
            ->leftJoin('messages_outcomes', 'messages_outcomes.msgid', '=', 'messages.id')
5✔
43
            ->where('messages_groups.arrival', '>=', $cutoff)
5✔
44
            ->whereNotNull('messages.lat')
5✔
45
            ->whereNotNull('messages.lng')
5✔
46
            ->whereNull('messages.deleted')
5✔
47
            ->where('messages_groups.collection', MessageGroup::COLLECTION_APPROVED)
5✔
48
            ->whereNull('users.deleted')
5✔
49
            ->where(function ($q) {
5✔
50
                // Include messages with no outcome, or Taken/Received (same as V1: outcome IS NULL OR outcome IN ('Taken','Received'))
51
                $q->whereNull('messages_outcomes.outcome')
5✔
52
                    ->orWhereIn('messages_outcomes.outcome', [Message::OUTCOME_TAKEN, Message::OUTCOME_RECEIVED]);
5✔
53
            })
5✔
54
            ->where(function ($q) {
5✔
55
                $q->whereNull('messages_spatial.msgid')
5✔
56
                    ->orWhereRaw('ST_X(messages_spatial.point) != messages.lng')
5✔
57
                    ->orWhereRaw('ST_Y(messages_spatial.point) != messages.lat')
5✔
58
                    ->orWhereNull('messages_spatial.groupid')
5✔
59
                    ->orWhereRaw('messages_spatial.groupid != messages_groups.groupid')
5✔
60
                    ->orWhereRaw('messages_groups.arrival != messages_spatial.arrival');
5✔
61
            })
5✔
62
            ->select(
5✔
63
                'messages.id',
5✔
64
                'messages.lat',
5✔
65
                'messages.lng',
5✔
66
                'messages_groups.groupid',
5✔
67
                'messages_groups.arrival',
5✔
68
                'messages_groups.msgtype',
5✔
69
            )
5✔
70
            ->distinct()
5✔
71
            ->get();
5✔
72

73
        $count = 0;
5✔
74
        foreach ($msgs as $msg) {
5✔
75
            if (!$dryRun) {
5✔
76
                // Coordinates come from DB, not user input — safe to embed in WKT.
77
                $wkt = "POINT({$msg->lng} {$msg->lat})";
5✔
78
                $srid = self::SRID;
5✔
79

80
                DB::statement(
5✔
81
                    "INSERT INTO messages_spatial (msgid, point, groupid, msgtype, arrival)
5✔
82
                     VALUES (?, ST_GeomFromText('$wkt', $srid), ?, ?, ?)
5✔
83
                     ON DUPLICATE KEY UPDATE
84
                       point = ST_GeomFromText('$wkt', $srid),
5✔
85
                       groupid = ?,
86
                       msgtype = ?,
87
                       arrival = ?",
5✔
88
                    [$msg->id, $msg->groupid, $msg->msgtype, $msg->arrival,
5✔
89
                     $msg->groupid, $msg->msgtype, $msg->arrival]
5✔
90
                );
5✔
91
            }
92
            $count++;
5✔
93
        }
94

95
        return $count;
5✔
96
    }
97

98
    private function updateOutcomesAndPromises(bool $dryRun = false): int
5✔
99
    {
100
        $msgs = DB::table('messages_spatial')
5✔
101
            ->leftJoin('messages_outcomes', 'messages_outcomes.msgid', '=', 'messages_spatial.msgid')
5✔
102
            ->leftJoin('messages_promises', 'messages_promises.msgid', '=', 'messages_spatial.msgid')
5✔
103
            ->select(
5✔
104
                'messages_spatial.id',
5✔
105
                'messages_spatial.msgid',
5✔
106
                'messages_spatial.successful',
5✔
107
                'messages_spatial.promised',
5✔
108
                'messages_outcomes.outcome',
5✔
109
                'messages_promises.promisedat',
5✔
110
            )
5✔
111
            ->orderByDesc('messages_outcomes.timestamp')
5✔
112
            ->get();
5✔
113

114
        $count = 0;
5✔
115
        foreach ($msgs as $msg) {
5✔
116
            if ($msg->outcome === Message::OUTCOME_WITHDRAWN || $msg->outcome === Message::OUTCOME_EXPIRED) {
5✔
117
                if (!$dryRun) {
1✔
118
                    DB::table('messages_spatial')->where('id', $msg->id)->delete();
1✔
119
                }
120
                $count++;
1✔
121
            } elseif ($msg->outcome === Message::OUTCOME_TAKEN || $msg->outcome === Message::OUTCOME_RECEIVED) {
5✔
122
                if (!$msg->successful) {
5✔
123
                    if (!$dryRun) {
5✔
124
                        DB::table('messages_spatial')->where('id', $msg->id)->update(['successful' => 1]);
5✔
125
                    }
126
                    $count++;
5✔
127
                }
128
            } elseif ($msg->successful) {
5✔
129
                if (!$dryRun) {
×
UNCOV
130
                    DB::table('messages_spatial')->where('id', $msg->id)->update(['successful' => 0]);
×
131
                }
UNCOV
132
                $count++;
×
133
            }
134

135
            if ($msg->promised && !$msg->promisedat) {
5✔
UNCOV
136
                if (!$dryRun) {
×
UNCOV
137
                    DB::table('messages_spatial')->where('id', $msg->id)->update(['promised' => 0]);
×
138
                }
UNCOV
139
                $count++;
×
140
            } elseif (!$msg->promised && $msg->promisedat) {
5✔
UNCOV
141
                if (!$dryRun) {
×
UNCOV
142
                    DB::table('messages_spatial')->where('id', $msg->id)->update(['promised' => 1]);
×
143
                }
UNCOV
144
                $count++;
×
145
            }
146
        }
147

148
        return $count;
5✔
149
    }
150

151
    private function removeDeletedMessages(bool $dryRun = false): int
5✔
152
    {
153
        $ids = DB::table('messages_spatial')
5✔
154
            ->join('messages', 'messages_spatial.msgid', '=', 'messages.id')
5✔
155
            ->leftJoin('users', 'users.id', '=', 'messages.fromuser')
5✔
156
            ->where(function ($q) {
5✔
157
                $q->whereNull('messages.fromuser')
5✔
158
                    ->orWhereNotNull('messages.deleted')
5✔
159
                    ->orWhereNotNull('users.deleted');
5✔
160
            })
5✔
161
            ->pluck('messages_spatial.id');
5✔
162

163
        if ($ids->isEmpty()) {
5✔
164
            return 0;
4✔
165
        }
166

167
        if (!$dryRun) {
1✔
168
            DB::table('messages_spatial')->whereIn('id', $ids)->delete();
1✔
169
        }
170

171
        return $ids->count();
1✔
172
    }
173

174
    private function removeOldMessages(bool $dryRun = false): int
5✔
175
    {
176
        $cutoff = date('Y-m-d', strtotime('Midnight ' . self::RECENT_DAYS . ' days ago'));
5✔
177

178
        $ids = DB::table('messages_spatial')
5✔
179
            ->join('messages_groups', 'messages_groups.msgid', '=', 'messages_spatial.msgid')
5✔
180
            ->where('messages_groups.arrival', '<', $cutoff)
5✔
181
            ->pluck('messages_spatial.id');
5✔
182

183
        if ($ids->isEmpty()) {
5✔
184
            return 0;
4✔
185
        }
186

187
        if (!$dryRun) {
1✔
188
            DB::table('messages_spatial')->whereIn('id', $ids)->delete();
1✔
189
        }
190

191
        return $ids->count();
1✔
192
    }
193

194
    private function removeNonApprovedMessages(bool $dryRun = false): int
5✔
195
    {
196
        $ids = DB::table('messages_spatial')
5✔
197
            ->join('messages_groups', 'messages_groups.msgid', '=', 'messages_spatial.msgid')
5✔
198
            ->where('messages_groups.collection', '!=', MessageGroup::COLLECTION_APPROVED)
5✔
199
            ->pluck('messages_spatial.id');
5✔
200

201
        if ($ids->isEmpty()) {
5✔
202
            return 0;
5✔
203
        }
204

UNCOV
205
        if (!$dryRun) {
×
UNCOV
206
            DB::table('messages_spatial')->whereIn('id', $ids)->delete();
×
207
        }
208

UNCOV
209
        return $ids->count();
×
210
    }
211
}
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