• 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

81.82
/iznik-batch/app/Services/RestartProjectService.php
1
<?php
2

3
namespace App\Services;
4

5
use App\Models\Group;
6
use App\Models\Location;
7
use Html2Text\Html2Text;
8
use Illuminate\Support\Facades\Http;
9
use Illuminate\Support\Facades\Log;
10

11
class RestartProjectService
12
{
13
    private const API_BASE     = 'https://restarters.net/api/v2';
14
    private const NEARBY_MILES = 15;
15
    private const DAYS_AHEAD   = 31;
16

17
    public function __construct(private CommunityEventService $events) {}
11✔
18

19
    public function sync(bool $dryRun = false): array
11✔
20
    {
21
        $added         = 0;
11✔
22
        $updated       = 0;
11✔
23
        $deleted       = 0;
11✔
24
        $now           = date('Y-m-d');
11✔
25
        $latest        = date('Y-m-d', strtotime('+' . self::DAYS_AHEAD . ' days'));
11✔
26
        $externalsSeen = [];
11✔
27

28
        $groups = $this->apiGet('/groups/names');
11✔
29

30
        if ($groups === null) {
11✔
NEW
31
            Log::error('Restart: failed to fetch groups list');
×
NEW
32
            return ['added' => $added, 'updated' => $updated, 'deleted' => $deleted];
×
33
        }
34

35
        foreach ($groups as $group) {
11✔
36
            if (str_contains($group['name'] ?? '', '[INACTIVE]')) {
8✔
37
                continue;
1✔
38
            }
39

40
            $details = $this->apiGet("/groups/{$group['id']}");
7✔
41
            if (!$details) {
7✔
NEW
42
                continue;
×
43
            }
44

45
            if (($details['location']['country_code'] ?? '') !== 'GB') {
7✔
46
                continue;
1✔
47
            }
48

49
            $lat = (float) ($details['location']['lat'] ?? 0);
6✔
50
            $lng = (float) ($details['location']['lng'] ?? 0);
6✔
51
            if (!$lat || !$lng) {
6✔
NEW
52
                continue;
×
53
            }
54

55
            $groupIds = Location::groupsNear($lat, $lng, self::NEARBY_MILES);
6✔
56
            if (empty($groupIds)) {
6✔
NEW
57
                Log::debug('Restart: no Freegle groups near', ['group' => $group['name'], 'lat' => $lat, 'lng' => $lng]);
×
NEW
58
                continue;
×
59
            }
60

61
            $freegleGroup = Group::find($groupIds[0]);
6✔
62
            if (!$freegleGroup) {
6✔
NEW
63
                continue;
×
64
            }
65

66
            if (!$freegleGroup->getSetting('communityevents', 1)) {
6✔
NEW
67
                Log::debug('Restart: communityevents disabled', ['freegle_group' => $freegleGroup->nameshort]);
×
NEW
68
                continue;
×
69
            }
70

71
            $events = $this->apiGet("/groups/{$group['id']}/events", [
6✔
72
                'start' => $now,
6✔
73
                'end'   => $latest,
6✔
74
            ]);
6✔
75
            if (!$events) {
6✔
76
                continue;
1✔
77
            }
78

79
            foreach ($events as $event) {
5✔
80
                if (!($event['approved'] ?? false)) {
5✔
NEW
81
                    continue;
×
82
                }
83

84
                $eventDetails = $this->apiGet("/events/{$event['id']}");
5✔
85
                if (!$eventDetails) {
5✔
NEW
86
                    continue;
×
87
                }
88

89
                $externalId         = "Restart-{$event['id']}";
5✔
90
                $externalsSeen[$externalId] = true;
5✔
91

92
                $html        = new Html2Text($eventDetails['description'] ?? '');
5✔
93
                $description = $html->getText();
5✔
94

95
                $title = $event['title'];
5✔
96
                if (!str_contains($title, 'Repair Cafe')) {
5✔
97
                    $title = "Repair Cafe: $title";
4✔
98
                }
99

100
                $url      = $eventDetails['link'] ?? ($details['website'] ?? null);
5✔
101
                $email    = $details['email'] ?? null;
5✔
102
                $location = $event['location'] ?? '';
5✔
103

104
                $existing = $this->events->findByExternalId($externalId);
5✔
105

106
                if ($existing) {
5✔
107
                    if ($dryRun) {
1✔
NEW
108
                        Log::debug('Restart: dry run — would update event', ['external_id' => $externalId]);
×
NEW
109
                        $updated++;
×
NEW
110
                        continue;
×
111
                    }
112

113
                    $pendingFlag = $existing->title !== $title ||
1✔
114
                                  $existing->location !== $location ||
1✔
115
                                  $existing->description !== $description;
1✔
116

117
                    $updateData = [
1✔
118
                        'title'        => $title,
1✔
119
                        'location'     => $location,
1✔
120
                        'description'  => $description,
1✔
121
                        'contacturl'   => $url,
1✔
122
                        'contactemail' => $email,
1✔
123
                    ];
1✔
124

125
                    if ($pendingFlag && !$existing->pending) {
1✔
126
                        $updateData['pending'] = 1;
1✔
127
                    }
128

129
                    $this->events->updateEvent($existing->id, $updateData);
1✔
130
                    $this->events->removeDates($existing->id);
1✔
131
                    $this->events->addDate($existing->id, $event['start'], $event['end']);
1✔
132
                    $updated++;
1✔
133
                } else {
134
                    if ($dryRun) {
4✔
135
                        Log::debug('Restart: dry run — would create event', ['external_id' => $externalId]);
1✔
136
                        $added++;
1✔
137
                        continue;
1✔
138
                    }
139

140
                    $contactName = $eventDetails['group']['name'] ?? null;
3✔
141

142
                    $eid = $this->events->createEvent(
3✔
143
                        null,
3✔
144
                        $title,
3✔
145
                        $location,
3✔
146
                        $contactName,
3✔
147
                        null,
3✔
148
                        $email,
3✔
149
                        $url,
3✔
150
                        $description,
3✔
151
                        $externalId
3✔
152
                    );
3✔
153

154
                    $this->events->addGroup($eid, $groupIds[0]);
3✔
155
                    $this->events->addDate($eid, $event['start'], $event['end']);
3✔
156
                    $added++;
3✔
157
                }
158
            }
159
        }
160

161
        $existings = $this->events->getUpcomingByExternalIdPrefix('Restart-', $now);
11✔
162
        foreach ($existings as $e) {
11✔
163
            if (!array_key_exists($e->externalid, $externalsSeen)) {
5✔
164
                if ($dryRun) {
1✔
NEW
165
                    Log::debug('Restart: dry run — would delete old event', ['external_id' => $e->externalid]);
×
NEW
166
                    $deleted++;
×
NEW
167
                    continue;
×
168
                }
169
                $this->events->markDeleted($e->id);
1✔
170
                $deleted++;
1✔
171
            }
172
        }
173

174
        return ['added' => $added, 'updated' => $updated, 'deleted' => $deleted];
11✔
175
    }
176

177
    private function apiGet(string $path, array $params = []): ?array
11✔
178
    {
179
        do {
180
            $response  = Http::get(self::API_BASE . $path, $params);
11✔
181
            $throttled = str_contains($response->body(), 'Too Many Requests');
11✔
182

183
            if ($throttled) {
11✔
NEW
184
                Log::warning('Restart: API throttled, retrying', ['path' => $path]);
×
NEW
185
                sleep(5);
×
186
            }
187
        } while ($throttled);
11✔
188

189
        if (!$response->successful()) {
11✔
NEW
190
            Log::error('Restart: API error', ['path' => $path, 'status' => $response->status()]);
×
NEW
191
            return null;
×
192
        }
193

194
        $data = $response->json('data');
11✔
195

196
        if ($data === null) {
11✔
NEW
197
            Log::warning('Restart: no data in response', ['path' => $path]);
×
198
        }
199

200
        return $data;
11✔
201
    }
202
}
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