repository = $repository; } /** * Create a new quote * * @since 1.0.0 * @param array $data The quote data * @return Quote|false The created quote or false on failure */ public function createQuote(array $data) { // Allow plugins to modify data before creation $data = apply_filters('easy_invoice_service_quote_create_data', $data); // Validate required fields $required_fields = ['title']; $errors = $this->validateRequiredFields($data, $required_fields); if (!empty($errors)) { $this->log('Quote creation failed: ' . implode(', ', $errors), 'error'); return false; } // Sanitize data $sanitized_data = $this->sanitizeQuoteData($data); // Allow plugins to perform actions before creation do_action('easy_invoice_service_quote_before_create', $sanitized_data); // Create the quote $quote = $this->repository->create($sanitized_data); if ($quote) { $this->log('Quote created successfully: ' . $quote->getId()); // Allow plugins to perform actions after creation do_action('easy_invoice_service_quote_created', $quote, $sanitized_data); return $quote; } $this->log('Quote creation failed', 'error'); return false; } /** * Update an existing quote * * @since 1.0.0 * @param int $id The quote ID * @param array $data The quote data * @return Quote|null The updated quote or null on failure */ public function updateQuote(int $id, array $data) { // Allow plugins to modify data before update $data = apply_filters('easy_invoice_service_quote_update_data', $data, $id); // Get the existing quote $quote = $this->repository->find($id); if (!$quote) { $this->log('Quote not found for update: ' . $id, 'error'); return null; } // Sanitize data $sanitized_data = $this->sanitizeQuoteData($data); // Allow plugins to perform actions before update do_action('easy_invoice_service_quote_before_update', $quote, $sanitized_data); // Update the quote $updated_quote = $this->repository->update($id, $sanitized_data); if ($updated_quote) { $this->log('Quote updated successfully: ' . $id); // Allow plugins to perform actions after update do_action('easy_invoice_service_quote_updated', $updated_quote, $sanitized_data); return $updated_quote; } $this->log('Quote update failed: ' . $id, 'error'); return null; } /** * Delete a quote * * @since 1.0.0 * @param int $id The quote ID * @return bool True if deleted successfully */ public function deleteQuote(int $id): bool { // Get the quote before deletion $quote = $this->repository->find($id); if (!$quote) { $this->log('Quote not found for deletion: ' . $id, 'error'); return false; } // Allow plugins to perform actions before deletion do_action('easy_invoice_service_quote_before_delete', $quote); // Delete the quote $deleted = $this->repository->delete($id); if ($deleted) { $this->log('Quote deleted successfully: ' . $id); // Allow plugins to perform actions after deletion do_action('easy_invoice_service_quote_deleted', $id); return true; } $this->log('Quote deletion failed: ' . $id, 'error'); return false; } /** * Get quote by ID * * @since 1.0.0 * @param int $id The quote ID * @return Quote|null The quote or null if not found */ public function getQuote(int $id) { $quote = $this->repository->find($id); // Allow plugins to modify the found quote return apply_filters('easy_invoice_service_quote_found', $quote, $id); } /** * Get all quotes * * @since 1.0.0 * @param array $args Optional arguments to filter the results * @return array Array of Quote models */ public function getAllQuotes(array $args = []): array { $quotes = $this->repository->all($args); // Allow plugins to modify the quotes list return apply_filters('easy_invoice_service_quotes_found', $quotes, $args); } /** * Get quotes by customer * * @since 1.0.0 * @param int $customer_id The customer ID * @return array Array of Quote models */ public function getQuotesByCustomer(int $customer_id): array { $quotes = $this->repository->findByCustomer($customer_id); // Allow plugins to modify the filtered results return apply_filters('easy_invoice_service_quotes_by_customer', $quotes, $customer_id); } /** * Get quotes by status * * @since 1.0.0 * @param string $status The quote status * @return array Array of Quote models */ public function getQuotesByStatus(string $status): array { $quotes = $this->repository->findByStatus($status); // Allow plugins to modify the filtered results return apply_filters('easy_invoice_service_quotes_by_status', $quotes, $status); } /** * Get quotes by expiry date * * @since 1.0.0 * @param string $start_date The start date in 'Y-m-d' format * @param string $end_date The end date in 'Y-m-d' format * @return array Array of Quote models */ public function getQuotesByExpiryDate(string $start_date, ?string $end_date = null): array { $quotes = $this->repository->findByExpiryDate($start_date, $end_date); // Allow plugins to modify the filtered results return apply_filters('easy_invoice_service_quotes_by_expiry_date', $quotes, $start_date, $end_date); } /** * Count quotes * * @since 1.0.0 * @param array $args Optional arguments to filter the results * @return int Number of quotes */ public function countQuotes(array $args = []): int { $count = $this->repository->count($args); // Allow plugins to modify the count return apply_filters('easy_invoice_service_quote_count', $count, $args); } /** * Calculate quote total * * @since 1.0.0 * @param Quote $quote The quote * @return float The calculated total */ public function calculateQuoteTotal(Quote $quote): float { $items = $quote->getItems(); $subtotal = 0; // Calculate subtotal from items foreach ($items as $item) { $quantity = floatval($item['quantity'] ?? 0); $price = floatval($item['price'] ?? 0); $subtotal += $quantity * $price; } // Apply discount $discount_type = $quote->getDiscountType(); $discount_value = floatval($quote->getDiscountValue() ?? 0); if ($discount_type === 'percentage' && $discount_value > 0) { $discount_amount = $subtotal * ($discount_value / 100); $subtotal -= $discount_amount; } elseif ($discount_type === 'fixed' && $discount_value > 0) { $subtotal -= $discount_value; } // Apply tax $tax_rate = floatval($quote->getTaxRate() ?? 0); $prices_include_tax = $quote->getPricesIncludeTax(); if ($tax_rate > 0) { if ($prices_include_tax) { // Tax is already included in prices $total = $subtotal; } else { // Add tax to subtotal $tax_amount = $subtotal * ($tax_rate / 100); $total = $subtotal + $tax_amount; } } else { $total = $subtotal; } // Allow plugins to modify the calculated total return apply_filters('easy_invoice_service_quote_total', $total, $quote, $subtotal); } /** * Accept a quote * * @since 1.0.0 * @param int $quote_id The quote ID * @return bool True if quote was accepted successfully */ public function acceptQuote(int $quote_id): bool { $quote = $this->repository->find($quote_id); if (!$quote) { $this->log('Quote not found for acceptance: ' . $quote_id, 'error'); return false; } // Allow plugins to perform actions before acceptance do_action('easy_invoice_service_quote_before_accept', $quote); // Update quote status to accepted $updated = $this->repository->update($quote_id, ['status' => 'accepted']); if ($updated) { $this->log('Quote accepted successfully: ' . $quote_id); // Send acceptance notification $this->sendQuoteAcceptanceNotification($updated); // Allow plugins to perform actions after acceptance do_action('easy_invoice_service_quote_accepted', $updated); return true; } $this->log('Quote acceptance failed: ' . $quote_id, 'error'); return false; } /** * Decline a quote * * @since 1.0.0 * @param int $quote_id The quote ID * @param string $reason The decline reason * @return bool True if quote was declined successfully */ public function declineQuote(int $quote_id, string $reason = ''): bool { $quote = $this->repository->find($quote_id); if (!$quote) { $this->log('Quote not found for decline: ' . $quote_id, 'error'); return false; } // Allow plugins to perform actions before decline do_action('easy_invoice_service_quote_before_decline', $quote, $reason); // Update quote status to declined $update_data = ['status' => 'declined']; if (!empty($reason)) { $update_data['decline_reason'] = $reason; } $updated = $this->repository->update($quote_id, $update_data); if ($updated) { $this->log('Quote declined successfully: ' . $quote_id); // Send decline notification $this->sendQuoteDeclineNotification($updated, $reason); // Allow plugins to perform actions after decline do_action('easy_invoice_service_quote_declined', $updated, $reason); return true; } $this->log('Quote decline failed: ' . $quote_id, 'error'); return false; } /** * Send quote to customer * * @since 1.0.0 * @param Quote $quote The quote * @param string $email The customer email * @return bool True if email was sent successfully */ public function sendQuoteToCustomer(Quote $quote, string $email): bool { // Allow plugins to modify email data $email_data = apply_filters('easy_invoice_service_quote_email_data', [ 'to' => $email, 'subject' => sprintf(__('Quote #%s from %s', 'easy-invoice'), $quote->getNumber(), get_bloginfo('name')), 'message' => $this->generateQuoteEmailMessage($quote), 'headers' => ['Content-Type: text/html; charset=UTF-8'] ], $quote); // Allow plugins to handle email sending $sent = apply_filters('easy_invoice_service_quote_email_send', null, $email_data, $quote); if ($sent === null) { $sent = $this->sendEmail( $email_data['to'], $email_data['subject'], $email_data['message'], $email_data['headers'] ); } if ($sent) { // Allow plugins to perform actions after email sent do_action('easy_invoice_service_quote_email_sent', $quote, $email, $sent); } return $sent; } /** * Send quote acceptance notification * * @since 1.0.0 * @param Quote $quote The quote * @return bool True if notification was sent successfully */ protected function sendQuoteAcceptanceNotification(Quote $quote): bool { $admin_email = get_option('admin_email'); $subject = sprintf(__('Quote #%s Accepted', 'easy-invoice'), $quote->getNumber()); $message = sprintf( '

%s

', __('A quote has been accepted by the customer.', 'easy-invoice') ); $message .= sprintf( '

%s: %s

', __('Quote Number', 'easy-invoice'), $quote->getNumber() ); $message .= sprintf( '

%s: %s

', __('Customer', 'easy-invoice'), $quote->getCustomerName() ); $message .= sprintf( '

%s: %s

', __('Amount', 'easy-invoice'), $this->formatCurrency($this->calculateQuoteTotal($quote), $quote->getCurrencyCode(), $quote->getCurrencyPosition()) ); // Allow plugins to modify the notification message $message = apply_filters('easy_invoice_service_quote_acceptance_notification_message', $message, $quote); return $this->sendEmail($admin_email, $subject, $message, ['Content-Type: text/html; charset=UTF-8']); } /** * Send quote decline notification * * @since 1.0.0 * @param Quote $quote The quote * @param string $reason The decline reason * @return bool True if notification was sent successfully */ protected function sendQuoteDeclineNotification(Quote $quote, string $reason = ''): bool { $admin_email = get_option('admin_email'); $subject = sprintf(__('Quote #%s Declined', 'easy-invoice'), $quote->getNumber()); $message = sprintf( '

%s

', __('A quote has been declined by the customer.', 'easy-invoice') ); $message .= sprintf( '

%s: %s

', __('Quote Number', 'easy-invoice'), $quote->getNumber() ); $message .= sprintf( '

%s: %s

', __('Customer', 'easy-invoice'), $quote->getCustomerName() ); if (!empty($reason)) { $message .= sprintf( '

%s: %s

', __('Reason', 'easy-invoice'), esc_html($reason) ); } // Allow plugins to modify the notification message $message = apply_filters('easy_invoice_service_quote_decline_notification_message', $message, $quote, $reason); return $this->sendEmail($admin_email, $subject, $message, ['Content-Type: text/html; charset=UTF-8']); } /** * Generate quote email message * * @since 1.0.0 * @param Quote $quote The quote * @return string The email message */ protected function generateQuoteEmailMessage(Quote $quote): string { $message = sprintf( '

%s

', __('Please find attached your quote.', 'easy-invoice') ); $message .= sprintf( '

%s: %s

', __('Quote Number', 'easy-invoice'), $quote->getNumber() ); $message .= sprintf( '

%s: %s

', __('Amount', 'easy-invoice'), $this->formatCurrency($this->calculateQuoteTotal($quote), $quote->getCurrencyCode(), $quote->getCurrencyPosition()) ); $message .= sprintf( '

%s: %s

', __('Valid Until', 'easy-invoice'), $quote->getExpiryDate() ); // Allow plugins to modify the email message return apply_filters('easy_invoice_service_quote_email_message', $message, $quote); } /** * Sanitize quote data * * @since 1.0.0 * @param array $data The quote data * @return array The sanitized data */ protected function sanitizeQuoteData(array $data): array { $sanitization_rules = [ 'title' => 'text_field', 'description' => 'textarea', 'number' => 'text_field', 'issue_date' => 'text_field', 'expiry_date' => 'text_field', 'status' => 'text_field', 'notes' => 'textarea', 'terms' => 'html', 'internal_notes' => 'textarea', 'payment_instructions' => 'textarea', 'payment_gateways' => 'array', 'quote_template' => 'text_field', 'customer_name' => 'text_field', 'customer_address' => 'textarea', 'customer_email' => 'email', 'shipping_name' => 'text_field', 'shipping_address' => 'textarea', 'discount_type' => 'text_field', 'discount_value' => 'float', 'tax_rate' => 'float', 'calculation_method' => 'text_field', 'prices_include_tax' => 'int', 'currency_code' => 'text_field', 'currency_position' => 'text_field', 'footer_text' => 'textarea', 'items' => 'array', 'custom_fields' => 'array', 'decline_reason' => 'textarea' ]; return $this->sanitizeData($data, $sanitization_rules); } }