| @@ -58,11 +58,12 @@ | ||
| 58 | 58 | ]; |
| 59 | 59 | } |
| 60 | 60 | |
| 61 | 61 | /** |
| 62 | - * maybeCreateCustomer method will update existing customer or create new | |
| 63 | - * This method will get request to create customer, this will check existence, if exist it will update otherwise it will create new. | |
| 64 | - * @param $customerData | |
| 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 | |
| 65 | 66 | * @return false|Customer |
| 66 | 67 | */ |
| 67 | 68 | public static function maybeCreateCustomer($customerData) |
| 68 | 69 | { |
| @@ -88,28 +89,25 @@ | ||
| 88 | 89 | } |
| 89 | 90 | |
| 90 | 91 | $customerData = self::explodeFullName($customerData); |
| 91 | 92 | |
| 92 | - // we have to create customer | |
| 93 | 93 | $customer = self::create($customerData); |
| 94 | 94 | |
| 95 | - /* | |
| 96 | - * Action on customer create | |
| 97 | - * | |
| 98 | - * @since v1.0.0 | |
| 99 | - * @param object $customer | |
| 100 | - */ | |
| 101 | - 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 | + } | |
| 102 | 104 | |
| 103 | 105 | return $customer; |
| 104 | 106 | } |
| 105 | 107 | |
| 106 | - // An existing row may never be claimed for a WordPress account it is not | |
| 107 | - // already linked to. Binding happens when the row is first created, when | |
| 108 | - // the account registers (ProfileInfoService::onWPUserRegister), or when | |
| 109 | - // an agent sets it explicitly from the customer profile. Allowing an | |
| 110 | - // email match to write user_id here is what made a portal takeover | |
| 111 | - // persistent. | |
| 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. | |
| 112 | 110 | $incomingUserId = (int) Arr::get($customerData, 'user_id'); |
| 113 | 111 | |
| 114 | 112 | if ($incomingUserId && $incomingUserId !== (int) $customer->user_id) { |
| 115 | 113 | unset($customerData['user_id']); |
| @@ -114,24 +112,11 @@ | ||
| 114 | 112 | if ($incomingUserId && $incomingUserId !== (int) $customer->user_id) { |
| 115 | 113 | unset($customerData['user_id']); |
| 116 | 114 | } |
| 117 | 115 | |
| 118 | - // Intake may not move the contact address of a record that belongs to a | |
| 119 | - // WordPress account. Every caller here builds its payload from an | |
| 120 | - // address it has not verified -- most of them from the signed-in | |
| 121 | - // account's own email, which WordPress lets that account change with no | |
| 122 | - // confirmation at all. | |
| 123 | - // | |
| 124 | - // Without this, ProfileInfoService::onWPProfileUpdate holding the | |
| 125 | - // address is worth nothing: it refuses the unverified change, and then | |
| 126 | - // the next POST to /customer-portal/tickets resolves the same record | |
| 127 | - // through here and writes the new address anyway. It also produced | |
| 128 | - // duplicate rows on an address belonging to somebody else, without the | |
| 129 | - // collision check the profile_update path applies. | |
| 130 | - // | |
| 131 | - // The address moves through the two paths that prove something instead: | |
| 132 | - // EmailClaimService, where the customer opens a link sent to the address | |
| 133 | - // being claimed, and an agent editing the record directly. | |
| 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. | |
| 134 | 119 | if ((int) $customer->user_id) { |
| 135 | 120 | unset($customerData['email']); |
| 136 | 121 | } |
| 137 | 122 | |
| @@ -145,9 +130,8 @@ | ||
| 145 | 130 | } |
| 146 | 131 | |
| 147 | 132 | |
| 148 | 133 | /** |
| 149 | - * One2Many: Customer has to many Click Tickets | |
| 150 | 134 | * @return Model Collection |
| 151 | 135 | */ |
| 152 | 136 | public function tickets() |
| 153 | 137 | { |
| @@ -160,11 +144,12 @@ | ||
| 160 | 144 | ); |
| 161 | 145 | } |
| 162 | 146 | |
| 163 | 147 | /** |
| 164 | - * getCustomerFromData method will return customer information by user id or email address | |
| 165 | - * @param $customerData | |
| 166 | - * @return false | |
| 148 | + * Look up by remote_uid, then user_id, then email. | |
| 149 | + * | |
| 150 | + * @param array $customerData | |
| 151 | + * @return false|Customer | |
| 167 | 152 | */ |
| 168 | 153 | public static function getCustomerFromData($customerData) |
| 169 | 154 | { |
| 170 | 155 | $remoteUid = Arr::get($customerData, 'remote_uid'); |
| @@ -178,18 +163,12 @@ | ||
| 178 | 163 | return $customer; |
| 179 | 164 | } |
| 180 | 165 | } |
| 181 | 166 | |
| 182 | - // A WordPress user id is the only identity that can be trusted here. An | |
| 183 | - // account email is editable by its own holder with no verification: the | |
| 184 | - // REST users endpoint calls wp_update_user() directly and skips the | |
| 185 | - // confirmation flow that profile.php runs. An email must therefore never | |
| 186 | - // be able to reach a customer row that belongs to somebody else. | |
| 187 | - // Both lookups order by id so the row that wins is always the oldest | |
| 188 | - // match rather than whatever the storage engine returns first. The email | |
| 189 | - // column carries a plain index, not a unique one, so duplicates are | |
| 190 | - // possible and an inbound reply must keep threading onto the original | |
| 191 | - // record. Helper::getCurrentPerson() orders for the same reason. | |
| 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. | |
| 192 | 171 | if ($userId) { |
| 193 | 172 | return self::where('user_id', $userId)->orderBy('id', 'ASC')->first() ?: false; |
| 194 | 173 | } |
| 195 | 174 | |
| @@ -200,9 +179,8 @@ | ||
| 200 | 179 | return false; |
| 201 | 180 | } |
| 202 | 181 | |
| 203 | 182 | /** |
| 204 | - * getTicketCounts method will return the number of tickets by a customer | |
| 205 | 183 | * @return mixed |
| 206 | 184 | */ |
| 207 | 185 | public function getTicketCounts() |
| 208 | 186 | { |
| @@ -209,9 +187,8 @@ | ||
| 209 | 187 | return Ticket::where('customer_id', $this->id)->count(); |
| 210 | 188 | } |
| 211 | 189 | |
| 212 | 190 | /** |
| 213 | - * getResponseCounts will return the number of responses by a customer | |
| 214 | 191 | * @return mixed |
| 215 | 192 | */ |
| 216 | 193 | public function getResponseCounts() |
| 217 | 194 | { |