'utf-8', 'format' => 'A4', 'margin_left' => 15, 'margin_right' => 15, 'margin_top' => 15, 'margin_bottom' => 20, 'tempDir' => $temp_dir, ) ); $mpdf->SetTitle( sprintf( 'Invoice %s', $invoice_number ) ); $mpdf->SetAuthor( get_option( 'wp_bnb_business_name', get_bloginfo( 'name' ) ) ); $mpdf->SetCreator( 'WP BnB' ); $mpdf->WriteHTML( $html ); // Save to file. $file_path = self::get_invoice_path( $order, $invoice_number ); $mpdf->Output( $file_path, 'F' ); // Store invoice number and path in order meta. $order->update_meta_data( '_bnb_invoice_number', $invoice_number ); $order->update_meta_data( '_bnb_invoice_path', $file_path ); $order->update_meta_data( '_bnb_invoice_date', current_time( 'mysql' ) ); $order->save(); /** * Fires after generating an invoice. * * @param \WC_Order $order WooCommerce order. * @param string $file_path Invoice file path. */ do_action( 'wp_bnb_wc_after_invoice_generate', $order, $file_path ); return $file_path; } catch ( \Exception $e ) { error_log( 'WP BnB Invoice generation failed: ' . $e->getMessage() ); return null; } } /** * Get invoice number for an order. * * @param \WC_Order $order WooCommerce order. * @return string Invoice number. */ public static function get_invoice_number( \WC_Order $order ): string { // Check if already has invoice number. $existing = $order->get_meta( '_bnb_invoice_number', true ); if ( $existing ) { return $existing; } // Generate new invoice number. return Manager::get_next_invoice_number(); } /** * Get invoice file path. * * @param \WC_Order $order WooCommerce order. * @param string $invoice_number Invoice number. * @return string File path. */ private static function get_invoice_path( \WC_Order $order, string $invoice_number ): string { $upload_dir = wp_upload_dir(); $invoice_dir = $upload_dir['basedir'] . '/' . self::INVOICE_DIR; // Create directory if needed. if ( ! file_exists( $invoice_dir ) ) { wp_mkdir_p( $invoice_dir ); // Add .htaccess to protect invoices. $htaccess = $invoice_dir . '/.htaccess'; if ( ! file_exists( $htaccess ) ) { file_put_contents( $htaccess, 'Deny from all' ); } // Add index.php for extra protection. $index = $invoice_dir . '/index.php'; if ( ! file_exists( $index ) ) { file_put_contents( $index, 'get_id() . '.pdf'; } /** * Check if invoice exists for an order. * * @param \WC_Order $order WooCommerce order. * @return bool */ public static function invoice_exists( \WC_Order $order ): bool { $path = $order->get_meta( '_bnb_invoice_path', true ); return $path && file_exists( $path ); } /** * Attach invoice to email. * * @param array $attachments Attachments array. * @param string $email_id Email ID. * @param \WC_Order $order WooCommerce order. * @param \WC_Email $email Email object. * @return array Modified attachments. */ public static function attach_invoice_to_email( array $attachments, string $email_id, $order, $email ): array { // Only attach to specific emails. $allowed_emails = array( 'customer_completed_order', 'customer_processing_order', 'customer_invoice', ); if ( ! in_array( $email_id, $allowed_emails, true ) ) { return $attachments; } // Check if order is a WC_Order. if ( ! $order instanceof \WC_Order ) { return $attachments; } // Check if invoice attachment is enabled. if ( ! Manager::is_invoice_attach_enabled() ) { return $attachments; } // Check if this order has a booking. $booking_id = Manager::get_booking_for_order( $order ); if ( ! $booking_id ) { return $attachments; } // Generate invoice if it doesn't exist. if ( ! self::invoice_exists( $order ) ) { self::generate_invoice( $order ); } // Get invoice path. $invoice_path = $order->get_meta( '_bnb_invoice_path', true ); if ( $invoice_path && file_exists( $invoice_path ) ) { $attachments[] = $invoice_path; } return $attachments; } /** * Add order action button for invoice. * * @param \WC_Order $order WooCommerce order. * @return void */ public static function add_order_action_button( \WC_Order $order ): void { // Check if this order has a booking. $booking_id = Manager::get_booking_for_order( $order ); if ( ! $booking_id ) { return; } // Generate download URL. $download_url = add_query_arg( array( 'bnb_download_invoice' => $order->get_id(), '_wpnonce' => wp_create_nonce( 'bnb_download_invoice_' . $order->get_id() ), ), admin_url( 'admin.php' ) ); ?> __( 'Permission denied.', 'wp-bnb' ) ) ); } $order_id = isset( $_POST['order_id'] ) ? absint( $_POST['order_id'] ) : 0; if ( ! $order_id ) { wp_send_json_error( array( 'message' => __( 'Invalid order ID.', 'wp-bnb' ) ) ); } $order = wc_get_order( $order_id ); if ( ! $order ) { wp_send_json_error( array( 'message' => __( 'Order not found.', 'wp-bnb' ) ) ); } $file_path = self::generate_invoice( $order ); if ( $file_path ) { wp_send_json_success( array( 'message' => __( 'Invoice generated successfully.', 'wp-bnb' ) ) ); } else { wp_send_json_error( array( 'message' => __( 'Failed to generate invoice.', 'wp-bnb' ) ) ); } } /** * Handle invoice download request. * * @return void */ public static function handle_invoice_download(): void { // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( ! isset( $_GET['bnb_download_invoice'] ) ) { return; } $order_id = absint( $_GET['bnb_download_invoice'] ); // Verify nonce. if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'bnb_download_invoice_' . $order_id ) ) { wp_die( esc_html__( 'Security check failed.', 'wp-bnb' ) ); } // Check capabilities. if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( esc_html__( 'You do not have permission to download invoices.', 'wp-bnb' ) ); } $order = wc_get_order( $order_id ); if ( ! $order ) { wp_die( esc_html__( 'Order not found.', 'wp-bnb' ) ); } // Generate invoice if needed. if ( ! self::invoice_exists( $order ) ) { self::generate_invoice( $order ); } $invoice_path = $order->get_meta( '_bnb_invoice_path', true ); if ( ! $invoice_path || ! file_exists( $invoice_path ) ) { wp_die( esc_html__( 'Invoice not found.', 'wp-bnb' ) ); } $invoice_number = $order->get_meta( '_bnb_invoice_number', true ); $filename = 'invoice-' . sanitize_file_name( $invoice_number ) . '.pdf'; // Output file. header( 'Content-Type: application/pdf' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'Content-Length: ' . filesize( $invoice_path ) ); header( 'Cache-Control: no-cache, no-store, must-revalidate' ); readfile( $invoice_path ); exit; } /** * Get invoice HTML content. * * @param \WC_Order $order WooCommerce order. * @param string $invoice_number Invoice number. * @param int|null $booking_id Booking ID. * @return string HTML content. */ private static function get_invoice_html( \WC_Order $order, string $invoice_number, ?int $booking_id ): string { // Get business info. $business = self::get_business_info(); // Get booking details. $check_in = ''; $check_out = ''; $room_name = ''; $building_name = ''; $nights = 0; $guests = 0; if ( $booking_id ) { $check_in = get_post_meta( $booking_id, '_bnb_booking_check_in', true ); $check_out = get_post_meta( $booking_id, '_bnb_booking_check_out', true ); $guests = get_post_meta( $booking_id, '_bnb_booking_adults', true ); $room_id = get_post_meta( $booking_id, '_bnb_booking_room_id', true ); if ( $room_id ) { $room = get_post( $room_id ); $room_name = $room ? $room->post_title : ''; $building = Room::get_building( $room_id ); $building_name = $building ? $building->post_title : ''; } if ( $check_in && $check_out ) { $check_in_date = \DateTime::createFromFormat( 'Y-m-d', $check_in ); $check_out_date = \DateTime::createFromFormat( 'Y-m-d', $check_out ); if ( $check_in_date && $check_out_date ) { $nights = $check_in_date->diff( $check_out_date )->days; } } } // Format dates. $date_format = get_option( 'date_format' ); $check_in_display = $check_in ? date_i18n( $date_format, strtotime( $check_in ) ) : ''; $check_out_display = $check_out ? date_i18n( $date_format, strtotime( $check_out ) ) : ''; $order_date = $order->get_date_created() ? $order->get_date_created()->date_i18n( $date_format ) : ''; // Get logo. $logo_html = ''; $logo_id = Manager::get_invoice_logo(); if ( $logo_id ) { $logo_url = wp_get_attachment_url( $logo_id ); if ( $logo_url ) { $logo_html = ''; } } // Get footer. $footer_text = Manager::get_invoice_footer(); // Build HTML. $html = ''; // Header. $html .= '
'; $html .= ''; $html .= '
'; $html .= '

' . esc_html__( 'INVOICE', 'wp-bnb' ) . '

'; $html .= '

' . esc_html( $invoice_number ) . '

'; $html .= '

' . esc_html( $order_date ) . '

'; $html .= '
'; $html .= '
'; // Addresses. $html .= '
'; // From. $html .= '
'; $html .= '

' . esc_html__( 'From', 'wp-bnb' ) . '

'; $html .= '

' . esc_html( $business['name'] ) . '

'; if ( $business['street'] ) { $html .= '

' . esc_html( $business['street'] ) . '

'; } if ( $business['city'] || $business['postal'] ) { $html .= '

' . esc_html( $business['postal'] . ' ' . $business['city'] ) . '

'; } if ( $business['country'] ) { $html .= '

' . esc_html( $business['country'] ) . '

'; } if ( $business['email'] ) { $html .= '

' . esc_html( $business['email'] ) . '

'; } if ( $business['phone'] ) { $html .= '

' . esc_html( $business['phone'] ) . '

'; } $html .= '
'; // To. $html .= '
'; $html .= '

' . esc_html__( 'Bill To', 'wp-bnb' ) . '

'; $html .= '

' . esc_html( $order->get_billing_first_name() . ' ' . $order->get_billing_last_name() ) . '

'; if ( $order->get_billing_address_1() ) { $html .= '

