Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ContactIngestionService
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 6
380
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 handle
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 insert
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 update
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 findExisting
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
56
 validate
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace App\Modules\ContactModule\Services;
4
5use App\Modules\ContactModule\Models\ContactModel;
6
7class ContactIngestionService
8{
9    protected ContactModel $model;
10    protected ContactNormalizerService $normalizer;
11
12    public function __construct()
13    {
14        $this->model = new ContactModel();
15        $this->normalizer = new ContactNormalizerService();
16    }
17
18    /**
19     * Entry point
20     * - normalize payload
21     * - deduplicate
22     * - upsert
23     */
24    public function handle(array $payload): array
25    {
26        $data = $this->normalizer->normalize($payload);
27
28        // 1. validation minimale métier
29        $this->validate($data);
30
31        // 2. recherche doublon
32        $existing = $this->findExisting($data);
33
34        if ($existing) {
35            $contactId = $this->update($existing, $data);
36
37            return [
38                'action' => 'updated',
39                'contact_id' => $contactId
40            ];
41        }
42
43        $contactId = $this->insert($data);
44
45        return [
46            'action' => 'created',
47            'contact_id' => $contactId
48        ];
49    }
50
51    /**
52     * Insert new contact
53     */
54    protected function insert(array $data): int
55    {
56        return (int) $this->model->insert($data, true);
57    }
58
59    /**
60     * Update existing contact
61     */
62    protected function update(array $existing, array $data): int
63    {
64        $id = (int) $existing['id'];
65
66        // merge metadata instead of overwrite
67        if (!empty($existing['metadata']) && is_array($existing['metadata'])) {
68            $data['metadata'] = array_merge($existing['metadata'], $data['metadata'] ?? []);
69        }
70
71        $this->model->update($id, $data);
72
73        return $id;
74    }
75
76    /**
77     * Deduplication strategy (multi-key)
78     */
79    protected function findExisting(array $data): ?array
80    {
81        $builder = $this->model;
82
83        // priority 1: phone (strong identifier)
84        if (!empty($data['phone'])) {
85            $result = $builder->where('phone', $data['phone'])->first();
86            if ($result) return $result;
87        }
88
89        // priority 2: email
90        if (!empty($data['email'])) {
91            $result = $builder->where('email', $data['email'])->first();
92            if ($result) return $result;
93        }
94
95        // priority 3: whatsapp fallback
96        if (!empty($data['whatsapp'])) {
97            $result = $builder->where('whatsapp', $data['whatsapp'])->first();
98            if ($result) return $result;
99        }
100
101        return null;
102    }
103
104    /**
105     * Business validation layer
106     */
107    protected function validate(array $data): void
108    {
109        if (
110            empty($data['phone']) &&
111            empty($data['email']) &&
112            empty($data['whatsapp'])
113        ) {
114            throw new \InvalidArgumentException(
115                'At least one identifier (phone, email, whatsapp) is required'
116            );
117        }
118
119        if (empty($data['list_id'])) {
120            throw new \InvalidArgumentException(
121                'list_id is required'
122            );
123        }
124    }
125}