Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 135 additions & 3 deletions adminpages/discountcodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@
$uses = intval($_POST['uses']);
$one_use_per_user = ! empty( $_POST['one_use_per_user'] ) ? 1 : 0;

//discount type, value, and which payments the discount applies to
$posted_discount_type = isset( $_POST['discount_type'] ) ? sanitize_text_field( $_POST['discount_type'] ) : 'set_price';
$discount_type = array_key_exists( $posted_discount_type, pmpro_get_discount_code_types() ) ? $posted_discount_type : 'set_price';
if ( 'set_price' === $discount_type ) {
$discount_value = 0;
$apply_to_initial = 1;
$apply_to_recurring = 1;
} else {
$discount_value = isset( $_POST['discount_value'] ) ? max( 0, (float) sanitize_text_field( $_POST['discount_value'] ) ) : 0;
if ( 'percentage' === $discount_type ) {
$discount_value = min( 100, $discount_value );
}
$apply_to_initial = ! empty( $_POST['apply_to_initial'] ) ? 1 : 0;
$apply_to_recurring = ! empty( $_POST['apply_to_recurring'] ) ? 1 : 0;
}

//fix up dates
$starts = date("Y-m-d", strtotime($starts_month . "/" . $starts_day . "/" . $starts_year, $now ));
$expires = date("Y-m-d", strtotime($expires_month . "/" . $expires_day . "/" . $expires_year, $now ));
Expand All @@ -83,14 +99,22 @@
'starts' => $starts,
'expires' => $expires,
'uses' => $uses,
'one_use_per_user' => $one_use_per_user
'one_use_per_user' => $one_use_per_user,
'discount_type' => $discount_type,
'discount_value' => $discount_value,
'apply_to_initial' => $apply_to_initial,
'apply_to_recurring' => $apply_to_recurring
),
array(
'%d',
'%s',
'%s',
'%s',
'%d',
'%d',
'%s',
'%f',
'%d',
'%d'
)
);
Expand Down Expand Up @@ -240,6 +264,33 @@
$expiration_warning_flag = true;
}

// For percentage/fixed codes, ignore the submitted pricing fields and store a
// snapshot of the calculated prices instead. The snapshot is only for backwards
// compatibility with code reading this table directly; checkout always
// recalculates from the level's current pricing.
if ( 'set_price' !== $discount_type ) {
$snapshot = pmpro_get_discount_code_level_snapshot(
(object) array(
'discount_type' => $discount_type,
'discount_value' => $discount_value,
'apply_to_initial' => $apply_to_initial,
'apply_to_recurring' => $apply_to_recurring,
),
$level_id
);
if ( ! empty( $snapshot ) ) {
$initial_payment = $snapshot['initial_payment'];
$billing_amount = $snapshot['billing_amount'];
$cycle_number = $snapshot['cycle_number'];
$cycle_period = $snapshot['cycle_period'];
$billing_limit = $snapshot['billing_limit'];
$trial_amount = $snapshot['trial_amount'];
$trial_limit = $snapshot['trial_limit'];
$expiration_number = $snapshot['expiration_number'];
$expiration_period = $snapshot['expiration_period'];
}
}

//okay, do the insert
$wpdb->insert(
$wpdb->pmpro_discount_codes_levels,
Expand Down Expand Up @@ -430,8 +481,18 @@
$code->starts = $temp_code->starts;
$code->expires = $temp_code->expires;
$code->uses = $temp_code->uses;
$code->discount_type = ! empty( $temp_code->discount_type ) ? $temp_code->discount_type : 'set_price';
$code->discount_value = ! empty( $temp_code->discount_value ) ? $temp_code->discount_value : 0;
$code->apply_to_initial = isset( $temp_code->apply_to_initial ) ? $temp_code->apply_to_initial : 1;
$code->apply_to_recurring = isset( $temp_code->apply_to_recurring ) ? $temp_code->apply_to_recurring : 1;
}
}

