array( 'name' => __( 'Name', 'abstract-one-crm-form-integration' ), 'mobile_number' => __( 'Mobile Number', 'abstract-one-crm-form-integration' ), ), 'optional' => array( 'alternate_number' => __( 'Alternate Number', 'abstract-one-crm-form-integration' ), 'email' => __( 'Email', 'abstract-one-crm-form-integration' ), 'notes' => __( 'Notes', 'abstract-one-crm-form-integration' ), 'profession' => __( 'Profession', 'abstract-one-crm-form-integration' ), 'location' => __( 'Location', 'abstract-one-crm-form-integration' ), 'utm_id' => __( 'UTM ID', 'abstract-one-crm-form-integration' ), 'utm_campaign' => __( 'UTM Campaign', 'abstract-one-crm-form-integration' ), 'utm_source' => __( 'UTM Source', 'abstract-one-crm-form-integration' ), 'utm_medium' => __( 'UTM Medium', 'abstract-one-crm-form-integration' ), 'utm_term' => __( 'UTM Term', 'abstract-one-crm-form-integration' ), ), ); } /** * Static CRM config fields stored per workflow. * * @return array */ public static function crm_config_fields() { return array( 'project_id' => array( 'label' => __( 'Project ID', 'abstract-one-crm-form-integration' ), 'doc' => 'https://docs.abstractone.in/docs/developer/get-project-id', ), 'campaign_id' => array( 'label' => __( 'Campaign ID', 'abstract-one-crm-form-integration' ), 'doc' => 'https://docs.abstractone.in/docs/developer/get-campaign-id', ), 'sub_source' => array( 'label' => __( 'Sub Source', 'abstract-one-crm-form-integration' ), 'doc' => 'https://docs.abstractone.in/docs/developer/get-sub-source', 'default' => 'Organic', ), ); } /** * Get workflow by ID. * * @param int $id Workflow ID. * @return array|null */ public static function get( $id ) { global $wpdb; $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM ' . AOLI_Database::table_name() . ' WHERE id = %d', absint( $id ) ), ARRAY_A ); return $row ? self::hydrate( $row ) : null; } /** * Get active workflows for a form. * * @param string $form_plugin Plugin slug. * @param string $form_id Form ID. * @return array */ public static function get_active_for_form( $form_plugin, $form_id ) { global $wpdb; $rows = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM ' . AOLI_Database::table_name() . ' WHERE form_plugin = %s AND form_id = %s AND status = %s', sanitize_key( $form_plugin ), sanitize_text_field( $form_id ), 'active' ), ARRAY_A ); return array_map( array( __CLASS__, 'hydrate' ), $rows ? $rows : array() ); } /** * List workflows with optional search. * * @param array $args Query args. * @return array{items: array, total: int} */ public static function list( $args = array() ) { global $wpdb; $defaults = array( 'search' => '', 'status' => '', 'per_page' => 20, 'page' => 1, 'orderby' => 'updated_at', 'order' => 'DESC', ); $args = wp_parse_args( $args, $defaults ); $table = AOLI_Database::table_name(); $where = array( '1=1' ); $params = array(); if ( '' !== $args['search'] ) { $where[] = '(name LIKE %s OR description LIKE %s)'; $like = '%' . $wpdb->esc_like( $args['search'] ) . '%'; $params[] = $like; $params[] = $like; } if ( in_array( $args['status'], array( 'active', 'inactive' ), true ) ) { $where[] = 'status = %s'; $params[] = $args['status']; } $allowed_orderby = array( 'name', 'form_plugin', 'status', 'created_at', 'updated_at' ); $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'updated_at'; $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; $where_sql = implode( ' AND ', $where ); $count_sql = "SELECT COUNT(*) FROM {$table} WHERE {$where_sql}"; $total = empty( $params ) ? (int) $wpdb->get_var( $count_sql ) : (int) $wpdb->get_var( $wpdb->prepare( $count_sql, $params ) ); $offset = max( 0, ( (int) $args['page'] - 1 ) * (int) $args['per_page'] ); $per_page = max( 1, (int) $args['per_page'] ); $list_sql = "SELECT * FROM {$table} WHERE {$where_sql} ORDER BY {$orderby} {$order} LIMIT %d OFFSET %d"; $params[] = $per_page; $params[] = $offset; $rows = $wpdb->get_results( $wpdb->prepare( $list_sql, $params ), ARRAY_A ); return array( 'items' => array_map( array( __CLASS__, 'hydrate' ), $rows ? $rows : array() ), 'total' => $total, ); } /** * Save workflow (insert or update). * * @param array $data Workflow data. * @return int|false Workflow ID or false. */ public static function save( $data ) { global $wpdb; $id = isset( $data['id'] ) ? absint( $data['id'] ) : 0; $field_mapping = isset( $data['field_mapping'] ) && is_array( $data['field_mapping'] ) ? $data['field_mapping'] : array(); $row = array( 'name' => sanitize_text_field( $data['name'] ?? '' ), 'description' => sanitize_textarea_field( $data['description'] ?? '' ), 'form_plugin' => sanitize_key( $data['form_plugin'] ?? '' ), 'form_id' => sanitize_text_field( $data['form_id'] ?? '' ), 'project_id' => sanitize_text_field( $data['project_id'] ?? '' ), 'campaign_id' => sanitize_text_field( $data['campaign_id'] ?? '' ), 'sub_source' => sanitize_text_field( $data['sub_source'] ?? 'Organic' ), 'field_mapping' => wp_json_encode( $field_mapping ), 'status' => ( isset( $data['status'] ) && 'inactive' === $data['status'] ) ? 'inactive' : 'active', ); if ( ! empty( $data['api_key'] ) ) { $row['api_key_enc'] = AOLI_Encryption::encrypt( $data['api_key'] ); } if ( $id > 0 ) { if ( empty( $data['api_key'] ) ) { unset( $row['api_key_enc'] ); } $updated = $wpdb->update( AOLI_Database::table_name(), $row, array( 'id' => $id ), null, array( '%d' ) ); return false !== $updated ? $id : false; } if ( empty( $row['api_key_enc'] ) ) { return false; } $inserted = $wpdb->insert( AOLI_Database::table_name(), $row ); return $inserted ? (int) $wpdb->insert_id : false; } /** * Delete workflows by IDs. * * @param array $ids Workflow IDs. * @return int Number deleted. */ public static function delete( $ids ) { global $wpdb; $ids = array_filter( array_map( 'absint', (array) $ids ) ); if ( empty( $ids ) ) { return 0; } $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $sql = 'DELETE FROM ' . AOLI_Database::table_name() . " WHERE id IN ({$placeholders})"; return (int) $wpdb->query( $wpdb->prepare( $sql, $ids ) ); } /** * Update status for workflows. * * @param array $ids Workflow IDs. * @param string $status active|inactive. * @return int */ public static function update_status( $ids, $status ) { global $wpdb; $ids = array_filter( array_map( 'absint', (array) $ids ) ); $status = 'inactive' === $status ? 'inactive' : 'active'; if ( empty( $ids ) ) { return 0; } $placeholders = implode( ',', array_fill( 0, count( $ids ), '%d' ) ); $params = array_merge( array( $status ), $ids ); $sql = 'UPDATE ' . AOLI_Database::table_name() . " SET status = %s WHERE id IN ({$placeholders})"; return (int) $wpdb->query( $wpdb->prepare( $sql, $params ) ); } /** * Validate workflow data before save. * * @param array $data Workflow data. * @param bool $is_update Is update. * @return array Errors keyed by field. */ public static function validate( $data, $is_update = false ) { $errors = array(); if ( empty( $data['name'] ) ) { $errors['name'] = __( 'Workflow name is required.', 'abstract-one-crm-form-integration' ); } if ( ! in_array( $data['form_plugin'] ?? '', array( 'cf7', 'wpforms' ), true ) ) { $errors['form_plugin'] = __( 'Please select a form plugin.', 'abstract-one-crm-form-integration' ); } if ( empty( $data['form_id'] ) ) { $errors['form_id'] = __( 'Please select a form.', 'abstract-one-crm-form-integration' ); } if ( ! $is_update && empty( $data['api_key'] ) ) { $errors['api_key'] = __( 'API key is required.', 'abstract-one-crm-form-integration' ); } if ( empty( $data['project_id'] ) ) { $errors['project_id'] = __( 'Project ID is required.', 'abstract-one-crm-form-integration' ); } if ( empty( $data['campaign_id'] ) ) { $errors['campaign_id'] = __( 'Campaign ID is required.', 'abstract-one-crm-form-integration' ); } if ( empty( $data['sub_source'] ) ) { $errors['sub_source'] = __( 'Sub source is required.', 'abstract-one-crm-form-integration' ); } $mapping = isset( $data['field_mapping'] ) && is_array( $data['field_mapping'] ) ? $data['field_mapping'] : array(); if ( empty( $mapping['name'] ) ) { $errors['field_mapping_name'] = __( 'Name field mapping is required.', 'abstract-one-crm-form-integration' ); } if ( empty( $mapping['mobile_number'] ) ) { $errors['field_mapping_mobile_number'] = __( 'Mobile number field mapping is required.', 'abstract-one-crm-form-integration' ); } return $errors; } /** * Hydrate DB row. * * @param array $row Raw row. * @return array */ private static function hydrate( $row ) { $row['field_mapping'] = json_decode( $row['field_mapping'] ?? '{}', true ); if ( ! is_array( $row['field_mapping'] ) ) { $row['field_mapping'] = array(); } $row['api_key'] = AOLI_Encryption::decrypt( $row['api_key_enc'] ?? '' ); unset( $row['api_key_enc'] ); return $row; } }