' . esc_html( $order->get_billing_address_1() ) . '

'; } if ( $order->get_billing_city() || $order->get_billing_postcode() ) { $html .= '

' . esc_html( $order->get_billing_postcode() . ' ' . $order->get_billing_city() ) . '

'; } if ( $order->get_billing_country() ) { $html .= '

' . esc_html( WC()->countries->countries[ $order->get_billing_country() ] ?? $order->get_billing_country() ) . '

'; } if ( $order->get_billing_email() ) { $html .= '

' . esc_html( $order->get_billing_email() ) . '

'; } $html .= '
'; $html .= '
'; // Booking details. if ( $booking_id && $room_name ) { $html .= '
'; $html .= '

' . esc_html__( 'Booking Details', 'wp-bnb' ) . '

'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= '
' . esc_html__( 'Room', 'wp-bnb' ) . '' . esc_html( $room_name ); if ( $building_name ) { $html .= ' (' . esc_html( $building_name ) . ')'; } $html .= '
' . esc_html__( 'Check-in', 'wp-bnb' ) . '' . esc_html( $check_in_display ) . '
' . esc_html__( 'Check-out', 'wp-bnb' ) . '' . esc_html( $check_out_display ) . '
' . esc_html__( 'Nights', 'wp-bnb' ) . '' . esc_html( $nights ) . '
' . esc_html__( 'Guests', 'wp-bnb' ) . '' . esc_html( $guests ) . '
'; $html .= '
'; } // Line items. $html .= '
'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; foreach ( $order->get_items() as $item ) { $qty = $item->get_quantity(); $total = $item->get_total(); $price = $qty > 0 ? $total / $qty : $total; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; } $html .= ''; $html .= '
' . esc_html__( 'Description', 'wp-bnb' ) . '' . esc_html__( 'Qty', 'wp-bnb' ) . '' . esc_html__( 'Price', 'wp-bnb' ) . '' . esc_html__( 'Total', 'wp-bnb' ) . '
' . esc_html( $item->get_name() ) . '' . esc_html( $qty ) . '' . wc_price( $price ) . '' . wc_price( $total ) . '
'; $html .= '
'; // Totals. $html .= '
'; $html .= ''; $html .= ''; if ( $order->get_total_tax() > 0 ) { $html .= ''; } $html .= ''; $html .= '
' . esc_html__( 'Subtotal', 'wp-bnb' ) . '' . wc_price( $order->get_subtotal() ) . '
' . esc_html__( 'Tax', 'wp-bnb' ) . '' . wc_price( $order->get_total_tax() ) . '
' . esc_html__( 'Total', 'wp-bnb' ) . '' . wc_price( $order->get_total() ) . '
'; $html .= '
'; // Payment info. $html .= '
'; $html .= '