// Discount rule settings for the form.
$code_discount_type = ! empty( $code->discount_type ) ? $code->discount_type : 'set_price';
$code_discount_value = ! empty( $code->discount_value ) ? $code->discount_value : '';
$code_apply_to_initial = isset( $code->apply_to_initial ) ? (int) $code->apply_to_initial : 1;
$code_apply_to_recurring = isset( $code->apply_to_recurring ) ? (int) $code->apply_to_recurring : 1;
?>
<form action="" method="post">
<input name="saveid" type="hidden" value="<?php echo esc_attr( $edit ); ?>" />
Expand Down Expand Up @@ -473,6 +534,18 @@
$month_options[ $i ] = date_i18n( 'F', mktime( 0, 0, 0, $i, 2 ) );
}

// Every discount code type other than set_price applies a formula to the level's
// regular pricing, so the formula fields show for any of these types.
$formula_discount_types = array_values( array_diff( array_keys( pmpro_get_discount_code_types() ), array( 'set_price' ) ) );
$code_is_formula = 'set_price' !== $code_discount_type;

// Per-level markup toggled by the Discount Type select: formula codes show a note in
// place of the per-level pricing fields. Same for every level, so built once here.
$formula_note_class = 'pmpro_discount_level_formula_note' . ( $code_is_formula ? '' : ' pmpro-hidden' );
$pricing_fields_class = 'form-table pmpro_discount_level_pricing_fields' . ( $code_is_formula ? ' pmpro-hidden' : '' );
$formula_note_depends = esc_attr( wp_json_encode( array( array( 'id' => 'discount_type', 'value' => $formula_discount_types ) ) ) );
$pricing_fields_depends = esc_attr( wp_json_encode( array( array( 'id' => 'discount_type', 'value' => 'set_price' ) ) ) );

pmpro_build_settings_section( array(
'id' => 'general-discount-code-settings',
'title' => __( 'General Discount Code Settings', 'paid-memberships-pro' ),
Expand Down Expand Up @@ -534,6 +607,50 @@
'value' => ! empty( $code->one_use_per_user ) ? 1 : 0,
'checkbox_label' => __( 'Restrict this discount code to a single use per unique user.', 'paid-memberships-pro' ),
),
array(
'name' => 'discount_type',
'label' => __( 'Discount Type', 'paid-memberships-pro' ),
'type' => 'select',
'value' => $code_discount_type,
'options' => pmpro_get_discount_code_types(),
'description' => __( 'Set custom pricing to define the exact prices for each level below, or apply a percentage or fixed amount discount to each level\'s regular pricing.', 'paid-memberships-pro' ),
),
array(
'name' => 'discount_value',
'label' => __( 'Discount Amount', 'paid-memberships-pro' ),
'type' => 'callback',
'row_class' => 'pmpro_discount_rule_row',
'depends' => array( array( 'id' => 'discount_type', 'value' => $formula_discount_types ) ),
'callback' => function() use ( $pmpro_currency_symbol, $code_discount_type, $code_discount_value ) {
?>
<span id="discount_value_unit_fixed" class="<?php echo esc_attr( 'fixed' === $code_discount_type ? '' : 'pmpro-hidden' ); ?>" data-pmpro-depends="<?php echo esc_attr( wp_json_encode( array( array( 'id' => 'discount_type', 'value' => 'fixed' ) ) ) ); ?>"><?php echo wp_kses_post( $pmpro_currency_symbol ); ?></span>
<input name="discount_value" id="discount_value" type="text" size="10" value="<?php echo esc_attr( pmpro_filter_price_for_text_field( $code_discount_value ) ); ?>" />
<span id="discount_value_unit_percentage" class="<?php echo esc_attr( 'percentage' === $code_discount_type ? '' : 'pmpro-hidden' ); ?>" data-pmpro-depends="<?php echo esc_attr( wp_json_encode( array( array( 'id' => 'discount_type', 'value' => 'percentage' ) ) ) ); ?>">%</span>
<p class="description"><?php esc_html_e( 'The percentage or amount to subtract from the level\'s regular pricing at checkout.', 'paid-memberships-pro' ); ?></p>
<?php
},
),
array(
'label' => __( 'Applies To', 'paid-memberships-pro' ),
'type' => 'callback',
'row_class' => 'pmpro_discount_rule_row',
'depends' => array( array( 'id' => 'discount_type', 'value' => $formula_discount_types ) ),
'callback' => function() use ( $code_apply_to_initial, $code_apply_to_recurring ) {
?>
<fieldset>
<label for="apply_to_initial">
<input name="apply_to_initial" id="apply_to_initial" type="checkbox" value="1" <?php checked( $code_apply_to_initial, 1 ); ?> />
<?php esc_html_e( 'Initial payment', 'paid-memberships-pro' ); ?>
</label>
<br />
<label for="apply_to_recurring">
<input name="apply_to_recurring" id="apply_to_recurring" type="checkbox" value="1" <?php checked( $code_apply_to_recurring, 1 ); ?> />
<?php esc_html_e( 'Recurring payments', 'paid-memberships-pro' ); ?>
</label>
</fieldset>
<?php
},
),
array(
'hook' => 'pmpro_discount_code_after_settings',
'args' => array( $edit ),
Expand All @@ -548,7 +665,11 @@
'title' => __( 'Membership Level Settings', 'paid-memberships-pro' ),
) );
?>
<p><?php esc_html_e('Which levels will this code apply to?', 'paid-memberships-pro' ); ?></p>
<p>
<?php esc_html_e('Which levels will this code apply to?', 'paid-memberships-pro' ); ?>
<button type="button" class="button button-secondary button-small" onclick="pmpro_toggleAllDiscountLevels(true);"><?php esc_html_e( 'Select All', 'paid-memberships-pro' ); ?></button>
<button type="button" class="button button-secondary button-small" onclick="pmpro_toggleAllDiscountLevels(false);"><?php esc_html_e( 'Deselect All', 'paid-memberships-pro' ); ?></button>
</p>

