| @@ -3,11 +3,14 @@ | ||
| 3 | 3 | namespace FluentSupport\App\Models; |
| 4 | 4 | |
| 5 | 5 | use FluentSupport\Framework\Database\Orm\Builder; |
| 6 | 6 | use FluentSupport\Framework\Support\Arr; |
| 7 | +use FluentSupport\App\Models\Traits\CustomerTrait; | |
| 7 | 8 | |
| 8 | 9 | class Customer extends Person |
| 9 | 10 | { |
| 11 | + use CustomerTrait; | |
| 12 | + | |
| 10 | 13 | protected static $type = 'customer'; |
| 11 | 14 | |
| 12 | 15 | protected $searchable = [ |
| 13 | 16 | 'id', |
| @@ -18,8 +21,16 @@ | ||
| 18 | 21 | 'address_line_2', |
| 19 | 22 | 'country' |
| 20 | 23 | ]; |
| 21 | 24 | |
| 25 | + /** | |
| 26 | + * @return array | |
| 27 | + */ | |
| 28 | + public function getSearchableFields() | |
| 29 | + { | |
| 30 | + return $this->searchable; | |
| 31 | + } | |
| 32 | + | |
| 22 | 33 | public static function boot() |
| 23 | 34 | { |
| 24 | 35 | parent::boot(); |
| 25 | 36 | |
| @@ -24,9 +35,9 @@ | ||
| 24 | 35 | parent::boot(); |
| 25 | 36 | |
| 26 | 37 | static::creating(function ($model) { |
| 27 | 38 | $model->person_type = static::$type; |
| 28 | - $model->hash = md5(time().wp_generate_uuid4()); | |
| 39 | + $model->hash = md5(time() . wp_generate_uuid4()); | |
| 29 | 40 | }); |
| 30 | 41 | |
| 31 | 42 | static::addGlobalScope(function (Builder $builder) { |
| 32 | 43 | $builder->where('person_type', 'customer'); |
| @@ -36,58 +47,91 @@ | ||
| 36 | 47 | |
| 37 | 48 | public static function mappables() |
| 38 | 49 | { |
| 39 | 50 | return [ |
| 40 | - 'title' => 'Customer Title', | |
| 41 | - 'address_line_1' => 'Address Line 1', | |
| 42 | - 'address_line_2' => 'Address Line 2', | |
| 43 | - 'city' => 'City', | |
| 44 | - 'state' => 'State', | |
| 45 | - 'zip' => 'Zip Code', | |
| 46 | - 'country' => 'Country', | |
| 51 | + 'title' => __('Customer Title', 'fluent-support'), | |
| 52 | + 'address_line_1' => __('Address Line 1', 'fluent-support'), | |
| 53 | + 'address_line_2' => __('Address Line 2', 'fluent-support'), | |
| 54 | + 'city' => __('City', 'fluent-support'), | |
| 55 | + 'state' => __('State', 'fluent-support'), | |
| 56 | + 'zip' => __('Zip Code', 'fluent-support'), | |
| 57 | + 'country' => __('Country', 'fluent-support'), | |
| 47 | 58 | ]; |
| 48 | 59 | } |
| 49 | 60 | |
| 61 | + /** | |
| 62 | + * Find the customer for this data or create one. An existing record is only | |
| 63 | + * updated when the data carries a user_id or remote_uid. | |
| 64 | + * | |
| 65 | + * @param array $customerData | |
| 66 | + * @return false|Customer | |
| 67 | + */ | |
| 50 | 68 | public static function maybeCreateCustomer($customerData) |
| 51 | 69 | { |
| 52 | 70 | $customer = self::getCustomerFromData($customerData); |
| 53 | - $user = get_user_by('email', $customerData['email']); | |
| 54 | - if($user) { | |
| 55 | - if($user->first_name) { | |
| 71 | + $email = Arr::get($customerData, 'email'); | |
| 72 | + $user = $email ? get_user_by('email', $email) : false; | |
| 73 | + if ($user) { | |
| 74 | + if ($user->first_name) { | |
| 56 | 75 | $customerData['first_name'] = $user->first_name; |
| 57 | 76 | } |
| 58 | - if($user->last_name) { | |
| 77 | + if ($user->last_name) { | |
| 59 | 78 | $customerData['last_name'] = $user->last_name; |
| 60 | 79 | } |
| 61 | - if(empty($customerData['first_name']) && empty($customerData['last_name'])) { | |
| 80 | + if (empty($customerData['first_name']) && empty($customerData['last_name'])) { | |
| 62 | 81 | $customerData['first_name'] = $user->display_name; |
| 63 | 82 | } |
| 64 | 83 | $customerData['user_id'] = $user->ID; |
| 65 | 84 | } |
| 66 | 85 | |
| 67 | - if(!$customer) { | |
| 68 | - if(!empty($customerData['last_ip_address'])) { | |
| 86 | + if (!$customer) { | |
| 87 | + if (!empty($customerData['last_ip_address'])) { | |
| 69 | 88 | $customerData['ip_address'] = $customerData['last_ip_address']; |
| 70 | 89 | } |
| 71 | - // we have to create customer | |
| 90 | + | |
| 91 | + $customerData = self::explodeFullName($customerData); | |
| 92 | + | |
| 72 | 93 | $customer = self::create($customerData); |
| 73 | 94 | |
| 74 | - do_action('fluent_support/customer_created', $customer); | |
| 95 | + if ($customer && $customer->exists) { | |
| 96 | + /* | |
| 97 | + * Action on customer create | |
| 98 | + * | |
| 99 | + * @since v1.0.0 | |
| 100 | + * @param object $customer | |
| 101 | + */ | |
| 102 | + do_action('fluent_support/customer_created', $customer); | |
| 103 | + } | |
| 75 | 104 | |
| 76 | - } else { | |
| 77 | - if(!empty($customerData['user_id']) || !empty($customerData['remote_uid'])) { | |
| 78 | - $customerData = array_filter($customerData); | |
| 79 | - $customer->fill($customerData); | |
| 80 | - $customer->save(); | |
| 81 | - } | |
| 105 | + return $customer; | |
| 82 | 106 | } |
| 83 | 107 | |
| 108 | + // Never link an existing row to a different WP account by email match. | |
| 109 | + // Linking happens on create, on registration, or by an agent. | |
| 110 | + $incomingUserId = (int) Arr::get($customerData, 'user_id'); | |
| 111 | + | |
| 112 | + if ($incomingUserId && $incomingUserId !== (int) $customer->user_id) { | |
| 113 | + unset($customerData['user_id']); | |
| 114 | + } | |
| 115 | + | |
| 116 | + // The incoming email is unverified (WP lets users change theirs without | |
| 117 | + // confirmation), so it must not overwrite a linked customer's address. | |
| 118 | + // That only changes via EmailClaimService or an agent edit. | |
| 119 | + if ((int) $customer->user_id) { | |
| 120 | + unset($customerData['email']); | |
| 121 | + } | |
| 122 | + | |
| 123 | + if (!empty($customerData['user_id']) || !empty($customerData['remote_uid'])) { | |
| 124 | + $customerData = array_filter($customerData); | |
| 125 | + $customer->fill($customerData); | |
| 126 | + $customer->save(); | |
| 127 | + } | |
| 128 | + | |
| 84 | 129 | return $customer; |
| 85 | 130 | } |
| 86 | 131 | |
| 87 | 132 | |
| 88 | 133 | /** |
| 89 | - * One2Many: Customer has to many Click Tickets | |
| 90 | 134 | * @return Model Collection |
| 91 | 135 | */ |
| 92 | 136 | public function tickets() |
| 93 | 137 | { |
| @@ -99,35 +143,54 @@ | ||
| 99 | 143 | $class, $foreign_key, 'id' |
| 100 | 144 | ); |
| 101 | 145 | } |
| 102 | 146 | |
| 147 | + /** | |
| 148 | + * Look up by remote_uid, then user_id, then email. | |
| 149 | + * | |
| 150 | + * @param array $customerData | |
| 151 | + * @return false|Customer | |
| 152 | + */ | |
| 103 | 153 | public static function getCustomerFromData($customerData) |
| 104 | 154 | { |
| 105 | 155 | $remoteUid = Arr::get($customerData, 'remote_uid'); |
| 106 | - $email = $customerData['email']; | |
| 156 | + $email = Arr::get($customerData, 'email'); | |
| 157 | + $userId = (int) Arr::get($customerData, 'user_id'); | |
| 107 | 158 | |
| 108 | - $customer = false; | |
| 109 | - if($remoteUid) { | |
| 159 | + if ($remoteUid) { | |
| 110 | 160 | $customer = self::where('remote_uid', $remoteUid)->first(); |
| 161 | + | |
| 162 | + if ($customer) { | |
| 163 | + return $customer; | |
| 164 | + } | |
| 111 | 165 | } |
| 112 | 166 | |
| 113 | - if(!$customer) { | |
| 114 | - if(!empty($customerData['user_id'])) { | |
| 115 | - $customer = self::where('user_id', $customerData['user_id'])->first(); | |
| 116 | - } | |
| 117 | - if(!$customer) { | |
| 118 | - $customer = self::where('email', $email)->first(); | |
| 119 | - } | |
| 167 | + // When a user id is given, don't fall back to email: a WP account's email | |
| 168 | + // is unverified (the REST users endpoint skips confirmation), so it must | |
| 169 | + // not reach another person's row. Email isn't unique, so take the oldest | |
| 170 | + // match to keep replies threading onto the original record. | |
| 171 | + if ($userId) { | |
| 172 | + return self::where('user_id', $userId)->orderBy('id', 'ASC')->first() ?: false; | |
| 120 | 173 | } |
| 121 | 174 | |
| 122 | - return $customer; | |
| 175 | + if ($email) { | |
| 176 | + return self::where('email', $email)->orderBy('id', 'ASC')->first() ?: false; | |
| 177 | + } | |
| 178 | + | |
| 179 | + return false; | |
| 123 | 180 | } |
| 124 | 181 | |
| 182 | + /** | |
| 183 | + * @return mixed | |
| 184 | + */ | |
| 125 | 185 | public function getTicketCounts() |
| 126 | 186 | { |
| 127 | 187 | return Ticket::where('customer_id', $this->id)->count(); |
| 128 | 188 | } |
| 129 | 189 | |
| 190 | + /** | |
| 191 | + * @return mixed | |
| 192 | + */ | |
| 130 | 193 | public function getResponseCounts() |
| 131 | 194 | { |
| 132 | 195 | return Conversation::where('person_id', $this->id)->count(); |
| 133 | 196 | } |