' . esc_html__( 'Payment Status:', 'wp-bnb' ) . ' '; if ( $order->is_paid() ) { $html .= ''; } else { $html .= '' . esc_html__( 'PENDING', 'wp-bnb' ) . ''; } $html .= '

'; $html .= '

' . esc_html__( 'Payment Method:', 'wp-bnb' ) . ' ' . esc_html( $order->get_payment_method_title() ) . '

'; $html .= '
'; // Footer. $html .= ''; $html .= ''; /** * Filter the invoice HTML. * * @param string $html Invoice HTML. * @param \WC_Order $order WooCommerce order. */ return apply_filters( 'wp_bnb_wc_invoice_html', $html, $order ); } /** * Get invoice CSS styles. * * @return string CSS content. */ private static function get_invoice_css(): string { return ' body { font-family: DejaVu Sans, sans-serif; font-size: 10pt; color: #333; line-height: 1.4; } h1 { font-size: 24pt; color: #2271b1; margin: 0; text-align: right; } h3 { font-size: 11pt; color: #50575e; margin: 15pt 0 5pt 0; } .invoice-header { margin-bottom: 30pt; overflow: hidden; } .logo { float: left; width: 50%; } .invoice-title { float: right; width: 50%; text-align: right; } .invoice-number { font-size: 14pt; font-weight: bold; color: #333; margin: 5pt 0; } .invoice-date { font-size: 10pt; color: #787c82; margin: 0; } .addresses { margin-bottom: 20pt; overflow: hidden; } .address { width: 48%; } .address.from { float: left; } .address.to { float: right; } .address p { margin: 2pt 0; font-size: 9pt; } .booking-details { margin-bottom: 20pt; background: #f9f9f9; padding: 10pt; border-radius: 4pt; } .details-table { width: 100%; border-collapse: collapse; } .details-table td { padding: 4pt 8pt; font-size: 9pt; border-bottom: 1px solid #e1e4e8; } .details-table td:first-child { width: 120pt; } .line-items { margin-bottom: 20pt; } .items-table { width: 100%; border-collapse: collapse; } .items-table th { background: #f6f7f7; text-align: left; padding: 8pt; font-size: 9pt; border-bottom: 2px solid #c3c4c7; } .items-table td { padding: 8pt; font-size: 9pt; border-bottom: 1px solid #e1e4e8; } .items-table .qty, .items-table .price, .items-table .total { text-align: right; width: 70pt; } .totals { margin-bottom: 20pt; } .totals-table { width: 250pt; margin-left: auto; border-collapse: collapse; } .totals-table td { padding: 6pt 8pt; font-size: 10pt; } .totals-table td:last-child { text-align: right; } .totals-table .grand-total td { border-top: 2px solid #333; font-size: 12pt; } .payment-info { margin-bottom: 30pt; padding: 10pt; background: #f9f9f9; border-radius: 4pt; } .payment-info p { margin: 4pt 0; font-size: 9pt; } .payment-info .paid { color: #00a32a; font-weight: bold; } .payment-info .unpaid { color: #dba617; font-weight: bold; } .footer { text-align: center; margin-top: 40pt; padding-top: 15pt; border-top: 1px solid #c3c4c7; } .footer p { font-size: 9pt; color: #787c82; margin: 3pt 0; } .custom-footer { font-size: 8pt; } '; } /** * Get business information from settings. * * @return array Business info. */ private static function get_business_info(): array { return array( 'name' => get_option( 'wp_bnb_business_name', get_bloginfo( 'name' ) ), 'street' => get_option( 'wp_bnb_address_street', '' ), 'city' => get_option( 'wp_bnb_address_city', '' ), 'postal' => get_option( 'wp_bnb_address_postal', '' ), 'country' => get_option( 'wp_bnb_address_country', '' ), 'email' => get_option( 'wp_bnb_contact_email', get_option( 'admin_email' ) ), 'phone' => get_option( 'wp_bnb_contact_phone', '' ), ); } }