<div class="pmpro_discount_levels">
<?php
Expand Down Expand Up @@ -605,7 +726,10 @@
<label for="<?php echo esc_attr( $level_checkbox_id ); ?>"><?php echo esc_html( $level->name );?></label>
</div>
<div class="<?php echo esc_attr( $level_pricing_class ); ?>" data-pmpro-depends="<?php echo $level_pricing_depends; ?>">
<table class="form-table">
<div class="<?php echo esc_attr( $formula_note_class ); ?>" data-pmpro-depends="<?php echo $formula_note_depends; ?>">
<p class="description"><?php esc_html_e( 'The discount will be applied to this level\'s regular pricing at checkout. Edit the membership level to change its regular pricing.', 'paid-memberships-pro' ); ?></p>
</div>
<table class="<?php echo esc_attr( $pricing_fields_class ); ?>" data-pmpro-depends="<?php echo $pricing_fields_depends; ?>">
<tbody>
<tr>
<th scope="row" valign="top"><label for="initial_payment"><?php esc_html_e('Initial Payment', 'paid-memberships-pro' );?></label></th>
Expand Down Expand Up @@ -751,6 +875,14 @@
</div> <!-- end pmpro_levels_div -->
<?php pmpro_build_settings_section_close(); ?>

<script>
// The depends handler in pmpro-admin.js hides/shows each level's pricing panel, so
// firing change after setting the checkboxes is all the visibility work needed here.
function pmpro_toggleAllDiscountLevels( checked ) {
jQuery('.pmpro_discount_levels input[name="levels[]"]').prop('checked', checked).trigger('change');
}
</script>

