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

LeTraceurSnorkLibrary / MessSaga / 24455793340

15 Apr 2026 12:57PM UTC coverage: 39.248% (+2.7%) from 36.562%
24455793340

Pull #20

github

web-flow
Merge 4e3783ced into 4ac78237e
Pull Request #20: feat: tariffs

161 of 280 new or added lines in 14 files covered. (57.5%)

2 existing lines in 2 files now uncovered.

741 of 1888 relevant lines covered (39.25%)

0.78 hits per line

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

0.0
/app/Services/ImportService.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace App\Services;
6

7
use App\Models\MediaAttachment;
8
use App\Models\MessengerAccount;
9
use App\Models\User;
10
use App\Services\Import\Archives\DTO\ArchiveExtractionResult;
11
use App\Services\Import\DTO\PreparedMessageRowResult;
12
use App\Services\Import\MessageInsertService;
13
use App\Services\Import\MessagePreparationService;
14
use App\Services\Import\Strategies\ImportStrategyInterface;
15
use App\Services\Media\Storage\MediaStorageInterface;
16
use App\Services\Parsers\ParserRegistry;
17
use App\Services\Quota\UserMediaQuotaService;
18
use Illuminate\Database\QueryException;
19
use Illuminate\Support\Facades\DB;
20
use Illuminate\Support\Facades\Log;
21
use InvalidArgumentException;
22
use RuntimeException;
23

