user_id );
if ( ! $user ) {
return;
}
DevisQR_Texts::begin_customer_output();
$mail = self::build_customer_received( $quote, DevisQR_Database::get_quote_items( $quote->id ), $user->display_name );
DevisQR_Texts::end_customer_output();
wp_mail( $user->user_email, $mail['subject'], $mail['html'], self::headers(), self::pdf_attachment( $quote ) );
}
/**
* Admin: new request.
*
* @param object $quote Quote.
*/
public static function send_new_quote_admin( $quote ) {
$s = DevisQR_Settings::resolved();
DevisQR_Texts::begin_customer_output();
$mail = self::build_admin_new( $quote, DevisQR_Database::get_quote_items( $quote->id ) );
DevisQR_Texts::end_customer_output();
$recipients = array_map( 'trim', explode( ',', $s['notification_email'] ) );
$headers = array( 'Content-Type: text/html; charset=UTF-8' );
$user = get_userdata( $quote->user_id );
if ( $user && is_email( $user->user_email ) ) {
$headers[] = 'Reply-To: ' . $user->user_email; // Answer the customer directly from the mailbox.
}
wp_mail( $recipients, $mail['subject'], $mail['html'], $headers, self::pdf_attachment( $quote ) );
}
/**
* Customer: status changed.
*
* @param object $quote Quote.
*/
public static function send_status_update( $quote ) {
$user = get_userdata( $quote->user_id );
if ( ! $user ) {
return;
}
DevisQR_Texts::begin_customer_output();
$mail = self::build_customer_status( $quote, $user->display_name );
DevisQR_Texts::end_customer_output();
$attachments = in_array( $quote->status, array( 'quoted', 'accepted' ), true ) ? self::pdf_attachment( $quote ) : array();
wp_mail( $user->user_email, $mail['subject'], $mail['html'], self::headers(), $attachments );
}
/**
* Test message (Settings → E-mails): the "request received" template with sample data.
*
* @param string $to Recipient.
* @return bool
*/
public static function send_test( $to ) {
$s = DevisQR_Settings::resolved();
$now = current_time( 'mysql' );
$quote = (object) array(
'id' => 0,
'quote_number' => DevisQR_Settings::get( 'quote_prefix' ) . '-' . gmdate( 'Ym' ) . '-0001',
'status' => 'pending',
'created_at' => $now,
'expires_at' => gmdate( 'Y-m-d H:i:s', strtotime( $now ) + (int) $s['validity_days'] * DAY_IN_SECONDS ),
'total_ht' => 240.00,
'total_tax' => 48.00,
'total_ttc' => 288.00,
'discount_percent' => 0,
'customer_note' => __( 'Sample message left by the customer with the request.', 'nuxwp-devis' ),
'admin_note' => '',
'pdf_path' => '',
);
$items = array(
(object) array( 'product_name' => __( 'Sample product A', 'nuxwp-devis' ), 'product_sku' => 'SKU-A', 'quantity' => 2, 'line_total_ttc' => 96.00 ),
(object) array( 'product_name' => __( 'Sample product B', 'nuxwp-devis' ), 'product_sku' => 'SKU-B', 'quantity' => 1, 'line_total_ttc' => 192.00 ),
);
DevisQR_Texts::begin_customer_output();
$mail = self::build_customer_received( $quote, $items, wp_get_current_user()->display_name );
DevisQR_Texts::end_customer_output();
return (bool) wp_mail( $to, '[' . __( 'Test', 'nuxwp-devis' ) . '] ' . $mail['subject'], $mail['html'], self::headers() );
}
/* ------------------------------------------------------------------ */
/* Builders (subject + html) */
/* ------------------------------------------------------------------ */
/**
* Customer: request received.
*
* @param object $quote Quote.
* @param array $items Items.
* @param string $name Customer display name.
* @return array {subject, html}
*/
private static function build_customer_received( $quote, $items, $name ) {
$s = DevisQR_Settings::resolved();
$account_url = wc_get_account_endpoint_url( DevisQR_Settings::account_endpoint() );
$days = max( 1, (int) $s['validity_days'] );
/* translators: %d: number of days */
$days_text = sprintf( _n( '%d day', '%d days', $days, 'nuxwp-devis' ), $days );
/* translators: 1: quote number, 2: site name */
$subject = sprintf( __( 'Your quote request %1$s — %2$s', 'nuxwp-devis' ), $quote->quote_number, self::site_name() );
$html = self::greeting( $name )
/* translators: %s: quote number */
. self::p( sprintf( esc_html__( 'We have received your quote request %s.', 'nuxwp-devis' ), '' . esc_html( $quote->quote_number ) . '' ) )
. self::p( esc_html__( 'Our team is reviewing your cart and will send you the final quote shortly.', 'nuxwp-devis' ) )
/* translators: %s: duration, e.g. "15 days" */
. self::p( sprintf( esc_html__( 'The quote will remain valid for %s, subject to product availability.', 'nuxwp-devis' ), '' . esc_html( $days_text ) . '' ) )
. self::quote_card( $quote, false )
. self::section_title( __( 'Quote details', 'nuxwp-devis' ) )
. self::items_table( $items )
. self::totals_block( $quote );
if ( ! empty( $quote->customer_note ) ) {
$html .= self::note_box( __( 'Your message', 'nuxwp-devis' ), $quote->customer_note );
}
$html .= self::p( esc_html__( 'To view your quote and turn it into an order, open the “My quotes” section of your account.', 'nuxwp-devis' ), 'margin-top:20px;' )
. self::button( $account_url, __( 'View my quotes', 'nuxwp-devis' ) )
. self::contact_line( $s );
/* translators: %s: quote number */
$preheader = sprintf( __( 'Your quote request %s has been received.', 'nuxwp-devis' ), $quote->quote_number );
return array( 'subject' => $subject, 'html' => self::wrap( $html, $preheader ) );
}
/**
* Admin: new request.
*
* @param object $quote Quote.
* @param array $items Items.
* @return array {subject, html}
*/
private static function build_admin_new( $quote, $items ) {
$user = get_userdata( $quote->user_id );
$company = $user ? get_user_meta( $user->ID, 'billing_company', true ) : '';
$phone = $user ? get_user_meta( $user->ID, 'billing_phone', true ) : '';
$name = $user ? $user->display_name : __( 'Deleted user', 'nuxwp-devis' );
$admin_url = admin_url( 'admin.php?page=devisqr-quotes#/quote/' . (int) $quote->id );
/* translators: 1: quote number, 2: customer name, 3: total */
$subject = sprintf( __( 'New quote request %1$s — %2$s (%3$s)', 'nuxwp-devis' ), $quote->quote_number, $name, DevisQR_Helpers::price_text( $quote->total_ttc ) );
$rows = array(
array( __( 'Customer', 'nuxwp-devis' ), esc_html( $name ) ),
);
if ( $company ) {
$rows[] = array( __( 'Company', 'nuxwp-devis' ), esc_html( $company ) );
}
if ( $user ) {
$rows[] = array( __( 'E-mail', 'nuxwp-devis' ), '' . esc_html( $user->user_email ) . '' );
}
if ( $phone ) {
$rows[] = array( __( 'Phone', 'nuxwp-devis' ), esc_html( $phone ) );
}
$html = self::title( __( 'New quote request received', 'nuxwp-devis' ) )
. self::quote_card( $quote, false )
. self::section_title( __( 'Customer information', 'nuxwp-devis' ) )
. self::kv_table( $rows );
if ( ! empty( $quote->customer_note ) ) {
$html .= self::note_box( __( 'Customer message', 'nuxwp-devis' ), $quote->customer_note );
}
$html .= self::section_title( __( 'Requested products', 'nuxwp-devis' ) )
. self::items_table( $items )
. self::totals_block( $quote )
. self::button( $admin_url, __( 'Process the quote', 'nuxwp-devis' ) );
return array( 'subject' => $subject, 'html' => self::wrap( $html, $subject ) );
}
/**
* Customer: status changed.
*
* @param object $quote Quote.
* @param string $name Customer display name.
* @return array {subject, html}
*/
private static function build_customer_status( $quote, $name ) {
$s = DevisQR_Settings::resolved();
$labels = DevisQR_Helpers::status_labels();
$label = isset( $labels[ $quote->status ] ) ? $labels[ $quote->status ] : $quote->status;
$account_url = wc_get_account_endpoint_url( DevisQR_Settings::account_endpoint() );
$messages = array(
'pending' => __( 'Your quote is back under review by our team.', 'nuxwp-devis' ),
'quoted' => __( 'Our team has priced your quote. You can review it in your account.', 'nuxwp-devis' ),
'accepted' => __( 'Your quote has been accepted. You can now proceed to payment from your account.', 'nuxwp-devis' ),
'rejected' => __( 'Your quote has been declined. Feel free to contact us for more information.', 'nuxwp-devis' ),
'expired' => __( 'Your quote has expired. You can submit a new quote request at any time.', 'nuxwp-devis' ),
'converted' => __( 'Your quote has been turned into an order. Thank you!', 'nuxwp-devis' ),
);
$message = isset( $messages[ $quote->status ] ) ? $messages[ $quote->status ] : $messages['quoted'];
/* translators: 1: quote number, 2: status label, 3: site name */
$subject = sprintf( __( 'Quote %1$s — %2$s — %3$s', 'nuxwp-devis' ), $quote->quote_number, $label, self::site_name() );
$cta_text = 'accepted' === $quote->status ? __( 'Accept and pay', 'nuxwp-devis' ) : __( 'View my quote', 'nuxwp-devis' );
$html = self::greeting( $name )
. self::p( esc_html( $message ) )
. self::quote_card( $quote, in_array( $quote->status, array( 'quoted', 'accepted' ), true ), $label )
. self::totals_block( $quote );
if ( ! empty( $quote->admin_note ) ) {
$html .= self::note_box( __( 'Message from our team', 'nuxwp-devis' ), $quote->admin_note );
}
$html .= self::button( $account_url, $cta_text )
. self::contact_line( $s );
/* translators: 1: quote number, 2: status label */
$preheader = sprintf( __( 'Quote %1$s: %2$s', 'nuxwp-devis' ), $quote->quote_number, $label );
return array( 'subject' => $subject, 'html' => self::wrap( $html, $preheader ) );
}
/* ------------------------------------------------------------------ */
/* Layout */
/* ------------------------------------------------------------------ */
/**
* Accent colour: cart button background, filterable.
*
* @return string
*/
private static function accent() {
$style = DevisQR_Settings::get( 'style_cart' );
$accent = ( is_array( $style ) && ! empty( $style['bg'] ) ) ? self::hex( $style['bg'] ) : '';
$accent = $accent ? $accent : '#1e293b';
/**
* Filter the accent colour used in e-mails (header line, buttons).
*
* @param string $accent Hex colour.
*/
$filtered = self::hex( (string) apply_filters( 'devisqr_email_accent_color', $accent ) );
return $filtered ? $filtered : $accent;
}
/**
* Normalise a hex colour (#abc or #aabbcc) to 6 digits, or '' when invalid.
*
* @param string $value Colour.
* @return string
*/
private static function hex( $value ) {
$value = trim( (string) $value );
if ( preg_match( '/\A#([0-9a-fA-F]{3})\z/', $value, $m ) ) {
return '#' . $m[1][0] . $m[1][0] . $m[1][1] . $m[1][1] . $m[1][2] . $m[1][2];
}
return preg_match( '/\A#[0-9a-fA-F]{6}\z/', $value ) ? $value : '';
}
/**
* Decoded site name.
*
* @return string
*/
private static function site_name() {
return wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES );
}
/**
* Logo URL (see DevisQR_Helpers::logo()).
*
* @return string
*/
private static function logo_url() {
$logo = DevisQR_Helpers::logo();
return $logo ? $logo['url'] : '';
}
/**
* Page wrapper: logo/name, accent rule, white card, company footer.
*
* @param string $content Inner HTML.
* @param string $preheader Hidden preview text.
* @return string
*/
private static function wrap( $content, $preheader = '' ) {
$s = DevisQR_Settings::resolved();
$site_name = self::site_name();
$site_url = home_url();
$accent = self::accent();
$logo = self::logo_url();
$brand = $logo
? ''
: '' . esc_html( $site_name ) . '';
$address = array_filter( array( $s['company_address'], trim( $s['company_postcode'] . ' ' . $s['company_city'] ) ) );
$footer = '
' . esc_html( $s['company_name'] ) . ''
. ( $address ? '
' . esc_html( implode( ' · ', $address ) ) : '' )
. ( $s['company_phone'] ? '
' . esc_html( $s['company_phone'] ) : '' )
. '
' /* translators: %s: site link */ . sprintf( esc_html__( 'This message was sent by %s', 'nuxwp-devis' ), '' . esc_html( $site_name ) . '' ) . '
'; return '
|
' . $html . '
'; } /** * Greeting line. * * @param string $name Customer name. * @return string */ private static function greeting( $name ) { /* translators: %s: customer name */ return self::p( sprintf( esc_html__( 'Hello %s,', 'nuxwp-devis' ), esc_html( $name ) ), 'margin-bottom:16px;' ); } /** * Main title (admin mail). * * @param string $text Title. * @return string */ private static function title( $text ) { return '' . esc_html( $text ) . '
'; } /** * Section title. * * @param string $text Title. * @return string */ private static function section_title( $text ) { return '' . esc_html( $text ) . '
'; } /** * Quote card: number · date · (valid until). * * @param object $quote Quote. * @param bool $with_exp Show the expiry date. * @param string $status Status label to show (status e-mail), or ''. * @return string */ private static function quote_card( $quote, $with_exp, $status = '' ) { $cells = array( array( __( 'Quote number', 'nuxwp-devis' ), $quote->quote_number ), ); if ( $status ) { $cells[] = array( __( 'Status', 'nuxwp-devis' ), $status ); } $cells[] = array( __( 'Date', 'nuxwp-devis' ), DevisQR_Helpers::date_text( $quote->created_at ) ); if ( $with_exp && ! empty( $quote->expires_at ) ) { $cells[] = array( __( 'Valid until', 'nuxwp-devis' ), DevisQR_Helpers::date_text( $quote->expires_at ) ); } $html = '|
' . esc_html( $cell[0] ) . ' ' . esc_html( $cell[1] ) . ' | ';
}
return $html . '
| ' . esc_html( $row[0] ) . ' | ' . $row[1] . ' |
| ' . esc_html__( 'Product', 'nuxwp-devis' ) . ' | ' . esc_html__( 'Qty', 'nuxwp-devis' ) . ' | ' . esc_html__( 'Total incl. tax', 'nuxwp-devis' ) . ' |
| ' . esc_html( $item->product_name ) . $sku . ' | ' . esc_html( DevisQR_Helpers::qty_text( $item->quantity ) ) . ' | ' . esc_html( DevisQR_Helpers::price_text( $item->line_total_ttc ) ) . ' |
| ' . esc_html__( 'Global discount', 'nuxwp-devis' ) . ' | −' . esc_html( DevisQR_Helpers::percent_text( $quote->discount_percent ) ) . ' |
| ' . esc_html__( 'Total excl. tax', 'nuxwp-devis' ) . ' | ' . esc_html( DevisQR_Helpers::price_text( $quote->total_ht ) ) . ' |
| ' . esc_html__( 'Tax', 'nuxwp-devis' ) . ' | ' . esc_html( DevisQR_Helpers::price_text( $quote->total_tax ) ) . ' |
| ' . esc_html__( 'Total incl. tax', 'nuxwp-devis' ) . ' | ' . esc_html( DevisQR_Helpers::price_text( $quote->total_ttc ) ) . ' |
|
' . esc_html( $label ) . ' ' . nl2br( esc_html( $text ) ) . ' |
| ' . esc_html( $text ) . ' |
' . sprintf( esc_html__( 'Any question? Contact us at %s', 'nuxwp-devis' ), $link ) . '
'; } /* ------------------------------------------------------------------ */ /* Transport */ /* ------------------------------------------------------------------ */ /** * Headers for customer e-mails. * * @return array */ private static function headers() { $s = DevisQR_Settings::resolved(); $name = str_replace( array( '<', '>', '"', "\r", "\n" ), '', self::site_name() ); // wp_mail() splits the From header on '<'. return array( 'Content-Type: text/html; charset=UTF-8', 'From: ' . trim( $name ) . ' <' . $s['from_email'] . '>', 'Reply-To: ' . $s['contact_email'], ); } /** * Attachments (PDF if present). * * @param object $quote Quote. * @return array */ private static function pdf_attachment( $quote ) { if ( empty( $quote->pdf_path ) ) { return array(); } $path = DevisQR_Database::resolve_storage_path( $quote->pdf_path, 'pdf' ); return $path ? array( $path ) : array(); } }