Initial commit: WooCommerce Email Discount Manager plugin
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
/**
|
||||
* Audit log admin view.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays recorded REST API activity.
|
||||
*/
|
||||
class WC_EDM_Admin_Audit {
|
||||
|
||||
/**
|
||||
* Entries shown per page.
|
||||
*/
|
||||
const PER_PAGE = 50;
|
||||
|
||||
/**
|
||||
* Handle the clear-log action. Runs on admin_init.
|
||||
*/
|
||||
public function handle_actions() {
|
||||
if ( ! isset( $_GET['action'] ) || 'clear_audit_log' !== $_GET['action'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_admin_referer( 'wc_edm_clear_audit_log' );
|
||||
|
||||
WC_EDM_DB::clear_audit_log();
|
||||
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'audit', 'message' => 'deleted' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the audit log tab.
|
||||
*/
|
||||
public function render() {
|
||||
$outcome = isset( $_GET['outcome'] ) ? sanitize_key( wp_unslash( $_GET['outcome'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$outcome = in_array( $outcome, array( 'allowed', 'denied', 'rate_limited' ), true ) ? $outcome : '';
|
||||
|
||||
$paged = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$offset = ( $paged - 1 ) * self::PER_PAGE;
|
||||
|
||||
$entries = WC_EDM_DB::get_audit_entries( self::PER_PAGE, $offset, $outcome );
|
||||
$total = WC_EDM_DB::count_audit_entries( $outcome );
|
||||
|
||||
$counts = array(
|
||||
'' => WC_EDM_DB::count_audit_entries(),
|
||||
'allowed' => WC_EDM_DB::count_audit_entries( 'allowed' ),
|
||||
'denied' => WC_EDM_DB::count_audit_entries( 'denied' ),
|
||||
'rate_limited' => WC_EDM_DB::count_audit_entries( 'rate_limited' ),
|
||||
);
|
||||
|
||||
$labels = array(
|
||||
'' => __( 'All', 'wc-email-discount' ),
|
||||
'allowed' => __( 'Changes', 'wc-email-discount' ),
|
||||
'denied' => __( 'Rejected', 'wc-email-discount' ),
|
||||
'rate_limited' => __( 'Rate limited', 'wc-email-discount' ),
|
||||
);
|
||||
|
||||
$clear_url = wp_nonce_url(
|
||||
WC_EDM_Helpers::admin_url( array( 'tab' => 'audit', 'action' => 'clear_audit_log' ) ),
|
||||
'wc_edm_clear_audit_log'
|
||||
);
|
||||
?>
|
||||
<div class="wc-edm-header-row">
|
||||
<h2><?php esc_html_e( 'API Audit Log', 'wc-email-discount' ); ?></h2>
|
||||
<?php if ( $counts[''] > 0 ) : ?>
|
||||
<a class="button wc-edm-delete" href="<?php echo esc_url( $clear_url ); ?>">
|
||||
<?php esc_html_e( 'Clear Log', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<p class="description">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: retention in days */
|
||||
esc_html__( 'Records every change made through the REST API, plus rejected and rate limited requests. Entries older than %d days are removed automatically.', 'wc-email-discount' ),
|
||||
(int) WC_EDM_Audit_Log::retention_days()
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
|
||||
<ul class="subsubsub">
|
||||
<?php $first = true; ?>
|
||||
<?php foreach ( $labels as $key => $label ) : ?>
|
||||
<li>
|
||||
<?php echo $first ? '' : ' | '; ?>
|
||||
<a href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array_filter( array( 'tab' => 'audit', 'outcome' => $key ) ) ) ); ?>"
|
||||
class="<?php echo $outcome === $key ? 'current' : ''; ?>">
|
||||
<?php echo esc_html( $label ); ?>
|
||||
<span class="count">(<?php echo esc_html( $counts[ $key ] ); ?>)</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php $first = false; ?>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<div class="clear"></div>
|
||||
|
||||
<?php if ( empty( $entries ) ) : ?>
|
||||
<p><?php esc_html_e( 'No activity recorded yet.', 'wc-email-discount' ); ?></p>
|
||||
<?php return; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<table class="widefat striped wc-edm-table wc-edm-audit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'When', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'User', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'IP', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Request', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Result', 'wc-email-discount' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $entries as $entry ) : ?>
|
||||
<?php
|
||||
$details = $entry->details ? json_decode( $entry->details, true ) : array();
|
||||
$badge = array(
|
||||
'allowed' => 'wc-edm-badge-group',
|
||||
'denied' => 'wc-edm-badge-denied',
|
||||
'rate_limited' => 'wc-edm-badge-coupon',
|
||||
);
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( mysql2date( get_option( 'date_format' ) . ' H:i:s', $entry->created_at ) ); ?></td>
|
||||
<td>
|
||||
<?php if ( $entry->user_login ) : ?>
|
||||
<?php echo esc_html( $entry->user_login ); ?>
|
||||
<?php else : ?>
|
||||
<em><?php esc_html_e( 'Not signed in', 'wc-email-discount' ); ?></em>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><code><?php echo esc_html( $entry->ip ? $entry->ip : '—' ); ?></code></td>
|
||||
<td>
|
||||
<code><?php echo esc_html( $entry->method ); ?></code>
|
||||
<span class="wc-edm-item-meta"><?php echo esc_html( $entry->route ); ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="wc-edm-badge <?php echo esc_attr( $badge[ $entry->outcome ] ?? '' ); ?>">
|
||||
<?php echo esc_html( $labels[ $entry->outcome ] ?? $entry->outcome ); ?>
|
||||
</span>
|
||||
<span class="wc-edm-item-meta">
|
||||
<?php echo esc_html( $entry->status ); ?>
|
||||
<?php echo ! empty( $details['code'] ) ? esc_html( ' · ' . $details['code'] ) : ''; ?>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php
|
||||
$pages = (int) ceil( $total / self::PER_PAGE );
|
||||
|
||||
if ( $pages > 1 ) :
|
||||
?>
|
||||
<div class="tablenav"><div class="tablenav-pages">
|
||||
<?php
|
||||
echo wp_kses_post(
|
||||
paginate_links(
|
||||
array(
|
||||
'base' => esc_url_raw( add_query_arg( 'paged', '%#%' ) ),
|
||||
'format' => '',
|
||||
'current' => $paged,
|
||||
'total' => $pages,
|
||||
'prev_text' => '«',
|
||||
'next_text' => '»',
|
||||
)
|
||||
)
|
||||
);
|
||||
?>
|
||||
</div></div>
|
||||
<?php
|
||||
endif;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
<?php
|
||||
/**
|
||||
* Email assignments admin (CRUD).
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* List, create, edit and delete email assignments.
|
||||
*/
|
||||
class WC_EDM_Admin_Emails {
|
||||
|
||||
/**
|
||||
* Handle save/delete/toggle actions for the emails tab. Runs on admin_init.
|
||||
*/
|
||||
public function handle_actions() {
|
||||
// Save (create or update).
|
||||
if ( isset( $_POST['wc_edm_action'] ) && 'save_email' === $_POST['wc_edm_action'] ) {
|
||||
check_admin_referer( 'wc_edm_save_email' );
|
||||
|
||||
$id = isset( $_POST['email_id'] ) ? absint( $_POST['email_id'] ) : 0;
|
||||
$pattern = isset( $_POST['email_pattern'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['email_pattern'] ) ) ) : '';
|
||||
$type = isset( $_POST['assignment_type'] ) ? sanitize_key( wp_unslash( $_POST['assignment_type'] ) ) : 'group';
|
||||
|
||||
if ( ! in_array( $type, array( 'group', 'coupon', 'custom' ), true ) ) {
|
||||
$type = 'group';
|
||||
}
|
||||
|
||||
if ( '' === $pattern ) {
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'message' => 'error' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'email_pattern' => $pattern,
|
||||
'assignment_type' => $type,
|
||||
'group_id' => null,
|
||||
'coupon_code' => null,
|
||||
'custom_discount' => null,
|
||||
'is_active' => isset( $_POST['is_active'] ) ? 1 : 0,
|
||||
'free_shipping' => isset( $_POST['free_shipping'] ) ? 1 : 0,
|
||||
);
|
||||
|
||||
// Only the fields for the chosen type are stored.
|
||||
switch ( $type ) {
|
||||
case 'group':
|
||||
$data['group_id'] = isset( $_POST['group_id'] ) ? absint( $_POST['group_id'] ) : 0;
|
||||
if ( ! $data['group_id'] ) {
|
||||
$data['group_id'] = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'coupon':
|
||||
$data['coupon_code'] = isset( $_POST['coupon_code'] )
|
||||
? wc_format_coupon_code( wp_unslash( $_POST['coupon_code'] ) )
|
||||
: '';
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
$data['custom_discount'] = isset( $_POST['custom_discount'] )
|
||||
? WC_EDM_Helpers::sanitize_percent( $_POST['custom_discount'] )
|
||||
: 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( $id ) {
|
||||
WC_EDM_DB::update_email( $id, $data );
|
||||
} else {
|
||||
$id = WC_EDM_DB::insert_email( $data );
|
||||
}
|
||||
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'message' => $id ? 'saved' : 'error' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete.
|
||||
if ( isset( $_GET['action'], $_GET['id'] ) && 'delete' === $_GET['action'] ) {
|
||||
$id = absint( $_GET['id'] );
|
||||
check_admin_referer( 'wc_edm_delete_email_' . $id );
|
||||
|
||||
WC_EDM_DB::delete_email( $id );
|
||||
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'message' => 'deleted' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
// Toggle active.
|
||||
if ( isset( $_GET['action'], $_GET['id'] ) && 'toggle' === $_GET['action'] ) {
|
||||
$id = absint( $_GET['id'] );
|
||||
check_admin_referer( 'wc_edm_toggle_email_' . $id );
|
||||
|
||||
$assignment = WC_EDM_DB::get_email( $id );
|
||||
|
||||
if ( $assignment ) {
|
||||
WC_EDM_DB::set_email_active( $id, $assignment->is_active ? 0 : 1 );
|
||||
}
|
||||
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'message' => 'saved' ) ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the emails tab (list or edit form).
|
||||
*/
|
||||
public function render() {
|
||||
$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( 'new' === $action || 'edit' === $action ) {
|
||||
$id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$this->render_form( $id );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->render_list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list of email assignments.
|
||||
*/
|
||||
private function render_list() {
|
||||
$assignments = WC_EDM_DB::get_emails();
|
||||
?>
|
||||
<div class="wc-edm-header-row">
|
||||
<h2><?php esc_html_e( 'Email Assignments', 'wc-email-discount' ); ?></h2>
|
||||
<a class="button button-primary" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'action' => 'new' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Add New Assignment', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Patterns: full email (john@example.com), domain (@company.com) or partial match (john).', 'wc-email-discount' ); ?>
|
||||
</p>
|
||||
|
||||
<?php if ( empty( $assignments ) ) : ?>
|
||||
<p><?php esc_html_e( 'No email assignments found.', 'wc-email-discount' ); ?></p>
|
||||
<?php return; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<table class="widefat striped wc-edm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Email Pattern', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Type', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Assignment', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Status', 'wc-email-discount' ); ?></th>
|
||||
<th class="wc-edm-actions-col"><?php esc_html_e( 'Actions', 'wc-email-discount' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $assignments as $assignment ) : ?>
|
||||
<?php
|
||||
$id = (int) $assignment->id;
|
||||
|
||||
$edit_url = WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'action' => 'edit', 'id' => $id ) );
|
||||
|
||||
$delete_url = wp_nonce_url(
|
||||
WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'action' => 'delete', 'id' => $id ) ),
|
||||
'wc_edm_delete_email_' . $id
|
||||
);
|
||||
|
||||
$toggle_url = wp_nonce_url(
|
||||
WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'action' => 'toggle', 'id' => $id ) ),
|
||||
'wc_edm_toggle_email_' . $id
|
||||
);
|
||||
|
||||
$type_labels = array(
|
||||
'group' => __( 'Group', 'wc-email-discount' ),
|
||||
'coupon' => __( 'Coupon', 'wc-email-discount' ),
|
||||
'custom' => __( 'Custom', 'wc-email-discount' ),
|
||||
);
|
||||
?>
|
||||
<tr>
|
||||
<td><a href="<?php echo esc_url( $edit_url ); ?>"><strong><?php echo esc_html( $assignment->email_pattern ); ?></strong></a></td>
|
||||
<td>
|
||||
<span class="wc-edm-badge wc-edm-badge-<?php echo esc_attr( $assignment->assignment_type ); ?>">
|
||||
<?php echo esc_html( $type_labels[ $assignment->assignment_type ] ?? $assignment->assignment_type ); ?>
|
||||
</span>
|
||||
</td>
|
||||
<td><?php echo esc_html( WC_EDM_Helpers::describe_assignment( $assignment ) ); ?></td>
|
||||
<td>
|
||||
<a href="<?php echo esc_url( $toggle_url ); ?>" class="wc-edm-status <?php echo $assignment->is_active ? 'is-active' : 'is-inactive'; ?>">
|
||||
<?php echo $assignment->is_active ? esc_html__( 'Active', 'wc-email-discount' ) : esc_html__( 'Inactive', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
</td>
|
||||
<td class="wc-edm-actions-col">
|
||||
<a class="button button-small" href="<?php echo esc_url( $edit_url ); ?>"><?php esc_html_e( 'Edit', 'wc-email-discount' ); ?></a>
|
||||
<a class="button button-small wc-edm-delete" href="<?php echo esc_url( $delete_url ); ?>"><?php esc_html_e( 'Delete', 'wc-email-discount' ); ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the create/edit form.
|
||||
*
|
||||
* @param int $id Assignment ID (0 for new).
|
||||
*/
|
||||
private function render_form( $id ) {
|
||||
$assignment = $id ? WC_EDM_DB::get_email( $id ) : null;
|
||||
$groups = WC_EDM_DB::get_groups();
|
||||
|
||||
if ( $id && ! $assignment ) {
|
||||
echo '<p>' . esc_html__( 'Assignment not found.', 'wc-email-discount' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $assignment ? $assignment->assignment_type : 'group';
|
||||
$is_active = $assignment ? (int) $assignment->is_active : 1;
|
||||
$free_shipping = $assignment ? (int) ! empty( $assignment->free_shipping ) : 0;
|
||||
|
||||
$coupon_codes = WC_EDM_Helpers::get_coupon_codes();
|
||||
$current_coupon = $assignment && $assignment->coupon_code ? $assignment->coupon_code : '';
|
||||
|
||||
// Keep a stored code selectable even if the coupon was since deleted.
|
||||
if ( $current_coupon && ! in_array( $current_coupon, $coupon_codes, true ) ) {
|
||||
array_unshift( $coupon_codes, $current_coupon );
|
||||
}
|
||||
?>
|
||||
<h2><?php echo $assignment ? esc_html__( 'Edit Email Assignment', 'wc-email-discount' ) : esc_html__( 'Add Email Assignment', 'wc-email-discount' ); ?></h2>
|
||||
|
||||
<form method="post" action="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails' ) ) ); ?>">
|
||||
<?php wp_nonce_field( 'wc_edm_save_email' ); ?>
|
||||
<input type="hidden" name="wc_edm_action" value="save_email">
|
||||
<input type="hidden" name="email_id" value="<?php echo esc_attr( $assignment ? (int) $assignment->id : 0 ); ?>">
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wc-edm-email-pattern"><?php esc_html_e( 'Email Pattern', 'wc-email-discount' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wc-edm-email-pattern" name="email_pattern" class="regular-text" required
|
||||
value="<?php echo esc_attr( $assignment ? $assignment->email_pattern : '' ); ?>"
|
||||
placeholder="john@example.com">
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Full email (john@example.com), domain (@company.com) or partial match (john).', 'wc-email-discount' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Assignment Type', 'wc-email-discount' ); ?></th>
|
||||
<td>
|
||||
<fieldset id="wc-edm-assignment-type">
|
||||
<label>
|
||||
<input type="radio" name="assignment_type" value="group" <?php checked( $type, 'group' ); ?>>
|
||||
<?php esc_html_e( 'Discount Group', 'wc-email-discount' ); ?>
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="radio" name="assignment_type" value="coupon" <?php checked( $type, 'coupon' ); ?>>
|
||||
<?php esc_html_e( 'Coupon Code', 'wc-email-discount' ); ?>
|
||||
</label><br>
|
||||
<label>
|
||||
<input type="radio" name="assignment_type" value="custom" <?php checked( $type, 'custom' ); ?>>
|
||||
<?php esc_html_e( 'Custom Discount %', 'wc-email-discount' ); ?>
|
||||
</label>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="wc-edm-type-fields" data-type="group">
|
||||
<th scope="row">
|
||||
<label for="wc-edm-email-group"><?php esc_html_e( 'Discount Group', 'wc-email-discount' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<select id="wc-edm-email-group" name="group_id">
|
||||
<option value=""><?php esc_html_e( '— Select a group —', 'wc-email-discount' ); ?></option>
|
||||
<?php foreach ( $groups as $group ) : ?>
|
||||
<option value="<?php echo esc_attr( (int) $group->id ); ?>"
|
||||
<?php selected( $assignment && (int) $assignment->group_id === (int) $group->id ); ?>>
|
||||
<?php echo esc_html( $group->name . ' (' . wc_format_localized_decimal( $group->discount_percent ) . '%)' ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Inherits the group\'s discount percent and categories.', 'wc-email-discount' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="wc-edm-type-fields" data-type="coupon">
|
||||
<th scope="row">
|
||||
<label for="wc-edm-email-coupon"><?php esc_html_e( 'Coupon Code', 'wc-email-discount' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<?php if ( empty( $coupon_codes ) ) : ?>
|
||||
<input type="text" id="wc-edm-email-coupon" name="coupon_code" class="regular-text" value="">
|
||||
<p class="description"><?php esc_html_e( 'No WooCommerce coupons found — enter a coupon code manually, or create one under Marketing → Coupons.', 'wc-email-discount' ); ?></p>
|
||||
<?php else : ?>
|
||||
<select id="wc-edm-email-coupon" name="coupon_code">
|
||||
<option value=""><?php esc_html_e( '— Select a coupon —', 'wc-email-discount' ); ?></option>
|
||||
<?php foreach ( $coupon_codes as $code ) : ?>
|
||||
<option value="<?php echo esc_attr( $code ); ?>" <?php selected( $current_coupon, $code ); ?>>
|
||||
<?php echo esc_html( $code ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e( 'Your existing WooCommerce coupons. The selected coupon is auto-applied at checkout.', 'wc-email-discount' ); ?></p>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="wc-edm-type-fields" data-type="custom">
|
||||
<th scope="row">
|
||||
<label for="wc-edm-email-custom"><?php esc_html_e( 'Custom Discount %', 'wc-email-discount' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="wc-edm-email-custom" name="custom_discount" min="0" max="100" step="0.01"
|
||||
value="<?php echo esc_attr( $assignment && null !== $assignment->custom_discount ? $assignment->custom_discount : '' ); ?>"> %
|
||||
<p class="description"><?php esc_html_e( 'Applied to the entire cart.', 'wc-email-discount' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Free Shipping', 'wc-email-discount' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="free_shipping" value="1" <?php checked( $free_shipping, 1 ); ?>>
|
||||
<?php esc_html_e( 'Grant free shipping', 'wc-email-discount' ); ?>
|
||||
</label>
|
||||
<p class="description"><?php esc_html_e( 'All shipping methods become free at checkout for matching emails. Works with every assignment type.', 'wc-email-discount' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Status', 'wc-email-discount' ); ?></th>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" name="is_active" value="1" <?php checked( $is_active, 1 ); ?>>
|
||||
<?php esc_html_e( 'Active', 'wc-email-discount' ); ?>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<button type="submit" class="button button-primary"><?php esc_html_e( 'Save Assignment', 'wc-email-discount' ); ?></button>
|
||||
<a class="button" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Cancel', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
/**
|
||||
* Discount groups admin (CRUD).
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* List, create, edit and delete discount groups.
|
||||
*/
|
||||
class WC_EDM_Admin_Groups {
|
||||
|
||||
/**
|
||||
* Handle save/delete actions for the groups tab. Runs on admin_init.
|
||||
*/
|
||||
public function handle_actions() {
|
||||
// Save (create or update).
|
||||
if ( isset( $_POST['wc_edm_action'] ) && 'save_group' === $_POST['wc_edm_action'] ) {
|
||||
check_admin_referer( 'wc_edm_save_group' );
|
||||
|
||||
$id = isset( $_POST['group_id'] ) ? absint( $_POST['group_id'] ) : 0;
|
||||
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
|
||||
$percent = isset( $_POST['discount_percent'] ) ? WC_EDM_Helpers::sanitize_percent( $_POST['discount_percent'] ) : 0;
|
||||
|
||||
$category_ids = isset( $_POST['category_ids'] ) && is_array( $_POST['category_ids'] )
|
||||
? array_map( 'absint', wp_unslash( $_POST['category_ids'] ) )
|
||||
: array();
|
||||
|
||||
if ( '' === $name ) {
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'message' => 'error' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'name' => $name,
|
||||
'discount_percent' => $percent,
|
||||
);
|
||||
|
||||
if ( $id ) {
|
||||
WC_EDM_DB::update_group( $id, $data );
|
||||
} else {
|
||||
$id = WC_EDM_DB::insert_group( $data );
|
||||
}
|
||||
|
||||
if ( $id ) {
|
||||
WC_EDM_DB::set_group_categories( $id, $category_ids );
|
||||
}
|
||||
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'message' => $id ? 'saved' : 'error' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
// Delete.
|
||||
if ( isset( $_GET['action'], $_GET['id'] ) && 'delete' === $_GET['action'] ) {
|
||||
$id = absint( $_GET['id'] );
|
||||
check_admin_referer( 'wc_edm_delete_group_' . $id );
|
||||
|
||||
WC_EDM_DB::delete_group( $id );
|
||||
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'message' => 'deleted' ) ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the groups tab (list or edit form).
|
||||
*/
|
||||
public function render() {
|
||||
$action = isset( $_GET['action'] ) ? sanitize_key( wp_unslash( $_GET['action'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
if ( 'new' === $action || 'edit' === $action ) {
|
||||
$id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$this->render_form( $id );
|
||||
return;
|
||||
}
|
||||
|
||||
$this->render_list();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list of groups.
|
||||
*/
|
||||
private function render_list() {
|
||||
$groups = WC_EDM_DB::get_groups();
|
||||
?>
|
||||
<div class="wc-edm-header-row">
|
||||
<h2><?php esc_html_e( 'Discount Groups', 'wc-email-discount' ); ?></h2>
|
||||
<a class="button button-primary" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'new' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Add New Group', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<?php if ( empty( $groups ) ) : ?>
|
||||
<p><?php esc_html_e( 'No discount groups found.', 'wc-email-discount' ); ?></p>
|
||||
<?php return; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<table class="widefat striped wc-edm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Name', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Discount', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Categories', 'wc-email-discount' ); ?></th>
|
||||
<th class="wc-edm-actions-col"><?php esc_html_e( 'Actions', 'wc-email-discount' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $groups as $group ) : ?>
|
||||
<?php
|
||||
$category_ids = WC_EDM_DB::get_group_category_ids( (int) $group->id );
|
||||
$names = array();
|
||||
|
||||
foreach ( $category_ids as $category_id ) {
|
||||
$term = get_term( $category_id, 'product_cat' );
|
||||
if ( $term && ! is_wp_error( $term ) ) {
|
||||
$names[] = $term->name;
|
||||
}
|
||||
}
|
||||
|
||||
$edit_url = WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'edit', 'id' => (int) $group->id ) );
|
||||
|
||||
$delete_url = wp_nonce_url(
|
||||
WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'delete', 'id' => (int) $group->id ) ),
|
||||
'wc_edm_delete_group_' . (int) $group->id
|
||||
);
|
||||
?>
|
||||
<tr>
|
||||
<td><a href="<?php echo esc_url( $edit_url ); ?>"><strong><?php echo esc_html( $group->name ); ?></strong></a></td>
|
||||
<td><span class="wc-edm-badge wc-edm-badge-percent"><?php echo esc_html( wc_format_localized_decimal( $group->discount_percent ) ); ?>%</span></td>
|
||||
<td><?php echo $names ? esc_html( implode( ', ', $names ) ) : '<em>' . esc_html__( 'None', 'wc-email-discount' ) . '</em>'; ?></td>
|
||||
<td class="wc-edm-actions-col">
|
||||
<a class="button button-small" href="<?php echo esc_url( $edit_url ); ?>"><?php esc_html_e( 'Edit', 'wc-email-discount' ); ?></a>
|
||||
<a class="button button-small wc-edm-delete" href="<?php echo esc_url( $delete_url ); ?>"><?php esc_html_e( 'Delete', 'wc-email-discount' ); ?></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the create/edit form.
|
||||
*
|
||||
* @param int $id Group ID (0 for new).
|
||||
*/
|
||||
private function render_form( $id ) {
|
||||
$group = $id ? WC_EDM_DB::get_group( $id ) : null;
|
||||
$selected_ids = $id ? WC_EDM_DB::get_group_category_ids( $id ) : array();
|
||||
$categories = WC_EDM_Helpers::get_product_categories();
|
||||
|
||||
$categories_by_id = array();
|
||||
foreach ( $categories as $category ) {
|
||||
$categories_by_id[ $category->term_id ] = $category;
|
||||
}
|
||||
|
||||
if ( $id && ! $group ) {
|
||||
echo '<p>' . esc_html__( 'Group not found.', 'wc-email-discount' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<h2><?php echo $group ? esc_html__( 'Edit Discount Group', 'wc-email-discount' ) : esc_html__( 'Add Discount Group', 'wc-email-discount' ); ?></h2>
|
||||
|
||||
<form method="post" action="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups' ) ) ); ?>">
|
||||
<?php wp_nonce_field( 'wc_edm_save_group' ); ?>
|
||||
<input type="hidden" name="wc_edm_action" value="save_group">
|
||||
<input type="hidden" name="group_id" value="<?php echo esc_attr( $group ? (int) $group->id : 0 ); ?>">
|
||||
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wc-edm-group-name"><?php esc_html_e( 'Group Name', 'wc-email-discount' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="text" id="wc-edm-group-name" name="name" class="regular-text" required
|
||||
value="<?php echo esc_attr( $group ? $group->name : '' ); ?>">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label for="wc-edm-group-percent"><?php esc_html_e( 'Discount %', 'wc-email-discount' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<input type="number" id="wc-edm-group-percent" name="discount_percent" min="0" max="100" step="0.01" required
|
||||
value="<?php echo esc_attr( $group ? $group->discount_percent : '' ); ?>"> %
|
||||
<p class="description"><?php esc_html_e( 'Applies to all products in the categories below.', 'wc-email-discount' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php esc_html_e( 'Product Categories', 'wc-email-discount' ); ?></th>
|
||||
<td>
|
||||
<div class="wc-edm-category-picker">
|
||||
<select id="wc-edm-category-select">
|
||||
<option value=""><?php esc_html_e( '— Select a category —', 'wc-email-discount' ); ?></option>
|
||||
<?php foreach ( $categories as $category ) : ?>
|
||||
<option value="<?php echo esc_attr( $category->term_id ); ?>"
|
||||
<?php disabled( in_array( $category->term_id, $selected_ids, true ) ); ?>>
|
||||
<?php echo esc_html( $category->name ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="button" class="button" id="wc-edm-add-category">
|
||||
<?php esc_html_e( 'Add Category', 'wc-email-discount' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul id="wc-edm-selected-categories" class="wc-edm-sortable">
|
||||
<?php foreach ( $selected_ids as $category_id ) : ?>
|
||||
<?php
|
||||
if ( ! isset( $categories_by_id[ $category_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
?>
|
||||
<li data-id="<?php echo esc_attr( $category_id ); ?>">
|
||||
<span class="dashicons dashicons-menu wc-edm-drag-handle"></span>
|
||||
<span class="wc-edm-item-label"><?php echo esc_html( $categories_by_id[ $category_id ]->name ); ?></span>
|
||||
<button type="button" class="wc-edm-remove-category" aria-label="<?php esc_attr_e( 'Remove', 'wc-email-discount' ); ?>">×</button>
|
||||
<input type="hidden" name="category_ids[]" value="<?php echo esc_attr( $category_id ); ?>">
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="description"><?php esc_html_e( 'Drag to reorder. The discount applies to all listed categories.', 'wc-email-discount' ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<p class="submit">
|
||||
<button type="submit" class="button button-primary"><?php esc_html_e( 'Save Group', 'wc-email-discount' ); ?></button>
|
||||
<a class="button" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Cancel', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
/**
|
||||
* Shipping rules admin.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure per-group show/hide rules for shipping methods.
|
||||
*/
|
||||
class WC_EDM_Admin_Shipping {
|
||||
|
||||
/**
|
||||
* Handle save actions for the shipping tab. Runs on admin_init.
|
||||
*/
|
||||
public function handle_actions() {
|
||||
if ( ! isset( $_POST['wc_edm_action'] ) || 'save_shipping_rules' !== $_POST['wc_edm_action'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
check_admin_referer( 'wc_edm_save_shipping_rules' );
|
||||
|
||||
$group_id = isset( $_POST['group_id'] ) ? absint( $_POST['group_id'] ) : 0;
|
||||
|
||||
if ( ! $group_id || ! WC_EDM_DB::get_group( $group_id ) ) {
|
||||
wp_safe_redirect( WC_EDM_Helpers::admin_url( array( 'tab' => 'shipping', 'message' => 'error' ) ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$order = isset( $_POST['method_order'] ) && is_array( $_POST['method_order'] )
|
||||
? array_map( 'sanitize_text_field', wp_unslash( $_POST['method_order'] ) )
|
||||
: array();
|
||||
|
||||
$raw_rules = isset( $_POST['rule'] ) && is_array( $_POST['rule'] )
|
||||
? array_map( 'sanitize_text_field', wp_unslash( $_POST['rule'] ) )
|
||||
: array();
|
||||
|
||||
$known_methods = WC_EDM_Helpers::get_all_shipping_methods();
|
||||
|
||||
// Rules are replaced wholesale: only methods with an explicit show/hide choice are stored.
|
||||
$rules = array();
|
||||
|
||||
foreach ( $order as $index => $method_id ) {
|
||||
if ( ! isset( $known_methods[ $method_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$action = isset( $raw_rules[ $method_id ] ) ? $raw_rules[ $method_id ] : '';
|
||||
|
||||
if ( ! in_array( $action, array( 'show', 'hide' ), true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rules[] = array(
|
||||
'shipping_method_id' => $method_id,
|
||||
'action' => $action,
|
||||
'sort_order' => $index,
|
||||
);
|
||||
}
|
||||
|
||||
WC_EDM_DB::replace_shipping_rules( $group_id, $rules );
|
||||
|
||||
wp_safe_redirect(
|
||||
WC_EDM_Helpers::admin_url(
|
||||
array(
|
||||
'tab' => 'shipping',
|
||||
'group_id' => $group_id,
|
||||
'message' => 'saved',
|
||||
)
|
||||
)
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the shipping rules tab.
|
||||
*/
|
||||
public function render() {
|
||||
$groups = WC_EDM_DB::get_groups();
|
||||
?>
|
||||
<h2><?php esc_html_e( 'Shipping Rules', 'wc-email-discount' ); ?></h2>
|
||||
<p class="description">
|
||||
<?php esc_html_e( 'Choose which shipping methods are shown or hidden for customers in a discount group. If a group has no rules, all shipping methods behave normally. If any method is set to "Show", only "Show" methods are offered.', 'wc-email-discount' ); ?>
|
||||
</p>
|
||||
|
||||
<?php if ( empty( $groups ) ) : ?>
|
||||
<p>
|
||||
<?php esc_html_e( 'Create a discount group first to configure shipping rules.', 'wc-email-discount' ); ?>
|
||||
<a href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'new' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Add Discount Group', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
<?php return; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
$group_id = isset( $_GET['group_id'] ) ? absint( $_GET['group_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
?>
|
||||
|
||||
<form method="get" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>" class="wc-edm-group-selector">
|
||||
<input type="hidden" name="page" value="wc-email-discounts">
|
||||
<input type="hidden" name="tab" value="shipping">
|
||||
<label for="wc-edm-shipping-group"><?php esc_html_e( 'Discount group:', 'wc-email-discount' ); ?></label>
|
||||
<select id="wc-edm-shipping-group" name="group_id">
|
||||
<option value=""><?php esc_html_e( '— Select a group —', 'wc-email-discount' ); ?></option>
|
||||
<?php foreach ( $groups as $group ) : ?>
|
||||
<option value="<?php echo esc_attr( (int) $group->id ); ?>" <?php selected( $group_id, (int) $group->id ); ?>>
|
||||
<?php echo esc_html( $group->name ); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button type="submit" class="button"><?php esc_html_e( 'Select', 'wc-email-discount' ); ?></button>
|
||||
</form>
|
||||
|
||||
<?php
|
||||
if ( ! $group_id || ! WC_EDM_DB::get_group( $group_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->render_rules_form( $group_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the rules form for a selected group.
|
||||
*
|
||||
* @param int $group_id Group ID.
|
||||
*/
|
||||
private function render_rules_form( $group_id ) {
|
||||
$methods = WC_EDM_Helpers::get_all_shipping_methods();
|
||||
$rules = WC_EDM_DB::get_shipping_rules( $group_id );
|
||||
|
||||
if ( empty( $methods ) ) {
|
||||
echo '<p>' . esc_html__( 'No shipping methods found. Configure shipping zones in WooCommerce first.', 'wc-email-discount' ) . '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Existing rules by method id, and the display order: ruled methods first (by sort_order), then the rest.
|
||||
$rules_by_method = array();
|
||||
foreach ( $rules as $rule ) {
|
||||
$rules_by_method[ $rule->shipping_method_id ] = $rule;
|
||||
}
|
||||
|
||||
$ordered_keys = array_merge(
|
||||
array_values( array_intersect( array_keys( $rules_by_method ), array_keys( $methods ) ) ),
|
||||
array_values( array_diff( array_keys( $methods ), array_keys( $rules_by_method ) ) )
|
||||
);
|
||||
?>
|
||||
<form method="post" action="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'shipping' ) ) ); ?>">
|
||||
<?php wp_nonce_field( 'wc_edm_save_shipping_rules' ); ?>
|
||||
<input type="hidden" name="wc_edm_action" value="save_shipping_rules">
|
||||
<input type="hidden" name="group_id" value="<?php echo esc_attr( $group_id ); ?>">
|
||||
|
||||
<ul id="wc-edm-shipping-methods" class="wc-edm-sortable wc-edm-shipping-list">
|
||||
<?php foreach ( $ordered_keys as $method_id ) : ?>
|
||||
<?php
|
||||
$method = $methods[ $method_id ];
|
||||
$action = isset( $rules_by_method[ $method_id ] ) ? $rules_by_method[ $method_id ]->action : '';
|
||||
?>
|
||||
<li>
|
||||
<span class="dashicons dashicons-menu wc-edm-drag-handle"></span>
|
||||
<span class="wc-edm-item-label">
|
||||
<strong><?php echo esc_html( $method['label'] ); ?></strong>
|
||||
<span class="wc-edm-item-meta"><?php echo esc_html( $method['zone'] ); ?> · <code><?php echo esc_html( $method_id ); ?></code></span>
|
||||
</span>
|
||||
<span class="wc-edm-rule-options">
|
||||
<label>
|
||||
<input type="radio" name="rule[<?php echo esc_attr( $method_id ); ?>]" value="" <?php checked( $action, '' ); ?>>
|
||||
<?php esc_html_e( 'Default', 'wc-email-discount' ); ?>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="rule[<?php echo esc_attr( $method_id ); ?>]" value="show" <?php checked( $action, 'show' ); ?>>
|
||||
<?php esc_html_e( 'Show', 'wc-email-discount' ); ?>
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="rule[<?php echo esc_attr( $method_id ); ?>]" value="hide" <?php checked( $action, 'hide' ); ?>>
|
||||
<?php esc_html_e( 'Hide', 'wc-email-discount' ); ?>
|
||||
</label>
|
||||
</span>
|
||||
<input type="hidden" name="method_order[]" value="<?php echo esc_attr( $method_id ); ?>">
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<p class="description"><?php esc_html_e( 'Drag to reorder — the order here controls the order shipping methods appear in at checkout.', 'wc-email-discount' ); ?></p>
|
||||
|
||||
<p class="submit">
|
||||
<button type="submit" class="button button-primary"><?php esc_html_e( 'Save Shipping Rules', 'wc-email-discount' ); ?></button>
|
||||
</p>
|
||||
</form>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin menu, tabs and routing.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the admin page under WooCommerce and routes tabs/actions.
|
||||
*/
|
||||
class WC_EDM_Admin {
|
||||
|
||||
/**
|
||||
* Admin page slug.
|
||||
*/
|
||||
const PAGE_SLUG = 'wc-email-discounts';
|
||||
|
||||
/**
|
||||
* Groups controller.
|
||||
*
|
||||
* @var WC_EDM_Admin_Groups
|
||||
*/
|
||||
private $groups;
|
||||
|
||||
/**
|
||||
* Emails controller.
|
||||
*
|
||||
* @var WC_EDM_Admin_Emails
|
||||
*/
|
||||
private $emails;
|
||||
|
||||
/**
|
||||
* Shipping controller.
|
||||
*
|
||||
* @var WC_EDM_Admin_Shipping
|
||||
*/
|
||||
private $shipping;
|
||||
|
||||
/**
|
||||
* Audit log controller.
|
||||
*
|
||||
* @var WC_EDM_Admin_Audit
|
||||
*/
|
||||
private $audit;
|
||||
|
||||
/**
|
||||
* Hook everything up.
|
||||
*/
|
||||
public function __construct() {
|
||||
// Each sub-controller lives in its own file. Guarding here means an
|
||||
// incomplete plugin folder costs a tab, not the whole admin screen.
|
||||
$this->groups = class_exists( 'WC_EDM_Admin_Groups' ) ? new WC_EDM_Admin_Groups() : null;
|
||||
$this->emails = class_exists( 'WC_EDM_Admin_Emails' ) ? new WC_EDM_Admin_Emails() : null;
|
||||
$this->shipping = class_exists( 'WC_EDM_Admin_Shipping' ) ? new WC_EDM_Admin_Shipping() : null;
|
||||
$this->audit = class_exists( 'WC_EDM_Admin_Audit' ) ? new WC_EDM_Admin_Audit() : null;
|
||||
|
||||
add_action( 'admin_menu', array( $this, 'register_menu' ) );
|
||||
add_action( 'admin_init', array( $this, 'handle_actions' ) );
|
||||
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the submenu page under WooCommerce.
|
||||
*/
|
||||
public function register_menu() {
|
||||
add_submenu_page(
|
||||
'woocommerce',
|
||||
__( 'Email Discounts', 'wc-email-discount' ),
|
||||
__( 'Email Discounts', 'wc-email-discount' ),
|
||||
'manage_woocommerce',
|
||||
self::PAGE_SLUG,
|
||||
array( $this, 'render_page' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue admin CSS/JS on the plugin page only.
|
||||
*
|
||||
* @param string $hook Current admin page hook.
|
||||
*/
|
||||
public function enqueue_assets( $hook ) {
|
||||
if ( false === strpos( $hook, self::PAGE_SLUG ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_enqueue_style(
|
||||
'wc-edm-admin',
|
||||
WC_EDM_PLUGIN_URL . 'assets/css/admin.css',
|
||||
array(),
|
||||
WC_EDM_VERSION
|
||||
);
|
||||
|
||||
wp_enqueue_script(
|
||||
'wc-edm-admin',
|
||||
WC_EDM_PLUGIN_URL . 'assets/js/admin.js',
|
||||
array( 'jquery', 'jquery-ui-sortable' ),
|
||||
WC_EDM_VERSION,
|
||||
true
|
||||
);
|
||||
|
||||
wp_localize_script(
|
||||
'wc-edm-admin',
|
||||
'wcEdmAdmin',
|
||||
array(
|
||||
'confirmDelete' => __( 'Are you sure you want to delete this item? This cannot be undone.', 'wc-email-discount' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently active tab.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function current_tab() {
|
||||
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'dashboard'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$tabs = array_keys( $this->get_tabs() );
|
||||
|
||||
return in_array( $tab, $tabs, true ) ? $tab : 'dashboard';
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab definitions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function get_tabs() {
|
||||
$tabs = array( 'dashboard' => __( 'Dashboard', 'wc-email-discount' ) );
|
||||
|
||||
// Only offer tabs whose controller actually loaded.
|
||||
if ( $this->groups ) {
|
||||
$tabs['groups'] = __( 'Discount Groups', 'wc-email-discount' );
|
||||
}
|
||||
|
||||
if ( $this->emails ) {
|
||||
$tabs['emails'] = __( 'Email Assignments', 'wc-email-discount' );
|
||||
}
|
||||
|
||||
if ( $this->shipping ) {
|
||||
$tabs['shipping'] = __( 'Shipping Rules', 'wc-email-discount' );
|
||||
}
|
||||
|
||||
if ( $this->audit ) {
|
||||
$tabs['audit'] = __( 'Audit Log', 'wc-email-discount' );
|
||||
}
|
||||
|
||||
return $tabs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle POST/GET actions before any output (allows redirects).
|
||||
*/
|
||||
public function handle_actions() {
|
||||
if ( ! isset( $_GET['page'] ) || self::PAGE_SLUG !== $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( $this->current_tab() ) {
|
||||
case 'groups':
|
||||
$this->groups->handle_actions();
|
||||
break;
|
||||
case 'emails':
|
||||
$this->emails->handle_actions();
|
||||
break;
|
||||
case 'shipping':
|
||||
$this->shipping->handle_actions();
|
||||
break;
|
||||
case 'audit':
|
||||
$this->audit->handle_actions();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the admin page with tab navigation.
|
||||
*/
|
||||
public function render_page() {
|
||||
if ( ! current_user_can( 'manage_woocommerce' ) ) {
|
||||
wp_die( esc_html__( 'You do not have permission to access this page.', 'wc-email-discount' ) );
|
||||
}
|
||||
|
||||
$current = $this->current_tab();
|
||||
?>
|
||||
<div class="wrap wc-edm-wrap">
|
||||
<h1><?php esc_html_e( 'Email Discounts', 'wc-email-discount' ); ?></h1>
|
||||
|
||||
<?php $this->render_notices(); ?>
|
||||
|
||||
<nav class="nav-tab-wrapper wc-edm-nav">
|
||||
<?php foreach ( $this->get_tabs() as $slug => $label ) : ?>
|
||||
<a href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => $slug ) ) ); ?>"
|
||||
class="nav-tab <?php echo $current === $slug ? 'nav-tab-active' : ''; ?>">
|
||||
<?php echo esc_html( $label ); ?>
|
||||
</a>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
|
||||
<div class="wc-edm-content">
|
||||
<?php
|
||||
switch ( $current ) {
|
||||
case 'groups':
|
||||
$this->groups->render();
|
||||
break;
|
||||
case 'emails':
|
||||
$this->emails->render();
|
||||
break;
|
||||
case 'shipping':
|
||||
$this->shipping->render();
|
||||
break;
|
||||
case 'audit':
|
||||
$this->audit->render();
|
||||
break;
|
||||
default:
|
||||
$this->render_dashboard();
|
||||
break;
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render success/error notices based on the ?message= query arg.
|
||||
*/
|
||||
private function render_notices() {
|
||||
if ( ! isset( $_GET['message'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
return;
|
||||
}
|
||||
|
||||
$message = sanitize_key( wp_unslash( $_GET['message'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
|
||||
$messages = array(
|
||||
'saved' => array( 'success', __( 'Saved successfully.', 'wc-email-discount' ) ),
|
||||
'deleted' => array( 'success', __( 'Deleted successfully.', 'wc-email-discount' ) ),
|
||||
'error' => array( 'error', __( 'Something went wrong. Please try again.', 'wc-email-discount' ) ),
|
||||
);
|
||||
|
||||
if ( ! isset( $messages[ $message ] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
list( $type, $text ) = $messages[ $message ];
|
||||
|
||||
printf(
|
||||
'<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>',
|
||||
esc_attr( $type ),
|
||||
esc_html( $text )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the dashboard tab: stats cards, quick actions, recent groups.
|
||||
*/
|
||||
private function render_dashboard() {
|
||||
$group_count = WC_EDM_DB::count_groups();
|
||||
$email_count = WC_EDM_DB::count_emails();
|
||||
$active_count = WC_EDM_DB::count_emails( true );
|
||||
$recent = WC_EDM_DB::get_recent_groups( 5 );
|
||||
?>
|
||||
<div class="wc-edm-stats">
|
||||
<div class="wc-edm-stat-card">
|
||||
<span class="wc-edm-stat-number"><?php echo esc_html( $group_count ); ?></span>
|
||||
<span class="wc-edm-stat-label"><?php esc_html_e( 'Discount Groups', 'wc-email-discount' ); ?></span>
|
||||
</div>
|
||||
<div class="wc-edm-stat-card">
|
||||
<span class="wc-edm-stat-number"><?php echo esc_html( $email_count ); ?></span>
|
||||
<span class="wc-edm-stat-label"><?php esc_html_e( 'Email Assignments', 'wc-email-discount' ); ?></span>
|
||||
</div>
|
||||
<div class="wc-edm-stat-card">
|
||||
<span class="wc-edm-stat-number"><?php echo esc_html( $active_count ); ?></span>
|
||||
<span class="wc-edm-stat-label"><?php esc_html_e( 'Active Assignments', 'wc-email-discount' ); ?></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2><?php esc_html_e( 'Quick Actions', 'wc-email-discount' ); ?></h2>
|
||||
<p class="wc-edm-quick-actions">
|
||||
<a class="button button-primary" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'new' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Add Discount Group', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
<a class="button button-primary" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'emails', 'action' => 'new' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Add Email Assignment', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
<a class="button" href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'shipping' ) ) ); ?>">
|
||||
<?php esc_html_e( 'Manage Shipping Rules', 'wc-email-discount' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2><?php esc_html_e( 'Recent Discount Groups', 'wc-email-discount' ); ?></h2>
|
||||
<?php if ( empty( $recent ) ) : ?>
|
||||
<p><?php esc_html_e( 'No discount groups yet. Create your first one to get started.', 'wc-email-discount' ); ?></p>
|
||||
<?php else : ?>
|
||||
<table class="widefat striped wc-edm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Name', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Discount', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Categories', 'wc-email-discount' ); ?></th>
|
||||
<th><?php esc_html_e( 'Created', 'wc-email-discount' ); ?></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ( $recent as $group ) : ?>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="<?php echo esc_url( WC_EDM_Helpers::admin_url( array( 'tab' => 'groups', 'action' => 'edit', 'id' => (int) $group->id ) ) ); ?>">
|
||||
<strong><?php echo esc_html( $group->name ); ?></strong>
|
||||
</a>
|
||||
</td>
|
||||
<td><?php echo esc_html( wc_format_localized_decimal( $group->discount_percent ) ); ?>%</td>
|
||||
<td><?php echo esc_html( count( WC_EDM_DB::get_group_category_ids( (int) $group->id ) ) ); ?></td>
|
||||
<td><?php echo esc_html( date_i18n( get_option( 'date_format' ), strtotime( $group->created_at ) ) ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
/**
|
||||
* Audit log for REST API activity.
|
||||
*
|
||||
* Records every write to the wc-edm REST API and every rejected request,
|
||||
* so unauthorized access attempts are visible after the fact.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes and maintains the audit log.
|
||||
*/
|
||||
class WC_EDM_Audit_Log {
|
||||
|
||||
/**
|
||||
* Cron hook used for pruning old entries.
|
||||
*/
|
||||
const CRON_HOOK = 'wc_edm_prune_audit_log';
|
||||
|
||||
/**
|
||||
* Hook the daily prune job.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( self::CRON_HOOK, array( __CLASS__, 'prune' ) );
|
||||
add_action( 'init', array( __CLASS__, 'schedule_prune' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure the daily prune job is scheduled.
|
||||
*/
|
||||
public static function schedule_prune() {
|
||||
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
|
||||
wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', self::CRON_HOOK );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the scheduled prune job (used on deactivation).
|
||||
*/
|
||||
public static function unschedule_prune() {
|
||||
$timestamp = wp_next_scheduled( self::CRON_HOOK );
|
||||
|
||||
if ( $timestamp ) {
|
||||
wp_unschedule_event( $timestamp, self::CRON_HOOK );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether audit logging is enabled.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
/**
|
||||
* Filter whether REST activity is written to the audit log.
|
||||
*
|
||||
* @param bool $enabled Default true.
|
||||
*/
|
||||
return (bool) apply_filters( 'wc_edm_audit_log_enabled', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* How many days entries are kept.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function retention_days() {
|
||||
/**
|
||||
* Filter the audit log retention period in days.
|
||||
*
|
||||
* @param int $days Default 30.
|
||||
*/
|
||||
return max( 1, (int) apply_filters( 'wc_edm_audit_log_retention_days', 30 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the client IP address.
|
||||
*
|
||||
* The shop may sit behind a reverse proxy (e.g. Cloudflare), where
|
||||
* REMOTE_ADDR is the proxy rather than the visitor. Proxy headers are
|
||||
* only trustworthy when the proxy is guaranteed to set them, so this is
|
||||
* filterable for setups that need different handling.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_client_ip() {
|
||||
$ip = '';
|
||||
|
||||
if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
|
||||
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CF_CONNECTING_IP'] ) );
|
||||
} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
|
||||
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
|
||||
}
|
||||
|
||||
$ip = filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '';
|
||||
|
||||
/**
|
||||
* Filter the resolved client IP used for logging and rate limiting.
|
||||
*
|
||||
* @param string $ip Resolved IP address.
|
||||
*/
|
||||
return (string) apply_filters( 'wc_edm_client_ip', $ip );
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an entry.
|
||||
*
|
||||
* @param string $outcome One of allowed|denied|rate_limited.
|
||||
* @param string $method HTTP method.
|
||||
* @param string $route REST route.
|
||||
* @param int $status HTTP status code.
|
||||
* @param array $details Optional extra context.
|
||||
* @return void
|
||||
*/
|
||||
public static function record( $outcome, $method, $route, $status = 0, $details = array() ) {
|
||||
if ( ! self::is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! in_array( $outcome, array( 'allowed', 'denied', 'rate_limited' ), true ) ) {
|
||||
$outcome = 'allowed';
|
||||
}
|
||||
|
||||
$user = wp_get_current_user();
|
||||
|
||||
WC_EDM_DB::insert_audit_entry(
|
||||
array(
|
||||
'user_id' => $user ? (int) $user->ID : 0,
|
||||
'user_login' => $user && $user->user_login ? $user->user_login : '',
|
||||
'ip' => self::get_client_ip(),
|
||||
'method' => strtoupper( substr( (string) $method, 0, 10 ) ),
|
||||
'route' => substr( (string) $route, 0, 191 ),
|
||||
'status' => (int) $status,
|
||||
'outcome' => $outcome,
|
||||
'details' => ! empty( $details ) ? wp_json_encode( $details ) : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete entries older than the retention period.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function prune() {
|
||||
WC_EDM_DB::prune_audit_log( self::retention_days() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
<?php
|
||||
/**
|
||||
* Database tables and CRUD.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles custom tables and all CRUD operations.
|
||||
*/
|
||||
class WC_EDM_DB {
|
||||
|
||||
/**
|
||||
* Get a fully prefixed table name.
|
||||
*
|
||||
* @param string $name Short table name (groups, group_categories, emails, shipping_rules).
|
||||
* @return string
|
||||
*/
|
||||
public static function table( $name ) {
|
||||
global $wpdb;
|
||||
return $wpdb->prefix . 'wc_edm_' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create/upgrade the custom tables via dbDelta(), then add foreign keys.
|
||||
*/
|
||||
public static function create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
$groups = self::table( 'groups' );
|
||||
$group_categories = self::table( 'group_categories' );
|
||||
$emails = self::table( 'emails' );
|
||||
$shipping_rules = self::table( 'shipping_rules' );
|
||||
$audit_log = self::table( 'audit_log' );
|
||||
|
||||
$sql = "CREATE TABLE {$groups} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
name varchar(191) NOT NULL,
|
||||
discount_percent decimal(5,2) NOT NULL DEFAULT 0.00,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
) {$charset_collate};
|
||||
CREATE TABLE {$group_categories} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
group_id bigint(20) unsigned NOT NULL,
|
||||
category_id bigint(20) unsigned NOT NULL,
|
||||
sort_order int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY group_category (group_id,category_id),
|
||||
KEY group_id (group_id)
|
||||
) {$charset_collate};
|
||||
CREATE TABLE {$emails} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
email_pattern varchar(191) NOT NULL,
|
||||
assignment_type enum('group','coupon','custom') NOT NULL DEFAULT 'group',
|
||||
group_id bigint(20) unsigned DEFAULT NULL,
|
||||
coupon_code varchar(191) DEFAULT NULL,
|
||||
custom_discount decimal(5,2) DEFAULT NULL,
|
||||
is_active tinyint(1) NOT NULL DEFAULT 1,
|
||||
free_shipping tinyint(1) NOT NULL DEFAULT 0,
|
||||
created_at datetime NOT NULL,
|
||||
updated_at datetime NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY email_pattern (email_pattern),
|
||||
KEY group_id (group_id)
|
||||
) {$charset_collate};
|
||||
CREATE TABLE {$shipping_rules} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
group_id bigint(20) unsigned NOT NULL,
|
||||
shipping_method_id varchar(191) NOT NULL,
|
||||
action enum('show','hide') NOT NULL DEFAULT 'show',
|
||||
sort_order int(11) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
KEY group_id (group_id)
|
||||
) {$charset_collate};
|
||||
CREATE TABLE {$audit_log} (
|
||||
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
||||
created_at datetime NOT NULL,
|
||||
user_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
||||
user_login varchar(191) DEFAULT NULL,
|
||||
ip varchar(45) DEFAULT NULL,
|
||||
method varchar(10) NOT NULL DEFAULT '',
|
||||
route varchar(191) NOT NULL DEFAULT '',
|
||||
status smallint(5) unsigned NOT NULL DEFAULT 0,
|
||||
outcome enum('allowed','denied','rate_limited') NOT NULL DEFAULT 'allowed',
|
||||
details text DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
KEY created_at (created_at),
|
||||
KEY user_id (user_id),
|
||||
KEY outcome (outcome)
|
||||
) {$charset_collate};";
|
||||
|
||||
dbDelta( $sql );
|
||||
|
||||
self::add_foreign_keys();
|
||||
|
||||
update_option( 'wc_edm_db_version', WC_EDM_VERSION );
|
||||
}
|
||||
|
||||
/**
|
||||
* Run create_tables() again when the plugin was updated without
|
||||
* re-activation (dbDelta adds any missing columns).
|
||||
*/
|
||||
public static function maybe_upgrade() {
|
||||
if ( get_option( 'wc_edm_db_version' ) !== WC_EDM_VERSION ) {
|
||||
self::create_tables();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add foreign key constraints (dbDelta cannot handle these).
|
||||
*/
|
||||
private static function add_foreign_keys() {
|
||||
$groups = self::table( 'groups' );
|
||||
|
||||
self::maybe_add_foreign_key(
|
||||
self::table( 'group_categories' ),
|
||||
'fk_wc_edm_group_categories_group',
|
||||
"FOREIGN KEY (group_id) REFERENCES {$groups} (id) ON DELETE CASCADE"
|
||||
);
|
||||
|
||||
self::maybe_add_foreign_key(
|
||||
self::table( 'emails' ),
|
||||
'fk_wc_edm_emails_group',
|
||||
"FOREIGN KEY (group_id) REFERENCES {$groups} (id) ON DELETE SET NULL"
|
||||
);
|
||||
|
||||
self::maybe_add_foreign_key(
|
||||
self::table( 'shipping_rules' ),
|
||||
'fk_wc_edm_shipping_rules_group',
|
||||
"FOREIGN KEY (group_id) REFERENCES {$groups} (id) ON DELETE CASCADE"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a foreign key if it does not already exist.
|
||||
*
|
||||
* @param string $table Table name.
|
||||
* @param string $constraint Constraint name.
|
||||
* @param string $definition FOREIGN KEY definition SQL.
|
||||
*/
|
||||
private static function maybe_add_foreign_key( $table, $constraint, $definition ) {
|
||||
global $wpdb;
|
||||
|
||||
$exists = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
'SELECT CONSTRAINT_NAME FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE CONSTRAINT_SCHEMA = %s AND TABLE_NAME = %s AND CONSTRAINT_NAME = %s AND CONSTRAINT_TYPE = %s',
|
||||
DB_NAME,
|
||||
$table,
|
||||
$constraint,
|
||||
'FOREIGN KEY'
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $exists ) {
|
||||
// Table/constraint names are internal, not user input.
|
||||
$wpdb->query( "ALTER TABLE {$table} ADD CONSTRAINT {$constraint} {$definition}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Discount groups
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get all discount groups.
|
||||
*
|
||||
* @return array Array of row objects.
|
||||
*/
|
||||
public static function get_groups() {
|
||||
global $wpdb;
|
||||
$table = self::table( 'groups' );
|
||||
return $wpdb->get_results( "SELECT * FROM {$table} ORDER BY name ASC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Count discount groups.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function count_groups() {
|
||||
global $wpdb;
|
||||
$table = self::table( 'groups' );
|
||||
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently created groups.
|
||||
*
|
||||
* @param int $limit Number of groups.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_recent_groups( $limit = 5 ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'groups' );
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare( "SELECT * FROM {$table} ORDER BY created_at DESC LIMIT %d", $limit ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single group.
|
||||
*
|
||||
* @param int $id Group ID.
|
||||
* @return object|null
|
||||
*/
|
||||
public static function get_group( $id ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'groups' );
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a group.
|
||||
*
|
||||
* @param array $data { name, discount_percent }.
|
||||
* @return int New group ID (0 on failure).
|
||||
*/
|
||||
public static function insert_group( $data ) {
|
||||
global $wpdb;
|
||||
$now = current_time( 'mysql' );
|
||||
|
||||
$wpdb->insert(
|
||||
self::table( 'groups' ),
|
||||
array(
|
||||
'name' => $data['name'],
|
||||
'discount_percent' => $data['discount_percent'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
),
|
||||
array( '%s', '%f', '%s', '%s' )
|
||||
);
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a group.
|
||||
*
|
||||
* @param int $id Group ID.
|
||||
* @param array $data { name, discount_percent }.
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_group( $id, $data ) {
|
||||
global $wpdb;
|
||||
|
||||
return false !== $wpdb->update(
|
||||
self::table( 'groups' ),
|
||||
array(
|
||||
'name' => $data['name'],
|
||||
'discount_percent' => $data['discount_percent'],
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( 'id' => $id ),
|
||||
array( '%s', '%f', '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a group and its related rows.
|
||||
*
|
||||
* @param int $id Group ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_group( $id ) {
|
||||
global $wpdb;
|
||||
|
||||
// Manual cleanup in case foreign keys are unsupported (e.g. MyISAM).
|
||||
$wpdb->delete( self::table( 'group_categories' ), array( 'group_id' => $id ), array( '%d' ) );
|
||||
$wpdb->delete( self::table( 'shipping_rules' ), array( 'group_id' => $id ), array( '%d' ) );
|
||||
$wpdb->update( self::table( 'emails' ), array( 'group_id' => null ), array( 'group_id' => $id ), array( '%d' ), array( '%d' ) );
|
||||
|
||||
return false !== $wpdb->delete( self::table( 'groups' ), array( 'id' => $id ), array( '%d' ) );
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Group categories (M2M)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get category IDs for a group, ordered by sort_order.
|
||||
*
|
||||
* @param int $group_id Group ID.
|
||||
* @return int[]
|
||||
*/
|
||||
public static function get_group_category_ids( $group_id ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'group_categories' );
|
||||
|
||||
return array_map(
|
||||
'intval',
|
||||
$wpdb->get_col(
|
||||
$wpdb->prepare( "SELECT category_id FROM {$table} WHERE group_id = %d ORDER BY sort_order ASC, id ASC", $group_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the categories for a group. Sort order follows array order.
|
||||
*
|
||||
* @param int $group_id Group ID.
|
||||
* @param int[] $category_ids Ordered category IDs.
|
||||
*/
|
||||
public static function set_group_categories( $group_id, $category_ids ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->delete( self::table( 'group_categories' ), array( 'group_id' => $group_id ), array( '%d' ) );
|
||||
|
||||
$category_ids = array_values( array_unique( array_map( 'intval', $category_ids ) ) );
|
||||
|
||||
foreach ( $category_ids as $index => $category_id ) {
|
||||
if ( $category_id <= 0 ) {
|
||||
continue;
|
||||
}
|
||||
$wpdb->insert(
|
||||
self::table( 'group_categories' ),
|
||||
array(
|
||||
'group_id' => $group_id,
|
||||
'category_id' => $category_id,
|
||||
'sort_order' => $index,
|
||||
),
|
||||
array( '%d', '%d', '%d' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Email assignments
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get all email assignments.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_emails() {
|
||||
global $wpdb;
|
||||
$table = self::table( 'emails' );
|
||||
return $wpdb->get_results( "SELECT * FROM {$table} ORDER BY email_pattern ASC" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active email assignments.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function get_active_emails() {
|
||||
global $wpdb;
|
||||
$table = self::table( 'emails' );
|
||||
return $wpdb->get_results( "SELECT * FROM {$table} WHERE is_active = 1" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Count email assignments.
|
||||
*
|
||||
* @param bool $active_only Count only active assignments.
|
||||
* @return int
|
||||
*/
|
||||
public static function count_emails( $active_only = false ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'emails' );
|
||||
$sql = "SELECT COUNT(*) FROM {$table}";
|
||||
if ( $active_only ) {
|
||||
$sql .= ' WHERE is_active = 1';
|
||||
}
|
||||
return (int) $wpdb->get_var( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single email assignment.
|
||||
*
|
||||
* @param int $id Assignment ID.
|
||||
* @return object|null
|
||||
*/
|
||||
public static function get_email( $id ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'emails' );
|
||||
return $wpdb->get_row(
|
||||
$wpdb->prepare( "SELECT * FROM {$table} WHERE id = %d", $id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an email assignment.
|
||||
*
|
||||
* @param array $data Assignment data.
|
||||
* @return int New ID (0 on failure).
|
||||
*/
|
||||
public static function insert_email( $data ) {
|
||||
global $wpdb;
|
||||
$now = current_time( 'mysql' );
|
||||
|
||||
$wpdb->insert(
|
||||
self::table( 'emails' ),
|
||||
array(
|
||||
'email_pattern' => $data['email_pattern'],
|
||||
'assignment_type' => $data['assignment_type'],
|
||||
'group_id' => $data['group_id'],
|
||||
'coupon_code' => $data['coupon_code'],
|
||||
'custom_discount' => $data['custom_discount'],
|
||||
'is_active' => $data['is_active'],
|
||||
'free_shipping' => $data['free_shipping'],
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
)
|
||||
);
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an email assignment.
|
||||
*
|
||||
* @param int $id Assignment ID.
|
||||
* @param array $data Assignment data.
|
||||
* @return bool
|
||||
*/
|
||||
public static function update_email( $id, $data ) {
|
||||
global $wpdb;
|
||||
|
||||
return false !== $wpdb->update(
|
||||
self::table( 'emails' ),
|
||||
array(
|
||||
'email_pattern' => $data['email_pattern'],
|
||||
'assignment_type' => $data['assignment_type'],
|
||||
'group_id' => $data['group_id'],
|
||||
'coupon_code' => $data['coupon_code'],
|
||||
'custom_discount' => $data['custom_discount'],
|
||||
'is_active' => $data['is_active'],
|
||||
'free_shipping' => $data['free_shipping'],
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( 'id' => $id )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle an email assignment's active flag.
|
||||
*
|
||||
* @param int $id Assignment ID.
|
||||
* @param int $is_active 1 or 0.
|
||||
* @return bool
|
||||
*/
|
||||
public static function set_email_active( $id, $is_active ) {
|
||||
global $wpdb;
|
||||
|
||||
return false !== $wpdb->update(
|
||||
self::table( 'emails' ),
|
||||
array(
|
||||
'is_active' => $is_active ? 1 : 0,
|
||||
'updated_at' => current_time( 'mysql' ),
|
||||
),
|
||||
array( 'id' => $id ),
|
||||
array( '%d', '%s' ),
|
||||
array( '%d' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an email assignment.
|
||||
*
|
||||
* @param int $id Assignment ID.
|
||||
* @return bool
|
||||
*/
|
||||
public static function delete_email( $id ) {
|
||||
global $wpdb;
|
||||
return false !== $wpdb->delete( self::table( 'emails' ), array( 'id' => $id ), array( '%d' ) );
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Shipping rules
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get shipping rules for a group, ordered by sort_order.
|
||||
*
|
||||
* @param int $group_id Group ID.
|
||||
* @return array
|
||||
*/
|
||||
public static function get_shipping_rules( $group_id ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'shipping_rules' );
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare( "SELECT * FROM {$table} WHERE group_id = %d ORDER BY sort_order ASC, id ASC", $group_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all shipping rules for a group.
|
||||
*
|
||||
* @param int $group_id Group ID.
|
||||
* @param array $rules Array of { shipping_method_id, action, sort_order }.
|
||||
*/
|
||||
public static function replace_shipping_rules( $group_id, $rules ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->delete( self::table( 'shipping_rules' ), array( 'group_id' => $group_id ), array( '%d' ) );
|
||||
|
||||
foreach ( $rules as $rule ) {
|
||||
if ( empty( $rule['shipping_method_id'] ) || ! in_array( $rule['action'], array( 'show', 'hide' ), true ) ) {
|
||||
continue;
|
||||
}
|
||||
$wpdb->insert(
|
||||
self::table( 'shipping_rules' ),
|
||||
array(
|
||||
'group_id' => $group_id,
|
||||
'shipping_method_id' => $rule['shipping_method_id'],
|
||||
'action' => $rule['action'],
|
||||
'sort_order' => (int) $rule['sort_order'],
|
||||
),
|
||||
array( '%d', '%s', '%s', '%d' )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit log
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Insert an audit log entry.
|
||||
*
|
||||
* @param array $data Entry data.
|
||||
* @return int New entry ID (0 on failure).
|
||||
*/
|
||||
public static function insert_audit_entry( $data ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->insert(
|
||||
self::table( 'audit_log' ),
|
||||
array(
|
||||
'created_at' => current_time( 'mysql' ),
|
||||
'user_id' => (int) $data['user_id'],
|
||||
'user_login' => $data['user_login'],
|
||||
'ip' => $data['ip'],
|
||||
'method' => $data['method'],
|
||||
'route' => $data['route'],
|
||||
'status' => (int) $data['status'],
|
||||
'outcome' => $data['outcome'],
|
||||
'details' => $data['details'],
|
||||
),
|
||||
array( '%s', '%d', '%s', '%s', '%s', '%s', '%d', '%s', '%s' )
|
||||
);
|
||||
|
||||
return (int) $wpdb->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get audit log entries, newest first.
|
||||
*
|
||||
* @param int $limit Max entries.
|
||||
* @param int $offset Offset.
|
||||
* @param string $outcome Optional outcome filter (allowed/denied/rate_limited).
|
||||
* @return array
|
||||
*/
|
||||
public static function get_audit_entries( $limit = 50, $offset = 0, $outcome = '' ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'audit_log' );
|
||||
|
||||
if ( in_array( $outcome, array( 'allowed', 'denied', 'rate_limited' ), true ) ) {
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT * FROM {$table} WHERE outcome = %s ORDER BY id DESC LIMIT %d OFFSET %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
$outcome,
|
||||
$limit,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $wpdb->get_results(
|
||||
$wpdb->prepare( "SELECT * FROM {$table} ORDER BY id DESC LIMIT %d OFFSET %d", $limit, $offset ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Count audit log entries.
|
||||
*
|
||||
* @param string $outcome Optional outcome filter.
|
||||
* @return int
|
||||
*/
|
||||
public static function count_audit_entries( $outcome = '' ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'audit_log' );
|
||||
|
||||
if ( in_array( $outcome, array( 'allowed', 'denied', 'rate_limited' ), true ) ) {
|
||||
return (int) $wpdb->get_var(
|
||||
$wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE outcome = %s", $outcome ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete audit entries older than the retention period.
|
||||
*
|
||||
* @param int $days Retention in days.
|
||||
* @return int Rows deleted.
|
||||
*/
|
||||
public static function prune_audit_log( $days ) {
|
||||
global $wpdb;
|
||||
$table = self::table( 'audit_log' );
|
||||
|
||||
$cutoff = gmdate( 'Y-m-d H:i:s', strtotime( current_time( 'mysql' ) . ' -' . absint( $days ) . ' days' ) );
|
||||
|
||||
return (int) $wpdb->query(
|
||||
$wpdb->prepare( "DELETE FROM {$table} WHERE created_at < %s", $cutoff ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete every audit log entry.
|
||||
*
|
||||
* @return int Rows deleted.
|
||||
*/
|
||||
public static function clear_audit_log() {
|
||||
global $wpdb;
|
||||
$table = self::table( 'audit_log' );
|
||||
|
||||
return (int) $wpdb->query( "DELETE FROM {$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
<?php
|
||||
/**
|
||||
* Frontend / checkout integration.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies discounts, auto-applies coupons and filters shipping methods on checkout.
|
||||
*/
|
||||
class WC_EDM_Frontend {
|
||||
|
||||
/**
|
||||
* Session key holding the tracked guest email.
|
||||
*/
|
||||
const SESSION_EMAIL_KEY = 'wc_edm_email';
|
||||
|
||||
/**
|
||||
* Guard so the coupon is only auto-applied once per request.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $coupon_applied = false;
|
||||
|
||||
/**
|
||||
* Hook everything up.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
|
||||
|
||||
add_action( 'wp_ajax_wc_edm_track_email', array( $this, 'ajax_track_email' ) );
|
||||
add_action( 'wp_ajax_nopriv_wc_edm_track_email', array( $this, 'ajax_track_email' ) );
|
||||
|
||||
add_action( 'woocommerce_cart_calculate_fees', array( $this, 'apply_discount_fees' ), 20 );
|
||||
add_action( 'woocommerce_before_calculate_totals', array( $this, 'maybe_apply_coupon' ), 20 );
|
||||
add_filter( 'woocommerce_package_rates', array( $this, 'filter_shipping_rates' ), 20, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the checkout email tracking script.
|
||||
*/
|
||||
public function enqueue_scripts() {
|
||||
if ( ! function_exists( 'is_checkout' ) || ( ! is_checkout() && ! is_cart() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$params = array(
|
||||
'ajax_url' => admin_url( 'admin-ajax.php' ),
|
||||
'nonce' => wp_create_nonce( 'wc-edm-track-email' ),
|
||||
);
|
||||
|
||||
$script = '
|
||||
jQuery( function( $ ) {
|
||||
var wcEdm = ' . wp_json_encode( $params ) . ';
|
||||
var timer = null;
|
||||
var lastTracked = "";
|
||||
|
||||
function trackEmail( email ) {
|
||||
if ( ! email || email.indexOf( "@" ) < 1 || email === lastTracked ) {
|
||||
return;
|
||||
}
|
||||
lastTracked = email;
|
||||
$.post( wcEdm.ajax_url, {
|
||||
action: "wc_edm_track_email",
|
||||
nonce: wcEdm.nonce,
|
||||
email: email
|
||||
}, function() {
|
||||
$( document.body ).trigger( "update_checkout" );
|
||||
} );
|
||||
}
|
||||
|
||||
$( document.body ).on( "keyup change blur", "#billing_email", function() {
|
||||
var email = $( this ).val();
|
||||
clearTimeout( timer );
|
||||
timer = setTimeout( function() {
|
||||
trackEmail( email );
|
||||
}, 600 );
|
||||
} );
|
||||
|
||||
// Track a prefilled email on page load.
|
||||
var initial = $( "#billing_email" ).val();
|
||||
if ( initial ) {
|
||||
trackEmail( initial );
|
||||
}
|
||||
} );';
|
||||
|
||||
wp_add_inline_script( 'jquery', $script );
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX: store the checkout email in the WooCommerce session so
|
||||
* discounts apply when totals are recalculated.
|
||||
*/
|
||||
public function ajax_track_email() {
|
||||
check_ajax_referer( 'wc-edm-track-email', 'nonce' );
|
||||
|
||||
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
|
||||
if ( ! WC()->session ) {
|
||||
wp_send_json_error( array( 'message' => 'No session.' ) );
|
||||
}
|
||||
|
||||
if ( $email && is_email( $email ) ) {
|
||||
WC()->session->set( self::SESSION_EMAIL_KEY, $email );
|
||||
} else {
|
||||
WC()->session->set( self::SESSION_EMAIL_KEY, null );
|
||||
}
|
||||
|
||||
$this->clear_shipping_cache();
|
||||
|
||||
$match = $email ? WC_EDM_Helpers::find_email_match( $email ) : null;
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'matched' => (bool) $match,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached shipping rates so shipping rules re-evaluate.
|
||||
*/
|
||||
private function clear_shipping_cache() {
|
||||
if ( ! WC()->cart || ! WC()->session ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$packages = WC()->cart->get_shipping_packages();
|
||||
|
||||
foreach ( array_keys( $packages ) as $index ) {
|
||||
WC()->session->set( 'shipping_for_package_' . $index, null );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the email address of the current customer.
|
||||
*
|
||||
* Logged-in users use their account email; guests use the tracked
|
||||
* session email or the billing email WooCommerce already knows.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_customer_email() {
|
||||
if ( is_user_logged_in() ) {
|
||||
$user = wp_get_current_user();
|
||||
if ( $user && $user->user_email ) {
|
||||
return $user->user_email;
|
||||
}
|
||||
}
|
||||
|
||||
if ( WC()->session ) {
|
||||
$session_email = WC()->session->get( self::SESSION_EMAIL_KEY );
|
||||
if ( $session_email ) {
|
||||
return $session_email;
|
||||
}
|
||||
}
|
||||
|
||||
if ( WC()->customer ) {
|
||||
$billing_email = WC()->customer->get_billing_email();
|
||||
if ( $billing_email ) {
|
||||
return $billing_email;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current customer's email assignment match, if any.
|
||||
*
|
||||
* @return object|null
|
||||
*/
|
||||
private function get_current_match() {
|
||||
$email = $this->get_customer_email();
|
||||
|
||||
if ( ! $email ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return WC_EDM_Helpers::find_email_match( $email );
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply group/custom discounts as negative fees.
|
||||
*
|
||||
* @param WC_Cart $cart Cart object.
|
||||
*/
|
||||
public function apply_discount_fees( $cart ) {
|
||||
if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$match = $this->get_current_match();
|
||||
|
||||
if ( ! $match ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'group' === $match->assignment_type ) {
|
||||
$this->apply_group_fee( $cart, $match );
|
||||
} elseif ( 'custom' === $match->assignment_type ) {
|
||||
$this->apply_custom_fee( $cart, $match );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group discount: percentage off cart items in the group's categories.
|
||||
*
|
||||
* @param WC_Cart $cart Cart object.
|
||||
* @param object $match Email assignment row.
|
||||
*/
|
||||
private function apply_group_fee( $cart, $match ) {
|
||||
if ( ! $match->group_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$group = WC_EDM_DB::get_group( (int) $match->group_id );
|
||||
|
||||
if ( ! $group || (float) $group->discount_percent <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$category_ids = WC_EDM_DB::get_group_category_ids( (int) $group->id );
|
||||
|
||||
if ( empty( $category_ids ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eligible_total = 0;
|
||||
|
||||
foreach ( $cart->get_cart() as $cart_item ) {
|
||||
$product_id = (int) $cart_item['product_id']; // Parent ID for variations.
|
||||
|
||||
if ( has_term( $category_ids, 'product_cat', $product_id ) ) {
|
||||
$eligible_total += (float) $cart_item['line_total'];
|
||||
}
|
||||
}
|
||||
|
||||
if ( $eligible_total <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discount = $eligible_total * ( (float) $group->discount_percent / 100 );
|
||||
|
||||
$cart->add_fee(
|
||||
/* translators: %s: discount group name */
|
||||
sprintf( __( 'Email discount: %s', 'wc-email-discount' ), $group->name ),
|
||||
-$discount,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom discount: percentage off the entire cart contents.
|
||||
*
|
||||
* @param WC_Cart $cart Cart object.
|
||||
* @param object $match Email assignment row.
|
||||
*/
|
||||
private function apply_custom_fee( $cart, $match ) {
|
||||
$percent = (float) $match->custom_discount;
|
||||
|
||||
if ( $percent <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cart_total = 0;
|
||||
|
||||
foreach ( $cart->get_cart() as $cart_item ) {
|
||||
$cart_total += (float) $cart_item['line_total'];
|
||||
}
|
||||
|
||||
if ( $cart_total <= 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$discount = $cart_total * ( $percent / 100 );
|
||||
|
||||
$cart->add_fee(
|
||||
/* translators: %s: discount percent */
|
||||
sprintf( __( 'Email discount (%s%%)', 'wc-email-discount' ), wc_format_localized_decimal( $percent ) ),
|
||||
-$discount,
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-apply a coupon for coupon-type assignments.
|
||||
*
|
||||
* @param WC_Cart $cart Cart object.
|
||||
*/
|
||||
public function maybe_apply_coupon( $cart ) {
|
||||
if ( $this->coupon_applied || ( is_admin() && ! defined( 'DOING_AJAX' ) ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$match = $this->get_current_match();
|
||||
|
||||
if ( ! $match || 'coupon' !== $match->assignment_type || empty( $match->coupon_code ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$code = wc_format_coupon_code( $match->coupon_code );
|
||||
|
||||
if ( ! wc_get_coupon_id_by_code( $code ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $cart->has_discount( $code ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->coupon_applied = true;
|
||||
$cart->apply_coupon( $code );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter shipping rates for the matched email assignment.
|
||||
*
|
||||
* Group assignments get their show/hide rules applied; any assignment
|
||||
* with the free shipping flag gets all remaining rates at zero cost.
|
||||
*
|
||||
* @param array $rates Available shipping rates (rate_id => WC_Shipping_Rate).
|
||||
* @param array $package Shipping package.
|
||||
* @return array
|
||||
*/
|
||||
public function filter_shipping_rates( $rates, $package ) {
|
||||
$match = $this->get_current_match();
|
||||
|
||||
if ( ! $match ) {
|
||||
return $rates;
|
||||
}
|
||||
|
||||
if ( 'group' === $match->assignment_type && $match->group_id ) {
|
||||
$rates = $this->apply_group_shipping_rules( $rates, (int) $match->group_id );
|
||||
}
|
||||
|
||||
if ( ! empty( $match->free_shipping ) ) {
|
||||
foreach ( $rates as $rate ) {
|
||||
$rate->set_cost( 0 );
|
||||
$rate->set_taxes( array() );
|
||||
}
|
||||
}
|
||||
|
||||
return $rates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a group's show/hide shipping rules.
|
||||
*
|
||||
* If the group has no rules, all rates pass through untouched.
|
||||
* "show" rules act as a whitelist; "hide" rules remove rates.
|
||||
* Rates are reordered to follow the rules' sort order.
|
||||
*
|
||||
* @param array $rates Available shipping rates (rate_id => WC_Shipping_Rate).
|
||||
* @param int $group_id Discount group ID.
|
||||
* @return array
|
||||
*/
|
||||
private function apply_group_shipping_rules( $rates, $group_id ) {
|
||||
$rules = WC_EDM_DB::get_shipping_rules( $group_id );
|
||||
|
||||
if ( empty( $rules ) ) {
|
||||
return $rates;
|
||||
}
|
||||
|
||||
$show = array();
|
||||
$hide = array();
|
||||
|
||||
foreach ( $rules as $rule ) {
|
||||
if ( 'show' === $rule->action ) {
|
||||
$show[] = $rule->shipping_method_id;
|
||||
} else {
|
||||
$hide[] = $rule->shipping_method_id;
|
||||
}
|
||||
}
|
||||
|
||||
$filtered = array();
|
||||
|
||||
foreach ( $rates as $rate_id => $rate ) {
|
||||
// Whitelist mode: only explicitly shown methods survive.
|
||||
if ( ! empty( $show ) && ! in_array( $rate_id, $show, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( in_array( $rate_id, $hide, true ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filtered[ $rate_id ] = $rate;
|
||||
}
|
||||
|
||||
// Never leave the customer without shipping because of misconfigured rules.
|
||||
if ( empty( $filtered ) ) {
|
||||
return $rates;
|
||||
}
|
||||
|
||||
// Reorder rates to follow the rules' sort order; unmatched rates keep their position at the end.
|
||||
$rule_order = wp_list_pluck( $rules, 'shipping_method_id' );
|
||||
$ordered = array();
|
||||
|
||||
foreach ( $rule_order as $method_id ) {
|
||||
if ( isset( $filtered[ $method_id ] ) ) {
|
||||
$ordered[ $method_id ] = $filtered[ $method_id ];
|
||||
unset( $filtered[ $method_id ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $ordered + $filtered;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
/**
|
||||
* Utility functions.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper methods shared between admin and frontend.
|
||||
*/
|
||||
class WC_EDM_Helpers {
|
||||
|
||||
/**
|
||||
* Find the best matching email assignment for an email address.
|
||||
*
|
||||
* Match priority:
|
||||
* 1. Exact email match (john@example.com)
|
||||
* 2. Domain match (@company.com)
|
||||
* 3. Partial match (john)
|
||||
*
|
||||
* @param string $email Email address.
|
||||
* @return object|null Matching row from the emails table, or null.
|
||||
*/
|
||||
public static function find_email_match( $email ) {
|
||||
$email = strtolower( trim( (string) $email ) );
|
||||
|
||||
if ( '' === $email || ! is_email( $email ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$assignments = WC_EDM_DB::get_active_emails();
|
||||
|
||||
if ( empty( $assignments ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$domain_match = null;
|
||||
$partial_match = null;
|
||||
|
||||
foreach ( $assignments as $assignment ) {
|
||||
$pattern = strtolower( trim( $assignment->email_pattern ) );
|
||||
|
||||
if ( '' === $pattern ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1. Exact match wins immediately.
|
||||
if ( $pattern === $email ) {
|
||||
return $assignment;
|
||||
}
|
||||
|
||||
// 2. Domain match: pattern starts with '@'.
|
||||
if ( null === $domain_match && 0 === strpos( $pattern, '@' ) ) {
|
||||
if ( substr( $email, -strlen( $pattern ) ) === $pattern ) {
|
||||
$domain_match = $assignment;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Partial match: pattern occurs anywhere in the email.
|
||||
if ( null === $partial_match && false === strpos( $pattern, '@' ) && false !== strpos( $email, $pattern ) ) {
|
||||
$partial_match = $assignment;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $domain_match ) {
|
||||
return $domain_match;
|
||||
}
|
||||
|
||||
return $partial_match;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all product categories.
|
||||
*
|
||||
* @return WP_Term[]
|
||||
*/
|
||||
public static function get_product_categories() {
|
||||
$terms = get_terms(
|
||||
array(
|
||||
'taxonomy' => 'product_cat',
|
||||
'hide_empty' => false,
|
||||
'orderby' => 'name',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
return is_wp_error( $terms ) ? array() : $terms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get every shipping method instance across all zones (including "Rest of the world").
|
||||
*
|
||||
* @return array Map of method key ("flat_rate:3") => array{ label, zone, method }.
|
||||
*/
|
||||
public static function get_all_shipping_methods() {
|
||||
$methods = array();
|
||||
|
||||
if ( ! class_exists( 'WC_Shipping_Zones' ) ) {
|
||||
return $methods;
|
||||
}
|
||||
|
||||
$zones = WC_Shipping_Zones::get_zones();
|
||||
|
||||
// Zone 0 = "Locations not covered by your other zones".
|
||||
$rest_of_world = new WC_Shipping_Zone( 0 );
|
||||
$zones[] = array(
|
||||
'zone_name' => $rest_of_world->get_zone_name(),
|
||||
'shipping_methods' => $rest_of_world->get_shipping_methods(),
|
||||
);
|
||||
|
||||
foreach ( $zones as $zone ) {
|
||||
$zone_name = $zone['zone_name'];
|
||||
|
||||
foreach ( $zone['shipping_methods'] as $method ) {
|
||||
$key = $method->id . ':' . $method->get_instance_id();
|
||||
|
||||
$methods[ $key ] = array(
|
||||
'key' => $key,
|
||||
'label' => $method->get_title(),
|
||||
'zone' => $zone_name,
|
||||
'method' => $method->id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all published WooCommerce coupon codes.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public static function get_coupon_codes() {
|
||||
$posts = get_posts(
|
||||
array(
|
||||
'post_type' => 'shop_coupon',
|
||||
'post_status' => 'publish',
|
||||
'posts_per_page' => 500,
|
||||
'orderby' => 'title',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
|
||||
$codes = array();
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$codes[] = wc_format_coupon_code( $post->post_title );
|
||||
}
|
||||
|
||||
return array_values( array_unique( $codes ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable description of an email assignment's target.
|
||||
*
|
||||
* @param object $assignment Row from the emails table.
|
||||
* @return string
|
||||
*/
|
||||
public static function describe_assignment( $assignment ) {
|
||||
$extra = ! empty( $assignment->free_shipping )
|
||||
? ' + ' . __( 'free shipping', 'wc-email-discount' )
|
||||
: '';
|
||||
|
||||
return self::describe_assignment_type( $assignment ) . $extra;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe the discount part of an assignment (without extras).
|
||||
*
|
||||
* @param object $assignment Row from the emails table.
|
||||
* @return string
|
||||
*/
|
||||
private static function describe_assignment_type( $assignment ) {
|
||||
switch ( $assignment->assignment_type ) {
|
||||
case 'group':
|
||||
$group = $assignment->group_id ? WC_EDM_DB::get_group( (int) $assignment->group_id ) : null;
|
||||
if ( $group ) {
|
||||
/* translators: 1: group name, 2: discount percent */
|
||||
return sprintf( __( '%1$s (%2$s%%)', 'wc-email-discount' ), $group->name, wc_format_localized_decimal( $group->discount_percent ) );
|
||||
}
|
||||
return __( 'Group missing', 'wc-email-discount' );
|
||||
|
||||
case 'coupon':
|
||||
/* translators: %s: coupon code */
|
||||
return sprintf( __( 'Coupon: %s', 'wc-email-discount' ), $assignment->coupon_code );
|
||||
|
||||
case 'custom':
|
||||
/* translators: %s: discount percent */
|
||||
return sprintf( __( '%s%% off entire cart', 'wc-email-discount' ), wc_format_localized_decimal( $assignment->custom_discount ) );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a discount percent value into the 0–100 range.
|
||||
*
|
||||
* @param mixed $value Raw value.
|
||||
* @return float
|
||||
*/
|
||||
public static function sanitize_percent( $value ) {
|
||||
$value = (float) wc_format_decimal( wp_unslash( $value ) );
|
||||
return max( 0, min( 100, $value ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Build an admin URL for the plugin page.
|
||||
*
|
||||
* @param array $args Extra query args (tab, action, id, ...).
|
||||
* @return string
|
||||
*/
|
||||
public static function admin_url( $args = array() ) {
|
||||
return add_query_arg(
|
||||
array_merge( array( 'page' => 'wc-email-discounts' ), $args ),
|
||||
admin_url( 'admin.php' )
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
/**
|
||||
* Rate limiting for the wc-edm REST API.
|
||||
*
|
||||
* Uses a transient counter per identity (user ID when authenticated,
|
||||
* client IP otherwise) inside a rolling window.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple transient-backed request throttle.
|
||||
*/
|
||||
class WC_EDM_Rate_Limiter {
|
||||
|
||||
/**
|
||||
* Transient key prefix.
|
||||
*/
|
||||
const PREFIX = 'wc_edm_rl_';
|
||||
|
||||
/**
|
||||
* Whether rate limiting is active.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_enabled() {
|
||||
/**
|
||||
* Filter whether the REST API is rate limited.
|
||||
*
|
||||
* @param bool $enabled Default true.
|
||||
*/
|
||||
return (bool) apply_filters( 'wc_edm_rate_limit_enabled', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum requests allowed per window.
|
||||
*
|
||||
* @param string $method HTTP method of the current request.
|
||||
* @return int
|
||||
*/
|
||||
public static function max_requests( $method = 'GET' ) {
|
||||
$default = self::is_write_method( $method ) ? 20 : 60;
|
||||
|
||||
/**
|
||||
* Filter the request allowance per window.
|
||||
*
|
||||
* Writes get a tighter default than reads.
|
||||
*
|
||||
* @param int $default Allowed requests.
|
||||
* @param string $method HTTP method.
|
||||
*/
|
||||
return max( 1, (int) apply_filters( 'wc_edm_rate_limit_max_requests', $default, $method ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Length of the rolling window in seconds.
|
||||
*
|
||||
* @param string $method HTTP method of the current request.
|
||||
* @return int
|
||||
*/
|
||||
public static function window( $method = 'GET' ) {
|
||||
/**
|
||||
* Filter the rate limit window in seconds.
|
||||
*
|
||||
* @param int $seconds Default 60.
|
||||
* @param string $method HTTP method.
|
||||
*/
|
||||
return max( 1, (int) apply_filters( 'wc_edm_rate_limit_window', MINUTE_IN_SECONDS, $method ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a method mutates data.
|
||||
*
|
||||
* @param string $method HTTP method.
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_write_method( $method ) {
|
||||
return in_array( strtoupper( (string) $method ), array( 'POST', 'PUT', 'PATCH', 'DELETE' ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Identity used to bucket requests.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function identity() {
|
||||
$user_id = get_current_user_id();
|
||||
|
||||
if ( $user_id ) {
|
||||
return 'u' . $user_id;
|
||||
}
|
||||
|
||||
$ip = WC_EDM_Audit_Log::get_client_ip();
|
||||
|
||||
return 'i' . md5( $ip ? $ip : 'unknown' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this rate limit hit should be written to the audit log.
|
||||
*
|
||||
* A blocked caller that keeps hammering would otherwise write one log row
|
||||
* per rejected request, turning the audit log into an amplifier. Only the
|
||||
* first hit per identity per window is recorded.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function should_log_limit_hit() {
|
||||
$key = self::PREFIX . 'logged_' . self::identity();
|
||||
|
||||
if ( get_transient( $key ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
set_transient( $key, 1, self::window() );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a request and report whether the caller is over the limit.
|
||||
*
|
||||
* @param string $method HTTP method.
|
||||
* @return array{limited:bool,count:int,max:int,retry_after:int}
|
||||
*/
|
||||
public static function check( $method = 'GET' ) {
|
||||
$max = self::max_requests( $method );
|
||||
$window = self::window( $method );
|
||||
|
||||
if ( ! self::is_enabled() ) {
|
||||
return array(
|
||||
'limited' => false,
|
||||
'count' => 0,
|
||||
'max' => $max,
|
||||
'retry_after' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
// The key carries the window slice, so each window gets its own
|
||||
// counter and expires on its own. Re-arming a single transient's TTL
|
||||
// on every request would instead extend the block indefinitely.
|
||||
$now = time();
|
||||
$slice = (int) floor( $now / $window );
|
||||
$retry_after = ( ( $slice + 1 ) * $window ) - $now;
|
||||
$bucket = self::is_write_method( $method ) ? 'w' : 'r';
|
||||
$key = self::PREFIX . $bucket . '_' . self::identity() . '_' . $slice;
|
||||
|
||||
$count = (int) get_transient( $key ) + 1;
|
||||
|
||||
set_transient( $key, $count, $window );
|
||||
|
||||
return array(
|
||||
'limited' => $count > $max,
|
||||
'count' => $count,
|
||||
'max' => $max,
|
||||
'retry_after' => max( 1, $retry_after ),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,796 @@
|
||||
<?php
|
||||
/**
|
||||
* REST API for remote management.
|
||||
*
|
||||
* All routes require an authenticated user with the manage_woocommerce
|
||||
* capability (e.g. via a WordPress Application Password over HTTPS).
|
||||
* There is deliberately no token-in-code or unauthenticated access.
|
||||
*
|
||||
* @package WC_Email_Discount_Manager
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers wc-edm/v1 REST routes for groups, emails and shipping rules.
|
||||
*/
|
||||
class WC_EDM_REST_API {
|
||||
|
||||
/**
|
||||
* REST namespace.
|
||||
*/
|
||||
const REST_NAMESPACE = 'wc-edm/v1';
|
||||
|
||||
/**
|
||||
* Hook route registration.
|
||||
*/
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
add_filter( 'rest_request_after_callbacks', array( $this, 'log_request' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission check shared by all routes.
|
||||
*
|
||||
* Rate limiting runs before the capability check so that unauthenticated
|
||||
* floods are throttled too.
|
||||
*
|
||||
* @param WP_REST_Request|null $request Current request.
|
||||
* @return bool|WP_Error
|
||||
*/
|
||||
public function permission_check( $request = null ) {
|
||||
$method = $request instanceof WP_REST_Request ? $request->get_method() : 'GET';
|
||||
$rate = WC_EDM_Rate_Limiter::check( $method );
|
||||
|
||||
if ( $rate['limited'] ) {
|
||||
return new WP_Error(
|
||||
'wc_edm_rate_limited',
|
||||
__( 'Too many requests. Please slow down.', 'wc-email-discount' ),
|
||||
array(
|
||||
'status' => 429,
|
||||
'retry_after' => $rate['retry_after'],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( current_user_can( 'manage_woocommerce' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'wc_edm_forbidden',
|
||||
__( 'You are not allowed to manage email discounts.', 'wc-email-discount' ),
|
||||
array( 'status' => rest_authorization_required_code() )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write an audit entry for wc-edm requests.
|
||||
*
|
||||
* Runs after the route callback (and also when a permission check failed),
|
||||
* so both successful writes and rejected attempts are recorded. Successful
|
||||
* reads are skipped to keep the log signal-heavy.
|
||||
*
|
||||
* @param WP_REST_Response|WP_HTTP_Response|WP_Error $response Result to send.
|
||||
* @param array $handler Route handler.
|
||||
* @param WP_REST_Request $request Current request.
|
||||
* @return mixed The unmodified response.
|
||||
*/
|
||||
public function log_request( $response, $handler, $request ) {
|
||||
if ( ! $request instanceof WP_REST_Request ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$route = $request->get_route();
|
||||
|
||||
if ( 0 !== strpos( ltrim( $route, '/' ), self::REST_NAMESPACE ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$method = $request->get_method();
|
||||
$is_write = WC_EDM_Rate_Limiter::is_write_method( $method );
|
||||
$details = array();
|
||||
$outcome = 'allowed';
|
||||
$status = 200;
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$error_data = $response->get_error_data();
|
||||
$status = is_array( $error_data ) && isset( $error_data['status'] ) ? (int) $error_data['status'] : 500;
|
||||
$outcome = 'wc_edm_rate_limited' === $response->get_error_code() ? 'rate_limited' : 'denied';
|
||||
|
||||
$details['code'] = $response->get_error_code();
|
||||
} elseif ( $response instanceof WP_HTTP_Response ) {
|
||||
$status = $response->get_status();
|
||||
|
||||
if ( $status >= 400 ) {
|
||||
$outcome = 'denied';
|
||||
$data = $response->get_data();
|
||||
$details['code'] = is_array( $data ) && isset( $data['code'] ) ? $data['code'] : 'error';
|
||||
}
|
||||
}
|
||||
|
||||
// Successful reads are noise; everything else is worth keeping.
|
||||
if ( 'allowed' === $outcome && ! $is_write ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Don't let a blocked caller flood the log with identical rows.
|
||||
if ( 'rate_limited' === $outcome && ! WC_EDM_Rate_Limiter::should_log_limit_hit() ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
WC_EDM_Audit_Log::record( $outcome, $method, $route, $status, $details );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register all routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
$perm = array( $this, 'permission_check' );
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/status',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_status' ),
|
||||
'permission_callback' => $perm,
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/groups',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_groups' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_group' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/groups/(?P<id>\d+)',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_group' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_group' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => array( $this, 'delete_group' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/emails',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_emails' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_email' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/emails/(?P<id>\d+)',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_email' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_email' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => array( $this, 'delete_email' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/shipping-methods',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_shipping_methods' ),
|
||||
'permission_callback' => $perm,
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/groups/(?P<id>\d+)/shipping-rules',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_shipping_rules' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( $this, 'update_shipping_rules' ),
|
||||
'permission_callback' => $perm,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/audit-log',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_audit_log' ),
|
||||
'permission_callback' => $perm,
|
||||
'args' => array(
|
||||
'limit' => array(
|
||||
'type' => 'integer',
|
||||
'default' => 50,
|
||||
),
|
||||
'offset' => array(
|
||||
'type' => 'integer',
|
||||
'default' => 0,
|
||||
),
|
||||
'outcome' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'allowed', 'denied', 'rate_limited' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
register_rest_route(
|
||||
self::REST_NAMESPACE,
|
||||
'/match',
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'match_email' ),
|
||||
'permission_callback' => $perm,
|
||||
'args' => array(
|
||||
'email' => array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Formatters
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a group row for API output.
|
||||
*
|
||||
* @param object $group Group row.
|
||||
* @return array
|
||||
*/
|
||||
private function format_group( $group ) {
|
||||
return array(
|
||||
'id' => (int) $group->id,
|
||||
'name' => $group->name,
|
||||
'discount_percent' => (float) $group->discount_percent,
|
||||
'category_ids' => WC_EDM_DB::get_group_category_ids( (int) $group->id ),
|
||||
'created_at' => $group->created_at,
|
||||
'updated_at' => $group->updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format an email assignment row for API output.
|
||||
*
|
||||
* @param object $assignment Email row.
|
||||
* @return array
|
||||
*/
|
||||
private function format_email( $assignment ) {
|
||||
return array(
|
||||
'id' => (int) $assignment->id,
|
||||
'email_pattern' => $assignment->email_pattern,
|
||||
'assignment_type' => $assignment->assignment_type,
|
||||
'group_id' => $assignment->group_id ? (int) $assignment->group_id : null,
|
||||
'coupon_code' => $assignment->coupon_code,
|
||||
'custom_discount' => null !== $assignment->custom_discount ? (float) $assignment->custom_discount : null,
|
||||
'is_active' => (bool) $assignment->is_active,
|
||||
'free_shipping' => ! empty( $assignment->free_shipping ),
|
||||
'created_at' => $assignment->created_at,
|
||||
'updated_at' => $assignment->updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard 404 error.
|
||||
*
|
||||
* @return WP_Error
|
||||
*/
|
||||
private function not_found() {
|
||||
return new WP_Error( 'wc_edm_not_found', __( 'Resource not found.', 'wc-email-discount' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Status
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /status — connectivity test plus quick stats.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_status() {
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'plugin' => 'woocommerce-email-discount-manager',
|
||||
'version' => WC_EDM_VERSION,
|
||||
'woocommerce' => defined( 'WC_VERSION' ) ? WC_VERSION : null,
|
||||
'groups' => WC_EDM_DB::count_groups(),
|
||||
'email_assignments' => WC_EDM_DB::count_emails(),
|
||||
'active_assignments' => WC_EDM_DB::count_emails( true ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Groups
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /groups
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_groups() {
|
||||
return rest_ensure_response( array_map( array( $this, 'format_group' ), WC_EDM_DB::get_groups() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /groups/{id}
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_group( $request ) {
|
||||
$group = WC_EDM_DB::get_group( (int) $request['id'] );
|
||||
return $group ? rest_ensure_response( $this->format_group( $group ) ) : $this->not_found();
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /groups
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function create_group( $request ) {
|
||||
$name = sanitize_text_field( (string) $request['name'] );
|
||||
|
||||
if ( '' === $name ) {
|
||||
return new WP_Error( 'wc_edm_invalid', __( 'A group name is required.', 'wc-email-discount' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$id = WC_EDM_DB::insert_group(
|
||||
array(
|
||||
'name' => $name,
|
||||
'discount_percent' => WC_EDM_Helpers::sanitize_percent( $request['discount_percent'] ?? 0 ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $id ) {
|
||||
return new WP_Error( 'wc_edm_db_error', __( 'Could not create the group.', 'wc-email-discount' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
if ( is_array( $request['category_ids'] ) ) {
|
||||
WC_EDM_DB::set_group_categories( $id, array_map( 'absint', $request['category_ids'] ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $this->format_group( WC_EDM_DB::get_group( $id ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT/PATCH /groups/{id}
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function update_group( $request ) {
|
||||
$id = (int) $request['id'];
|
||||
$group = WC_EDM_DB::get_group( $id );
|
||||
|
||||
if ( ! $group ) {
|
||||
return $this->not_found();
|
||||
}
|
||||
|
||||
$name = null !== $request['name'] ? sanitize_text_field( (string) $request['name'] ) : $group->name;
|
||||
$percent = null !== $request['discount_percent'] ? WC_EDM_Helpers::sanitize_percent( $request['discount_percent'] ) : (float) $group->discount_percent;
|
||||
|
||||
if ( '' === $name ) {
|
||||
return new WP_Error( 'wc_edm_invalid', __( 'A group name is required.', 'wc-email-discount' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
WC_EDM_DB::update_group(
|
||||
$id,
|
||||
array(
|
||||
'name' => $name,
|
||||
'discount_percent' => $percent,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_array( $request['category_ids'] ) ) {
|
||||
WC_EDM_DB::set_group_categories( $id, array_map( 'absint', $request['category_ids'] ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $this->format_group( WC_EDM_DB::get_group( $id ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /groups/{id}
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function delete_group( $request ) {
|
||||
$id = (int) $request['id'];
|
||||
|
||||
if ( ! WC_EDM_DB::get_group( $id ) ) {
|
||||
return $this->not_found();
|
||||
}
|
||||
|
||||
WC_EDM_DB::delete_group( $id );
|
||||
|
||||
return rest_ensure_response( array( 'deleted' => true, 'id' => $id ) );
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Email assignments
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /emails
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_emails() {
|
||||
return rest_ensure_response( array_map( array( $this, 'format_email' ), WC_EDM_DB::get_emails() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /emails/{id}
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_email( $request ) {
|
||||
$assignment = WC_EDM_DB::get_email( (int) $request['id'] );
|
||||
return $assignment ? rest_ensure_response( $this->format_email( $assignment ) ) : $this->not_found();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and normalize email assignment input.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @param object|null $existing Existing row when updating.
|
||||
* @return array|WP_Error
|
||||
*/
|
||||
private function prepare_email_data( $request, $existing = null ) {
|
||||
$pattern = null !== $request['email_pattern']
|
||||
? strtolower( sanitize_text_field( (string) $request['email_pattern'] ) )
|
||||
: ( $existing ? $existing->email_pattern : '' );
|
||||
|
||||
$type = null !== $request['assignment_type']
|
||||
? sanitize_key( (string) $request['assignment_type'] )
|
||||
: ( $existing ? $existing->assignment_type : 'group' );
|
||||
|
||||
if ( '' === $pattern ) {
|
||||
return new WP_Error( 'wc_edm_invalid', __( 'An email pattern is required.', 'wc-email-discount' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
if ( ! in_array( $type, array( 'group', 'coupon', 'custom' ), true ) ) {
|
||||
return new WP_Error( 'wc_edm_invalid', __( 'assignment_type must be one of: group, coupon, custom.', 'wc-email-discount' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$data = array(
|
||||
'email_pattern' => $pattern,
|
||||
'assignment_type' => $type,
|
||||
'group_id' => null,
|
||||
'coupon_code' => null,
|
||||
'custom_discount' => null,
|
||||
'is_active' => null !== $request['is_active']
|
||||
? ( rest_sanitize_boolean( $request['is_active'] ) ? 1 : 0 )
|
||||
: ( $existing ? (int) $existing->is_active : 1 ),
|
||||
'free_shipping' => null !== $request['free_shipping']
|
||||
? ( rest_sanitize_boolean( $request['free_shipping'] ) ? 1 : 0 )
|
||||
: ( $existing ? (int) ! empty( $existing->free_shipping ) : 0 ),
|
||||
);
|
||||
|
||||
switch ( $type ) {
|
||||
case 'group':
|
||||
$group_id = null !== $request['group_id']
|
||||
? absint( $request['group_id'] )
|
||||
: ( $existing && $existing->group_id ? (int) $existing->group_id : 0 );
|
||||
|
||||
if ( ! $group_id || ! WC_EDM_DB::get_group( $group_id ) ) {
|
||||
return new WP_Error( 'wc_edm_invalid', __( 'A valid group_id is required for group assignments.', 'wc-email-discount' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$data['group_id'] = $group_id;
|
||||
break;
|
||||
|
||||
case 'coupon':
|
||||
$coupon = null !== $request['coupon_code']
|
||||
? wc_format_coupon_code( (string) $request['coupon_code'] )
|
||||
: ( $existing ? (string) $existing->coupon_code : '' );
|
||||
|
||||
if ( '' === $coupon ) {
|
||||
return new WP_Error( 'wc_edm_invalid', __( 'A coupon_code is required for coupon assignments.', 'wc-email-discount' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$data['coupon_code'] = $coupon;
|
||||
break;
|
||||
|
||||
case 'custom':
|
||||
$data['custom_discount'] = null !== $request['custom_discount']
|
||||
? WC_EDM_Helpers::sanitize_percent( $request['custom_discount'] )
|
||||
: ( $existing && null !== $existing->custom_discount ? (float) $existing->custom_discount : 0 );
|
||||
break;
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /emails
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function create_email( $request ) {
|
||||
$data = $this->prepare_email_data( $request );
|
||||
|
||||
if ( is_wp_error( $data ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$id = WC_EDM_DB::insert_email( $data );
|
||||
|
||||
if ( ! $id ) {
|
||||
return new WP_Error( 'wc_edm_db_error', __( 'Could not create the assignment.', 'wc-email-discount' ), array( 'status' => 500 ) );
|
||||
}
|
||||
|
||||
return rest_ensure_response( $this->format_email( WC_EDM_DB::get_email( $id ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT/PATCH /emails/{id}
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function update_email( $request ) {
|
||||
$id = (int) $request['id'];
|
||||
$existing = WC_EDM_DB::get_email( $id );
|
||||
|
||||
if ( ! $existing ) {
|
||||
return $this->not_found();
|
||||
}
|
||||
|
||||
$data = $this->prepare_email_data( $request, $existing );
|
||||
|
||||
if ( is_wp_error( $data ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
WC_EDM_DB::update_email( $id, $data );
|
||||
|
||||
return rest_ensure_response( $this->format_email( WC_EDM_DB::get_email( $id ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /emails/{id}
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function delete_email( $request ) {
|
||||
$id = (int) $request['id'];
|
||||
|
||||
if ( ! WC_EDM_DB::get_email( $id ) ) {
|
||||
return $this->not_found();
|
||||
}
|
||||
|
||||
WC_EDM_DB::delete_email( $id );
|
||||
|
||||
return rest_ensure_response( array( 'deleted' => true, 'id' => $id ) );
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Shipping
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /shipping-methods — all shipping method instances across zones.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_shipping_methods() {
|
||||
return rest_ensure_response( array_values( WC_EDM_Helpers::get_all_shipping_methods() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /groups/{id}/shipping-rules
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function get_shipping_rules( $request ) {
|
||||
$group_id = (int) $request['id'];
|
||||
|
||||
if ( ! WC_EDM_DB::get_group( $group_id ) ) {
|
||||
return $this->not_found();
|
||||
}
|
||||
|
||||
$rules = array();
|
||||
|
||||
foreach ( WC_EDM_DB::get_shipping_rules( $group_id ) as $rule ) {
|
||||
$rules[] = array(
|
||||
'shipping_method_id' => $rule->shipping_method_id,
|
||||
'action' => $rule->action,
|
||||
'sort_order' => (int) $rule->sort_order,
|
||||
);
|
||||
}
|
||||
|
||||
return rest_ensure_response( $rules );
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /groups/{id}/shipping-rules — replace all rules for the group.
|
||||
*
|
||||
* Body: { "rules": [ { "shipping_method_id": "flat_rate:3", "action": "show" }, ... ] }
|
||||
* Sort order follows array order. An empty rules array clears all rules.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response|WP_Error
|
||||
*/
|
||||
public function update_shipping_rules( $request ) {
|
||||
$group_id = (int) $request['id'];
|
||||
|
||||
if ( ! WC_EDM_DB::get_group( $group_id ) ) {
|
||||
return $this->not_found();
|
||||
}
|
||||
|
||||
$raw = $request['rules'];
|
||||
|
||||
if ( ! is_array( $raw ) ) {
|
||||
return new WP_Error( 'wc_edm_invalid', __( 'A rules array is required.', 'wc-email-discount' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$rules = array();
|
||||
|
||||
foreach ( array_values( $raw ) as $index => $rule ) {
|
||||
$method_id = isset( $rule['shipping_method_id'] ) ? sanitize_text_field( (string) $rule['shipping_method_id'] ) : '';
|
||||
$action = isset( $rule['action'] ) ? sanitize_key( (string) $rule['action'] ) : '';
|
||||
|
||||
if ( '' === $method_id || ! in_array( $action, array( 'show', 'hide' ), true ) ) {
|
||||
return new WP_Error(
|
||||
'wc_edm_invalid',
|
||||
__( 'Each rule needs a shipping_method_id and an action of "show" or "hide".', 'wc-email-discount' ),
|
||||
array( 'status' => 400 )
|
||||
);
|
||||
}
|
||||
|
||||
$rules[] = array(
|
||||
'shipping_method_id' => $method_id,
|
||||
'action' => $action,
|
||||
'sort_order' => $index,
|
||||
);
|
||||
}
|
||||
|
||||
WC_EDM_DB::replace_shipping_rules( $group_id, $rules );
|
||||
|
||||
return $this->get_shipping_rules( $request );
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit log
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /audit-log — recent REST activity, newest first.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function get_audit_log( $request ) {
|
||||
$limit = min( 200, max( 1, (int) $request['limit'] ) );
|
||||
$offset = max( 0, (int) $request['offset'] );
|
||||
$outcome = $request['outcome'] ? sanitize_key( (string) $request['outcome'] ) : '';
|
||||
|
||||
$entries = array();
|
||||
|
||||
foreach ( WC_EDM_DB::get_audit_entries( $limit, $offset, $outcome ) as $entry ) {
|
||||
$entries[] = array(
|
||||
'id' => (int) $entry->id,
|
||||
'created_at' => $entry->created_at,
|
||||
'user_id' => (int) $entry->user_id,
|
||||
'user_login' => $entry->user_login,
|
||||
'ip' => $entry->ip,
|
||||
'method' => $entry->method,
|
||||
'route' => $entry->route,
|
||||
'status' => (int) $entry->status,
|
||||
'outcome' => $entry->outcome,
|
||||
'details' => $entry->details ? json_decode( $entry->details, true ) : null,
|
||||
);
|
||||
}
|
||||
|
||||
$response = rest_ensure_response( $entries );
|
||||
$response->header( 'X-WP-Total', (string) WC_EDM_DB::count_audit_entries( $outcome ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Match test
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
/**
|
||||
* GET /match?email=... — test which assignment an email would get.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public function match_email( $request ) {
|
||||
$email = sanitize_email( (string) $request['email'] );
|
||||
$match = $email ? WC_EDM_Helpers::find_email_match( $email ) : null;
|
||||
|
||||
return rest_ensure_response(
|
||||
array(
|
||||
'email' => $email,
|
||||
'matched' => (bool) $match,
|
||||
'match' => $match ? $this->format_email( $match ) : null,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user