PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.3.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.3.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 1.7.2 All 33 releases
fluent-booking / app / Services / Integrations / FluentCRM / CrmContactService.php

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

285 lines 8.6 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 private static function getContact($email)
233 {
234 if (!self::isActive() || !$email) {
235 return null;
236 }
237
238 return FluentCrmApi('contacts')->getContact($email) ?: null;
239 }
240
241 private static function taxonomyIds($contact, $type)
242 {
243 $relation = $type === 'tags' ? $contact->tags : $contact->lists;
244
245 return array_map('intval', $relation->pluck('id')->toArray());
246 }
247
248 private static function taxonomyOptions($type, $search = '', $limit = 50)
249 {
250 $model = self::taxonomyModel($type);
251 $query = $model::orderBy('title', 'ASC');
252
253 $search = trim((string) $search);
254 if ($search !== '') {
255 $query->where('title', 'LIKE', '%' . addcslashes($search, '%_\\') . '%');
256 }
257
258 return $query->limit($limit)->get()->map(function ($item) {
259 return [
260 'id' => (int) $item->id,
261 'title' => $item->title,
262 ];
263 })->toArray();
264 }
265
266 private static function selectedOptions($contact, $type)
267 {
268 $relation = $type === 'tags' ? $contact->tags : $contact->lists;
269
270 return $relation->map(function ($item) {
271 return [
272 'id' => (int) $item->id,
273 'title' => $item->title,
274 ];
275 })->values()->toArray();
276 }
277
278 private static function taxonomyModel($type)
279 {
280 return $type === 'tags'
281 ? \FluentCrm\App\Models\Tag::class
282 : \FluentCrm\App\Models\Lists::class;
283 }
284 }
285