We recently had a request to add a duplicate coupon feature to some of the Woo sites that my team manages. It’s not unusual for store managers to need to recreate the same coupon over and over and this feature would save them some repetitive effort. During a moment between projects, I went on and picked up the task.
The store manager who mentioned this idea to us, passed along the URL to an existing plugin that adds this feature. While it worked without any problems, I did take issue with its approach.
WooCommerce coupons are a “custom post type” which means their data is stored in the _posts and _postmeta tables in the WordPress database. This plugin elected to directly copy and manipulate information in these tables.
Whenever possible, I want to use WooCommerce’s API for tasks like these. It makes the code easier to understand and is more likely to work even when there are changes made to the Woo core. So my approach was based on utilizing Woo’s WC_Coupon class and its methods to make the copy.
With that in mind, let’s walk through how I went about doing this.
Setup
First I began by creating a class to encapsulate this functionality. This isn’t going to be too complicated so we only need a single class with two methods in it. In order to add a new item to the “actions” beneath the coupon title in the list of coupons, we’ll need to use the post_row_actions filter.
Secondly, the action we’re going to be using when the form submitted is called duplicate_coupon and it’s tied to admin.php so we’ll need to hook into the admin_action_duplicate_coupon action. Since we’re in a class, we need to add these inside the classes __construct method. This is called automatically whenever the class is initialized.
class WooCommerce_Duplicate_Coupons {
/**
* Loads necessary hooks and filters.
*/
public function __construct() {
add_filter( 'post_row_actions', [ $this, 'add_duplicate_coupon_link' ], 10, 2 );
add_action( 'admin_action_duplicate_coupon', [ $this, 'duplicate_coupon' ] );
}
Typically you can add a function to an action/filter by just passing the name of the function. Since we’re inside a class, we need to change this up by passing an array with $this and the method (function) as the array items. That way, WordPress will know where to go to access this code. Also, we’ll need to make sure that these methods are set to public visibility.
Add Duplicate Link
Now that we have the initial setup complete, we need to go about adding the “duplicate link” that will be used to trigger the actual duplication. We’ll handle this in its own function. Here’s what I came up with:
public function add_duplicate_coupon_link( array $actions, WP_Post $post ): array {
if ( $post->post_type === 'shop_coupon' ) {
$actions['duplicate'] = '<a href="' . wp_nonce_url( 'admin.php?action=duplicate_coupon&post_id=' . $post->ID, 'duplicate_coupon_' . $post->ID ) . '" title="Duplicate this coupon" rel="permalink">Duplicate</a>';
}
return $actions;
}
So this method takes two arguments, one is an array of actions, and the other is the WP_Post object (the existing coupon). First I check to make sure the post type is shop_coupon otherwise we don’t do anything. There’s no need to add this link on the blog posts list or anywhere else in the admin. I could also check the admin screen we’re on here, but since I already have the WP_Post object, I’m just checking the type there.
If this is a shop_coupon post type, we add a new item to the $actions array called duplicate. Its value is set to be a link that has the parameters we need inside it. Let’s take a closer look at what’s going on here.
- admin.php?action=duplicate_coupon — this is the action we’re wanting to use to process the data submitted with this URL.
- &post_id=’ . $post->ID, — this is the ID of the coupon that we’re wanting to copy. It’s a unique integer and the output will look like
&post_id=5when it’s run. - ‘duplicate_coupon_’ . $post->ID — this will be the name of our nonce. What’s a nonce? It’s a way of securing input from users to prevent accidental or malicious actions. It’s used in the
wp_nonce_url()method we’re calling here.
Lastly we return the modified array of actions.
Process Duplicate Request
Now comes the fun part; actually duplicating the coupon. Let’s take a closer look at how and why I choose this way of implementing this feature.
public function duplicate_coupon(): void {
// Input validation.
$post_id = isset( $_GET['post_id'] ) ? intval( $_GET['post_id'] ) : 0;
if ( ! $post_id || check_admin_referer( 'duplicate_coupon_' . $post_id ) === false ) {
wp_die( 'Invalid request.' );
}
if ( ! current_user_can( 'edit_shop_coupons' ) ) {
wp_die( 'You do not have permission to duplicate coupons.' );
}
if ( get_post_type( $post_id ) !== 'shop_coupon' ) {
wp_die( 'Invalid post type.' );
}
First we need to start of with validating the input from our user. If we don’t do this, we could make our store vulnerable to being hacked or we could unintentionally trigger this action over and over by visiting the same URL in our browser.
Our data is submitted via the URL so we’ll use the $_GET global variable for the details. We’ll start by validating the the post_id that is submitted exists and that it is an integer. If it isn’t we’re going to set the $post_id to 0? Why? So we can use it in the next check.
Here if the $post_id evaluates to false (which 0 would) or if the check_admin_referer() fails, we’ll call wp_die() which will cause our script to exit.
Next we check for the permissions of the current user to make sure they are authorized to work with coupons. We wouldn’t want someone who is simply a customer to be able to duplicate coupons! The edit_shop_coupons seems to be the appropriate permission for this situation.
Lastly, we check to ensure that this $post_id corresponds to a shop_coupon type. We don’t want to attempt to instantiate a WC_Coupon object with the wrong post type (like a blog or page).
Now that we’re confident this action is authorized, and that the data is correct, we can get down to actually copying the coupon.
$original_coupon = new WC_Coupon( $post_id );
// Create a new coupon object.
$duplicate = clone $original_coupon;
// Reset ID and date properties.
$duplicate->set_id( 0 );
$duplicate->set_date_created( null );
$duplicate->set_date_expires( null );
$duplicate->set_date_modified( null );
Here we’re loading the existing coupon with new WC_Coupon( $post_id )which will load the coupon we’re working with from the database. Our next task is to clone this coupon and assign it to a new variable, $duplicate so we can manipulate it.
Since this coupon has all of the original’s data, we need to alter some of it so it makes sense as a copy. First we’ll want to reset the ID so the system can give it a unique ID. This is an internal ID that’s generated by the _posts table and isn’t really visible to the user or the store manager.
Next we reset the dates to null so they can also be assigned by the system to represent our actions today.
$new_code = $original_coupon->get_code() . ' (copy)';
while ( wc_get_coupon_id_by_code( $new_code ) ) {
$new_code .= ' (copy)';
}
$duplicate->set_code( $new_code );
The actual coupon code is really the title of the post. To help the store manager find the new coupon, I’m simply appending (copy) to the end of the title. But what happens if you create a copy and then create a second copy directly from the original without changing any of the codes? We’d end up with two coupons called <coupon_code> (copy).
In the event of two coupons having the same code, WooCommerce will use the most recent one. It would be better to have every coupon with a unique code so I added a while loop here. This loop will keep loading coupons using the $new_code we’ve created until it doesn’t find a match (returns false). As long as it finds a match, it’ll keep adding additional (copy) instances to the end. Once it’s unique, we set the code.
$duplicate->set_usage_count( 0 );
$duplicate->set_used_by( [] );
The last two items we need to reset are the usage count and the used_by array. This way our new coupon won’t appear to have been used by anyone.
Lastly, we implement a try/catch block to save the coupon and redirect the user to the list of coupons or throw an error if there are any issues. And that’s really all there is to this.
try {
// Save the new coupon and redirect to the coupon list.
$duplicate->save();
wp_safe_redirect( admin_url( 'edit.php?post_type=shop_coupon' ) );
exit;
} catch ( Exception $e ) {
wp_die( 'An error occurred while duplicating the coupon: ' . esc_html( $e->getMessage() ) );
}
Possible Improvements
I went back and forth over the coupon code creation. In the end it seemed more important to me to make it easy to locate the copy by including (copy)in the title.
You could also add a bit of JavaScript here to trigger an alert where a custom coupon code could be entered. The alert could show the suggested coupon code of the existing one with (copy) appended to it. The user could replace that with whatever they want. Or the title field could be cleared out to allow a random one to be generated.
Another way to improve this would be to include some automated tests. That way you could be confident that changes to the WooCommerce core aren’t affecting the script by easily testing when a new version of Woo is released.
Hopefully, you found this helpful. To help anyone who wants to use it, the full code is included below.
<?php
/**
* Plugin Name: Duplicate WooCommerce Coupons
* Description: Duplicates WooCommerce coupons from the UI.
* Version: 1.0.0
* Requires at least: 6.0
* Requires PHP: 8.0
* Author: Bill Robbins
* Plugin URI: https://justabill.blog
* License: GPL v2 or later
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
*/
/**
* This class enables the duplication of WooCommerce coupons from the UI.
*/
class WooCommerce_Duplicate_Coupons {
/**
* Loads necessary hooks and filters.
*/
public function __construct() {
add_filter( 'post_row_actions', [ $this, 'add_duplicate_coupon_link' ], 10, 2 );
add_action( 'admin_action_duplicate_coupon', [ $this, 'duplicate_coupon' ] );
}
/**
* Add a "Duplicate" link to the row actions for coupons.
*
* @param array $actions An array of row action links.
* @param WP_Post $post The post object.
* @throws Exception If the coupon cannot be duplicated.
*
* @return array The modified array of row action links.
*/
public function add_duplicate_coupon_link( array $actions, WP_Post $post ): array {
if ( $post->post_type === 'shop_coupon' ) {
$actions['duplicate'] = '<a href="' . wp_nonce_url( 'admin.php?action=duplicate_coupon&post_id=' . $post->ID, 'duplicate_coupon_' . $post->ID ) . '" title="Duplicate this coupon" rel="permalink">Duplicate</a>';
}
return $actions;
}
/**
* Duplicate a coupon.
*
* It creates a new coupon with the same properties as the original, except for
* the ID, date properties, usage count, and used by meta.
*
* @return void
*/
public function duplicate_coupon(): void {
// Input validation.
$post_id = isset( $_GET['post_id'] ) ? intval( $_GET['post_id'] ) : 0;
if ( ! $post_id || check_admin_referer( 'duplicate_coupon_' . $post_id ) === false ) {
wp_die( 'Invalid request.' );
}
if ( ! current_user_can( 'edit_shop_coupons' ) ) {
wp_die( 'You do not have permission to duplicate coupons.' );
}
if ( get_post_type( $post_id ) !== 'shop_coupon' ) {
wp_die( 'Invalid post type.' );
}
$original_coupon = new WC_Coupon( $post_id );
// Create a new coupon object.
$duplicate = clone $original_coupon;
// Reset ID and date properties.
$duplicate->set_id( 0 );
$duplicate->set_date_created( null );
$duplicate->set_date_expires( null );
$duplicate->set_date_modified( null );
// Append "(copy)" to the existing coupon code (Post title) to make it unique.
$new_code = $original_coupon->get_code() . ' (copy)';
while ( wc_get_coupon_id_by_code( $new_code ) ) {
$new_code .= ' (copy)';
}
$duplicate->set_code( $new_code );
// Reset usage count and used by meta.
$duplicate->set_usage_count( 0 );
$duplicate->set_used_by( [] );
try {
// Save the new coupon and redirect to the coupon list.
$duplicate->save();
wp_safe_redirect( admin_url( 'edit.php?post_type=shop_coupon' ) );
exit;
} catch ( Exception $e ) {
wp_die( 'An error occurred while duplicating the coupon: ' . esc_html( $e->getMessage() ) );
}
}
}
new WooCommerce_Duplicate_Coupons();





Leave a Reply