PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.3
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.3
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 / QuoteRepository.php

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

619 lines 18.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Quote Repository
4 *
5 * @package EasyInvoice
6 * @author Your Name
7 * @copyright Copyright (c) 2023, Your Company
8 * @license http://opensource.org/licenses/gpl-2.0.php GNU Public License
9 * @since 1.0.0
10 */
11
12 namespace EasyInvoice\Repositories;
13
14 use EasyInvoice\Models\Quote;
15 use EasyInvoice\Constants\PostTypes;
16 use EasyInvoice\Interfaces\QuoteRepositoryInterface;
17
18 /**
19 * Quote Repository
20 *
21 * Handles database operations for quotes with extensible architecture for pro features.
22 *
23 * @since 1.0.0
24 */
25 class QuoteRepository implements QuoteRepositoryInterface {
26
27 /**
28 * Find quote by ID
29 *
30 * @since 1.0.0
31 * @param int $id Quote ID
32 * @return Quote|null
33 */
34 public function find(int $id): ?Quote {
35 $post = get_post($id);
36 if ($post && $post->post_type === PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
37 $quote = new Quote($post);
38
39 // Ensure quote has proper slug for pretty URLs
40 $quote->ensureProperSlug();
41
42 // Allow plugins to modify the found quote
43 return apply_filters('easy_invoice_quote_found', $quote, $id);
44 }
45 return null;
46 }
47
48 /**
49 * Find all quotes
50 *
51 * @since 1.0.0
52 * @param array $args Query arguments
53 * @return array
54 */
55 public function findAll(array $args = []): array {
56 $default_args = [
57 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
58 'post_status' => ['publish', 'draft', 'private', 'pending'],
59 'posts_per_page' => -1,
60 'orderby' => 'date',
61 'order' => 'DESC'
62 ];
63
64 $query_args = wp_parse_args($args, $default_args);
65
66 // Allow plugins to modify query arguments
67 $query_args = apply_filters('easy_invoice_quote_query_args', $query_args);
68
69 $posts = get_posts($query_args);
70
71 $quotes = [];
72 foreach ($posts as $post) {
73 $quote = new Quote($post);
74
75 // Ensure quote has proper slug for pretty URLs
76 $quote->ensureProperSlug();
77
78 $quotes[] = $quote;
79 }
80
81 // Allow plugins to modify the results
82 return apply_filters('easy_invoice_quotes_found', $quotes, $query_args);
83 }
84
85 /**
86 * Find all published quotes (for public access)
87 *
88 * @since 1.0.0
89 * @param array $args Query arguments
90 * @return array
91 */
92 public function findAllPublished(array $args = []): array {
93 $default_args = [
94 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
95 'post_status' => 'publish',
96 'posts_per_page' => -1,
97 'orderby' => 'date',
98 'order' => 'DESC'
99 ];
100
101 $query_args = wp_parse_args($args, $default_args);
102
103 // Allow plugins to modify query arguments
104 $query_args = apply_filters('easy_invoice_quote_published_query_args', $query_args);
105
106 $posts = get_posts($query_args);
107
108 $quotes = [];
109 foreach ($posts as $post) {
110 $quote = new Quote($post);
111
112 // Ensure quote has proper slug for pretty URLs
113 $quote->ensureProperSlug();
114
115 $quotes[] = $quote;
116 }
117
118 // Allow plugins to modify the results
119 return apply_filters('easy_invoice_quotes_published_found', $quotes, $query_args);
120 }
121
122 /**
123 * Find quotes by status
124 *
125 * @since 1.0.0
126 * @param string $status Quote status
127 * @return array
128 */
129 public function findByStatus(string $status): array {
130 $quotes = $this->findBy(['status' => $status]);
131
132 // Allow plugins to modify the filtered results
133 return apply_filters('easy_invoice_quotes_by_status', $quotes, $status);
134 }
135
136 /**
137 * Find all quote IDs by status
138 *
139 * @since 1.0.0
140 * @param string $status Quote status
141 * @return array
142 */
143 public function findAllIdsByStatus(string $status): array {
144 global $wpdb;
145
146 $post_ids = $wpdb->get_col($wpdb->prepare(
147 "SELECT p.ID
148 FROM {$wpdb->posts} p
149 INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id
150 WHERE p.post_type = %s
151 AND pm.meta_key = '_easy_invoice_quote_status'
152 AND pm.meta_value = %s",
153 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
154 $status
155 ));
156
157 $post_ids = array_map('intval', $post_ids);
158
159 // Allow plugins to modify the results
160 return apply_filters('easy_invoice_quote_ids_by_status', $post_ids, $status);
161 }
162
163 /**
164 * Find quotes by client
165 *
166 * @since 1.0.0
167 * @param int $client_id Client ID
168 * @return array
169 */
170 public function findByClient(int $client_id): array {
171 $quotes = $this->findAll();
172 $filtered_quotes = array_filter($quotes, function($quote) use ($client_id) {
173 return $quote->getClientId() === $client_id;
174 });
175
176 // Allow plugins to modify the filtered results
177 return apply_filters('easy_invoice_quotes_by_client', $filtered_quotes, $client_id);
178 }
179
180 /**
181 * Create new quote
182 *
183 * @since 1.0.0
184 * @param array $data Quote data
185 * @return Quote|null
186 */
187 public function create(array $data): ?Quote {
188 // Allow plugins to modify the data before creation
189 $data = apply_filters('easy_invoice_quote_create_data', $data);
190
191 $quote = new Quote();
192 $this->setQuoteData($quote, $data);
193
194 // Allow plugins to modify the quote before saving
195 do_action('easy_invoice_quote_before_create', $quote, $data);
196
197 if ($quote->save()) {
198 // Allow plugins to perform actions after creation
199 do_action('easy_invoice_quote_created', $quote, $data);
200 return $quote;
201 }
202
203 return null;
204 }
205
206 /**
207 * Update existing quote
208 *
209 * @since 1.0.0
210 * @param int $id Quote ID
211 * @param array $data Quote data
212 * @param Quote|null $existing_quote Optional existing quote object to update
213 * @return Quote|null
214 */
215 public function update(int $id, array $data, ?Quote $existing_quote = null): ?Quote {
216 // Use existing quote object if provided, otherwise find from database
217 $quote = $existing_quote ?: $this->find($id);
218 if (!$quote) {
219 return null;
220 }
221
222 // Prevent quote number from being changed on update
223 unset($data['number'], $data['quote-number']);
224
225 // Allow plugins to modify the data before update
226 $data = apply_filters('easy_invoice_quote_update_data', $data, $quote);
227
228 $this->setQuoteData($quote, $data);
229
230 // Allow plugins to modify the quote before saving
231 do_action('easy_invoice_quote_before_update', $quote, $data);
232
233 if ($quote->save()) {
234 // Allow plugins to perform actions after update
235 do_action('easy_invoice_quote_updated', $quote, $data);
236 return $quote;
237 }
238
239 return null;
240 }
241
242 /**
243 * Delete quote
244 *
245 * @since 1.0.0
246 * @param int $id Quote ID
247 * @return bool
248 */
249 public function delete(int $id): bool {
250 $quote = $this->find($id);
251 if (!$quote) {
252 return false;
253 }
254
255 // Allow plugins to perform actions before deletion
256 do_action('easy_invoice_quote_before_delete', $quote);
257
258 $result = wp_delete_post($id, true);
259
260 if ($result) {
261 // Allow plugins to perform actions after deletion
262 do_action('easy_invoice_quote_deleted', $id);
263 }
264
265 return $result !== false;
266 }
267
268 /**
269 * Find a quote by its number (stored in post meta)
270 *
271 * @param string $number Quote number
272 * @return Quote|null
273 */
274 public function findByNumber(string $number): ?Quote {
275 $args = [
276 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
277 'post_status' => ['publish', 'draft', 'private', 'pending'],
278 'posts_per_page' => 1,
279 'meta_query' => [
280 [
281 'key' => '_easy_invoice_quote_number',
282 'value' => $number,
283 'compare' => '=',
284 ],
285 ],
286 'orderby' => 'date',
287 'order' => 'DESC',
288 ];
289
290 $posts = get_posts($args);
291 if (!empty($posts)) {
292 $post = $posts[0];
293 if ($post && $post->post_type === PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
294 $quote = new Quote($post);
295 $quote->ensureProperSlug();
296 return apply_filters('easy_invoice_quote_found_by_number', $quote, $number);
297 }
298 }
299 return null;
300 }
301
302 /**
303 * Force publish quote (for URL fixes)
304 *
305 * @since 1.0.0
306 * @param int $id Quote ID
307 * @return bool
308 */
309 public function forcePublish($id) {
310 $quote = $this->find($id);
311 if (!$quote) {
312 return false;
313 }
314
315 // Save the quote (this will publish it and ensure proper permalinks)
316 return $quote->save();
317 }
318
319 /**
320 * Find published quote by ID
321 *
322 * @since 1.0.0
323 * @param int $id Quote ID
324 * @return Quote|null
325 */
326 public function findPublished(int $id): ?Quote {
327 $post = get_post($id);
328
329 if (!$post || $post->post_type !== PostTypes::EASY_INVOICE_QUOTE_POST_TYPE || $post->post_status !== 'publish') {
330 return null;
331 }
332
333 $quote = new Quote($post);
334
335 // Ensure quote has proper slug for pretty URLs
336 $quote->ensureProperSlug();
337
338 return $quote;
339 }
340
341 /**
342 * Find quotes by criteria
343 *
344 * @param array $criteria Array of criteria to filter by
345 * @return Quote[] Array of Quote objects
346 */
347 public function findBy(array $criteria = []): array {
348 $args = [
349 'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
350 'posts_per_page' => -1,
351 'post_status' => ['publish', 'draft', 'private', 'pending', 'trash']
352 ];
353
354 // Handle status criteria
355 if (isset($criteria['status'])) {
356 $args['meta_query'][] = [
357 'key' => '_easy_invoice_quote_status',
358 'value' => $criteria['status']
359 ];
360 }
361
362 // Handle client criteria
363 if (isset($criteria['client_id'])) {
364 $args['meta_query'][] = [
365 'key' => '_easy_invoice_quote_client_id',
366 'value' => $criteria['client_id']
367 ];
368 }
369
370 // Handle date range
371 if (isset($criteria['date_from'])) {
372 $args['date_query']['after'] = $criteria['date_from'];
373 }
374 if (isset($criteria['date_to'])) {
375 $args['date_query']['before'] = $criteria['date_to'];
376 }
377
378 $posts = get_posts($args);
379 $quotes = [];
380
381 foreach ($posts as $post) {
382 $quotes[] = new Quote($post);
383 }
384
385 return $quotes;
386 }
387
388 /**
389 * Update all existing draft quotes to published status for proper permalinks
390 *
391 * @since 1.0.0
392 * @return int Number of quotes updated
393 */
394 public function updateAllExistingQuotes(): int {
395 global $wpdb;
396
397 // Find all draft quotes
398 $draft_quotes = $wpdb->get_col($wpdb->prepare(
399 "SELECT ID FROM {$wpdb->posts}
400 WHERE post_type = %s
401 AND post_status = 'draft'",
402 PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
403 ));
404
405 $updated_count = 0;
406
407 foreach ($draft_quotes as $quote_id) {
408 $quote = $this->find($quote_id);
409 if ($quote) {
410 // Save the quote (this will publish it and ensure proper permalinks)
411 if ($quote->save()) {
412 $updated_count++;
413 }
414 }
415 }
416
417 // Allow plugins to perform actions after bulk update
418 do_action('easy_invoice_quotes_bulk_updated', $updated_count);
419
420 return $updated_count;
421 }
422
423 /**
424 * Get statistics
425 *
426 * @since 1.0.0
427 * @return array
428 */
429 public function getStatistics(): array {
430 $quotes = $this->findAll();
431
432 $stats = [
433 'total' => count($quotes),
434 'draft' => count($this->findByStatus('draft')),
435 'sent' => count($this->findByStatus('sent')),
436 'accepted' => count($this->findByStatus('accepted')),
437 'declined' => count($this->findByStatus('declined')),
438 'expired' => count($this->findByStatus('expired')),
439 ];
440
441 // Allow plugins to modify statistics
442 return apply_filters('easy_invoice_quote_statistics', $stats, $quotes);
443 }
444
445 /**
446 * Set quote data from array
447 *
448 * @since 1.0.0
449 * @param Quote $quote
450 * @param array $data
451 */
452 private function setQuoteData(Quote $quote, array $data): void {
453 // Allow plugins to modify the data setting process
454 do_action('easy_invoice_quote_set_data_before', $quote, $data);
455
456 // Set basic properties
457 if (isset($data['title'])) {
458 $quote->setTitle($data['title']);
459 }
460
461 if (isset($data['number']) && !empty($data['number'])) {
462 $quote->setNumber($data['number']);
463 }
464
465 if (isset($data['status'])) {
466 $quote->setStatus($data['status']);
467 }
468
469 if (isset($data['issue_date'])) {
470 $quote->setIssueDate($data['issue_date']);
471 }
472
473 if (isset($data['expiry_date'])) {
474 $quote->setExpiryDate($data['expiry_date']);
475 }
476
477 if (isset($data['client_id'])) {
478 $quote->setClientId($data['client_id']);
479 }
480
481 if (isset($data['customer_name'])) {
482 $quote->setCustomerName($data['customer_name']);
483 }
484
485 if (isset($data['customer_email'])) {
486 $quote->setCustomerEmail($data['customer_email']);
487 }
488
489 if (isset($data['customer_address'])) {
490 $quote->setCustomerAddress($data['customer_address']);
491 }
492
493 if (isset($data['notes'])) {
494 $quote->setNotes($data['notes']);
495 }
496
497 // Always update description if it exists in data (even if empty, to allow clearing)
498 if (array_key_exists('description', $data)) {
499 $quote->setDescription($data['description'] ?? '');
500 }
501
502 if (isset($data['terms'])) {
503 $quote->setTerms($data['terms']);
504 }
505
506 if (isset($data['internal_notes'])) {
507 $quote->setInternalNotes($data['internal_notes']);
508 }
509
510 if (isset($data['template'])) {
511 $quote->setTemplate($data['template']);
512 }
513
514 if (isset($data['items']) && is_array($data['items'])) {
515 // Process each item to ensure taxable field is properly set
516 $processed_items = array_map(function($item) {
517 if (is_array($item)) {
518 // Convert taxable field to boolean
519 $taxable = isset($item['taxable']) ? $item['taxable'] : true;
520 if (is_string($taxable)) {
521 $taxable = strtolower($taxable);
522 $taxable = $taxable === '1' || $taxable === 'true' || $taxable === 'yes' || $taxable === 'on';
523 }
524 $item['taxable'] = (bool) $taxable;
525 }
526 return $item;
527 }, $data['items']);
528
529 $quote->setItems($processed_items);
530 }
531
532 if (isset($data['discount_type'])) {
533 $quote->setDiscountType($data['discount_type']);
534 }
535
536 if (isset($data['discount_value'])) {
537 $quote->setDiscountValue($data['discount_value']);
538 }
539
540 if (isset($data['tax_rate'])) {
541 $quote->setTaxRate($data['tax_rate']);
542 }
543
544 if (isset($data['prices_include_tax'])) {
545 $quote->setPricesIncludeTax($data['prices_include_tax']);
546 }
547
548 if (isset($data['discount_calculation_method'])) {
549 $quote->setDiscountCalculationMethod($data['discount_calculation_method']);
550 } else {
551 // Default to before_tax if not set
552 $quote->setDiscountCalculationMethod('before_tax');
553 }
554
555 if (isset($data['currency_code'])) {
556 $quote->setCurrencyCode($data['currency_code']);
557 }
558
559 if (isset($data['currency_position'])) {
560 $quote->setCurrencyPosition($data['currency_position']);
561 }
562
563 // Handle new quote fields
564 if (isset($data['footer_text'])) {
565 $quote->setFooterText($data['footer_text']);
566 }
567
568 if (isset($data['accept_button'])) {
569 $quote->setAcceptButton($data['accept_button']);
570 }
571
572 if (isset($data['accept_action'])) {
573 $quote->setAcceptAction($data['accept_action']);
574 }
575
576 if (isset($data['accept_text'])) {
577 $quote->setAcceptText($data['accept_text']);
578 }
579
580 if (isset($data['accepted_message'])) {
581 $quote->setAcceptedMessage($data['accepted_message']);
582 }
583
584 if (isset($data['declined_message'])) {
585 $quote->setDeclinedMessage($data['declined_message']);
586 }
587
588 // Handle custom fields
589 if (isset($data['custom_fields']) && is_array($data['custom_fields'])) {
590 $quote->setCustomFields($data['custom_fields']);
591 }
592
593 // Populate client information if we have a client_id
594 if ($quote->getClientId() > 0) {
595 $this->populateCustomerFromClient($quote, $quote->getClientId());
596 }
597
598 // Allow plugins to modify the data setting process
599 do_action('easy_invoice_quote_set_data_after', $quote, $data);
600 }
601
602 /**
603 * Populate customer information from client
604 *
605 * @since 1.0.0
606 * @param Quote $quote
607 * @param int $client_id
608 */
609 private function populateCustomerFromClient(Quote $quote, int $client_id): void {
610 $client_repository = new \EasyInvoice\Repositories\ClientRepository();
611 $client = $client_repository->find($client_id);
612
613 if ($client) {
614 $quote->setCustomerName($client->getBusinessClientName() ?: '');
615 $quote->setCustomerEmail($client->getEmail() ?: '');
616 $quote->setCustomerAddress($client->getAddress() ?: '');
617 }
618 }
619 }