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

Freegle / Iznik / 11495

09 May 2026 07:35AM UTC coverage: 69.06% (-3.8%) from 72.847%
11495

Pull #408

circleci

edwh
docs(migration): mark restartproject and repaircafewales as migrated (PR #408)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull Request #408: feat(batch): migrate check_cgas, visualise, tn_sync + dry-run improvements

9127 of 10554 branches covered (86.48%)

Branch coverage included in aggregate %.

507 of 663 new or added lines in 16 files covered. (76.47%)

11902 existing lines in 138 files now uncovered.

101630 of 149824 relevant lines covered (67.83%)

19.56 hits per line

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

64.24
/iznik-batch/app/Services/TnSyncService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Models\Location;
6
use App\Models\User;
7
use Illuminate\Support\Facades\Cache;
8
use Illuminate\Support\Facades\DB;
9
use Illuminate\Support\Facades\Http;
10
use Illuminate\Support\Facades\Log;
11

12
class TnSyncService
13
{
14
    private const CACHE_KEY      = 'tn_sync_last_date';
15
    private const PAGE_SIZE      = 100;
16
    private const RATINGS_PATH   = '/fd/api/ratings';
17
    private const CHANGES_PATH   = '/fd/api/user-changes';
18

19
    private string $apiBase;
20
    private string $apiKey;
21

22
    public function __construct()
7✔
23
    {
24
        $this->apiBase = config('freegle.trashnothing.api_base', 'https://trashnothing.com');
7✔
25
        $this->apiKey  = config('freegle.trashnothing.key', '');
7✔
26
    }
27

28
    public function sync(bool $dryRun = false): array
7✔
29
    {
30
        $from = $this->getFromDate();
7✔
31
        $to   = date('c');
7✔
32

33
        Log::info('TN sync starting', ['from' => $from, 'to' => $to, 'dry_run' => $dryRun]);
7✔
34

35
        $ratings     = $this->syncRatings($from, $to, $dryRun);
7✔
36
        $userChanges = $this->syncUserChanges($from, $to, $dryRun);
7✔
37

38
        if (!$dryRun && ($ratings > 0 || $userChanges > 0)) {
7✔
39
            Cache::forever(self::CACHE_KEY, $to);
3✔
40
        }
41

42
        return ['ratings' => $ratings, 'user_changes' => $userChanges];
7✔
43
    }
44

45
    private function getFromDate(): string
7✔
46
    {
47
        $cached = Cache::get(self::CACHE_KEY);
7✔
48
        if ($cached) {
7✔
NEW
49
            return $cached;
×
50
        }
51

52
        $latest = DB::table('ratings')
7✔
53
            ->whereNotNull('tn_rating_id')
7✔
54
            ->max('timestamp');
7✔
55

56
        return $latest ? date('c', strtotime($latest)) : date('c', strtotime('30 days ago'));
7✔
57
    }
58

59
    private function syncRatings(string $from, string $to, bool $dryRun): int
7✔
60
    {
61
        $count = 0;
7✔
62
        $page  = 1;
7✔
63

64
        do {
65
            $response = Http::get($this->apiBase . self::RATINGS_PATH, [
7✔
66
                'key'      => $this->apiKey,
7✔
67
                'page'     => $page,
7✔
68
                'per_page' => self::PAGE_SIZE,
7✔
69
                'date_min' => $from,
7✔
70
                'date_max' => $to,
7✔
71
            ]);
7✔
72

73
            if (!$response->successful()) {
7✔
NEW
74
                Log::error('TN sync: ratings API error', ['status' => $response->status()]);
×
NEW
75
                break;
×
76
            }
77

78
            $ratings = $response->json('ratings', []);
7✔
79
            $page++;
7✔
80

81
            foreach ($ratings as $rating) {
7✔
82
                if (!($rating['ratee_fd_user_id'] ?? null)) {
3✔
NEW
83
                    continue;
×
84
                }
85

86
                $rateeId  = $rating['ratee_fd_user_id'];
3✔
87
                $ratingId = $rating['rating_id'];
3✔
88

89
                if (!User::find($rateeId)) {
3✔
NEW
90
                    continue;
×
91
                }
92

93
                if ($dryRun) {
3✔
94
                    Log::debug('TN sync: dry run — would sync rating', [
1✔
95
                        'ratee'     => $rateeId,
1✔
96
                        'rating_id' => $ratingId,
1✔
97
                        'rating'    => $rating['rating'] ?? null,
1✔
98
                    ]);
1✔
99
                    $count++;
1✔
100
                    continue;
1✔
101
                }
102

103
                try {
104
                    if ($rating['rating'] ?? null) {
2✔
105
                        DB::statement(
1✔
106
                            "INSERT INTO ratings (ratee, rating, timestamp, visible, tn_rating_id)
1✔
107
                             VALUES (?, ?, ?, 1, ?)
108
                             ON DUPLICATE KEY UPDATE rating = ?, timestamp = ?",
1✔
109
                            [
1✔
110
                                $rateeId,
1✔
111
                                $rating['rating'],
1✔
112
                                $rating['date'],
1✔
113
                                $ratingId,
1✔
114
                                $rating['rating'],
1✔
115
                                $rating['date'],
1✔
116
                            ]
1✔
117
                        );
1✔
118
                    } else {
119
                        DB::table('ratings')
1✔
120
                            ->where('ratee', $rateeId)
1✔
121
                            ->where('tn_rating_id', $ratingId)
1✔
122
                            ->delete();
1✔
123
                    }
124
                    $count++;
2✔
NEW
125
                } catch (\Throwable $e) {
×
NEW
126
                    Log::error('TN sync: rating update failed', [
×
NEW
127
                        'ratee'  => $rateeId,
×
NEW
128
                        'rating' => $rating,
×
NEW
129
                        'error'  => $e->getMessage(),
×
NEW
130
                    ]);
×
131
                }
132
            }
133
        } while (count($ratings) === self::PAGE_SIZE);
7✔
134

135
        return $count;
7✔
136
    }
137

138
    private function syncUserChanges(string $from, string $to, bool $dryRun): int
7✔
139
    {
140
        $count = 0;
7✔
141
        $page  = 1;
7✔
142

143
        do {
144
            $response = Http::get($this->apiBase . self::CHANGES_PATH, [
7✔
145
                'key'      => $this->apiKey,
7✔
146
                'page'     => $page,
7✔
147
                'per_page' => self::PAGE_SIZE,
7✔
148
                'date_min' => $from,
7✔
149
                'date_max' => $to,
7✔
150
            ]);
7✔
151

152
            if (!$response->successful()) {
7✔
NEW
153
                Log::error('TN sync: user-changes API error', ['status' => $response->status()]);
×
NEW
154
                break;
×
155
            }
156

157
            $changes = $response->json('changes', []);
7✔
158
            $page++;
7✔
159

160
            foreach ($changes as $change) {
7✔
161
                if (!($change['fd_user_id'] ?? null)) {
2✔
NEW
162
                    continue;
×
163
                }
164

165
                $userId = $change['fd_user_id'];
2✔
166
                $user   = User::find($userId);
2✔
167

168
                if (!$user || !$user->isTN()) {
2✔
169
                    continue;
1✔
170
                }
171

172
                if ($dryRun) {
1✔
NEW
173
                    Log::debug('TN sync: dry run — would apply user change', [
×
NEW
174
                        'user'           => $userId,
×
NEW
175
                        'account_removed'=> isset($change['account_removed']),
×
NEW
176
                        'has_reply_time' => isset($change['reply_time']),
×
NEW
177
                        'has_about_me'   => isset($change['about_me']),
×
NEW
178
                        'has_username'   => isset($change['username']),
×
NEW
179
                        'has_location'   => isset($change['location']),
×
NEW
180
                    ]);
×
NEW
181
                    $count++;
×
NEW
182
                    continue;
×
183
                }
184

185
                try {
186
                    if (isset($change['account_removed'])) {
1✔
NEW
187
                        Log::info('TN sync: account removed', ['user' => $userId]);
×
NEW
188
                        app(UserManagementService::class)->forgetUser($userId, 'TN account removed');
×
NEW
189
                        $count++;
×
NEW
190
                        continue;
×
191
                    }
192

193
                    if (isset($change['reply_time'])) {
1✔
194
                        DB::statement(
1✔
195
                            "REPLACE INTO users_replytime (userid, replytime, timestamp) VALUES (?, ?, ?)",
1✔
196
                            [$userId, $change['reply_time'], $change['date']]
1✔
197
                        );
1✔
198
                    }
199

200
                    if (isset($change['about_me'])) {
1✔
201
                        try {
NEW
202
                            DB::statement(
×
NEW
203
                                "REPLACE INTO users_aboutme (userid, timestamp, text) VALUES (?, ?, ?)",
×
NEW
204
                                [$userId, $change['date'], $change['about_me']]
×
NEW
205
                            );
×
NEW
206
                        } catch (\Throwable $e) {
×
NEW
207
                            Log::error('TN sync: about_me update failed', ['user' => $userId, 'error' => $e->getMessage()]);
×
208
                        }
209
                    }
210

211
                    if (isset($change['username'])) {
1✔
NEW
212
                        $oldName = User::removeTNGroup($user->fullname ?? '');
×
NEW
213
                        if ($oldName !== $change['username']) {
×
NEW
214
                            Log::info('TN sync: name change', ['user' => $userId, 'from' => $oldName, 'to' => $change['username']]);
×
NEW
215
                            DB::table('users')->where('id', $userId)->update(['fullname' => $change['username']]);
×
216

NEW
217
                            $this->renameTnEmails($userId, $oldName, $change['username']);
×
218
                        }
219
                    }
220

221
                    if (isset($change['location']['latitude'], $change['location']['longitude'])) {
1✔
NEW
222
                        $lat = (float) $change['location']['latitude'];
×
NEW
223
                        $lng = (float) $change['location']['longitude'];
×
224

NEW
225
                        $loc = Location::closestPostcode($lat, $lng);
×
226

NEW
227
                        if ($loc && $loc->id !== $user->lastlocation) {
×
NEW
228
                            Log::info('TN sync: location updated', ['user' => $userId, 'loc' => $loc->id, 'name' => $loc->name]);
×
NEW
229
                            DB::table('users')->where('id', $userId)->update(['lastlocation' => $loc->id]);
×
230
                        }
231
                    }
232

233
                    $count++;
1✔
NEW
234
                } catch (\Throwable $e) {
×
NEW
235
                    Log::error('TN sync: user change failed', ['user' => $userId, 'error' => $e->getMessage()]);
×
236
                }
237
            }
238
        } while (count($changes) === self::PAGE_SIZE);
7✔
239

240
        return $count;
7✔
241
    }
242

NEW
243
    private function renameTnEmails(int $userId, string $oldName, string $newName): void
×
244
    {
NEW
245
        $emails = DB::table('users_emails')->where('userid', $userId)->get();
×
246

NEW
247
        foreach ($emails as $emailRow) {
×
NEW
248
            if (str_contains($emailRow->email, "{$oldName}-")) {
×
NEW
249
                $newEmail = str_replace("{$oldName}-", "{$newName}-", $emailRow->email);
×
NEW
250
                Log::info('TN sync: rename email', ['user' => $userId, 'from' => $emailRow->email, 'to' => $newEmail]);
×
NEW
251
                DB::table('users_emails')->where('id', $emailRow->id)->update(['email' => $newEmail]);
×
252
            }
253
        }
254
    }
255
}
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