PluginProbe
Easy Invoice – Invoice Generator, PDF Quotes & Payments / 2.3.1
Easy Invoice – Invoice Generator, PDF Quotes & Payments v2.3.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 / Services / QuoteService.php

QuoteService.php in Easy Invoice – Invoice Generator, PDF Quotes & Payments 2.3.1, at includes/Services/QuoteService.php

571 lines 18.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Quote Service Class
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\Services;
13
14 use EasyInvoice\Models\Quote;
15 use EasyInvoice\Repositories\QuoteRepository;
16 use EasyInvoice\Interfaces\QuoteRepositoryInterface;
17
18 /**
19 * Quote Service Class
20 *
21 * Handles business logic for quotes and provides extension points for plugins.
22 *
23 * @since 1.0.0
24 */
25 class QuoteService extends BaseService {
26
27 /**
28 * Quote repository
29 *
30 * @var QuoteRepositoryInterface
31 */
32 private $repository;
33
34 /**
35 * Constructor
36 *
37 * @since 1.0.0
38 * @param QuoteRepositoryInterface $repository The quote repository
39 */
40 public function __construct(QuoteRepositoryInterface $repository) {
41 parent::__construct('QuoteService');
42 $this->repository = $repository;
43 }
44
45 /**
46 * Create a new quote
47 *
48 * @since 1.0.0
49 * @param array $data The quote data
50 * @return Quote|false The created quote or false on failure
51 */
52 public function createQuote(array $data) {
53 // Allow plugins to modify data before creation
54 $data = apply_filters('easy_invoice_service_quote_create_data', $data);
55
56 // Validate required fields
57 $required_fields = ['title'];
58 $errors = $this->validateRequiredFields($data, $required_fields);
59
60 if (!empty($errors)) {
61 $this->log('Quote creation failed: ' . implode(', ', $errors), 'error');
62 return false;
63 }
64
65 // Sanitize data
66 $sanitized_data = $this->sanitizeQuoteData($data);
67
68 // Allow plugins to perform actions before creation
69 do_action('easy_invoice_service_quote_before_create', $sanitized_data);
70
71 // Create the quote
72 $quote = $this->repository->create($sanitized_data);
73
74 if ($quote) {
75 $this->log('Quote created successfully: ' . $quote->getId());
76
77 // Allow plugins to perform actions after creation
78 do_action('easy_invoice_service_quote_created', $quote, $sanitized_data);
79
80 return $quote;
81 }
82
83 $this->log('Quote creation failed', 'error');
84 return false;
85 }
86
87 /**
88 * Update an existing quote
89 *
90 * @since 1.0.0
91 * @param int $id The quote ID
92 * @param array $data The quote data
93 * @return Quote|null The updated quote or null on failure
94 */
95 public function updateQuote(int $id, array $data) {
96 // Allow plugins to modify data before update
97 $data = apply_filters('easy_invoice_service_quote_update_data', $data, $id);
98
99 // Get the existing quote
100 $quote = $this->repository->find($id);
101 if (!$quote) {
102 $this->log('Quote not found for update: ' . $id, 'error');
103 return null;
104 }
105
106 // Sanitize data
107 $sanitized_data = $this->sanitizeQuoteData($data);
108
109 // Allow plugins to perform actions before update
110 do_action('easy_invoice_service_quote_before_update', $quote, $sanitized_data);
111
112 // Update the quote
113 $updated_quote = $this->repository->update($id, $sanitized_data);
114
115 if ($updated_quote) {
116 $this->log('Quote updated successfully: ' . $id);
117
118 // Allow plugins to perform actions after update
119 do_action('easy_invoice_service_quote_updated', $updated_quote, $sanitized_data);
120
121 return $updated_quote;
122 }
123
124 $this->log('Quote update failed: ' . $id, 'error');
125 return null;
126 }
127
128 /**
129 * Delete a quote
130 *
131 * @since 1.0.0
132 * @param int $id The quote ID
133 * @return bool True if deleted successfully
134 */
135 public function deleteQuote(int $id): bool {
136 // Get the quote before deletion
137 $quote = $this->repository->find($id);
138 if (!$quote) {
139 $this->log('Quote not found for deletion: ' . $id, 'error');
140 return false;
141 }
142
143 // Allow plugins to perform actions before deletion
144 do_action('easy_invoice_service_quote_before_delete', $quote);
145
146 // Delete the quote
147 $deleted = $this->repository->delete($id);
148
149 if ($deleted) {
150 $this->log('Quote deleted successfully: ' . $id);
151
152 // Allow plugins to perform actions after deletion
153 do_action('easy_invoice_service_quote_deleted', $id);
154
155 return true;
156 }
157
158 $this->log('Quote deletion failed: ' . $id, 'error');
159 return false;
160 }
161
162 /**
163 * Get quote by ID
164 *
165 * @since 1.0.0
166 * @param int $id The quote ID
167 * @return Quote|null The quote or null if not found
168 */
169 public function getQuote(int $id) {
170 $quote = $this->repository->find($id);
171
172 // Allow plugins to modify the found quote
173 return apply_filters('easy_invoice_service_quote_found', $quote, $id);
174 }
175
176 /**
177 * Get all quotes
178 *
179 * @since 1.0.0
180 * @param array $args Optional arguments to filter the results
181 * @return array Array of Quote models
182 */
183 public function getAllQuotes(array $args = []): array {
184 $quotes = $this->repository->all($args);
185
186 // Allow plugins to modify the quotes list
187 return apply_filters('easy_invoice_service_quotes_found', $quotes, $args);
188 }
189
190 /**
191 * Get quotes by customer
192 *
193 * @since 1.0.0
194 * @param int $customer_id The customer ID
195 * @return array Array of Quote models
196 */
197 public function getQuotesByCustomer(int $customer_id): array {
198 $quotes = $this->repository->findByCustomer($customer_id);
199
200 // Allow plugins to modify the filtered results
201 return apply_filters('easy_invoice_service_quotes_by_customer', $quotes, $customer_id);
202 }
203
204 /**
205 * Get quotes by status
206 *
207 * @since 1.0.0
208 * @param string $status The quote status
209 * @return array Array of Quote models
210 */
211 public function getQuotesByStatus(string $status): array {
212 $quotes = $this->repository->findByStatus($status);
213
214 // Allow plugins to modify the filtered results
215 return apply_filters('easy_invoice_service_quotes_by_status', $quotes, $status);
216 }
217
218 /**
219 * Get quotes by expiry date
220 *
221 * @since 1.0.0
222 * @param string $start_date The start date in 'Y-m-d' format
223 * @param string $end_date The end date in 'Y-m-d' format
224 * @return array Array of Quote models
225 */
226 public function getQuotesByExpiryDate(string $start_date, ?string $end_date = null): array {
227 $quotes = $this->repository->findByExpiryDate($start_date, $end_date);
228
229 // Allow plugins to modify the filtered results
230 return apply_filters('easy_invoice_service_quotes_by_expiry_date', $quotes, $start_date, $end_date);
231 }
232
233 /**
234 * Count quotes
235 *
236 * @since 1.0.0
237 * @param array $args Optional arguments to filter the results
238 * @return int Number of quotes
239 */
240 public function countQuotes(array $args = []): int {
241 $count = $this->repository->count($args);
242
243 // Allow plugins to modify the count
244 return apply_filters('easy_invoice_service_quote_count', $count, $args);
245 }
246
247 /**
248 * Calculate quote total
249 *
250 * @since 1.0.0
251 * @param Quote $quote The quote
252 * @return float The calculated total
253 */
254 public function calculateQuoteTotal(Quote $quote): float {
255 $items = $quote->getItems();
256 $subtotal = 0;
257
258 // Calculate subtotal from items
259 foreach ($items as $item) {
260 $quantity = floatval($item['quantity'] ?? 0);
261 $price = floatval($item['price'] ?? 0);
262 $subtotal += $quantity * $price;
263 }
264
265 // Apply discount
266 $discount_type = $quote->getDiscountType();
267 $discount_value = floatval($quote->getDiscountValue() ?? 0);
268
269 if ($discount_type === 'percentage' && $discount_value > 0) {
270 $discount_amount = $subtotal * ($discount_value / 100);
271 $subtotal -= $discount_amount;
272 } elseif ($discount_type === 'fixed' && $discount_value > 0) {
273 $subtotal -= $discount_value;
274 }
275
276 // Apply tax
277 $tax_rate = floatval($quote->getTaxRate() ?? 0);
278 $prices_include_tax = $quote->getPricesIncludeTax();
279
280 if ($tax_rate > 0) {
281 if ($prices_include_tax) {
282 // Tax is already included in prices
283 $total = $subtotal;
284 } else {
285 // Add tax to subtotal
286 $tax_amount = $subtotal * ($tax_rate / 100);
287 $total = $subtotal + $tax_amount;
288 }
289 } else {
290 $total = $subtotal;
291 }
292
293 // Allow plugins to modify the calculated total
294 return apply_filters('easy_invoice_service_quote_total', $total, $quote, $subtotal);
295 }
296
297 /**
298 * Accept a quote
299 *
300 * @since 1.0.0
301 * @param int $quote_id The quote ID
302 * @return bool True if quote was accepted successfully
303 */
304 public function acceptQuote(int $quote_id): bool {
305 $quote = $this->repository->find($quote_id);
306 if (!$quote) {
307 $this->log('Quote not found for acceptance: ' . $quote_id, 'error');
308 return false;
309 }
310
311 // Allow plugins to perform actions before acceptance
312 do_action('easy_invoice_service_quote_before_accept', $quote);
313
314 // Update quote status to accepted
315 $updated = $this->repository->update($quote_id, ['status' => 'accepted']);
316
317 if ($updated) {
318 $this->log('Quote accepted successfully: ' . $quote_id);
319
320 // Send acceptance notification
321 $this->sendQuoteAcceptanceNotification($updated);
322
323 // Allow plugins to perform actions after acceptance
324 do_action('easy_invoice_service_quote_accepted', $updated);
325
326 return true;
327 }
328
329 $this->log('Quote acceptance failed: ' . $quote_id, 'error');
330 return false;
331 }
332
333 /**
334 * Decline a quote
335 *
336 * @since 1.0.0
337 * @param int $quote_id The quote ID
338 * @param string $reason The decline reason
339 * @return bool True if quote was declined successfully
340 */
341 public function declineQuote(int $quote_id, string $reason = ''): bool {
342 $quote = $this->repository->find($quote_id);
343 if (!$quote) {
344 $this->log('Quote not found for decline: ' . $quote_id, 'error');
345 return false;
346 }
347
348 // Allow plugins to perform actions before decline
349 do_action('easy_invoice_service_quote_before_decline', $quote, $reason);
350
351 // Update quote status to declined
352 $update_data = ['status' => 'declined'];
353 if (!empty($reason)) {
354 $update_data['decline_reason'] = $reason;
355 }
356
357 $updated = $this->repository->update($quote_id, $update_data);
358
359 if ($updated) {
360 $this->log('Quote declined successfully: ' . $quote_id);
361
362 // Send decline notification
363 $this->sendQuoteDeclineNotification($updated, $reason);
364
365 // Allow plugins to perform actions after decline
366 do_action('easy_invoice_service_quote_declined', $updated, $reason);
367
368 return true;
369 }
370
371 $this->log('Quote decline failed: ' . $quote_id, 'error');
372 return false;
373 }
374
375 /**
376 * Send quote to customer
377 *
378 * @since 1.0.0
379 * @param Quote $quote The quote
380 * @param string $email The customer email
381 * @return bool True if email was sent successfully
382 */
383 public function sendQuoteToCustomer(Quote $quote, string $email): bool {
384 // Allow plugins to modify email data
385 $email_data = apply_filters('easy_invoice_service_quote_email_data', [
386 'to' => $email,
387 'subject' => sprintf(__('Quote #%s from %s', 'easy-invoice'), $quote->getNumber(), get_bloginfo('name')),
388 'message' => $this->generateQuoteEmailMessage($quote),
389 'headers' => ['Content-Type: text/html; charset=UTF-8']
390 ], $quote);
391
392 // Allow plugins to handle email sending
393 $sent = apply_filters('easy_invoice_service_quote_email_send', null, $email_data, $quote);
394
395 if ($sent === null) {
396 $sent = $this->sendEmail(
397 $email_data['to'],
398 $email_data['subject'],
399 $email_data['message'],
400 $email_data['headers']
401 );
402 }
403
404 if ($sent) {
405 // Allow plugins to perform actions after email sent
406 do_action('easy_invoice_service_quote_email_sent', $quote, $email, $sent);
407 }
408
409 return $sent;
410 }
411
412 /**
413 * Send quote acceptance notification
414 *
415 * @since 1.0.0
416 * @param Quote $quote The quote
417 * @return bool True if notification was sent successfully
418 */
419 protected function sendQuoteAcceptanceNotification(Quote $quote): bool {
420 $admin_email = get_option('admin_email');
421 $subject = sprintf(__('Quote #%s Accepted', 'easy-invoice'), $quote->getNumber());
422
423 $message = sprintf(
424 '<p>%s</p>',
425 __('A quote has been accepted by the customer.', 'easy-invoice')
426 );
427
428 $message .= sprintf(
429 '<p><strong>%s:</strong> %s</p>',
430 __('Quote Number', 'easy-invoice'),
431 $quote->getNumber()
432 );
433
434 $message .= sprintf(
435 '<p><strong>%s:</strong> %s</p>',
436 __('Customer', 'easy-invoice'),
437 $quote->getCustomerName()
438 );
439
440 $message .= sprintf(
441 '<p><strong>%s:</strong> %s</p>',
442 __('Amount', 'easy-invoice'),
443 $this->formatCurrency($this->calculateQuoteTotal($quote), $quote->getCurrencyCode(), $quote->getCurrencyPosition())
444 );
445
446 // Allow plugins to modify the notification message
447 $message = apply_filters('easy_invoice_service_quote_acceptance_notification_message', $message, $quote);
448
449 return $this->sendEmail($admin_email, $subject, $message, ['Content-Type: text/html; charset=UTF-8']);
450 }
451
452 /**
453 * Send quote decline notification
454 *
455 * @since 1.0.0
456 * @param Quote $quote The quote
457 * @param string $reason The decline reason
458 * @return bool True if notification was sent successfully
459 */
460 protected function sendQuoteDeclineNotification(Quote $quote, string $reason = ''): bool {
461 $admin_email = get_option('admin_email');
462 $subject = sprintf(__('Quote #%s Declined', 'easy-invoice'), $quote->getNumber());
463
464 $message = sprintf(
465 '<p>%s</p>',
466 __('A quote has been declined by the customer.', 'easy-invoice')
467 );
468
469 $message .= sprintf(
470 '<p><strong>%s:</strong> %s</p>',
471 __('Quote Number', 'easy-invoice'),
472 $quote->getNumber()
473 );
474
475 $message .= sprintf(
476 '<p><strong>%s:</strong> %s</p>',
477 __('Customer', 'easy-invoice'),
478 $quote->getCustomerName()
479 );
480
481 if (!empty($reason)) {
482 $message .= sprintf(
483 '<p><strong>%s:</strong> %s</p>',
484 __('Reason', 'easy-invoice'),
485 esc_html($reason)
486 );
487 }
488
489 // Allow plugins to modify the notification message
490 $message = apply_filters('easy_invoice_service_quote_decline_notification_message', $message, $quote, $reason);
491
492 return $this->sendEmail($admin_email, $subject, $message, ['Content-Type: text/html; charset=UTF-8']);
493 }
494
495 /**
496 * Generate quote email message
497 *
498 * @since 1.0.0
499 * @param Quote $quote The quote
500 * @return string The email message
501 */
502 protected function generateQuoteEmailMessage(Quote $quote): string {
503 $message = sprintf(
504 '<p>%s</p>',
505 __('Please find attached your quote.', 'easy-invoice')
506 );
507
508 $message .= sprintf(
509 '<p><strong>%s:</strong> %s</p>',
510 __('Quote Number', 'easy-invoice'),
511 $quote->getNumber()
512 );
513
514 $message .= sprintf(
515 '<p><strong>%s:</strong> %s</p>',
516 __('Amount', 'easy-invoice'),
517 $this->formatCurrency($this->calculateQuoteTotal($quote), $quote->getCurrencyCode(), $quote->getCurrencyPosition())
518 );
519
520 $message .= sprintf(
521 '<p><strong>%s:</strong> %s</p>',
522 __('Valid Until', 'easy-invoice'),
523 $quote->getExpiryDate()
524 );
525
526 // Allow plugins to modify the email message
527 return apply_filters('easy_invoice_service_quote_email_message', $message, $quote);
528 }
529
530 /**
531 * Sanitize quote data
532 *
533 * @since 1.0.0
534 * @param array $data The quote data
535 * @return array The sanitized data
536 */
537 protected function sanitizeQuoteData(array $data): array {
538 $sanitization_rules = [
539 'title' => 'text_field',
540 'description' => 'textarea',
541 'number' => 'text_field',
542 'issue_date' => 'text_field',
543 'expiry_date' => 'text_field',
544 'status' => 'text_field',
545 'notes' => 'textarea',
546 'terms' => 'html',
547 'internal_notes' => 'textarea',
548 'payment_instructions' => 'textarea',
549 'payment_gateways' => 'array',
550 'quote_template' => 'text_field',
551 'customer_name' => 'text_field',
552 'customer_address' => 'textarea',
553 'customer_email' => 'email',
554 'shipping_name' => 'text_field',
555 'shipping_address' => 'textarea',
556 'discount_type' => 'text_field',
557 'discount_value' => 'float',
558 'tax_rate' => 'float',
559 'calculation_method' => 'text_field',
560 'prices_include_tax' => 'int',
561 'currency_code' => 'text_field',
562 'currency_position' => 'text_field',
563 'footer_text' => 'textarea',
564 'items' => 'array',
565 'custom_fields' => 'array',
566 'decline_reason' => 'textarea'
567 ];
568
569 return $this->sanitizeData($data, $sanitization_rules);
570 }
571 }