PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.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.5.0, at app/Services/Integrations/FluentCRM/CrmContactService.php

357 lines 11.2 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 /**
195 * Create (or reuse) a tag/list definition in FluentCRM. Idempotent on
196 * slug, so typing an existing name returns that existing tag/list.
197 *
198 * @param string $type 'tags' | 'lists'
199 * @return array|null {id, title} or null when inactive / empty title.
200 */
201 public static function createTaxonomy($type, $title)
202 {
203 if (!self::isActive()) {
204 return null;
205 }
206
207 $title = trim((string) $title);
208 if ($title === '') {
209 return null;
210 }
211
212 $model = self::taxonomyModel($type);
213 $slug = sanitize_title($title);
214
215 if ($slug === '') {
216 return null;
217 }
218
219 $item = $model::firstOrCreate(['slug' => $slug], ['title' => $title]);
220
221 if ($item->wasRecentlyCreated) {
222 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.DynamicHooknameFound -- FluentCRM's own hook names, fired so CRM listeners see parity with native tag/list creation
223 do_action($type === 'tags' ? 'fluent_crm/tag_created' : 'fluent_crm/list_created', $item);
224 }
225
226 return [
227 'id' => (int) $item->id,
228 'title' => $item->title,
229 ];
230 }
231
232 /**
233 * The contact a note can be copied to, or null when FluentCRM is inactive,
234 * the email has no contact, or the current user cannot manage contacts.
235 *
236 * @param string $email
237 * @return array|null {contact_id, name, profile_url}
238 */
239 public static function getNoteTarget($email)
240 {
241 if (!self::canWriteNotes()) {
242 return null;
243 }
244
245 $contact = self::getContact($email);
246
247 if (!$contact) {
248 return null;
249 }
250
251 return [
252 'contact_id' => (int) $contact->id,
253 'name' => trim((string) $contact->full_name) ?: (string) $contact->email,
254 'profile_url' => fluentcrm_menu_url_base() . 'subscribers/' . $contact->id,
255 ];
256 }
257
258 /**
259 * Add a note to the contact's FluentCRM profile, as FluentCRM's own
260 * "Add note" does: same permission, same sanitizer, same hook.
261 *
262 * @param string $email
263 * @param string $title Plain text.
264 * @param string $description HTML. Passed through wp_kses_post.
265 * @param string $type A FluentCRM activity type (fluentcrm_activity_types()).
266 * @return int|\WP_Error The new note id.
267 */
268 public static function addContactNote($email, $title, $description, $type = 'note')
269 {
270 if (!self::canWriteNotes()) {
271 return new \WP_Error('crm_permission', __('You do not have permission to add notes to CRM contacts.', 'fluent-booking'));
272 }
273
274 $contact = self::getContact($email);
275
276 if (!$contact) {
277 return new \WP_Error('crm_contact_not_found', __('No CRM contact found for this booking.', 'fluent-booking'));
278 }
279
280 $data = \FluentCrm\App\Services\Sanitize::contactNote([
281 'subscriber_id' => (int) $contact->id,
282 'title' => (string) $title,
283 'description' => (string) $description,
284 'type' => $type,
285 'created_at' => current_time('mysql'),
286 ]);
287
288 $data['created_by'] = get_current_user_id();
289
290 $note = \FluentCrm\App\Models\SubscriberNote::create($data);
291
292 // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- FluentCRM's own hook, fired so CRM automations see a note added from here like one added in the CRM
293 do_action('fluent_crm/note_added', $note, $contact, $data);
294
295 return (int) $note->id;
296 }
297
298 private static function canWriteNotes()
299 {
300 return self::isActive()
301 && \FluentCrm\App\Services\PermissionManager::currentUserCan('fcrm_manage_contacts');
302 }
303
304 private static function getContact($email)
305 {
306 if (!self::isActive() || !$email) {
307 return null;
308 }
309
310 return FluentCrmApi('contacts')->getContact($email) ?: null;
311 }
312
313 private static function taxonomyIds($contact, $type)
314 {
315 $relation = $type === 'tags' ? $contact->tags : $contact->lists;
316
317 return array_map('intval', $relation->pluck('id')->toArray());
318 }
319
320 private static function taxonomyOptions($type, $search = '', $limit = 50)
321 {
322 $model = self::taxonomyModel($type);
323 $query = $model::orderBy('title', 'ASC');
324
325 $search = trim((string) $search);
326 if ($search !== '') {
327 $query->where('title', 'LIKE', '%' . addcslashes($search, '%_\\') . '%');
328 }
329
330 return $query->limit($limit)->get()->map(function ($item) {
331 return [
332 'id' => (int) $item->id,
333 'title' => $item->title,
334 ];
335 })->toArray();
336 }
337
338 private static function selectedOptions($contact, $type)
339 {
340 $relation = $type === 'tags' ? $contact->tags : $contact->lists;
341
342 return $relation->map(function ($item) {
343 return [
344 'id' => (int) $item->id,
345 'title' => $item->title,
346 ];
347 })->values()->toArray();
348 }
349
350 private static function taxonomyModel($type)
351 {
352 return $type === 'tags'
353 ? \FluentCrm\App\Models\Tag::class
354 : \FluentCrm\App\Models\Lists::class;
355 }
356 }
357