# easy-invoice/2.4.0/includes/Repositories/QuoteRepository.php

Easy Invoice – Invoice Generator, PDF Quotes &amp; Payments, version 2.4.0. 655 lines.

- Page: https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Repositories/QuoteRepository.php
- Raw: https://pluginprobe.com/plugins/easy-invoice/2.4.0/raw/includes/Repositories/QuoteRepository.php
- Modified: 2026-09-15T12:31:20+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/easy-invoice/2.4.0/code/includes/Repositories/QuoteRepository.php#L10-L20`.

```php
<?php
/**
 * Quote Repository
 *
 * @package     EasyInvoice
 * @author      Your Name
 * @copyright   Copyright (c) 2023, Your Company
 * @license     http://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       1.0.0
 */

namespace EasyInvoice\Repositories;

use EasyInvoice\Models\Quote;
use EasyInvoice\Constants\PostTypes;
use EasyInvoice\Interfaces\QuoteRepositoryInterface;

/**
 * Quote Repository
 *
 * Handles database operations for quotes with extensible architecture for pro features.
 *
 * @since 1.0.0
 */
class QuoteRepository implements QuoteRepositoryInterface {
    
    /**
     * Find quote by ID
     *
     * @since 1.0.0
     * @param int $id Quote ID
     * @return Quote|null
     */
    public function find(int $id): ?Quote {
        $post = get_post($id);
        if ($post && $post->post_type === PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
            $quote = new Quote($post);
            
            // Ensure quote has proper slug for pretty URLs
            $quote->ensureProperSlug();
            
            // Allow plugins to modify the found quote
            return apply_filters('easy_invoice_quote_found', $quote, $id);
        }
        return null;
    }
    
    /**
     * Find all quotes
     *
     * @since 1.0.0
     * @param array $args Query arguments
     * @return array
     */
    public function findAll(array $args = []): array {
        $default_args = [
            'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
            'post_status' => ['publish', 'draft', 'private', 'pending'],
            'posts_per_page' => -1,
            'orderby' => 'date',
            'order' => 'DESC'
        ];
        
        $query_args = wp_parse_args($args, $default_args);
        
        // Allow plugins to modify query arguments
        $query_args = apply_filters('easy_invoice_quote_query_args', $query_args);
        
        $posts = get_posts($query_args);
        
        $quotes = [];
        foreach ($posts as $post) {
            $quote = new Quote($post);
            
            // Ensure quote has proper slug for pretty URLs
            $quote->ensureProperSlug();
            
            $quotes[] = $quote;
        }
        
        // Allow plugins to modify the results
        return apply_filters('easy_invoice_quotes_found', $quotes, $query_args);
    }
    
    /**
     * Find all published quotes (for public access)
     *
     * @since 1.0.0
     * @param array $args Query arguments
     * @return array
     */
    public function findAllPublished(array $args = []): array {
        $default_args = [
            'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
            'post_status' => 'publish',
            'posts_per_page' => -1,
            'orderby' => 'date',
            'order' => 'DESC'
        ];
        
        $query_args = wp_parse_args($args, $default_args);
        
        // Allow plugins to modify query arguments
        $query_args = apply_filters('easy_invoice_quote_published_query_args', $query_args);
        
        $posts = get_posts($query_args);
        
        $quotes = [];
        foreach ($posts as $post) {
            $quote = new Quote($post);
            
            // Ensure quote has proper slug for pretty URLs
            $quote->ensureProperSlug();
            
            $quotes[] = $quote;
        }
        
        // Allow plugins to modify the results
        return apply_filters('easy_invoice_quotes_published_found', $quotes, $query_args);
    }
    
    /**
     * Find quotes by status
     *
     * @since 1.0.0
     * @param string $status Quote status
     * @return array
     */
    public function findByStatus(string $status): array {
        $quotes = $this->findBy(['status' => $status]);
        
        // Allow plugins to modify the filtered results
        return apply_filters('easy_invoice_quotes_by_status', $quotes, $status);
    }
    
    /**
     * Find all quote IDs by status
     *
     * @since 1.0.0
     * @param string $status Quote status
     * @return array
     */
    public function findAllIdsByStatus(string $status): array {
        global $wpdb;
        
        $post_ids = $wpdb->get_col($wpdb->prepare(
            "SELECT p.ID 
            FROM {$wpdb->posts} p 
            INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id 
            WHERE p.post_type = %s 
            AND pm.meta_key = '_easy_invoice_quote_status' 
            AND pm.meta_value = %s",
            PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
            $status
        ));
        
        $post_ids = array_map('intval', $post_ids);
        
        // Allow plugins to modify the results
        return apply_filters('easy_invoice_quote_ids_by_status', $post_ids, $status);
    }
    
    /**
     * Find quotes by client
     *
     * @since 1.0.0
     * @param int $client_id Client ID
     * @return array
     */
    public function findByClient(int $client_id): array {
        $quotes = $this->findAll();
        $filtered_quotes = array_filter($quotes, function($quote) use ($client_id) {
            return $quote->getClientId() === $client_id;
        });
        
        // Allow plugins to modify the filtered results
        return apply_filters('easy_invoice_quotes_by_client', $filtered_quotes, $client_id);
    }
    
    /**
     * Quotes for a client — the name QuoteService calls; it did not exist.
     *
     * @param int $customer_id Client id.
     * @return array
     */
    public function findByCustomer($customer_id): array {
        return $this->findByClient((int) $customer_id);
    }

    /**
     * Create new quote
     *
     * @since 1.0.0
     * @param array $data Quote data
     * @return Quote|null
     */
    public function create(array $data): ?Quote {
        // Allow plugins to modify the data before creation
        $data = apply_filters('easy_invoice_quote_create_data', $data);
        
        $quote = new Quote();
        // Same default as invoices: discount before tax unless chosen otherwise.
        if (empty($data['discount_calculation_method'])) {
            $data['discount_calculation_method'] = 'before_tax';
        }
        // Reserve a number under the number lock. Left to the form default,
        // the model peeked at the next number without taking it, so quotes
        // created at the same moment (or by two people with the builder
        // open) shared one number.
        $requested = (string) ($data['number'] ?? $data['quote_number'] ?? '');
        $data['number'] = (new \EasyInvoice\Services\QuoteNumberService())->claimOrGenerate($requested);
        unset($data['quote_number']);
        $this->setQuoteData($quote, $data);

        // Same as invoices: a client id without customer details is filled in
        // from the client record before the first save.
        if (($data['client_id'] ?? 0) > 0 && (empty($data['customer_email']) || empty($data['customer_name'])) && method_exists($quote, 'populateClientInfo')) {
            $quote->populateClientInfo();
        }
        
        // Allow plugins to modify the quote before saving
        do_action('easy_invoice_quote_before_create', $quote, $data);
        
        if ($quote->save()) {
            // Allow plugins to perform actions after creation
            do_action('easy_invoice_quote_created', $quote, $data);
            return $quote;
        }
        
        return null;
    }
    
    /**
     * Update existing quote
     *
     * @since 1.0.0
     * @param int $id Quote ID
     * @param array $data Quote data
     * @param Quote|null $existing_quote Optional existing quote object to update
     * @return Quote|null
     */
    public function update(int $id, array $data, ?Quote $existing_quote = null): ?Quote {
        // Use existing quote object if provided, otherwise find from database
        $quote = $existing_quote ?: $this->find($id);
        if (!$quote) {
            return null;
        }
        
        // Prevent quote number from being changed on update
        unset($data['number'], $data['quote-number']);
        
        // Allow plugins to modify the data before update
        $data = apply_filters('easy_invoice_quote_update_data', $data, $quote);
        
        $this->setQuoteData($quote, $data);
        
        // Allow plugins to modify the quote before saving
        do_action('easy_invoice_quote_before_update', $quote, $data);
        
        if ($quote->save()) {
            // Allow plugins to perform actions after update
            do_action('easy_invoice_quote_updated', $quote, $data);
            return $quote;
        }
        
        return null;
    }
    
    /**
     * Delete quote
     *
     * @since 1.0.0
     * @param int $id Quote ID
     * @return bool
     */
    public function delete(int $id): bool {
        $quote = $this->find($id);
        if (!$quote) {
            return false;
        }
        
        // Allow plugins to perform actions before deletion
        do_action('easy_invoice_quote_before_delete', $quote);
        
        $result = wp_delete_post($id, true);
        
        if ($result) {
            // Allow plugins to perform actions after deletion
            do_action('easy_invoice_quote_deleted', $id);
        }
        
        return $result !== false;
    }

    /**
     * Find a quote by its number (stored in post meta)
     *
     * @param string $number Quote number
     * @return Quote|null
     */
    public function findByNumber(string $number): ?Quote {
        $args = [
            'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
            'post_status' => ['publish', 'draft', 'private', 'pending'],
            'posts_per_page' => 1,
            'meta_query' => [
                [
                    'key' => '_easy_invoice_quote_number',
                    'value' => $number,
                    'compare' => '=',
                ],
            ],
            'orderby' => 'date',
            'order' => 'DESC',
        ];

        $posts = get_posts($args);
        if (!empty($posts)) {
            $post = $posts[0];
            if ($post && $post->post_type === PostTypes::EASY_INVOICE_QUOTE_POST_TYPE) {
                $quote = new Quote($post);
                $quote->ensureProperSlug();
                return apply_filters('easy_invoice_quote_found_by_number', $quote, $number);
            }
        }
        return null;
    }
    
    /**
     * Force publish quote (for URL fixes)
     *
     * @since 1.0.0
     * @param int $id Quote ID
     * @return bool
     */
    public function forcePublish($id) {
        $quote = $this->find($id);
        if (!$quote) {
            return false;
        }
        
        // Save the quote (this will publish it and ensure proper permalinks)
        return $quote->save();
    }
    
    /**
     * Find published quote by ID
     *
     * @since 1.0.0
     * @param int $id Quote ID
     * @return Quote|null
     */
    public function findPublished(int $id): ?Quote {
        $post = get_post($id);
        
        if (!$post || $post->post_type !== PostTypes::EASY_INVOICE_QUOTE_POST_TYPE || $post->post_status !== 'publish') {
            return null;
        }
        
        $quote = new Quote($post);
        
        // Ensure quote has proper slug for pretty URLs
        $quote->ensureProperSlug();
        
        return $quote;
    }
    
    /**
     * Find quotes by criteria
     * 
     * @param array $criteria Array of criteria to filter by
     * @return Quote[] Array of Quote objects
     */
    public function findBy(array $criteria = []): array {
        $args = [
            'post_type' => PostTypes::EASY_INVOICE_QUOTE_POST_TYPE,
            'posts_per_page' => -1,
            'post_status' => ['publish', 'draft', 'private', 'pending', 'trash']
        ];
        
        // Handle status criteria
        if (isset($criteria['status'])) {
            $args['meta_query'][] = [
                'key' => '_easy_invoice_quote_status',
                'value' => $criteria['status']
            ];
        }
        
        // Handle client criteria
        if (isset($criteria['client_id'])) {
            $args['meta_query'][] = [
                'key' => '_easy_invoice_quote_client_id',
                'value' => $criteria['client_id']
            ];
        }
        
        // Handle date range
        if (isset($criteria['date_from'])) {
            $args['date_query']['after'] = $criteria['date_from'];
        }
        if (isset($criteria['date_to'])) {
            $args['date_query']['before'] = $criteria['date_to'];
        }
        
        $posts = get_posts($args);
        $quotes = [];
        
        foreach ($posts as $post) {
            $quotes[] = new Quote($post);
        }
        
        return $quotes;
    }
    
    /**
     * Update all existing draft quotes to published status for proper permalinks
     *
     * @since 1.0.0
     * @return int Number of quotes updated
     */
    public function updateAllExistingQuotes(): int {
        global $wpdb;
        
        // Find all draft quotes
        $draft_quotes = $wpdb->get_col($wpdb->prepare(
            "SELECT ID FROM {$wpdb->posts} 
            WHERE post_type = %s 
            AND post_status = 'draft'",
            PostTypes::EASY_INVOICE_QUOTE_POST_TYPE
        ));
        
        $updated_count = 0;
        
        foreach ($draft_quotes as $quote_id) {
            $quote = $this->find($quote_id);
            if ($quote) {
                // Save the quote (this will publish it and ensure proper permalinks)
                if ($quote->save()) {
                    $updated_count++;
                }
            }
        }
        
        // Allow plugins to perform actions after bulk update
        do_action('easy_invoice_quotes_bulk_updated', $updated_count);
        
        return $updated_count;
    }
    
    /**
     * Get statistics
     *
     * @since 1.0.0
     * @return array
     */
    public function getStatistics(): array {
        $quotes = $this->findAll();
        
        $stats = [
            'total' => count($quotes),
            'draft' => count($this->findByStatus('draft')),
            'sent' => count($this->findByStatus('sent')),
            'accepted' => count($this->findByStatus('accepted')),
            'declined' => count($this->findByStatus('declined')),
            'expired' => count($this->findByStatus('expired')),
        ];
        
        // Allow plugins to modify statistics
        return apply_filters('easy_invoice_quote_statistics', $stats, $quotes);
    }
    
    /**
     * Set quote data from array
     *
     * @since 1.0.0
     * @param Quote $quote
     * @param array $data
     */
    private function setQuoteData(Quote $quote, array $data): void {
        // Allow plugins to modify the data setting process
        do_action('easy_invoice_quote_set_data_before', $quote, $data);
        
        // Set basic properties
        if (isset($data['title'])) {
            $quote->setTitle($data['title']);
        }
        
        if (isset($data['number']) && !empty($data['number'])) {
            $quote->setNumber($data['number']);
        }
        
        if (isset($data['status'])) {
            $quote->setStatus($data['status']);
        }
        
        if (isset($data['issue_date'])) {
            $quote->setIssueDate($data['issue_date']);
        }
        
        if (isset($data['expiry_date'])) {
            $quote->setExpiryDate($data['expiry_date']);
        }
        
        if (isset($data['client_id'])) {
            $quote->setClientId($data['client_id']);
        }
        
        if (isset($data['customer_name'])) {
            $quote->setCustomerName($data['customer_name']);
        }
        
        if (isset($data['customer_email'])) {
            $quote->setCustomerEmail($data['customer_email']);
        }
        
        if (isset($data['customer_address'])) {
            $quote->setCustomerAddress($data['customer_address']);
        }
        
        if (isset($data['notes'])) {
            $quote->setNotes($data['notes']);
        }
        
        // Always update description if it exists in data (even if empty, to allow clearing)
        if (array_key_exists('description', $data)) {
            $quote->setDescription($data['description'] ?? '');
        }
        
        if (isset($data['terms'])) {
            $quote->setTerms($data['terms']);
        }
        
        if (isset($data['internal_notes'])) {
            $quote->setInternalNotes($data['internal_notes']);
        }
        
        if (isset($data['template'])) {
            $quote->setTemplate($data['template']);
        }
        
        if (isset($data['items']) && is_array($data['items'])) {
            // Process each item to ensure taxable field is properly set
            $processed_items = array_map(function($item) {
                if (is_array($item)) {
                    // Convert taxable field to boolean
                    $taxable = isset($item['taxable']) ? $item['taxable'] : true;
                    if (is_string($taxable)) {
                        $taxable = strtolower($taxable);
                        $taxable = $taxable === '1' || $taxable === 'true' || $taxable === 'yes' || $taxable === 'on';
                    }
                    $item['taxable'] = (bool) $taxable;
                }
                return $item;
            }, $data['items']);
            
            $quote->setItems($processed_items);
        }
        
        if (isset($data['discount_type'])) {
            $quote->setDiscountType($data['discount_type']);
        }
        
        if (isset($data['discount_value'])) {
            $quote->setDiscountValue($data['discount_value']);
        }
        
        // Per-quote tax switch. The invoice repository picks this up through the
        // form processor's pass-through; quotes set every field by hand, and this
        // one was missing, so a quote built with tax on lost it (and passed a
        // tax-free invoice on conversion) whenever the site's global tax was off.
        if (array_key_exists('tax_enabled', $data)) {
            $enabled = $data['tax_enabled'];
            $quote->setTaxEnabled(($enabled === true || $enabled === 1 || in_array(strtolower((string) $enabled), ['1', 'yes', 'true', 'on'], true)) ? 'yes' : 'no');
        }

        if (isset($data['tax_rate'])) {
            $quote->setTaxRate($data['tax_rate']);
        }
        
        if (isset($data['prices_include_tax'])) {
            $quote->setPricesIncludeTax($data['prices_include_tax']);
        }

        if (isset($data['discount_calculation_method'])) {
            $quote->setDiscountCalculationMethod($data['discount_calculation_method']);
        } else {
            // Default to before_tax if not set
            $quote->setDiscountCalculationMethod('before_tax');
        }
        
        if (isset($data['currency_code'])) {
            $quote->setCurrencyCode($data['currency_code']);
        }
        
        if (isset($data['currency_position'])) {
            $quote->setCurrencyPosition($data['currency_position']);
        }
        
        // Handle new quote fields
        if (isset($data['footer_text'])) {
            $quote->setFooterText($data['footer_text']);
        }
        
        if (isset($data['accept_button'])) {
            $quote->setAcceptButton($data['accept_button']);
        }
        
        if (isset($data['accept_action'])) {
            $quote->setAcceptAction($data['accept_action']);
        }
        
        if (isset($data['accept_text'])) {
            $quote->setAcceptText($data['accept_text']);
        }
        
        if (isset($data['accepted_message'])) {
            $quote->setAcceptedMessage($data['accepted_message']);
        }
        
        if (isset($data['declined_message'])) {
            $quote->setDeclinedMessage($data['declined_message']);
        }
        
        // Handle custom fields
        if (isset($data['custom_fields']) && is_array($data['custom_fields'])) {
            $quote->setCustomFields($data['custom_fields']);
        }
        
        // Populate client information if we have a client_id
        if (is_numeric($quote->getClientId()) && (int) $quote->getClientId() > 0) {
            $this->populateCustomerFromClient($quote, (int) $quote->getClientId());
        }
        
        // Allow plugins to modify the data setting process
        do_action('easy_invoice_quote_set_data_after', $quote, $data);
    }
    
    /**
     * Populate customer information from client
     *
     * @since 1.0.0
     * @param Quote $quote
     * @param int $client_id
     */
    private function populateCustomerFromClient(Quote $quote, int $client_id): void {
        $client_repository = new \EasyInvoice\Repositories\ClientRepository();
        $client = $client_repository->find($client_id);
        
        if ($client) {
            $quote->setCustomerName($client->getBusinessClientName() ?: '');
            $quote->setCustomerEmail($client->getEmail() ?: '');
            $quote->setCustomerAddress($client->getAddress() ?: '');
        }
    }
} 
```
