PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.4.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.4.1
2.4.0 2.4.1 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.2.0 2.1.21 2.1.20 2.1.19 2.1.18 2.1.0 2.1.1 2.1.10 2.1.11 2.1.12 2.1.13 2.1.14 2.1.15 2.1.16 2.1.2 All 57 releases
easy-invoice / includes / Repositories / ClientRepository.php

ClientRepository.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.4.1, at includes/Repositories/ClientRepository.php

500 lines 18.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Client Repository Class
4 *
5 * @package Easy_Invoice
6 * @subpackage Repositories
7 */
8
9 namespace EasyInvoice\Repositories;
10
11 use EasyInvoice\Constants\ClientFields;
12 use EasyInvoice\Interfaces\ClientRepositoryInterface;
13 use EasyInvoice\Models\Client;
14 use WP_User;
15 use WP_User_Query;
16 use WP_Query;
17
18 /**
19 * ClientRepository Class
20 *
21 * Handles data access for client objects using WordPress users.
22 */
23 class ClientRepository implements ClientRepositoryInterface {
24 /** @var string Why the last create() returned null, in words for the form. */
25 protected $last_error = '';
26
27 public function getLastError(): string {
28 return (string) $this->last_error;
29 }
30
31
32 /**
33 * Find a client by ID
34 *
35 * @param int $id The client ID
36 * @return Client|null The client model or null if not found
37 */
38 public function find($id) {
39 // Regular client (WordPress user)
40 $user = get_user_by('id', $id);
41
42 if (!$user) {
43 return null;
44 }
45
46 return $this->createClientFromUser($user);
47 }
48
49 /**
50 * Get all clients
51 *
52 * @param array $args Optional arguments to filter the results
53 * @return array Array of Client models
54 */
55 public function all($args = []) {
56 $clients = [];
57
58 // Get all users to include administrators and other users
59 $user_query = new WP_User_Query([
60 'number' => -1,
61 'orderby' => 'ID',
62 'order' => 'DESC',
63 'role__not_in' => ['Administrator'], // exclude admins
64 ]);
65
66 foreach ($user_query->get_results() as $user) {
67 $client = $this->createClientFromUser($user);
68 if ($client) {
69 $clients[] = $client;
70 }
71 }
72
73 return $clients;
74 }
75
76 /**
77 * Create a new client
78 *
79 * @param array $data The client data
80 * @return Client The created client model
81 */
82 public function create($data) {
83 // Create regular client as WordPress user
84 return $this->createRegularClient($data);
85 }
86
87 /**
88 * Create a regular client (WordPress user)
89 *
90 * @param array $data The client data
91 * @return Client|null The created client model
92 */
93 protected function createRegularClient($data) {
94 // Create a new user if email is provided
95 if (empty($data[ClientFields::EMAIL])) {
96 return null;
97 }
98
99 $user_data = [
100 'user_email' => $data[ClientFields::EMAIL],
101 'user_login' => $data[ClientFields::USERNAME],
102 'user_pass' => !empty($data[ClientFields::PASSWORD]) ? $data[ClientFields::PASSWORD] : wp_generate_password(),
103 'display_name' => $data[ClientFields::FIRST_NAME] . ' ' . $data[ClientFields::LAST_NAME],
104 'first_name' => $data[ClientFields::FIRST_NAME],
105 'last_name' => $data[ClientFields::LAST_NAME],
106 'role' => 'customer',
107 ];
108
109 $user_id = wp_insert_user($user_data);
110
111 if (is_wp_error($user_id)) {
112 // Keep the reason for the caller: "that email is already registered"
113 // is something the person at the form can act on, "failed" is not.
114 $this->last_error = $user_id->get_error_message();
115 return null;
116 }
117 $this->last_error = '';
118
119 // Deny backend access on new clients via a per-user capability
120 // override. The user keeps the `customer` role (so Pro Client
121 // Portal's `in_array('customer', $user->roles)` check still
122 // recognises them) but `read` is explicitly set to false at the
123 // user level, which beats the role's `read => true` when
124 // WP_User::has_cap() resolves the merged capability map. Result:
125 // `current_user_can('read')` is false, /wp-admin/ is blocked,
126 // login still works (auth itself is cap-free).
127 //
128 // Per-user override only — existing customer-role users elsewhere
129 // on the site (including pre-existing EI clients and WooCommerce
130 // customers) are NOT affected; only the user we just created.
131 $wp_user = new \WP_User($user_id);
132 $wp_user->add_cap('read', false);
133
134 // Create client model
135 $client = new Client($user_id);
136
137 // Set the client data
138 $this->setClientData($client, $data);
139
140 /**
141 * Fires once a client (and their WordPress user) has been created.
142 *
143 * @param Client $client The new client; its id is the user id.
144 * @param array $data The submitted client data.
145 */
146 do_action('easy_invoice_client_created', $client, $data);
147
148 return $client;
149 }
150
151 /**
152 * Update an existing client
153 *
154 * @param int $id The client ID
155 * @param array $data The client data
156 * @return Client|null The updated client model or null if not found
157 */
158 public function update($id, $data) {
159 $client = $this->find($id);
160
161 if (!$client) {
162 return null;
163 }
164
165 // Regular client - update user data if email is provided
166 $user_data = ['ID' => $id];
167 if (!empty($data[ClientFields::EMAIL])) {
168 $user_data['user_email'] = $data[ClientFields::EMAIL];
169 }
170 if (!empty($data[ClientFields::USERNAME])) {
171 $user_data['user_login'] = $data[ClientFields::USERNAME];
172 }
173 if (!empty($data[ClientFields::PASSWORD])) {
174 $user_data['user_pass'] = $data[ClientFields::PASSWORD];
175 }
176 if (!empty($data[ClientFields::FIRST_NAME]) && !empty($data[ClientFields::LAST_NAME])) {
177 $user_data['display_name'] = $data[ClientFields::FIRST_NAME] . ' ' . $data[ClientFields::LAST_NAME];
178 }
179 if (!empty($data[ClientFields::FIRST_NAME])) {
180 $user_data['first_name'] = $data[ClientFields::FIRST_NAME];
181 }
182 if (!empty($data[ClientFields::LAST_NAME])) {
183 $user_data['last_name'] = $data[ClientFields::LAST_NAME];
184 }
185
186 // Only update user data if we have more than just the ID
187 if(count($user_data) > 1) {
188 $result = wp_update_user($user_data);
189 if (is_wp_error($result)) {
190 // Log the error but continue with client data update
191 }
192 }
193
194 // Set the client data
195 $this->setClientData($client, $data);
196
197 return $client;
198 }
199
200 /**
201 * Delete a client
202 *
203 * @param int $id The client ID
204 * @return bool True if successful, false otherwise
205 */
206 public function delete($id) {
207 // Check if client exists
208 $client = $this->find($id);
209
210 if (!$client) {
211 return false;
212 }
213
214 // Check if user exists and is not an administrator
215 $user = get_user_by('ID', $id);
216 if (!$user || in_array('administrator', $user->roles)) {
217 return false;
218 }
219
220 // Delete all invoices and quotes associated with this client
221 global $wpdb;
222
223 // Get all invoices and quotes for this client
224 $posts = $wpdb->get_results($wpdb->prepare(
225 "SELECT ID, post_type FROM {$wpdb->posts} WHERE post_type IN ('easy_invoice', 'easy_invoice_quote') AND ID IN (
226 SELECT post_id FROM {$wpdb->postmeta}
227 WHERE (meta_key = '_easy_invoice_client_id' OR meta_key = '_easy_invoice_quote_client_id')
228 AND meta_value = %d
229 )",
230 $id
231 ));
232
233 // Delete each post and its meta
234 foreach ($posts as $post) {
235 wp_delete_post($post->ID, true);
236 }
237
238 // Payments are deliberately NOT deleted here.
239 //
240 // There used to be a query for post type 'easy_payment' on meta
241 // '_easy_payment_client_id'. Neither exists — payments are
242 // 'easy_invoice_payment' and record '_invoice_id', with no client id at
243 // all — so it matched nothing on every run. Deleting a client has never
244 // removed a payment record.
245 //
246 // The query is gone rather than corrected. Resolving payments properly
247 // (through ClientLedger, which knows the real relationship) would make
248 // this destroy records it has never touched, on a path a user reaches by
249 // clicking Delete on a client. Widening a destructive operation as a
250 // side effect of fixing a broken query is not a safe trade: a payment is
251 // the evidence money changed hands, and the same reasoning that stops
252 // InvoiceRetention deleting an issued invoice applies to it.
253 //
254 // What changed is that the confirmation dialog now counts payments
255 // correctly (see EasyInvoiceAjax::handleDeleteClient), so a merchant is
256 // told what will be left behind instead of being shown zero.
257
258 // Delete the WordPress user (this will also delete all user meta)
259 $result = wp_delete_user($id);
260
261 // Return the result
262 return $result;
263
264 return $result;
265 }
266
267 /**
268 * Find clients by email
269 *
270 * @param string $email The client email
271 * @return array Array of Client models
272 */
273 public function findByEmail($email) {
274 $clients = [];
275
276 // Find regular clients by email
277 $user = get_user_by('email', $email);
278
279 if ($user) {
280 $clients[] = $this->createClientFromUser($user);
281 }
282
283 return $clients;
284 }
285
286 /**
287 * Find clients by business/client name
288 *
289 * @param string $business_client_name The business/client name
290 * @return array Array of Client models
291 */
292 public function findByBusinessClientName($business_client_name) {
293 $args = [
294 'meta_query' => [
295 [
296 'key' => ClientFields::BUSINESS_CLIENT_NAME,
297 'value' => $business_client_name,
298 'compare' => 'LIKE',
299 ],
300 ],
301 ];
302
303 return $this->all($args);
304 }
305
306 /**
307 * Search clients by name, email, company, or phone.
308 *
309 * Matches against:
310 * - User table columns: display_name, user_login, user_email,
311 * user_nicename, user_url (these are what WP_User_Query's
312 * `search` + `search_columns` can actually index).
313 * - WP standard meta: first_name, last_name, nickname.
314 * - WooCommerce billing meta: billing_first_name, billing_last_name,
315 * billing_email, billing_company, billing_phone — so customers
316 * imported via WooCommerce can be found by their billing details.
317 * - Easy Invoice's own meta: first/last/email/business name.
318 *
319 * The previous implementation gated every query on a meta_query
320 * requiring `_easy_invoice_client_business_client_name` OR
321 * `_easy_invoice_client_email` to EXIST, which silently excluded
322 * every WooCommerce customer (they don't have those EI-specific
323 * keys until they've been edited inside Easy Invoice). On stores
324 * with hundreds of imported WC customers, search returned nothing
325 * even though the unfiltered list showed every client.
326 *
327 * @param string $query The search query
328 * @return array Array of Client models
329 */
330 public function search($query) {
331 $query = trim((string) $query);
332
333 if ($query === '') {
334 // The picker opens with an empty query to show "some" clients. Build
335 // only the 50 it can show instead of a model for every user on the site.
336 $recent = new WP_User_Query([
337 'number' => 50,
338 'orderby' => 'display_name',
339 'order' => 'ASC',
340 'role__not_in' => ['Administrator'],
341 ]);
342 $clients = [];
343 foreach ($recent->get_results() as $user) {
344 $client = $this->createClientFromUser($user);
345 if ($client) {
346 $clients[] = $client;
347 }
348 }
349 return $clients;
350 }
351
352 $clients = [];
353 $seen_ids = [];
354 $like = '*' . $query . '*';
355
356 // ── 1) User table columns ──────────────────────────────────────
357 // These are the only columns WP_User_Query's `search_columns`
358 // accepts; passing `first_name` etc. is silently ignored, so we
359 // pick those up via meta in step 2 below.
360 $user_query = new WP_User_Query([
361 'number' => 50,
362 'orderby' => 'display_name',
363 'order' => 'ASC',
364 'search' => $like,
365 'search_columns' => ['user_login', 'user_email', 'user_nicename', 'display_name', 'user_url'],
366 'role__not_in' => ['Administrator'],
367 ]);
368 foreach ($user_query->get_results() as $user) {
369 if (isset($seen_ids[$user->ID])) {
370 continue;
371 }
372 $seen_ids[$user->ID] = true;
373 $client = $this->createClientFromUser($user);
374 if ($client) {
375 $clients[] = $client;
376 }
377 }
378
379 // ── 2) User-meta columns ───────────────────────────────────────
380 // Search across every meta key we know a client's name / email /
381 // company / phone could be stored under. WP-standard, WooCommerce
382 // billing_*, and Easy Invoice's own keys are all OR'd together so
383 // the user only has to match ONE for a row to qualify.
384 $meta_keys = [
385 // WP standard
386 'first_name', 'last_name', 'nickname',
387 // WooCommerce billing — covers imported store customers
388 'billing_first_name', 'billing_last_name', 'billing_email',
389 'billing_company', 'billing_phone',
390 // Easy Invoice's own — note PHONE here too so EI-native clients
391 // (whose phone lives in _easy_invoice_client_phone, not billing_*)
392 // are searchable by phone number on equal footing with WC customers.
393 ClientFields::FIRST_NAME, ClientFields::LAST_NAME,
394 ClientFields::EMAIL, ClientFields::BUSINESS_CLIENT_NAME,
395 ClientFields::PHONE,
396 ];
397
398 $meta_query = ['relation' => 'OR'];
399 foreach ($meta_keys as $key) {
400 $meta_query[] = [
401 'key' => $key,
402 'value' => $query,
403 'compare' => 'LIKE',
404 ];
405 }
406
407 $meta_user_query = new WP_User_Query([
408 'number' => 50,
409 'orderby' => 'display_name',
410 'order' => 'ASC',
411 'role__not_in' => ['Administrator'],
412 'meta_query' => $meta_query,
413 ]);
414 foreach ($meta_user_query->get_results() as $user) {
415 if (isset($seen_ids[$user->ID])) {
416 continue;
417 }
418 $seen_ids[$user->ID] = true;
419 $client = $this->createClientFromUser($user);
420 if ($client) {
421 $clients[] = $client;
422 }
423 }
424
425 // Cap the dropdown at 50 results so the autocomplete stays
426 // responsive on stores with thousands of customers. The user
427 // can refine the query to narrow further.
428 return array_slice($clients, 0, 50);
429 }
430
431 /**
432 * Create a client model from a WordPress user
433 *
434 * @param WP_User $user The WordPress user
435 * @return Client The client model
436 */
437 protected function createClientFromUser(WP_User $user) {
438 try {
439 $client = new Client($user->ID);
440 return $client;
441 } catch (\Exception $e) {
442 return null;
443 }
444 }
445
446 /**
447 * Set the client data
448 *
449 * @param Client $client The client model
450 * @param array $data The client data
451 */
452 protected function setClientData(Client $client, array $data) {
453 if (isset($data[ClientFields::BUSINESS_CLIENT_NAME])) {
454 $client->business_client_name = $data[ClientFields::BUSINESS_CLIENT_NAME];
455 update_user_meta($client->getId(), ClientFields::BUSINESS_CLIENT_NAME, $data[ClientFields::BUSINESS_CLIENT_NAME]);
456 }
457
458 if (isset($data[ClientFields::EMAIL])) {
459 $client->email = $data[ClientFields::EMAIL];
460 update_user_meta($client->getId(), ClientFields::EMAIL, $data[ClientFields::EMAIL]);
461 }
462
463 if (isset($data[ClientFields::USERNAME])) {
464 $client->username = $data[ClientFields::USERNAME];
465 update_user_meta($client->getId(), ClientFields::USERNAME, $data[ClientFields::USERNAME]);
466 }
467
468 if (isset($data[ClientFields::ADDRESS])) {
469 $client->address = $data[ClientFields::ADDRESS];
470 update_user_meta($client->getId(), ClientFields::ADDRESS, $data[ClientFields::ADDRESS]);
471 }
472
473 if (isset($data[ClientFields::EXTRA_INFO])) {
474 $client->extra_info = $data[ClientFields::EXTRA_INFO];
475 update_user_meta($client->getId(), ClientFields::EXTRA_INFO, $data[ClientFields::EXTRA_INFO]);
476 }
477
478 if (isset($data[ClientFields::FIRST_NAME])) {
479 $client->first_name = $data[ClientFields::FIRST_NAME];
480 update_user_meta($client->getId(), ClientFields::FIRST_NAME, $data[ClientFields::FIRST_NAME]);
481 }
482
483 if (isset($data[ClientFields::LAST_NAME])) {
484 $client->last_name = $data[ClientFields::LAST_NAME];
485 update_user_meta($client->getId(), ClientFields::LAST_NAME, $data[ClientFields::LAST_NAME]);
486 }
487
488 if (isset($data[ClientFields::WEBSITE])) {
489 $client->website = $data[ClientFields::WEBSITE];
490 update_user_meta($client->getId(), ClientFields::WEBSITE, $data[ClientFields::WEBSITE]);
491 }
492 if (isset($data[ClientFields::PHONE])) {
493 $client->phone = $data[ClientFields::PHONE];
494 update_user_meta($client->getId(), ClientFields::PHONE, $data[ClientFields::PHONE]);
495 }
496
497 // Reset the dirty flag after saving
498 $client->resetDirty();
499 }
500 }