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); } /** * 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(); $this->setQuoteData($quote, $data); // 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']); } 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 ($quote->getClientId() > 0) { $this->populateCustomerFromClient($quote, $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() ?: ''); } } }