WP maple cart and page subtitle plugin upload
This commit is contained in:
parent
b3e87772ec
commit
c85895d306
18 changed files with 5741 additions and 0 deletions
|
|
@ -0,0 +1,534 @@
|
|||
<?php
|
||||
/**
|
||||
* Mailjet Integration for Maple Carts.
|
||||
*
|
||||
* Syncs abandoned cart data to Mailjet contact properties
|
||||
* for use in Mailjet automations.
|
||||
*
|
||||
* @package MapleCarts
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Mailjet integration class.
|
||||
*/
|
||||
class Maple_Carts_Mailjet {
|
||||
|
||||
/**
|
||||
* Single instance.
|
||||
*
|
||||
* @var Maple_Carts_Mailjet
|
||||
*/
|
||||
private static $instance = null;
|
||||
|
||||
/**
|
||||
* Mailjet API base URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $api_url = 'https://api.mailjet.com/v3/REST';
|
||||
|
||||
/**
|
||||
* Contact properties to sync.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $contact_properties = [
|
||||
'maple_has_abandoned_cart' => 'bool',
|
||||
'maple_cart_total' => 'float',
|
||||
'maple_cart_currency' => 'str',
|
||||
'maple_cart_product_count' => 'int',
|
||||
'maple_cart_product_names' => 'str',
|
||||
'maple_cart_categories' => 'str',
|
||||
'maple_cart_value_level' => 'str',
|
||||
'maple_abandonment_type' => 'str',
|
||||
'maple_cart_recovery_url' => 'str',
|
||||
'maple_cart_first_seen_at' => 'datetime',
|
||||
'maple_cart_abandoned_at' => 'datetime',
|
||||
'maple_cart_coupon_code' => 'str',
|
||||
'maple_customer_first_name' => 'str',
|
||||
'maple_customer_last_name' => 'str',
|
||||
'maple_recovered_via' => 'str',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get instance.
|
||||
*
|
||||
* @return Maple_Carts_Mailjet
|
||||
*/
|
||||
public static function instance() {
|
||||
if ( is_null( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
// Admin AJAX hooks - always register so settings page works.
|
||||
add_action( 'wp_ajax_maple_carts_test_mailjet', [ $this, 'ajax_test_connection' ] );
|
||||
add_action( 'wp_ajax_maple_carts_setup_mailjet_properties', [ $this, 'ajax_setup_properties' ] );
|
||||
|
||||
// Only initialize sync hooks if Mailjet mode is enabled.
|
||||
if ( 'mailjet' !== Maple_Carts::get_option( 'email_mode', 'builtin' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Hook into cart status changes.
|
||||
add_action( 'maple_carts_cart_abandoned', [ $this, 'sync_abandoned_cart' ], 10, 2 );
|
||||
add_action( 'maple_carts_cart_recovered', [ $this, 'clear_abandoned_cart' ], 10, 2 );
|
||||
add_action( 'maple_carts_cart_converted', [ $this, 'clear_abandoned_cart' ], 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Mailjet is configured.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function is_configured() {
|
||||
$api_key = Maple_Carts::get_option( 'mailjet_api_key', '' );
|
||||
$secret_key = Maple_Carts::get_option( 'mailjet_secret_key', '' );
|
||||
|
||||
return ! empty( $api_key ) && ! empty( $secret_key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request to Mailjet.
|
||||
*
|
||||
* @param string $endpoint API endpoint.
|
||||
* @param string $method HTTP method.
|
||||
* @param array $data Request data.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private function api_request( $endpoint, $method = 'GET', $data = [] ) {
|
||||
$api_key = Maple_Carts::get_option( 'mailjet_api_key', '' );
|
||||
$secret_key = Maple_Carts::get_option( 'mailjet_secret_key', '' );
|
||||
|
||||
if ( empty( $api_key ) || empty( $secret_key ) ) {
|
||||
return new WP_Error( 'not_configured', __( 'Mailjet API credentials not configured.', 'maple-carts' ) );
|
||||
}
|
||||
|
||||
$url = $this->api_url . $endpoint;
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'headers' => [
|
||||
'Authorization' => 'Basic ' . base64_encode( $api_key . ':' . $secret_key ),
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'timeout' => 30,
|
||||
];
|
||||
|
||||
if ( ! empty( $data ) && in_array( $method, [ 'POST', 'PUT' ], true ) ) {
|
||||
$args['body'] = wp_json_encode( $data );
|
||||
}
|
||||
|
||||
$response = wp_remote_request( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
if ( $code >= 400 ) {
|
||||
$error_message = isset( $data['ErrorMessage'] ) ? $data['ErrorMessage'] : __( 'Unknown API error', 'maple-carts' );
|
||||
return new WP_Error( 'api_error', $error_message, [ 'status' => $code ] );
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup contact properties in Mailjet.
|
||||
* Should be run once during initial setup.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function setup_contact_properties() {
|
||||
$results = [];
|
||||
|
||||
foreach ( $this->contact_properties as $name => $type ) {
|
||||
// Check if property already exists.
|
||||
$existing = $this->api_request( '/contactmetadata/' . $name );
|
||||
|
||||
if ( ! is_wp_error( $existing ) && isset( $existing['Data'][0] ) ) {
|
||||
$results[ $name ] = 'exists';
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create the property.
|
||||
$datatype = $this->get_mailjet_datatype( $type );
|
||||
$response = $this->api_request( '/contactmetadata', 'POST', [
|
||||
'Name' => $name,
|
||||
'Datatype' => $datatype,
|
||||
'NameSpace' => 'static',
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$results[ $name ] = 'error: ' . $response->get_error_message();
|
||||
} else {
|
||||
$results[ $name ] = 'created';
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Mailjet datatype from our type.
|
||||
*
|
||||
* @param string $type Our type.
|
||||
* @return string Mailjet datatype.
|
||||
*/
|
||||
private function get_mailjet_datatype( $type ) {
|
||||
$map = [
|
||||
'bool' => 'bool',
|
||||
'int' => 'int',
|
||||
'float' => 'float',
|
||||
'str' => 'str',
|
||||
'datetime' => 'datetime',
|
||||
];
|
||||
|
||||
return isset( $map[ $type ] ) ? $map[ $type ] : 'str';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync abandoned cart data to Mailjet.
|
||||
*
|
||||
* @param int $cart_id Cart ID.
|
||||
* @param object $cart Cart object.
|
||||
*/
|
||||
public function sync_abandoned_cart( $cart_id, $cart ) {
|
||||
if ( empty( $cart->email ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse cart contents for product names and categories.
|
||||
$cart_contents = maybe_unserialize( $cart->cart_contents );
|
||||
$product_names = [];
|
||||
$categories = [];
|
||||
|
||||
if ( is_array( $cart_contents ) ) {
|
||||
foreach ( $cart_contents as $item ) {
|
||||
$product_names[] = $item['name'] ?? 'Product';
|
||||
|
||||
// Get product categories.
|
||||
$product_id = $item['product_id'] ?? 0;
|
||||
if ( $product_id ) {
|
||||
$terms = get_the_terms( $product_id, 'product_cat' );
|
||||
if ( $terms && ! is_wp_error( $terms ) ) {
|
||||
foreach ( $terms as $term ) {
|
||||
$categories[ $term->name ] = true; // Use as keys to dedupe.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse billing data for name.
|
||||
$billing_data = maybe_unserialize( $cart->billing_data );
|
||||
$first_name = $billing_data['first_name'] ?? '';
|
||||
$last_name = $billing_data['last_name'] ?? '';
|
||||
|
||||
// Calculate value level for segmentation.
|
||||
$cart_total = (float) $cart->cart_total;
|
||||
$value_level = $this->get_cart_value_level( $cart_total );
|
||||
|
||||
// Generate recovery URL with coupon and UTM source for attribution.
|
||||
$recovery_url = Maple_Carts_Tracking::instance()->generate_recovery_url(
|
||||
$cart->session_id,
|
||||
$cart->coupon_code ?? ''
|
||||
);
|
||||
// Add UTM source for recovery attribution.
|
||||
$recovery_url = add_query_arg( 'utm_source', 'mailjet', $recovery_url );
|
||||
|
||||
// Prepare contact data using items_count from database (no JSON parsing needed).
|
||||
$contact_data = [
|
||||
'maple_has_abandoned_cart' => true,
|
||||
'maple_cart_total' => $cart_total,
|
||||
'maple_cart_currency' => $cart->currency ?? 'USD',
|
||||
'maple_cart_product_count' => (int) ( $cart->items_count ?? 0 ),
|
||||
'maple_cart_product_names' => implode( ', ', array_slice( $product_names, 0, 5 ) ), // Limit to 5.
|
||||
'maple_cart_categories' => implode( ', ', array_slice( array_keys( $categories ), 0, 5 ) ), // Limit to 5.
|
||||
'maple_cart_value_level' => $value_level,
|
||||
'maple_abandonment_type' => $cart->abandonment_type ?? 'checkout',
|
||||
'maple_cart_recovery_url' => $recovery_url,
|
||||
'maple_cart_first_seen_at' => $cart->first_seen_at ?? '',
|
||||
'maple_cart_abandoned_at' => $cart->abandoned_at ?? current_time( 'c' ),
|
||||
'maple_cart_coupon_code' => $cart->coupon_code ?? '',
|
||||
'maple_customer_first_name' => $first_name,
|
||||
'maple_customer_last_name' => $last_name,
|
||||
'maple_recovered_via' => '', // Clear any previous recovery.
|
||||
];
|
||||
|
||||
$this->update_contact( $cart->email, $contact_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cart value level for segmentation.
|
||||
*
|
||||
* @param float $total Cart total.
|
||||
* @return string Value level: low, medium, or high.
|
||||
*/
|
||||
private function get_cart_value_level( $total ) {
|
||||
if ( $total < 20 ) {
|
||||
return 'low';
|
||||
} elseif ( $total < 80 ) {
|
||||
return 'medium';
|
||||
} else {
|
||||
return 'high';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear abandoned cart data from Mailjet contact.
|
||||
*
|
||||
* @param int $cart_id Cart ID.
|
||||
* @param object $cart Cart object.
|
||||
*/
|
||||
public function clear_abandoned_cart( $cart_id, $cart ) {
|
||||
if ( empty( $cart->email ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Map recovery_source to Mailjet-friendly value.
|
||||
$recovered_via = '';
|
||||
if ( 'recovered' === $cart->status && ! empty( $cart->recovery_source ) ) {
|
||||
$recovered_via = $cart->recovery_source; // mailjet, builtin_email, email_link, direct.
|
||||
} elseif ( 'converted' === $cart->status ) {
|
||||
$recovered_via = 'purchase'; // Direct purchase without abandonment.
|
||||
}
|
||||
|
||||
// Clear the abandoned cart flag and data, but record recovery attribution.
|
||||
$contact_data = [
|
||||
'maple_has_abandoned_cart' => false,
|
||||
'maple_cart_total' => 0,
|
||||
'maple_cart_currency' => '',
|
||||
'maple_cart_product_count' => 0,
|
||||
'maple_cart_product_names' => '',
|
||||
'maple_cart_categories' => '',
|
||||
'maple_cart_value_level' => '',
|
||||
'maple_abandonment_type' => '',
|
||||
'maple_cart_recovery_url' => '',
|
||||
'maple_cart_first_seen_at' => '',
|
||||
'maple_cart_abandoned_at' => '',
|
||||
'maple_cart_coupon_code' => '',
|
||||
'maple_customer_first_name' => '',
|
||||
'maple_customer_last_name' => '',
|
||||
'maple_recovered_via' => $recovered_via,
|
||||
];
|
||||
|
||||
$this->update_contact( $cart->email, $contact_data );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update or create contact in Mailjet.
|
||||
*
|
||||
* @param string $email Contact email.
|
||||
* @param array $contact_data Contact properties.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private function update_contact( $email, $contact_data ) {
|
||||
// First, ensure contact exists.
|
||||
$contact = $this->get_or_create_contact( $email );
|
||||
|
||||
if ( is_wp_error( $contact ) ) {
|
||||
$this->log_error( 'Failed to get/create contact: ' . $contact->get_error_message() );
|
||||
return $contact;
|
||||
}
|
||||
|
||||
// Format data for Mailjet.
|
||||
$formatted_data = [];
|
||||
foreach ( $contact_data as $name => $value ) {
|
||||
$formatted_data[] = [
|
||||
'Name' => $name,
|
||||
'Value' => $value,
|
||||
];
|
||||
}
|
||||
|
||||
// Update contact data.
|
||||
$response = $this->api_request(
|
||||
'/contactdata/' . $contact['ID'],
|
||||
'PUT',
|
||||
[ 'Data' => $formatted_data ]
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$this->log_error( 'Failed to update contact data: ' . $response->get_error_message() );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create contact in Mailjet.
|
||||
*
|
||||
* @param string $email Contact email.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private function get_or_create_contact( $email ) {
|
||||
// Try to get existing contact.
|
||||
$response = $this->api_request( '/contact/' . urlencode( $email ) );
|
||||
|
||||
if ( ! is_wp_error( $response ) && isset( $response['Data'][0] ) ) {
|
||||
return $response['Data'][0];
|
||||
}
|
||||
|
||||
// Create new contact.
|
||||
$response = $this->api_request( '/contact', 'POST', [
|
||||
'Email' => $email,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if ( isset( $response['Data'][0] ) ) {
|
||||
return $response['Data'][0];
|
||||
}
|
||||
|
||||
return new WP_Error( 'create_failed', __( 'Failed to create contact', 'maple-carts' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test Mailjet connection.
|
||||
*
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
public function test_connection() {
|
||||
return $this->api_request( '/user' );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for testing Mailjet connection.
|
||||
*/
|
||||
public function ajax_test_connection() {
|
||||
check_ajax_referer( 'maple_carts_admin', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_send_json_error( 'Unauthorized' );
|
||||
}
|
||||
|
||||
// Temporarily set credentials from POST for testing.
|
||||
$api_key = isset( $_POST['api_key'] ) ? sanitize_text_field( wp_unslash( $_POST['api_key'] ) ) : '';
|
||||
$secret_key = isset( $_POST['secret_key'] ) ? sanitize_text_field( wp_unslash( $_POST['secret_key'] ) ) : '';
|
||||
|
||||
if ( empty( $api_key ) || empty( $secret_key ) ) {
|
||||
wp_send_json_error( __( 'Please enter both API Key and Secret Key.', 'maple-carts' ) );
|
||||
}
|
||||
|
||||
// Make test request.
|
||||
$url = $this->api_url . '/user';
|
||||
|
||||
$response = wp_remote_get( $url, [
|
||||
'headers' => [
|
||||
'Authorization' => 'Basic ' . base64_encode( $api_key . ':' . $secret_key ),
|
||||
],
|
||||
'timeout' => 15,
|
||||
] );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
wp_send_json_error( $response->get_error_message() );
|
||||
}
|
||||
|
||||
$code = wp_remote_retrieve_response_code( $response );
|
||||
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
|
||||
if ( 200 === $code && isset( $body['Data'][0] ) ) {
|
||||
$user = $body['Data'][0];
|
||||
wp_send_json_success( [
|
||||
'message' => sprintf(
|
||||
/* translators: %s: Mailjet username */
|
||||
__( 'Connected successfully! Account: %s', 'maple-carts' ),
|
||||
$user['Username'] ?? $user['Email'] ?? 'Unknown'
|
||||
),
|
||||
] );
|
||||
} else {
|
||||
wp_send_json_error( __( 'Authentication failed. Please check your API credentials.', 'maple-carts' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX handler for setting up Mailjet contact properties.
|
||||
*/
|
||||
public function ajax_setup_properties() {
|
||||
check_ajax_referer( 'maple_carts_admin', 'nonce' );
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_send_json_error( 'Unauthorized' );
|
||||
}
|
||||
|
||||
$results = $this->setup_contact_properties();
|
||||
|
||||
if ( is_wp_error( $results ) ) {
|
||||
wp_send_json_error( $results->get_error_message() );
|
||||
}
|
||||
|
||||
$created = 0;
|
||||
$existed = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ( $results as $status ) {
|
||||
if ( 'created' === $status ) {
|
||||
$created++;
|
||||
} elseif ( 'exists' === $status ) {
|
||||
$existed++;
|
||||
} else {
|
||||
$errors++;
|
||||
}
|
||||
}
|
||||
|
||||
wp_send_json_success( [
|
||||
'message' => sprintf(
|
||||
/* translators: %1$d: created count, %2$d: existed count, %3$d: error count */
|
||||
__( 'Properties setup complete. Created: %1$d, Already existed: %2$d, Errors: %3$d', 'maple-carts' ),
|
||||
$created,
|
||||
$existed,
|
||||
$errors
|
||||
),
|
||||
'details' => $results,
|
||||
] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log error for debugging.
|
||||
*
|
||||
* @param string $message Error message.
|
||||
*/
|
||||
private function log_error( $message ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
error_log( 'Maple Carts Mailjet: ' . $message );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available contact properties for display.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available_properties() {
|
||||
return [
|
||||
'maple_has_abandoned_cart' => __( 'Has abandoned cart (true/false)', 'maple-carts' ),
|
||||
'maple_cart_total' => __( 'Cart total value', 'maple-carts' ),
|
||||
'maple_cart_currency' => __( 'Cart currency code', 'maple-carts' ),
|
||||
'maple_cart_product_count' => __( 'Number of items in cart', 'maple-carts' ),
|
||||
'maple_cart_product_names' => __( 'Product names (comma separated)', 'maple-carts' ),
|
||||
'maple_cart_categories' => __( 'Product categories (comma separated)', 'maple-carts' ),
|
||||
'maple_cart_value_level' => __( 'Cart value: low (<$20), medium ($20-80), high ($80+)', 'maple-carts' ),
|
||||
'maple_abandonment_type' => __( 'Abandonment stage: checkout or payment', 'maple-carts' ),
|
||||
'maple_cart_recovery_url' => __( 'Recovery URL with coupon', 'maple-carts' ),
|
||||
'maple_cart_first_seen_at' => __( 'First seen date/time (funnel entry)', 'maple-carts' ),
|
||||
'maple_cart_abandoned_at' => __( 'Abandonment date/time', 'maple-carts' ),
|
||||
'maple_cart_coupon_code' => __( 'Generated coupon code', 'maple-carts' ),
|
||||
'maple_customer_first_name' => __( 'Customer first name', 'maple-carts' ),
|
||||
'maple_customer_last_name' => __( 'Customer last name', 'maple-carts' ),
|
||||
'maple_recovered_via' => __( 'Recovery attribution: mailjet, builtin_email, email_link, direct, purchase', 'maple-carts' ),
|
||||
];
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue