PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.2.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.2.0
2.5.0 2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 All 34 releases
fluent-booking / app / Services / Integrations / FluentCRM / CrmContactService.php

CrmContactService.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.2.0, at app/Services/Integrations/FluentCRM/CrmContactService.php

247 lines 7.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentBooking\App\Services\Integrations\FluentCRM;
4
5 /**
6 * Read/manage a FluentCRM contact's profile, tags and lists for the
7 * booking details CRM Profile card. All methods are no-ops (return null)
8 * when FluentCRM is inactive or the booking email has no CRM contact.
9 */
10 class CrmContactService
11 {
12 public static function isActive()
13 {
14 return defined('FLUENTCRM');
15 }
16
17 /**
18 * Profile payload for the CRM Profile sidebar card.
19 */
20 public static function getProfileData($email)
21 {
22 $contact = self::getContact($email);
23
24 if (!$contact) {
25 return null;
26 }
27
28 return [
29 'contact_id' => (int) $contact->id,
30 'photo' => $contact->photo,
31 'full_name' => $contact->full_name,
32 'status' => $contact->status,
33 'profile_url' => fluentcrm_menu_url_base() . 'subscribers/' . $contact->id,
34 'stats' => $contact->stats(),
35 'tag_ids' => self::taxonomyIds($contact, 'tags'),
36 'list_ids' => self::taxonomyIds($contact, 'lists'),
37 'tags' => self::selectedOptions($contact, 'tags'),
38 'lists' => self::selectedOptions($contact, 'lists'),
39 ];
40 }
41
42 /**
43 * Current tag/list selection (ids + titles) for the contact.
44 */
45 public static function getContactState($email)
46 {
47 $contact = self::getContact($email);
48
49 if (!$contact) {
50 return null;
51 }
52
53 return [
54 'exists' => true,
55 'tag_ids' => self::taxonomyIds($contact, 'tags'),
56 'list_ids' => self::taxonomyIds($contact, 'lists'),
57 'selected_tags' => self::selectedOptions($contact, 'tags'),
58 'selected_lists' => self::selectedOptions($contact, 'lists'),
59 ];
60 }
61
62 /**
63 * Guest-field prefill for the CRM "Book Appointment" action.
64 * Null when FluentCRM is inactive or the contact is unknown.
65 * Extend the payload via the filter.
66 */
67 public static function getBookingPrefillData($contactId)
68 {
69 if (!self::isActive()) {
70 return null;
71 }
72
73 $contact = FluentCrmApi('contacts')->getContact((int) $contactId);
74
75 if (!$contact) {
76 return null;
77 }
78
79 $data = [
80 'name' => trim((string) $contact->full_name),
81 'email' => (string) $contact->email,
82 'phone' => (string) $contact->phone,
83 'timezone' => (string) $contact->timezone,
84 'address' => implode(', ', array_filter([
85 (string) $contact->address_line_1,
86 (string) $contact->address_line_2,
87 (string) $contact->city,
88 (string) $contact->state,
89 (string) $contact->postal_code,
90 (string) $contact->country,
91 ])),
92 ];
93
94 return apply_filters('fluent_booking/crm_booking_prefill_data', $data, $contact);
95 }
96
97 /**
98 * Bounded, optionally-searched option set for the tag/list picker.
99 */
100 public static function getOptions($type, $search = '', $limit = 50)
101 {
102 if (!self::isActive()) {
103 return [];
104 }
105
106 return self::taxonomyOptions($type, $search, $limit);
107 }
108
109 /**
110 * Bounded search of subscribed contacts for the booking typeahead.
111 */
112 public static function searchContacts($search, $limit = 20)
113 {
114 if (!self::isActive()) {
115 return [];
116 }
117
118 $search = trim((string) $search);
119 if (mb_strlen($search) < 3) {
120 return [];
121 }
122
123 $like = '%' . addcslashes($search, '%_\\') . '%';
124
125 return \FluentCrm\App\Models\Subscriber::where('status', 'subscribed')
126 ->where(function ($q) use ($like) {
127 $q->where('email', 'LIKE', $like)
128 ->orWhere('first_name', 'LIKE', $like)
129 ->orWhere('last_name', 'LIKE', $like);
130 })
131 ->orderBy('email', 'ASC')
132 ->limit($limit)
133 ->get()
134 ->map(function ($contact) {
135 return [
136 'id' => (int) $contact->id,
137 'name' => trim((string) $contact->full_name),
138 'email' => (string) $contact->email,
139 ];
140 })
141 ->toArray();
142 }
143
144 /**
145 * Attach/detach the contact's tags or lists to match the desired set.
146 * Only IDs the CRM actually knows about are honoured (no bogus pivots).
147 *
148 * @param string $type 'tags' | 'lists'
149 * @return array|null Fresh {tag_ids, list_ids} or null when no contact.
150 */
151 public static function syncTaxonomy($email, $type, array $desiredIds)
152 {
153 $contact = self::getContact($email);
154
155 if (!$contact) {
156 return null;
157 }
158
159 $model = self::taxonomyModel($type);
160 $desiredIds = array_values(array_map('intval', $desiredIds));
161 $valid = $desiredIds
162 ? array_map('intval', $model::whereIn('id', $desiredIds)->pluck('id')->toArray())
163 : [];
164 $desired = array_values(array_intersect($desiredIds, $valid));
165
166 $current = self::taxonomyIds($contact, $type);
167 $toAttach = array_values(array_diff($desired, $current));
168 $toDetach = array_values(array_diff($current, $desired));
169
170 if ($type === 'tags') {
171 if ($toAttach) {
172 $contact->attachTags($toAttach);
173 }
174 if ($toDetach) {
175 $contact->detachTags($toDetach);
176 }
177 } else {
178 if ($toAttach) {
179 $contact->attachLists($toAttach);
180 }
181 if ($toDetach) {
182 $contact->detachLists($toDetach);
183 }
184 }
185
186 $contact->load(['tags', 'lists']);
187
188 return [
189 'tag_ids' => self::taxonomyIds($contact, 'tags'),
190 'list_ids' => self::taxonomyIds($contact, 'lists'),
191 ];
192 }
193
194 private static function getContact($email)
195 {
196 if (!self::isActive() || !$email) {
197 return null;
198 }
199
200 return FluentCrmApi('contacts')->getContact($email) ?: null;
201 }
202
203 private static function taxonomyIds($contact, $type)
204 {
205 $relation = $type === 'tags' ? $contact->tags : $contact->lists;
206
207 return array_map('intval', $relation->pluck('id')->toArray());
208 }
209
210 private static function taxonomyOptions($type, $search = '', $limit = 50)
211 {
212 $model = self::taxonomyModel($type);
213 $query = $model::orderBy('title', 'ASC');
214
215 $search = trim((string) $search);
216 if ($search !== '') {
217 $query->where('title', 'LIKE', '%' . addcslashes($search, '%_\\') . '%');
218 }
219
220 return $query->limit($limit)->get()->map(function ($item) {
221 return [
222 'id' => (int) $item->id,
223 'title' => $item->title,
224 ];
225 })->toArray();
226 }
227
228 private static function selectedOptions($contact, $type)
229 {
230 $relation = $type === 'tags' ? $contact->tags : $contact->lists;
231
232 return $relation->map(function ($item) {
233 return [
234 'id' => (int) $item->id,
235 'title' => $item->title,
236 ];
237 })->values()->toArray();
238 }
239
240 private static function taxonomyModel($type)
241 {
242 return $type === 'tags'
243 ? \FluentCrm\App\Models\Tag::class
244 : \FluentCrm\App\Models\Lists::class;
245 }
246 }
247