24
class ImportService
25
{
26
    /**
27
     * @param ParserRegistry            $parserRegistry
28
     * @param MessagePreparationService $messagePreparationService
29
     * @param MessageInsertService      $messageInsertService
30
     * @param MediaStorageInterface     $mediaStorage
31
     * @param UserMediaQuotaService     $userMediaQuotaService
32
     */
33
    public function __construct(
34
        protected ParserRegistry            $parserRegistry,
35
        protected MessagePreparationService $messagePreparationService,
36
        protected MessageInsertService      $messageInsertService,
37
        protected MediaStorageInterface     $mediaStorage,
38
        protected UserMediaQuotaService     $userMediaQuotaService,
39
    ) {
40
    }
×
41

42
    /**
43
     * @param int                     $userId
44
     * @param string                  $messengerType
45
     * @param ImportStrategyInterface $strategy
46
     * @param ArchiveExtractionResult $extractedExportFile
47
     *
48
     * @throws QueryException
49
     * @return void
50
     */
51
    public function import(
52
        int                     $userId,
53
        string                  $messengerType,
54
        ImportStrategyInterface $strategy,
55
        ArchiveExtractionResult $extractedExportFile
56
    ): void {
57
        $exportFilePath = $extractedExportFile->getExportFileAbsolutePath();
×
58
        $mediaRootPath  = $extractedExportFile->getMediaRootPath();
×
59

60
        if ($exportFilePath === null) {
×
61
            return;
×
62
        }
63

64
        try {
65
            $parser               = $this->parserRegistry->get($messengerType);
×
66
            $importedConversation = $parser->parse($exportFilePath);
×
67
        } catch (RuntimeException|InvalidArgumentException $e) {
×
68
            Log::error('Import parsing failed', [
×
69
                'user_id'        => $userId,
×
70
                'messenger_type' => $messengerType,
×
71
                'path'           => $exportFilePath,
×
72
                'error'          => $e->getMessage(),
×
73
                'trace'          => $e->getTraceAsString(),
×
74
            ]);
×
75

76
            return;
×
77
        }
78

79
        if (!$importedConversation->hasConversation()) {
×
80
            Log::notice('Import skipped - no conversation data', [
×
81
                'user_id'        => $userId,
×
82
                'messenger_type' => $messengerType,
×
83
            ]);
×
84

85
            return;
×
86
        }
87

88
        $conversation = DB::transaction(function () use (
×
89
            $userId,
×
90
            $messengerType,
×
91
            $importedConversation,
×
92
            $strategy
×
93
        ) {
×
94
            $conversationData = $importedConversation->getConversationData();
×
95

96
            $account = MessengerAccount::firstOrCreate(
×
97
                [
×
98
                    'user_id' => $userId,
×
99
                    'type'    => $messengerType,
×
100
                ],
×
101
                [
×
102
                    'name' => $conversationData['account_name'] ?? ucfirst($messengerType),
×
103
                    'meta' => $conversationData['account_meta'] ?? [],
×
104
                ],
×
105
            );
×
106

107
            return $strategy->resolveConversation(
×
108
                account: $account,
×
109
                conversationData: $conversationData
×
110
            );
×
111
        });
×
112

113
        if (!$conversation) {
×
114
            Log::warning('Import aborted - no conversation target', [
×
115
                'mode'    => $strategy->getName(),
×
116
                'user_id' => $userId,
×
117
            ]);
×
118

119
            return;
×
120
        }
121

NEW
122
        $user             = User::query()->find($userId);
×
NEW
123
        $quotaSnapshot    = isset($user)
×
NEW
124
            ? $this->userMediaQuotaService->snapshot($user)
×
NEW
125
            : null;
×
NEW
126
        $canUploadMedia   = $quotaSnapshot?->canUploadMedia() ?? false;
×
127

128
        $messagesRelation    = $parser->getMessagesRelation($conversation);
×
129
        $existingExternalIds = $messagesRelation
×
130
            ->whereNotNull('external_id')
×
131
            ->pluck('external_id')
×
132
            ->map(static fn($id): string => (string)$id)
×
133
            ->flip();
×
134
        $existingDedupHashes = $messagesRelation
×
135
            ->whereNotNull('dedup_hash')
×
136
            ->pluck('dedup_hash')
×
137
            ->map(static fn($hash): string => (string)$hash)
×
138
            ->flip();
×
139

140
        $preparedMessages  = [];
×
141
        $copiedMediaPaths  = [];
×
142
        $messageModelClass = $parser->getMessageModelClass();
×
NEW
143
        $messages          = $importedConversation->getMessages();
×
144

NEW
145
        $allowedAttachmentIndexes = [];
×
NEW
146
        $attachmentSizeByIndex    = [];
×
NEW
147
        if ($canUploadMedia && $user !== null) {
×
NEW
148
            $remainingStorageBytes = max(0, ($quotaSnapshot?->getStorageLimitBytes() ?? 0) - ($quotaSnapshot?->getStorageUsedBytes() ?? 0));
×
NEW
149
            $remainingMediaFiles   = max(0, ($quotaSnapshot?->getFilesLimitCount() ?? 0) - ($quotaSnapshot?->getFilesUsedCount() ?? 0));
×
150

NEW
151
            $attachmentCandidates = [];
×
NEW
152
            foreach ($messages as $index => $message) {
×
NEW
153
                $attachmentSizeBytes = $this->messagePreparationService->estimateAttachmentSizeBytes(
×
NEW
154
                    $mediaRootPath,
×
NEW
155
                    $message
×
NEW
156
                );
×
NEW
157
                if ($attachmentSizeBytes === null || $attachmentSizeBytes < 0) {
×
NEW
158
                    continue;
×
159
                }
160

NEW
161
                $attachmentCandidates[] = [
×
NEW
162
                    'index'      => (int)$index,
×
NEW
163
                    'size_bytes' => $attachmentSizeBytes,
×
NEW
164
                ];
×
165
            }
166

NEW
167
            usort($attachmentCandidates, static fn(array $a, array $b): int => $a['size_bytes'] <=> $b['size_bytes']);
×
168

NEW
169
            foreach ($attachmentCandidates as $candidate) {
×
NEW
170
                if ($remainingMediaFiles <= 0 || $remainingStorageBytes <= 0) {
×
NEW
171
                    break;
×
172
                }
173

NEW
174
                $sizeBytes = (int)$candidate['size_bytes'];
×
NEW
175
                if ($sizeBytes > $remainingStorageBytes) {
×
NEW
176
                    continue;
×
177
                }
178

NEW
179
                $index                            = (int)$candidate['index'];
×
NEW
180
                $allowedAttachmentIndexes[$index] = true;
×
NEW
181
                $attachmentSizeByIndex[$index]    = $sizeBytes;
×
NEW
182
                $remainingStorageBytes -= $sizeBytes;
×
NEW
183
                $remainingMediaFiles--;
×
184
            }
185
        }
186

NEW
187
        foreach ($messages as $messageIndex => $message) {
×
188
            $externalId = $this->messagePreparationService->normalizeExternalId($message['external_id'] ?? null);
×
189
            $dedupHash  = $this->messagePreparationService->buildDeduplicationHash($message);
×
190

191
            if ($externalId !== null && $existingExternalIds->has($externalId)) {
×
192
                continue;
×
193
            }
194
            if ($existingDedupHashes->has($dedupHash)) {
×
195
                continue;
×
196
            }
197

198
            if ($externalId !== null) {
×
199
                $existingExternalIds->put($externalId, true);
×
200
            }
201
            $existingDedupHashes->put($dedupHash, true);
×
202

NEW
203
            $attachmentStoredPath = null;
×
NEW
204
            $attachmentSizeBytes  = null;
×
NEW
205
            if ($canUploadMedia && isset($allowedAttachmentIndexes[$messageIndex])) {
×
NEW
206
                $attachmentStoredPath = $this->messagePreparationService->copyAttachmentForMessage(
×
NEW
207
                    $mediaRootPath,
×
NEW
208
                    $message,
×
NEW
209
                    $conversation->id
×
NEW
210
                );
×
NEW
211
                $attachmentSizeBytes  = $attachmentSizeByIndex[$messageIndex] ?? null;
×
212
            }
NEW
213
            if ($attachmentStoredPath === null) {
×
NEW
214
                $attachmentSizeBytes = null;
×
215
            }
216
            if ($attachmentStoredPath !== null) {
×
217
                $copiedMediaPaths[$attachmentStoredPath] = true;
×
218
            }
219

220
            $message['dedup_hash'] = $dedupHash;
×
221

222
            $preparedMessages[] = $this->messagePreparationService->prepareMessageRowForInsert(
×
223
                $message,
×
224
                $conversation->id,
×
225
                $messageModelClass,
×
226
                $attachmentStoredPath,
×
NEW
227
                $attachmentSizeBytes,
×
UNCOV
228
            );
×
229
        }
230

231
        $importedCount = 0;
×
232
        try {
233
            DB::transaction(function () use (
×
234
                $messageModelClass,
×
235
                $preparedMessages,
×
236
                $conversation,
×
237
                &$importedCount,
×
238
                &$copiedMediaPaths
×
239
            ) {
×
240
                /**
241
                 * @var PreparedMessageRowResult $prepared
242
                 */
243
                foreach ($preparedMessages as $prepared) {
×
244
                    $msg = $this->messageInsertService->createMessageSafely($messageModelClass, $prepared->getRow());
×
245
                    if ($msg === null) {
×
246
                        $preparedMedia = $prepared->getMedia();
×
247
                        $preparedPath  = is_array($preparedMedia)
×
248
                            ? ($preparedMedia['stored_path'] ?? null)
×
249
                            : null;
×
250
                        if (is_string($preparedPath) && $preparedPath !== '' && $this->mediaStorage->exists($preparedPath)) {
×
251
                            $this->mediaStorage->delete($preparedPath);
×
252
                            unset($copiedMediaPaths[$preparedPath]);
×
253
                        }
254

255
                        continue;
×
256
                    }
257

258
                    $media = $prepared->getMedia();
×
259
                    if (isset($media)) {
×
260
                        $media = MediaAttachment::create(array_merge($media, [
×
261
                            'conversation_id' => $conversation->id,
×
262
                        ]));
×
263
                        $msg->update(['media_attachment_id' => $media->id]);
×
264
                    }
265

266
                    $importedCount++;
×
267
                }
268
            });
×
269
        } catch (QueryException $e) {
×
270
            foreach (array_keys($copiedMediaPaths) as $path) {
×
271
                if ($this->mediaStorage->exists($path)) {
×
272
                    $this->mediaStorage->delete($path);
×
273
                }
274
            }
275

276
            throw $e;
×
277
        }
278

279
        if ($importedCount > 0) {
×
280
            Log::info('Messages imported', [
×
281
                'conversation_id' => $conversation->id,
×
282
                'count'           => $importedCount,
×
283
            ]);
×
284
        } else {
285
            Log::info('No new messages to import', [
×
286
                'conversation_id' => $conversation->id,
×
287
            ]);
×
288
        }
289
    }
290
}
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