<p class="submit">
<input name="save" type="submit" class="button button-primary" value="<?php esc_attr_e( 'Save Code', 'paid-memberships-pro' ) ?>" />
<input name="cancel" type="button" class="button" value="<?php esc_attr_e( 'Cancel', 'paid-memberships-pro' ) ?>" onclick="location.href='<?php echo esc_url( admin_url( 'admin.php?page=pmpro-discountcodes') ); ?>';" />
Expand Down
6 changes: 6 additions & 0 deletions classes/class-pmpro-discount-code-list-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,12 @@ public function column_discount_code( $item ) {

?>
<strong><a title="<?php echo esc_attr( sprintf( __( 'Edit Code: %s', 'paid-memberships-pro' ), $item->id ) ); ?>" href="<?php echo esc_url( add_query_arg( array( 'page' => 'pmpro-discountcodes', 'edit' => $item->id ), admin_url('admin.php' ) ) ); ?>"><?php echo esc_html( $item->code ); ?></a></strong>
<?php
$rule_text = pmpro_get_discount_code_rule_text( $item );
if ( ! empty( $rule_text ) ) {
echo '<br /><span class="description">' . esc_html( $rule_text ) . '</span>';
}
?>
<div class="row-actions">
<?php
$delete_text = esc_html(
Expand Down
80 changes: 72 additions & 8 deletions classes/class-pmpro-discount-codes.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,33 @@ class PMPro_Discount_Code {
*/
public $uses = 0;

/**
* Discount type: set_price, percentage, or fixed.
*
* @var string $discount_type
*/
public $discount_type = 'set_price';

/**
* Discount value for percentage/fixed codes, e.g. 20 for 20% off or 10 for 10 off.
*
* @var float $discount_value
*/
public $discount_value = 0;

/**
* Whether a percentage/fixed discount applies to the initial payment.
*
* @var int $apply_to_initial
*/
public $apply_to_initial = 1;

/**
* Whether a percentage/fixed discount applies to recurring payments.
*
* @var int $apply_to_recurring
*/
public $apply_to_recurring = 1;

/**
* Levels and billing settings tied to the discount code.
Expand Down Expand Up @@ -151,7 +178,11 @@ function get_discount_code_by_code( $code ) {
$this->starts = $dcobj->starts;
$this->expires = $dcobj->expires;
$this->uses = $dcobj->uses;

$this->discount_type = ! empty( $dcobj->discount_type ) ? $dcobj->discount_type : 'set_price';
$this->discount_value = isset( $dcobj->discount_value ) ? $dcobj->discount_value : 0;
$this->apply_to_initial = isset( $dcobj->apply_to_initial ) ? $dcobj->apply_to_initial : 1;
$this->apply_to_recurring = isset( $dcobj->apply_to_recurring ) ? $dcobj->apply_to_recurring : 1;

foreach( $levels as $level ) {
$this->levels[$level->level_id] = array(
'initial_payment' => $level->initial_payment,
Expand Down Expand Up @@ -234,28 +265,45 @@ function save() {
}
}

// Make sure the discount type and value are valid.
if ( empty( $this->discount_type ) || ! array_key_exists( $this->discount_type, pmpro_get_discount_code_types() ) ) {
$this->discount_type = 'set_price';
}
$this->discount_value = max( 0, floatval( $this->discount_value ) );
if ( 'percentage' === $this->discount_type ) {
$this->discount_value = min( 100, $this->discount_value );
}

// If the code doesn't exist, create it otherwise update it.
if ( empty( $this->id ) ) {

$before_action = 'pmpro_add_discount_code';
$after_action = 'pmpro_added_discount_code';

$this->sqlQuery = "INSERT INTO $wpdb->pmpro_discount_codes ( `code`, `starts`, `expires`, `uses` )
$this->sqlQuery = "INSERT INTO $wpdb->pmpro_discount_codes ( `code`, `starts`, `expires`, `uses`, `discount_type`, `discount_value`, `apply_to_initial`, `apply_to_recurring` )
VALUES ('" . esc_sql( $this->code ) . "',
'" . esc_sql( $this->starts ) ."',
'" . esc_sql( $this->expires ) ."',
" . intval( $this->uses ) ."
)";
" . intval( $this->uses ) .",
'" . esc_sql( $this->discount_type ) . "',
" . floatval( $this->discount_value ) . ",
" . ( ! empty( $this->apply_to_initial ) ? 1 : 0 ) . ",
" . ( ! empty( $this->apply_to_recurring ) ? 1 : 0 ) . "
)";
} else {

$before_action = 'pmpro_update_discount_code';
$after_action = 'pmpro_updated_discount_code';

$this->sqlQuery = "UPDATE $wpdb->pmpro_discount_codes
SET `code` = '" . esc_sql( $this->code ) ."',
`starts` = '" . esc_sql( $this->starts ) . "',
`expires` = '" . esc_sql( $this->expires ) . "',
`uses` = " . intval( $this->uses ) . "
`uses` = " . intval( $this->uses ) . ",
`discount_type` = '" . esc_sql( $this->discount_type ) . "',
`discount_value` = " . floatval( $this->discount_value ) . ",
`apply_to_initial` = " . ( ! empty( $this->apply_to_initial ) ? 1 : 0 ) . ",
`apply_to_recurring` = " . ( ! empty( $this->apply_to_recurring ) ? 1 : 0 ) . "
WHERE code = '" . esc_sql( $this->code ) . "'
LIMIT 1";
}
Expand Down Expand Up @@ -310,9 +358,25 @@ function save() {
if ( $wpdb->query( $this->sqlQuery ) !== false ) {
$sql_okay = true;
}

// For percentage/fixed codes, overwrite the pricing columns with a snapshot of
// the calculated prices. Checkout always recalculates from the level's pricing.
if ( 'set_price' !== $this->discount_type ) {
$snapshot = pmpro_get_discount_code_level_snapshot( $this, $level_id );
if ( ! empty( $snapshot ) ) {
$wpdb->update(
$wpdb->pmpro_discount_codes_levels,
$snapshot,
array(
'code_id' => $this->id,
'level_id' => $level_id,
)
);
}
}
}
}
}

do_action( $after_action, $this );

unset( $this->sqlQuery ); //remove SQL query.
Expand Down
5 changes: 2 additions & 3 deletions classes/class.memberorder.php
Original file line number Diff line number Diff line change
Expand Up @@ -1238,9 +1238,8 @@ function getMembershipLevel($force = false)
else
$discount_code = $this->discount_code;

$sqlQuery = $wpdb->prepare( "SELECT l.id, cl.*, l.name, l.description, l.allow_signups FROM $wpdb->pmpro_discount_codes_levels cl LEFT JOIN $wpdb->pmpro_membership_levels l ON cl.level_id = l.id LEFT JOIN $wpdb->pmpro_discount_codes dc ON dc.id = cl.code_id WHERE dc.code = %s AND cl.level_id = %d LIMIT 1", $discount_code, $this->membership_id );

$this->membership_level = $wpdb->get_row($sqlQuery);
// Resolve the effective level pricing for this code.
$this->membership_level = pmpro_get_discounted_level_for_code( $this->membership_id, $discount_code );
}

//just get the info from the membership table (sigh, I really need to standardize the column names for membership_id/level_id) but we're checking if we got the information already or not
Expand Down
11 changes: 11 additions & 0 deletions css/admin.css
Original file line number Diff line number Diff line change
Expand Up @@ -2314,6 +2314,17 @@ table.pmprommpu_levels tr.remove_level td {background: #F2DEDE; }
padding: 0 calc( var(--pmpro--spacing--medium) / 2 ) calc( var(--pmpro--spacing--medium) / 2 ) calc( var(--pmpro--spacing--medium) / 2 );
}

/* The pricing panel has no top padding (the form-table provides its own spacing), so the
formula note supplies matching top spacing when it is the only visible child. Padding,
not margin: a margin would collapse through the unpadded panel and land outside it. */
.pmpro_discount_levels .pmpro_discount_level .pmpro_discount_level_formula_note {
padding-top: calc( var(--pmpro--spacing--medium) / 2 );
}

.pmpro_discount_levels .pmpro_discount_level .pmpro_discount_level_formula_note p {
margin: 0;
}

.pmpro_discount_code-tag {
border: 2px dashed var(--pmpro--color--blue-darkest);
display: block;
Expand Down
Loading