├── .editorconfig
├── .github
└── workflows
│ └── php-tests.yml
├── .gitignore
├── assets
├── admin.js
└── onboarding.js
├── bin
└── archive.sh
├── changelog.txt
├── composer.json
├── composer.lock
├── includes
├── class-wc-coupon-restrictions-admin.php
├── class-wc-coupon-restrictions-cli.php
├── class-wc-coupon-restrictions-onboarding.php
├── class-wc-coupon-restrictions-settings.php
├── class-wc-coupon-restrictions-table.php
├── class-wc-coupon-restrictions-validation-cart.php
├── class-wc-coupon-restrictions-validation-checkout.php
└── class-wc-coupon-restrictions-validation.php
├── languages
└── woocommerce-coupon-restrictions.pot
├── license.txt
├── phpcs.xml
├── phpunit.xml
├── readme.md
├── readme.txt
├── tests
├── bin
│ ├── install-woocommerce.sh
│ └── install-wp-tests.sh
├── phpunit
│ ├── Integration
│ │ ├── ApplyCountryRestrictionCouponTest.php
│ │ ├── ApplyExistingCustomerCouponTest.php
│ │ ├── ApplyGenericCouponTest.php
│ │ ├── ApplyNewCustomerCouponTest.php
│ │ ├── ApplyPostcodeRestrictionCouponTest.php
│ │ ├── ApplyRoleRestrictionCouponTest.php
│ │ ├── ApplyStateRestrictionCouponTest.php
│ │ ├── CheckoutCountryRestrictionCouponTest.php
│ │ ├── CheckoutExistingCustomerCouponTest.php
│ │ ├── CheckoutLimitPerIPTest.php
│ │ ├── CheckoutLimitPerShippingAddressTest.php
│ │ ├── CheckoutLimitSimilarEmailsTest.php
│ │ ├── CheckoutNewCustomerCouponTest.php
│ │ ├── CheckoutPostcodeRestrictionCouponTest.php
│ │ ├── CheckoutRoleRestrictionCouponTest.php
│ │ ├── CheckoutStateRestrictionCouponTest.php
│ │ ├── OnboardingSetupTest.php
│ │ ├── RestrictionsTableTest.php
│ │ ├── ReturningCustomerTest.php
│ │ └── ValidationsTest.php
│ ├── Unit
│ │ └── ValidationTest.php
│ ├── bootstrap.php
│ └── helpers
│ │ ├── class-wc-helper-coupon.php
│ │ ├── class-wc-helper-customer.php
│ │ ├── class-wc-helper-order.php
│ │ ├── class-wc-helper-product.php
│ │ └── class-wc-helper-shipping.php
├── readme.md
└── wp-tests-config.php
└── woocommerce-coupon-restrictions.php
/.editorconfig:
--------------------------------------------------------------------------------
1 | # This file is for unifying the coding style for different editors and IDEs
2 | # editorconfig.org
3 |
4 | # WordPress Coding Standards
5 | # https://make.wordpress.org/core/handbook/coding-standards/
6 |
7 | root = true
8 |
9 | [*]
10 | charset = utf-8
11 | end_of_line = lf
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 | indent_style = tab
15 | indent_size = 4
16 |
17 | [{.jshintrc,*.json}]
18 | indent_style = space
19 | indent_size = 4
20 |
21 | [*.yml]
22 | indent_style = space
23 | indent_size = 2
24 |
25 | [{*.txt,wp-config-sample.php}]
26 | end_of_line = crlf
27 |
--------------------------------------------------------------------------------
/.github/workflows/php-tests.yml:
--------------------------------------------------------------------------------
1 | name: PHPUnit Tests
2 |
3 | on: [push]
4 |
5 | jobs:
6 | test:
7 | strategy:
8 | matrix:
9 | php_ver:
10 | - "8.3"
11 | wp_ver:
12 | - "latest"
13 | wc_ver:
14 | - "latest"
15 | - "9.4.0"
16 |
17 | runs-on: ubuntu-latest
18 |
19 | steps:
20 | - name: Checkout the files
21 | uses: actions/checkout@v3
22 |
23 | - name: Install SVN
24 | run: sudo apt-get update && sudo apt-get install -y subversion
25 |
26 | # Sets PHP version to 8.3
27 | - name: Setup PHP with tools
28 | uses: shivammathur/setup-php@v2
29 | with:
30 | php-version: "8.3"
31 | tools: phpunit
32 |
33 | - name: Cache Composer dependencies
34 | uses: actions/cache@v4
35 | with:
36 | path: /tmp/composer-cache
37 | key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
38 |
39 | - name: Install dependencies
40 | uses: php-actions/composer@v6
41 | with:
42 | php_version: ${{ matrix.php_ver }}
43 | version: 2
44 |
45 | - name: Start MySQL
46 | run: sudo /etc/init.d/mysql start
47 |
48 | - name: Run install-wp-tests.sh
49 | run: sudo ./tests/bin/install-wp-tests.sh wordpress_test root root localhost ${{ matrix.wp_ver }}
50 |
51 | - name: Run install-woocommerce.sh
52 | run: sudo ./tests/bin/install-woocommerce.sh ${{ matrix.wc_ver }}
53 |
54 | - name: Run phpunit
55 | run: ./vendor/bin/phpunit -c phpunit.xml
56 | env:
57 | WC_VERSION: ${{ matrix.wc_ver }}
58 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | .DS_Store
3 | Icon
4 |
5 | # Node
6 | node_modules
7 | npm-debug.log
8 |
9 | # Composer
10 | dist
11 | vendor
12 |
13 | # Caches
14 | .phpcs-cache
15 |
16 | # Tests
17 | tests/tmp
18 | .phpunit.result.cache
19 |
--------------------------------------------------------------------------------
/assets/admin.js:
--------------------------------------------------------------------------------
1 | jQuery(function ($) {
2 | var location_restrictions_group = document.querySelector(
3 | ".woocommerce-coupon-restrictions-locations"
4 | );
5 | var location_restrictions_cb = document.querySelector(
6 | "#location_restrictions"
7 | );
8 |
9 | if (location_restrictions_cb.checked) {
10 | location_restrictions_group.removeAttribute("style");
11 | }
12 |
13 | location_restrictions_cb.addEventListener("change", function () {
14 | if (this.checked) {
15 | location_restrictions_group.removeAttribute("style");
16 | } else {
17 | location_restrictions_group.style.display = "none";
18 | }
19 | });
20 |
21 | document
22 | .querySelector("#wccr-add-all-countries")
23 | .addEventListener("click", function () {
24 | $("#wccr-restricted-countries")
25 | .select2("destroy")
26 | .find("option")
27 | .prop("selected", "selected")
28 | .end()
29 | .select2();
30 | });
31 |
32 | document
33 | .querySelector("#wccr-clear-all-countries")
34 | .addEventListener("click", function () {
35 | $("#wccr-restricted-countries")
36 | .select2("destroy")
37 | .find("option")
38 | .prop("selected", false)
39 | .end()
40 | .select2();
41 | });
42 | });
43 |
--------------------------------------------------------------------------------
/assets/onboarding.js:
--------------------------------------------------------------------------------
1 | jQuery( function( $ ) {
2 | // Displays the pointers once the coupon tabs load.
3 | const observer = new MutationObserver((mutations, obs) => {
4 | const tabs = document.getElementsByClassName('coupon_data_tabs')[0];
5 | if (tabs) {
6 | initPointers();
7 | obs.disconnect();
8 | return;
9 | }
10 | });
11 |
12 | observer.observe(document, {
13 | childList: true,
14 | subtree: true
15 | });
16 |
17 | function initPointers() {
18 | $.each( WCCR_POINTERS.pointers, function( i ) {
19 | revealTab( i );
20 | showPointer( i );
21 | return false;
22 | });
23 | }
24 |
25 | function showPointer( id ) {
26 | var pointer = WCCR_POINTERS.pointers[ id ];
27 | var options = $.extend( pointer.options, {
28 | pointerClass: 'wp-pointer wc-pointer',
29 | close: function() {
30 | revealTab( pointer.next );
31 | if ( pointer.next ) {
32 | showPointer( pointer.next );
33 | }
34 | },
35 | buttons: function( event, t ) {
36 | const btnClose = $( `${WCCR_POINTERS.close}` );
37 | const btnNext = $( `${WCCR_POINTERS.next}` );
38 | const btnComplete = $( `${WCCR_POINTERS.enjoy}` );
39 |
40 | let wrapper = $( `
` );
41 |
42 | btnClose.bind( 'click.pointer', function(e) {
43 | e.preventDefault();
44 | t.element.pointer('destroy');
45 |
46 | // Updates the URL so pointers won't show on page refresh.
47 | var url = window.location.href;
48 | url = url.replace('&woocommerce-coupon-restriction-pointers=1', '');
49 | window.history.pushState(null, null, url);
50 | });
51 |
52 | btnNext.bind( 'click.pointer', function(e) {
53 | e.preventDefault();
54 | t.element.pointer('close');
55 | });
56 |
57 | btnComplete.bind( 'click.pointer', function(e) {
58 | e.preventDefault();
59 | t.element.pointer('close');
60 | });
61 |
62 | wrapper.append( btnClose );
63 |
64 | if ('multiple-restictions' !== id) {
65 | wrapper.append( btnNext );
66 | } else {
67 | wrapper.append( btnComplete );
68 | }
69 |
70 | return wrapper;
71 | },
72 | });
73 |
74 | const thisPointer = $( pointer.target ).pointer( options );
75 | $('html, body').animate({ scrollTop: $( pointer.target ).offset().top - 200 });
76 | thisPointer.pointer( 'open' );
77 |
78 | if ( pointer.next_trigger ) {
79 | $( pointer.next_trigger.target ).on( pointer.next_trigger.event, function() {
80 | setTimeout( function() { thisPointer.pointer( 'close' ); }, 400 );
81 | });
82 | }
83 | }
84 |
85 | function revealTab( pointer ) {
86 | if ( 'coupon-restrictions-panel' === pointer ) {
87 | $('#woocommerce-coupon-data .usage_restriction_tab a').trigger('click');
88 | }
89 | if ( 'usage-limit' === pointer ) {
90 | $('#woocommerce-coupon-data .usage_limit_tab a').trigger('click');
91 | }
92 | if ( 'role-restriction' === pointer ) {
93 | $('#woocommerce-coupon-data .usage_restriction_tab a').trigger('click');
94 | }
95 | if ( 'location-restrictions' === pointer ) {
96 | $('#usage_restriction_coupon_data .checkbox').trigger('click');
97 | }
98 | }
99 | });
100 |
--------------------------------------------------------------------------------
/bin/archive.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | VERSION=$1
4 | SLUG="woocommerce-coupon-restrictions"
5 |
6 | if [[ "" = "$VERSION" ]]; then
7 | VERSION=$(sed -n "s/ \* Version:[ ]*\(.*\)/\1/p" ${SLUG}.php)
8 | fi
9 |
10 | mkdir -p dist
11 | git archive --prefix="${SLUG}/" --output="dist/${SLUG}-${VERSION}.zip" HEAD
12 |
--------------------------------------------------------------------------------
/changelog.txt:
--------------------------------------------------------------------------------
1 | *** WooCommerce Coupon Restrictions Changelog ***
2 |
3 | 2024-02-25 - version 2.2.3
4 |
5 | * Update: Declare compatibility with latest version of WooCommerce.
6 | * Update: Fix deprecated call to is_coupon_valid.
7 | * Update: Improve docblock documentation.
8 | * Update: Improve automated testing suite.
9 |
10 | 2024-02-05 - version 2.2.2
11 |
12 | * Bugfix: Add explicit check for array type for meta values that require it.
13 |
14 | 2023-04-07 - version 2.2.1
15 |
16 | * Bugfix: Fatal error on subscription renewal with coupon applied due to missing class.
17 |
18 | 2023-03-09 - version 2.2.0
19 |
20 | * Feature: WP CLI command to add historic order data to verification table.
21 | * Update: Improve documentation around enhanced usage limits.
22 | * Update: Declare compatibility for High Performance Order Storage.
23 |
24 | 2023-02-10 - version 2.1.0
25 |
26 | * Bugfix: Coupon verification records were not storing properly on checkouts with payment.
27 | * Bugfix: Show compatibility notice if WooCommerce is not active.
28 |
29 | 2023-01-02 - version 2.0.0
30 |
31 | * Update: Major plugin rewrite.
32 | * Enhancement: Enhanced usage limits to help reduce coupon fraud.
33 |
34 | 2022-11-09 - version 1.8.6
35 |
36 | * Update: Tested to WooCommerce 7.1.0.
37 | * Update: Migrate inline admin javascript to enqueued file.
38 |
39 | 2022-03-06 - version 1.8.5
40 |
41 | * Bugfix: Rename translation file.
42 | * Update: Migrate inline javascript to file.
43 |
44 | 2022-01-12 - version 1.8.4
45 |
46 | * Bugfix: Display specific coupon validation messages during ajax checkout validation.
47 | * Update: Tested to WooCommerce 6.1.0.
48 | * Update: WooCommerce 4.8.1 or higher required.
49 |
50 | 2021-03-28 - version 1.8.3
51 |
52 | * Enhancement: Adds "Guest (No Account)" option to the roles restriction.
53 |
54 | 2021-01-12 - version 1.8.2
55 |
56 | * Enhancement: Reduces use of coupon meta by only storing non-default values.
57 | * Update: Tested to WooCommerce 4.8.0.
58 | * Update: PHP 7.0 or higher required.
59 | * Update: WooCommerce 3.9.0 or higher required.
60 |
61 | 2020-06-14 - version 1.8.1
62 |
63 | * Enhancement: Add all countries easily for country restriction.
64 | * Enhancement: Improved automated testing suite.
65 | * Update: Tested to WooCommerce 4.2.0.
66 |
67 | 2019-09-25 - version 1.8.0
68 |
69 | * Enhancement: Zip code restrictions now allow wildcard matches.
70 | * Enhancement: Filter to skip pre-checkout validation.
71 | * Bugfix: If user is logged in and has no orders associated with their account but does have previous guest orders, those guest orders will now be checked to verify if customer is a new customer when "check guest orders" is selected.
72 |
73 | 2019-03-12 - version 1.7.2
74 |
75 | * Bugfix: Fixes 500 when saving coupons in specific server environments.
76 |
77 | 2019-03-03 - version 1.7.1
78 |
79 | * Enhancement: Adds ability to restrict coupon by state.
80 |
81 | 2019-02-11 - version 1.7.0
82 |
83 | * Update: New setting for new/existing customer verification method. Defaults to account check.
84 | * Bugfix: Resolves bug applying coupon when there is no session (subscription renewals). Props @giantpeach.
85 |
86 | 2018-07-17 - version 1.6.2
87 |
88 | * Bugfix: PHP5.6 compatibility fixes for onboarding script.
89 |
90 | 2018-06-21 - version 1.6.1
91 |
92 | * Update: Use WooCommerce data store methods for saving and reading coupon meta.
93 | * Update: WC_Coupon_Restrictions() now returns shared instance of class rather than singleton.
94 | * Bugfix: Display onboarding notice on initial activation.
95 | * Bugfix: If the session data is blank for country or zipcode, a coupon with location restrictions will now apply until session or checkout has data to validate it.
96 |
97 | 2018-06-15 - version 1.6.0
98 |
99 | * Enhancement: Coupon validation now uses stored session data.
100 | * Enhancement: Checkout validation now uses $posted data.
101 | * Update: Additional unit and integration tests.
102 | * Update: Returns a main instance of the class to avoid the need for globals.
103 |
104 | 2018-05-17 - version 1.5.0
105 |
106 | * Update: Improve coupon validation messages.
107 | * Update: Use "Zip code" as default label.
108 | * Update: Improve customer restriction UX. Use radio buttons rather than select.
109 | * Update: Adds "Location Restrictions" checkbox. Additional options display when checked.
110 | * Update: Country restriction only permits selection of countries that shop currently sells to.
111 | * Update: New onboarding flow that shows where the new coupon options are located.
112 | * Bugfix: Bug with new customer coupon validation at checkout.
113 |
114 | 2018-02-15 - version 1.4.1
115 |
116 | * Update: Remove upgrade routine.
117 |
118 | 2017-12-27 - version 1.4.0
119 |
120 | * Enhancement: Adds option to restrict location based on shipping or billing address.
121 | * Enhancement: Adds option to restrict to postal code or zip code.
122 | * Update: Use WooCommerce order wrapper to fetch orders.
123 | * Update: Organize plugin into multiple classes.
124 | * Update: Upgrade script for sites using earlier version of plugin.
125 | * Update: Unit test framework added.
126 |
127 | 2017-01-31 - version 1.3.0
128 |
129 | * Enhancement: Adds option to restrict to existing customers.
130 | * Enhancement: Adds option to restrict by shipping country.
131 | * Update: Compatibility updates for WooCommerce 2.7.0.
132 |
133 | 2016-11-25 - version 1.2.0
134 |
135 | * Update: Compatibility updates for WooCommerce 2.6.8.
136 |
137 | 2015-12-28 - version 1.1.0
138 |
139 | * Bugfix: Coupons without the new customer restriction were improperly marked invalid for logged in users.
140 | * Bugfix: Filter woocommerce_coupon_is_valid in addition to woocommerce_coupon_error.
141 |
142 | 2015-06-18 - version 1.0.0
143 |
144 | * Initial release.
145 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "devinsays/woocommerce-coupon-restrictions",
3 | "description": "This extension allows you to create coupons with additional restriction options.",
4 | "keywords": [
5 | "wordpress"
6 | ],
7 | "type": "wordpress-plugin",
8 | "homepage": "https://github.com/devinsays/woocommerce-coupon-restrictions",
9 | "license": "GPL-2.0+",
10 | "authors": [
11 | {
12 | "name": "Devin Price",
13 | "homepage": "https://devpress.com"
14 | }
15 | ],
16 | "support": {
17 | "issues": "https://github.com/devinsays/woocommerce-coupon-restrictions/issues",
18 | "source": "https://github.com/devinsays/woocommerce-coupon-restrictions"
19 | },
20 | "require-dev": {
21 | "automattic/vipwpcs": "^3.0.1",
22 | "assertwell/wp-core-test-framework": "^0.3.0",
23 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
24 | "phpunit/phpunit": "^9",
25 | "yoast/phpunit-polyfills": "^2.0.0",
26 | "wp-cli/i18n-command": "^2.6.0"
27 | },
28 | "scripts": {
29 | "make-pot": "wp i18n make-pot . languages/woocommerce-coupon-restrictions.pot"
30 | },
31 | "autoload-dev": {
32 | "psr-4": {
33 | "WooCommerceCouponRestrictions\\Test\\": "tests/phpunit/"
34 | },
35 | "classmap": [
36 | "includes/"
37 | ]
38 | },
39 | "config": {
40 | "allow-plugins": {
41 | "dealerdirect/phpcodesniffer-composer-installer": true
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/includes/class-wc-coupon-restrictions-cli.php:
--------------------------------------------------------------------------------
1 | explainer_text();
18 | $code = $this->ask( __( 'Coupon code to update data for:', 'woocommerce-coupon-restrictions' ) );
19 |
20 | $coupon = new WC_Coupon( $code );
21 | if ( ! $coupon ) {
22 | WP_CLI::error( __( 'Coupon not found.', 'woocommerce-coupon-restrictions' ) );
23 | exit;
24 | }
25 |
26 | $usage_count = $coupon->get_usage_count();
27 | if ( ! $usage_count ) {
28 | WP_CLI::error( __( 'Coupon has not been used for any orders.', 'woocommerce-coupon-restrictions' ) );
29 | exit;
30 | }
31 |
32 | if ( ! WC_Coupon_Restrictions_Validation::has_enhanced_usage_restrictions( $coupon ) ) {
33 | WP_CLI::error( __( 'Coupon does not have any enhanced usage restrictions set.', 'woocommerce-coupon-restrictions' ) );
34 | exit;
35 | }
36 |
37 | /* translators: %s: usage count of coupon */
38 | WP_CLI::log( sprintf( __( 'Coupon has been used %d times.', 'woocommerce-coupon-restrictions' ), $usage_count ) );
39 | WP_CLI::log( '' );
40 |
41 | // This allows us to continue processing an update if it was interrupted.
42 | $processed_key = 'wcr_processing_' . $code;
43 | $processed_value = get_transient( $processed_key );
44 | $offset_key = 'wcr_offset_' . $code;
45 | $offset_value = get_transient( $offset_key );
46 | $last_processed_id = 0;
47 | $offset = 0;
48 |
49 | if ( $processed_value ) {
50 | /* translators: %s: last order processed for WP CLI command. */
51 | WP_CLI::warning( sprintf( __( 'An update has already been started. The last order id processed was: %d.', 'woocommerce-coupon-restrictions' ), $processed_value ) );
52 | $answer = $this->ask( sprintf( __( 'Would you like to continue processing from order id %d? [yes/no]', 'woocommerce-coupon-restrictions' ), $processed_value ) );
53 | if ( 'yes' === trim( $answer ) || 'y' === trim( $answer ) ) {
54 | $last_processed_id = $processed_value;
55 | $offset = $offset_value;
56 | } else {
57 | WP_CLI::log( __( 'Data update will be restarted. All order data for coupon will be refreshed.', 'woocommerce-coupon-restrictions' ) );
58 | }
59 | WP_CLI::log( '' );
60 | }
61 |
62 | WP_CLI::log( __( 'Orders will be checked in batches.', 'woocommerce-coupon-restrictions' ) );
63 | $limit_answer = $this->ask( __( 'How many orders would you like to process per batch? [100]:', 'woocommerce-coupon-restrictions' ) );
64 |
65 | $limit = intval( $limit_answer ) ? intval( $limit_answer ) : 100;
66 | $count = 0;
67 | $date = $coupon->get_date_created()->date( 'Y-m-d' );
68 |
69 | while ( true ) {
70 | WP_CLI::log( sprintf( __( 'Querying order batch starting at order id: %d', 'woocommerce-coupon-restrictions' ), $last_processed_id ) );
71 | $ids = self::get_order_batch( $limit, $offset, $date );
72 | if ( ! $ids && $count === 0 ) {
73 | WP_CLI::warning( __( 'No orders available to process.', 'woocommerce-coupon-restrictions' ) );
74 | break;
75 | }
76 |
77 | foreach ( $ids as $order_id ) {
78 | self::maybe_add_record( $order_id, $code );
79 |
80 | // Updates the counters
81 | $last_processed_id = $order_id;
82 | $offset++;
83 | $count++;
84 | }
85 |
86 | // We'll update the transient after each batch, so we can continue processing if interrupted.
87 | set_transient( $processed_key, $last_processed_id, HOUR_IN_SECONDS );
88 | set_transient( $offset_key, $offset, HOUR_IN_SECONDS );
89 |
90 | if ( count( $ids ) < $limit ) {
91 | WP_CLI::log( '' );
92 | WP_CLI::success( __( 'Finished updating verification table.', 'woocommerce-coupon-restrictions' ) );
93 | break;
94 | }
95 | }
96 | }
97 |
98 | /**
99 | * Checks if the order has the coupon code being checked.
100 | * If so, it checks if the order already exists in the table.
101 | * If not, it adds the record.
102 | *
103 | * @param int $order_id
104 | * @param string $code
105 | *
106 | * @return string
107 | */
108 | public function maybe_add_record( $order_id, $code ) {
109 | $order = wc_get_order( $order_id );
110 | $coupons = $order->get_coupon_codes();
111 |
112 | if ( in_array( $code, $coupons, true ) ) {
113 | $records = WC_Coupon_Restrictions_Table::get_records_for_order_id( $order_id );
114 | if ( $records ) {
115 | WP_CLI::log( "Record already exists for order: $order_id" );
116 | } else {
117 | $result = WC_Coupon_Restrictions_Table::maybe_add_record( $order_id );
118 | if ( $result ) {
119 | WP_CLI::log( "Record added for order: $order_id" );
120 | }
121 | }
122 | }
123 | }
124 |
125 | /**
126 | * Returns an array of orders created after a specific date.
127 | *
128 | * @param int $limit Limit query to this many orders.
129 | * @param int $offset Offset query by this many orders.
130 | * @param string $date Date to start querying from.
131 | *
132 | * @return array
133 | */
134 | public static function get_order_batch( $limit = 100, $offset = 0, $date = '' ) {
135 | $limit = intval( $limit ) ? intval( $limit ) : 100;
136 |
137 | $args = array(
138 | 'date_created' => '>=' . $date,
139 | 'orderby' => 'ID',
140 | 'order' => 'ASC',
141 | 'limit' => intval( $limit ),
142 | 'offset' => intval( $offset ),
143 | 'return' => 'ids',
144 | );
145 |
146 | $orders = new WC_Order_Query( $args );
147 | return $orders->get_orders();
148 | }
149 |
150 | /**
151 | * Explainer text to show when the command is run.
152 | *
153 | * @return void
154 | */
155 | public function explainer_text() {
156 | WP_CLI::log( '' );
157 | WP_CLI::log( __( 'This command updates the coupon restrictions verification table.', 'woocommerce-coupon-restrictions' ) );
158 | WP_CLI::log( __( 'This can be run if enhanced usage limits have been added to an existing coupon.', 'woocommerce-coupon-restrictions' ) );
159 | WP_CLI::log( __( 'After the update, enhanced usage restriction verifications will work for future checkouts.', 'woocommerce-coupon-restrictions' ) );
160 | WP_CLI::log( '' );
161 | }
162 |
163 | /**
164 | * Ask a question and returns input.
165 | *
166 | * @param string $question
167 | * @param bool $case_sensitive
168 | *
169 | * @return string
170 | */
171 | public function ask( $question, $case_sensitive = false ) {
172 | fwrite( STDOUT, trim( $question ) . ' ' );
173 | $answer = trim( fgets( STDIN ) );
174 | $answer = $case_sensitive ? $answer : strtolower( $answer );
175 | return $answer;
176 | }
177 | }
178 |
179 | WP_CLI::add_command( 'wcr', 'WC_Coupon_Restrictions_CLI' );
180 |
--------------------------------------------------------------------------------
/includes/class-wc-coupon-restrictions-onboarding.php:
--------------------------------------------------------------------------------
1 | upgrade_routine()
23 | // when plugin is activated for the first time.
24 | if ( get_transient( 'woocommerce-coupon-restrictions-activated' ) ) {
25 | // Loads the notice script and dismiss notice script.
26 | add_action( 'admin_enqueue_scripts', array( $this, 'init_install_notice' ) );
27 |
28 | // Deletes the transient via query string (when user clicks to start onboarding).
29 | add_action( 'init', array( $this, 'dismiss_notice_via_query' ) );
30 |
31 | // Deletes the transient via ajax (when user dismisses notice).
32 | add_action( 'wp_ajax_wc_customer_coupons_dismiss_notice', array( $this, 'dismiss_notice_via_ajax' ) );
33 | }
34 |
35 | // Initialize the pointers for onboarding flow.
36 | add_action( 'admin_enqueue_scripts', array( $this, 'init_pointers_for_screen' ) );
37 | }
38 |
39 | /**
40 | * Loads everything required to display and dismiss the install notice.
41 | *
42 | * @since 1.5.0
43 | *
44 | * @return void
45 | */
46 | public function init_install_notice() {
47 | if ( ! current_user_can( 'manage_options' ) ) {
48 | return;
49 | }
50 |
51 | // Display the onboarding notice.
52 | add_action( 'admin_notices', array( $this, 'install_notice' ) );
53 |
54 | // Loads jQuery if not already available.
55 | wp_enqueue_script( 'jquery-core' );
56 |
57 | // Inline script deletes the transient when notice is dismissed.
58 | $notice_dismiss_script = $this->install_notice_dismiss();
59 | wp_add_inline_script( 'jquery-core', $notice_dismiss_script );
60 | }
61 |
62 | /**
63 | * Plugin action links.
64 | *
65 | * @since 1.5.0
66 | *
67 | * @param array $links List of existing plugin action links.
68 | * @return array List of modified plugin action links.
69 | */
70 | public function plugin_action_links( $links ) {
71 | // URL for coupon screen onboarding
72 | $coupon_url = admin_url( 'post-new.php?post_type=shop_coupon&woocommerce-coupon-restriction-pointers=1' );
73 | $setting_url = admin_url( 'admin.php?page=wc-settings' );
74 |
75 | $custom = array(
76 | '' . esc_html__( 'New Coupon', 'woocommerce-coupon-restrictions' ) . '',
77 | '' . esc_html__( 'Settings', 'woocommerce-coupon-restrictions' ) . '',
78 | '' . esc_html__( 'Docs', 'woocommerce-coupon-restrictions' ) . '',
79 | );
80 | $links = array_merge( $custom, $links );
81 | return $links;
82 | }
83 |
84 | /**
85 | * Deletes the admin notice transient if query string is present.
86 | *
87 | * @since 1.5.0
88 | */
89 | public function dismiss_notice_via_query() {
90 | if (
91 | current_user_can( 'manage_options' ) &&
92 | isset( $_GET['woocommerce-coupon-restriction-pointers'] )
93 | ) {
94 | delete_transient( 'woocommerce-coupon-restrictions-activated' );
95 | }
96 | }
97 |
98 | /**
99 | * Deletes the admin notice transient via ajax.
100 | *
101 | * @since 1.5.0
102 | */
103 | public function dismiss_notice_via_ajax() {
104 | if ( ! check_ajax_referer( 'wc_customer_coupons_nonce', 'nonce', false ) ) {
105 | wp_send_json_error();
106 | exit;
107 | }
108 |
109 | $notice = delete_transient( 'woocommerce-coupon-restrictions-activated' );
110 | if ( $notice ) {
111 | wp_send_json_success();
112 | exit;
113 | }
114 |
115 | wp_send_json_error();
116 | exit;
117 | }
118 |
119 | /**
120 | * Displays a welcome notice.
121 | *
122 | * @since 1.5.0
123 | */
124 | public function install_notice() {
125 | if ( current_user_can( 'manage_options' ) ) {
126 | $url = admin_url( 'post-new.php?post_type=shop_coupon&woocommerce-coupon-restriction-pointers=1' );
127 | ?>
128 |
129 |
130 |
131 |
132 |
133 |
134 | id ) {
185 | $this->display_pointers( $this->get_pointers() );
186 | }
187 | }
188 |
189 | /**
190 | * Defines all the pointers.
191 | *
192 | * @since 1.5.0
193 | */
194 | public function get_pointers() {
195 | return array(
196 | 'coupon-restrictions-panel' => array(
197 | 'target' => '#woocommerce-coupon-data .usage_restriction_options',
198 | 'next' => 'customer-restriction-type',
199 | 'next_trigger' => array(
200 | 'target' => '#title',
201 | 'event' => 'input',
202 | ),
203 | 'options' => array(
204 | 'content' => '' . esc_html__( 'Usage Restrictions', 'woocommerce-coupon-restrictions' ) . '
' .
205 | '' . esc_html__( 'Coupon restrictions can be found in this panel.', 'woocommerce-coupon-restrictions' ) . '
',
206 | 'position' => array(
207 | 'edge' => 'top',
208 | 'align' => 'left',
209 | ),
210 | ),
211 | ),
212 | 'customer-restriction-type' => array(
213 | 'target' => '#usage_restriction_coupon_data .customer_restriction_type_field .woocommerce-help-tip',
214 | 'next' => 'usage-limit',
215 | 'next_trigger' => array(
216 | 'target' => '#title',
217 | 'event' => 'input',
218 | ),
219 | 'options' => array(
220 | 'content' => '' . esc_html__( 'Customer Restrictions', 'woocommerce-coupon-restrictions' ) . '
' .
221 | '' . esc_html__( 'You now have the option to restrict coupons to new customers or existing customers.', 'woocommerce-coupon-restrictions' ) . '
' .
222 | '' . esc_html__( 'Customers are considered "new" until they complete a purchase.', 'woocommerce-coupon-restrictions' ) . '
',
223 | 'position' => array(
224 | 'edge' => 'left',
225 | 'align' => 'left',
226 | ),
227 | ),
228 | ),
229 | 'usage-limit' => array(
230 | 'target' => '#usage_limit_coupon_data .usage_limit_per_user_field .woocommerce-help-tip',
231 | 'next' => 'role-restriction',
232 | 'next_trigger' => array(
233 | 'target' => '#title',
234 | 'event' => 'input',
235 | ),
236 | 'options' => array(
237 | 'content' => '' . esc_html__( 'Limit User Tip', 'woocommerce-coupon-restrictions' ) . '
' .
238 | '' . esc_html__( 'If you are using a new customer restriction, you may also want to limit the coupon to 1 use.', 'woocommerce-coupon-restrictions' ) . '
' .
239 | '' . esc_html__( 'Payments can take a few minutes to process, and it is possible for a customer to place multiple orders in that time if a coupon does not have a 1 use limit.', 'woocommerce-coupon-restrictions' ) . '
',
240 | 'position' => array(
241 | 'edge' => 'left',
242 | 'align' => 'left',
243 | ),
244 | ),
245 | ),
246 | 'role-restriction' => array(
247 | 'target' => '#usage_restriction_coupon_data .role_restriction_only_field',
248 | 'next' => 'location-restrictions',
249 | 'next_trigger' => array(
250 | 'target' => '#title',
251 | 'event' => 'input',
252 | ),
253 | 'options' => array(
254 | 'content' => '' . esc_html__( 'Role Restrictions', 'woocommerce-coupon-restrictions' ) . '
' .
255 | '' . esc_html__( 'Coupons can be restricted to specific user roles. Customer must have an account for the coupon to apply.', 'woocommerce-coupon-restrictions' ) . '
',
256 | 'position' => array(
257 | 'edge' => 'right',
258 | 'align' => 'right',
259 | ),
260 | ),
261 | ),
262 | 'location-restrictions' => array(
263 | 'target' => '#usage_restriction_coupon_data .location_restrictions_field',
264 | 'next' => 'multiple-restictions',
265 | 'next_trigger' => array(
266 | 'target' => '#title',
267 | 'event' => 'input',
268 | ),
269 | 'options' => array(
270 | 'content' => '' . esc_html__( 'Location Restrictions', 'woocommerce-coupon-restrictions' ) . '
' .
271 | '' . esc_html__( 'Checking this box displays options for country and/or zip code restrictions.', 'woocommerce-coupon-restrictions' ) . '
',
272 | 'position' => array(
273 | 'edge' => 'right',
274 | 'align' => 'right',
275 | ),
276 | ),
277 | ),
278 | 'multiple-restictions' => array(
279 | 'target' => '#coupon_options .usage_restriction_options',
280 | 'options' => array(
281 | 'content' => '' . esc_html__( 'Multiple Restrictions', 'woocommerce-coupon-restrictions' ) . '
' .
282 | '' . esc_html__( 'If multiple coupon restrictions are set, the customer must meet all restrictions.', 'woocommerce-coupon-restrictions' ) . '
',
283 | 'position' => array(
284 | 'edge' => 'left',
285 | 'align' => 'left',
286 | ),
287 | ),
288 | ),
289 | );
290 | }
291 |
292 | /**
293 | * Displays the pointers.
294 | * Follow WooCommerce core pattern:
295 | * https://github.com/woocommerce/woocommerce/blob/master/includes/admin/class-wc-admin-pointers.php
296 | *
297 | * @param array $pointers Array of pointers with their settings
298 | * @return void
299 | *
300 | * @since 1.5.0
301 | */
302 | public function display_pointers( $pointers ) {
303 | wp_enqueue_style( 'wp-pointer' );
304 | wp_enqueue_script( 'wp-pointer' );
305 |
306 | $file = esc_url( WC_Coupon_Restrictions::plugin_asset_path() . '/assets/onboarding.js' );
307 | wp_register_script(
308 | 'wccr-onboarding-pointers',
309 | $file,
310 | array( 'wp-pointer' ),
311 | '1.8.6',
312 | true
313 | );
314 |
315 | wp_localize_script(
316 | 'wccr-onboarding-pointers',
317 | 'WCCR_POINTERS',
318 | array(
319 | 'pointers' => $pointers,
320 | 'close' => esc_html__( 'Dismiss', 'woocommerce-coupon-restrictions' ),
321 | 'next' => esc_html__( 'Next', 'woocommerce-coupon-restrictions' ),
322 | 'enjoy' => esc_html__( 'Enjoy!', 'woocommerce-coupon-restrictions' ),
323 | )
324 | );
325 |
326 | wp_enqueue_script( 'wccr-onboarding-pointers' );
327 | }
328 |
329 | }
330 |
--------------------------------------------------------------------------------
/includes/class-wc-coupon-restrictions-settings.php:
--------------------------------------------------------------------------------
1 | __( 'Coupon Restrictions', 'woocommerce' ),
29 | 'id' => 'coupon_restrictions_customer_query',
30 | 'default' => 'accounts',
31 | 'type' => 'radio',
32 | 'desc_tip' => __( 'If you\'re restricting any coupons to new customers, we recommend requiring a user account for each customer. Checking against orders can be slow for sites with more than 10,000 orders.', 'woocommerce' ),
33 | 'options' => array(
34 | 'accounts' => __( 'Verify new customers by checking against user accounts.', 'woocommerce' ),
35 | 'accounts-orders' => __( 'Verify new customers by checking against user accounts and all guest orders.', 'woocommerce' ),
36 | ),
37 | );
38 |
39 | $filtered_settings = array();
40 |
41 | foreach ( $settings as $setting ) {
42 | $filtered_settings[] = $setting;
43 | if ( 'woocommerce_calc_discounts_sequentially' === $setting['id'] ) {
44 | $filtered_settings[] = $coupon_restrictions;
45 | }
46 | }
47 |
48 | return $filtered_settings;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/includes/class-wc-coupon-restrictions-table.php:
--------------------------------------------------------------------------------
1 | prefix . self::$table_name;
34 | }
35 |
36 | /**
37 | * Checks if the table exists.
38 | *
39 | * @return bool
40 | */
41 | public static function table_exists() {
42 | global $wpdb;
43 | $table_name = self::get_table_name();
44 |
45 | if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name ) {
46 | return true;
47 | }
48 |
49 | return false;
50 | }
51 |
52 | /**
53 | * Creates the table if it does not exist.
54 | *
55 | * @return array Strings containing the results of update queries.
56 | */
57 | public static function maybe_create_table() {
58 | if ( self::table_exists() ) {
59 | return array();
60 | }
61 |
62 | global $wpdb;
63 | $table_name = self::get_table_name();
64 | $charset_collate = $wpdb->get_charset_collate();
65 |
66 | $sql = "CREATE TABLE $table_name (
67 | record_id mediumint(9) NOT NULL AUTO_INCREMENT,
68 | status varchar(20) NOT NULL,
69 | order_id bigint(20) UNSIGNED NOT NULL,
70 | coupon_code varchar(20) NOT NULL,
71 | email varchar(255) NOT NULL,
72 | ip varchar(15) NOT NULL,
73 | shipping_address varchar(255) NOT NULL,
74 | UNIQUE KEY record_id (record_id)
75 | ) $charset_collate;";
76 |
77 | require_once ABSPATH . 'wp-admin/includes/upgrade.php';
78 | return dbDelta( $sql );
79 | }
80 |
81 | /**
82 | * Deletes the table.
83 | * Currently just used for tests.
84 | *
85 | * @return void
86 | */
87 | public static function delete_table() {
88 | global $wpdb;
89 | $table_name = self::get_table_name();
90 | $wpdb->query( "DROP TABLE IF EXISTS {$table_name}" );
91 | }
92 |
93 | /**
94 | * If a customer uses a coupon with one of the enhanced usage limits we'll store their details.
95 | *
96 | * @param array $result
97 | * @param int $order_id
98 | *
99 | * @return array
100 | */
101 | public static function maybe_add_record_on_payment( $result, $order_id ) {
102 | self::maybe_add_record( $order_id );
103 | return $result;
104 | }
105 |
106 | /**
107 | * If a coupon with the enhanced usage limits is used we'll store the customer details.
108 | *
109 | * @param int $order_id
110 | *
111 | * @return bool
112 | */
113 | public static function maybe_add_record( $order_id ) {
114 | $order = wc_get_order( $order_id );
115 |
116 | // Check all the coupons.
117 | foreach ( $order->get_items( 'coupon' ) as $coupon_item ) {
118 | /** @var \WC_Order_Item_Coupon $coupon_item */
119 | $coupon = new \WC_Coupon( $coupon_item->get_code() );
120 |
121 | if ( WC_Coupon_Restrictions_Validation::has_enhanced_usage_restrictions( $coupon ) ) {
122 | // Store user details.
123 | self::store_customer_details( $order, $coupon_item->get_code() );
124 | return true;
125 | }
126 | }
127 |
128 | return false;
129 | }
130 |
131 | /**
132 | * Store the details so we can check run usage checks in the future.
133 | *
134 | * @param \WC_Order $order
135 | * @param string $coupon_code
136 | */
137 | protected static function store_customer_details( \WC_Order $order, string $coupon_code ) {
138 | global $wpdb;
139 |
140 | // Gather the data for each column in the database table.
141 | $data = array(
142 | 'status' => 'active',
143 | 'order_id' => $order->get_id(),
144 | 'coupon_code' => $coupon_code,
145 | 'email' => self::get_scrubbed_email( $order->get_billing_email() ),
146 | 'ip' => $order->get_customer_ip_address(),
147 | 'shipping_address' => self::format_address( $order->get_shipping_address_1(), $order->get_shipping_address_2(), $order->get_shipping_city(), $order->get_shipping_postcode() ),
148 | );
149 |
150 | // Insert data to the table.
151 | $wpdb->insert(
152 | self::get_table_name(),
153 | $data,
154 | array(
155 | '%s',
156 | '%d',
157 | '%s',
158 | '%s',
159 | '%s',
160 | '%s',
161 | )
162 | );
163 | }
164 |
165 | /**
166 | * Sets record to cancelled if order with coupon is cancelled.
167 | *
168 | * @param int $order_id Order ID.
169 | */
170 | public static function maybe_update_record_status( $order_id ) {
171 | $order = wc_get_order( $order_id );
172 |
173 | if ( ! $order ) {
174 | return;
175 | }
176 |
177 | // If order does not have any coupons, return early.
178 | if ( count( $order->get_coupon_codes() ) === 0 ) {
179 | return;
180 | }
181 |
182 | $records = self::get_records_for_order_id( $order_id );
183 |
184 | if ( ! $records ) {
185 | return;
186 | }
187 |
188 | foreach ( $records as $record ) {
189 | self::update_record_status( $record->record_id, 'cancelled' );
190 | }
191 | }
192 |
193 | /**
194 | * Returns all records for a specific order ID.
195 | *
196 | * @param int order_id
197 | *
198 | * @return array Array of records.
199 | */
200 | public static function get_records_for_order_id( $order_id ) {
201 | global $wpdb;
202 | $table_name = self::get_table_name();
203 | $results = $wpdb->get_results(
204 | $wpdb->prepare(
205 | "SELECT record_id FROM $table_name WHERE order_id = %d",
206 | $order_id
207 | )
208 | );
209 |
210 | return $results;
211 | }
212 |
213 | /**
214 | * Sets the record status.
215 | *
216 | * @param int $record_id
217 | * @param string $status
218 | */
219 | public static function update_record_status( $record_id, $status = 'active' ) {
220 | global $wpdb;
221 | $table_name = self::get_table_name();
222 | return $wpdb->update(
223 | $table_name,
224 | array(
225 | 'status' => $status,
226 | ),
227 | array(
228 | 'record_id' => $record_id,
229 | ),
230 | array(
231 | '%s',
232 | ),
233 | array(
234 | '%d',
235 | )
236 | );
237 | }
238 |
239 | /**
240 | * Deletes all records for a specific coupon in the verification table.
241 | *
242 | * @param string $code
243 | *
244 | * @return void
245 | */
246 | public static function delete_records_for_coupon( $code ) {
247 | global $wpdb;
248 | $table_name = self::get_table_name();
249 | $wpdb->get_results(
250 | $wpdb->prepare(
251 | "DELETE FROM $table_name WHERE coupon_code = %s",
252 | wc_sanitize_coupon_code( $code )
253 | )
254 | );
255 | }
256 |
257 | /**
258 | * Check if scrubbed email has been used with coupon previously.
259 | *
260 | * @param \WC_Coupon $coupon
261 | * @param string $email
262 | *
263 | * @return int
264 | */
265 | public static function get_similar_email_usage( $coupon_code, $email ) {
266 | $email = self::get_scrubbed_email( $email );
267 |
268 | global $wpdb;
269 | $table_name = self::get_table_name();
270 | $results = $wpdb->get_results(
271 | $wpdb->prepare(
272 | "SELECT record_id FROM $table_name WHERE coupon_code = %s AND email = %s AND status = 'active'",
273 | $coupon_code,
274 | $email
275 | )
276 | );
277 |
278 | if ( ! $results ) {
279 | return 0;
280 | }
281 |
282 | return count( $results );
283 | }
284 |
285 | /**
286 | * Returns amount of times a scrubbed shipping address has been used with a specific coupon.
287 | *
288 | * @param \WC_Coupon $coupon
289 | * @param string $email
290 | *
291 | * @return int $count
292 | */
293 | public static function get_shipping_address_usage( $coupon, $coupon_code, $posted ) {
294 | $shipping_address = self::format_address(
295 | $posted['shipping_address_1'],
296 | $posted['shipping_address_2'],
297 | $posted['shipping_city'],
298 | $posted['shipping_postcode'],
299 | );
300 |
301 | global $wpdb;
302 | $table_name = self::get_table_name();
303 | $results = $wpdb->get_results(
304 | $wpdb->prepare(
305 | "SELECT record_id FROM $table_name WHERE coupon_code = %s AND shipping_address = %s AND status = 'active'",
306 | $coupon_code,
307 | $shipping_address
308 | )
309 | );
310 |
311 | return ( count( $results ) );
312 | }
313 |
314 | /**
315 | * Returns amount of times an IP address has been used with a specific coupon.
316 | *
317 | * @param \WC_Coupon $coupon
318 | * @param string $email
319 | *
320 | * @return int $count
321 | */
322 | public static function get_ip_address_usage( $coupon, $coupon_code ) {
323 | $ip = \WC_Geolocation::get_ip_address();
324 |
325 | global $wpdb;
326 | $table_name = self::get_table_name();
327 | $results = $wpdb->get_results(
328 | $wpdb->prepare(
329 | "SELECT record_id FROM $table_name WHERE coupon_code = %s AND ip = %s AND status = 'active'",
330 | $coupon_code,
331 | $ip
332 | )
333 | );
334 |
335 | return ( count( $results ) );
336 | }
337 |
338 | /**
339 | * Keep only English characters and numbers.
340 | * If there are any non-English characters, we convert them to the closest English character.
341 | *
342 | * @param string $address_1
343 | * @param string $address_2
344 | * @param string $city
345 | * @param string $postcode
346 | *
347 | * @return string|string[]|null
348 | */
349 | public static function format_address( $address_1, $address_2, $city, $postcode ) {
350 | $address_index = implode(
351 | '',
352 | array_map(
353 | 'trim',
354 | array(
355 | $address_1,
356 | $address_2,
357 | $city,
358 | $postcode,
359 | )
360 | )
361 | );
362 |
363 | // Remove everything except a-z, A-Z and 0-9.
364 | $address_index = preg_replace( '/[^a-zA-Z0-9]+/', '', sanitize_title( $address_index ) );
365 | return strtoupper( $address_index );
366 | }
367 |
368 | /**
369 | * Strip any dots and "+" signs from email.
370 | *
371 | * @param string $email
372 | *
373 | * @return string
374 | */
375 | public static function get_scrubbed_email( string $email ) {
376 | list( $email_name, $email_domain ) = explode( '@', strtolower( trim( $email ) ) );
377 |
378 | // Let's ignore everything after "+".
379 | $email_name = explode( '+', $email_name )[0];
380 |
381 | // The dots in Gmail does not matter.
382 | if ( 'gmail.com' === $email_domain ) {
383 | $email_name = str_replace( '.', '', $email_name );
384 | }
385 |
386 | return strtolower( "$email_name@$email_domain" );
387 | }
388 | }
389 |
--------------------------------------------------------------------------------
/includes/class-wc-coupon-restrictions-validation-cart.php:
--------------------------------------------------------------------------------
1 | session ) {
47 | return true;
48 | }
49 |
50 | // Get customer session information.
51 | $session = WC()->session->get( 'customer' );
52 |
53 | // Customer information may not be available yet when coupon is applied.
54 | // If so, coupon will remain activate and we'll validate at checkout.
55 | if ( ! $session ) {
56 | return true;
57 | }
58 |
59 | // Gets the email if it is in the session and valid.
60 | $email = $this->get_email_from_session( $session );
61 |
62 | if ( $email ) {
63 | // Validate customer restrictions.
64 | $customer = $this->validate_customer_restrictions( $coupon, $email );
65 | if ( false === $customer ) {
66 | return false;
67 | }
68 |
69 | // Validate role restrictions.
70 | $role = WC_Coupon_Restrictions_Validation::role_restriction( $coupon, $email );
71 | if ( false === $role ) {
72 | add_filter( 'woocommerce_coupon_error', array( $this, 'validation_message_role_restriction' ), 10, 3 );
73 | return false;
74 | }
75 | }
76 |
77 | // Validate location restrictions.
78 | $location = $this->validate_location_restrictions( $coupon, $session );
79 | if ( false === $location ) {
80 | return false;
81 | }
82 |
83 | return true;
84 | }
85 |
86 | /**
87 | * Gets the user email from session.
88 | *
89 | * @param object $session
90 | * @return string | null
91 | */
92 | public function get_email_from_session( $session ) {
93 | if ( ! isset( $session['email'] ) ) {
94 | return null;
95 | }
96 |
97 | $email = esc_textarea( strtolower( $session['email'] ) );
98 |
99 | if ( ! is_email( $email ) ) {
100 | return null;
101 | }
102 |
103 | return $email;
104 | }
105 |
106 | /**
107 | * Validates customer restrictions.
108 | * Returns true if customer meets $coupon criteria.
109 | *
110 | * @param WC_Coupon $coupon
111 | * @param string $email
112 | * @return boolean
113 | */
114 | public function validate_customer_restrictions( $coupon, $email ) {
115 | // Validate new customer restriction.
116 | if ( false === WC_Coupon_Restrictions_Validation::new_customer_restriction( $coupon, $email ) ) {
117 | add_filter( 'woocommerce_coupon_error', array( $this, 'validation_message_new_customer_restriction' ), 10, 3 );
118 | return false;
119 | }
120 |
121 | // Validate existing customer restriction.
122 | if ( false === WC_Coupon_Restrictions_Validation::existing_customer_restriction( $coupon, $email ) ) {
123 | add_filter( 'woocommerce_coupon_error', array( $this, 'validation_message_existing_customer_restriction' ), 10, 3 );
124 | return false;
125 | }
126 |
127 | return true;
128 | }
129 |
130 | /**
131 | * Validates location restrictions.
132 | * Returns true if customer meets $coupon criteria.
133 | *
134 | * @param WC_Coupon $coupon
135 | * @param object $session
136 | * @return boolean
137 | */
138 | public function validate_location_restrictions( $coupon, $session ) {
139 | // If location restrictions aren't set, coupon is valid.
140 | if ( 'yes' !== $coupon->get_meta( 'location_restrictions' ) ) {
141 | return true;
142 | }
143 |
144 | // Defaults in case no conditions are met.
145 | $country_validation = true;
146 | $state_validation = true;
147 | $zipcode_validation = true;
148 |
149 | // Get the address type used for location restrictions (billing or shipping).
150 | $address = WC_Coupon_Restrictions_Validation::get_address_type_for_restriction( $coupon );
151 |
152 | if ( 'shipping' === $address && isset( $session['shipping_country'] ) ) {
153 | $country = esc_textarea( $session['shipping_country'] );
154 | if ( '' !== $country ) {
155 | $country_validation = WC_Coupon_Restrictions_Validation::country_restriction( $coupon, $country );
156 | }
157 | }
158 |
159 | if ( 'shipping' === $address && isset( $session['shipping_state'] ) ) {
160 | $state = esc_textarea( $session['shipping_state'] );
161 | if ( '' !== $state ) {
162 | $state_validation = WC_Coupon_Restrictions_Validation::state_restriction( $coupon, $state );
163 | }
164 | }
165 |
166 | if ( 'shipping' === $address && isset( $session['shipping_postcode'] ) ) {
167 | $zipcode = esc_textarea( $session['shipping_postcode'] );
168 | if ( '' !== $zipcode ) {
169 | $zipcode_validation = WC_Coupon_Restrictions_Validation::postcode_restriction( $coupon, $zipcode );
170 | }
171 | }
172 |
173 | if ( 'billing' === $address && isset( $session['country'] ) ) {
174 | $country = esc_textarea( $session['country'] );
175 | if ( '' !== $country ) {
176 | $country_validation = WC_Coupon_Restrictions_Validation::country_restriction( $coupon, $country );
177 | }
178 | }
179 |
180 | if ( 'billing' === $address && isset( $session['postcode'] ) ) {
181 | $state = esc_textarea( $session['state'] );
182 | if ( '' !== $state ) {
183 | $state_validation = WC_Coupon_Restrictions_Validation::state_restriction( $coupon, $state );
184 | }
185 | }
186 |
187 | if ( 'billing' === $address && isset( $session['postcode'] ) ) {
188 | $zipcode = esc_textarea( $session['postcode'] );
189 | if ( '' !== $zipcode ) {
190 | $zipcode_validation = WC_Coupon_Restrictions_Validation::postcode_restriction( $coupon, $zipcode );
191 | }
192 | }
193 |
194 | if ( false === $country_validation ) {
195 | add_filter( 'woocommerce_coupon_error', array( $this, 'validation_message_country_restriction' ), 10, 3 );
196 | }
197 |
198 | if ( false === $state_validation ) {
199 | add_filter( 'woocommerce_coupon_error', array( $this, 'validation_message_state_restriction' ), 10, 3 );
200 | }
201 |
202 | if ( false === $zipcode_validation ) {
203 | add_filter( 'woocommerce_coupon_error', array( $this, 'validation_message_zipcode_restriction' ), 10, 3 );
204 | }
205 |
206 | // Coupon is not valid if country, state or zipcode validation failed.
207 | if ( in_array( false, array( $country_validation, $state_validation, $zipcode_validation ) ) ) {
208 | return false;
209 | }
210 |
211 | // Coupon passed all validation, return true.
212 | return true;
213 | }
214 |
215 | /**
216 | * Applies new customer coupon error message.
217 | *
218 | * @return string $err
219 | */
220 | public function validation_message_new_customer_restriction( $err, $err_code, $coupon ) {
221 | $err = $this->coupon_error_message( 'new-customer', $err, $err_code, $coupon );
222 | return $err;
223 | }
224 |
225 | /**
226 | * Applies existing customer coupon error message.
227 | *
228 | * @return string $err
229 | */
230 | public function validation_message_existing_customer_restriction( $err, $err_code, $coupon ) {
231 | return $this->coupon_error_message( 'existing-customer', $err, $err_code, $coupon );
232 | }
233 |
234 | /**
235 | * Applies role restriction coupon error message.
236 | *
237 | * @return string $err
238 | */
239 | public function validation_message_role_restriction( $err, $err_code, $coupon ) {
240 | return $this->coupon_error_message( 'role-restriction', $err, $err_code, $coupon );
241 | }
242 |
243 | /**
244 | * Applies country restriction error message.
245 | *
246 | * @return string $err
247 | */
248 | public function validation_message_country_restriction( $err, $err_code, $coupon ) {
249 | return $this->coupon_error_message( 'country', $err, $err_code, $coupon );
250 | }
251 |
252 | /**
253 | * Applies state code restriction error message.
254 | *
255 | * @return string $err
256 | */
257 | public function validation_message_state_restriction( $err, $err_code, $coupon ) {
258 | return $this->coupon_error_message( 'state', $err, $err_code, $coupon );
259 | }
260 |
261 | /**
262 | * Applies zip code restriction error message.
263 | *
264 | * @return string $err
265 | */
266 | public function validation_message_zipcode_restriction( $err, $err_code, $coupon ) {
267 | return $this->coupon_error_message( 'zipcode', $err, $err_code, $coupon );
268 | }
269 |
270 | /**
271 | * Validation message helper.
272 | *
273 | * @param string $key
274 | * @param string $err
275 | * @param int $err_code
276 | * @param WC_Coupon $coupon
277 | * @return string
278 | */
279 | public function coupon_error_message( $key, $err, $err_code, $coupon ) {
280 | // Alter the validation message if coupon has been removed.
281 | if ( 100 === $err_code ) {
282 | $msg = WC_Coupon_Restrictions_Validation::message( $key, $coupon );
283 | $err = apply_filters( 'woocommerce-coupon-restrictions-removed-message', $msg );
284 | }
285 |
286 | // Return validation message.
287 | return $err;
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/includes/class-wc-coupon-restrictions-validation-checkout.php:
--------------------------------------------------------------------------------
1 | cart->applied_coupons ) ) {
29 | return;
30 | }
31 |
32 | // If no billing email is set, we'll default to empty string.
33 | // WooCommerce validation should catch this before we do.
34 | if ( ! isset( $posted['billing_email'] ) ) {
35 | $posted['billing_email'] = '';
36 | }
37 |
38 | foreach ( WC()->cart->applied_coupons as $code ) {
39 | $coupon = new WC_Coupon( $code );
40 |
41 | $discounts = new WC_Discounts( WC()->cart );
42 | if ( ! wc_coupons_enabled() || ! $discounts->is_coupon_valid( $coupon ) ) {
43 | continue;
44 | }
45 |
46 | $this->validate_new_customer_restriction( $coupon, $code, $posted );
47 | $this->validate_existing_customer_restriction( $coupon, $code, $posted );
48 | $this->validate_location_restrictions( $coupon, $code, $posted );
49 | $this->validate_role_restriction( $coupon, $code, $posted );
50 |
51 | if ( WC_Coupon_Restrictions_Validation::has_enhanced_usage_restrictions( $coupon ) ) {
52 | $this->validate_similar_emails_restriction( $coupon, $code, $posted );
53 | $this->validate_usage_limit_per_shipping_address( $coupon, $code, $posted );
54 | $this->validate_usage_limit_per_ip( $coupon, $code );
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Validates new customer coupon on checkout.
61 | *
62 | * @param WC_Coupon $coupon
63 | * @param string $code
64 | * @param array $posted
65 | * @return void
66 | */
67 | public function validate_new_customer_restriction( $coupon, $code, $posted ) {
68 | $email = strtolower( $posted['billing_email'] );
69 | $valid = WC_Coupon_Restrictions_Validation::new_customer_restriction( $coupon, $email );
70 |
71 | if ( false === $valid ) {
72 | $msg = WC_Coupon_Restrictions_Validation::message( 'new-customer', $coupon );
73 | $this->remove_coupon( $coupon, $code, $msg );
74 | }
75 | }
76 |
77 | /**
78 | * Validates existing customer coupon on checkout.
79 | *
80 | * @param WC_Coupon $coupon
81 | * @param string $code
82 | * @param array $posted
83 | * @return void
84 | */
85 | public function validate_existing_customer_restriction( $coupon, $code, $posted ) {
86 | $email = strtolower( $posted['billing_email'] );
87 | $valid = WC_Coupon_Restrictions_Validation::existing_customer_restriction( $coupon, $email );
88 |
89 | if ( false === $valid ) {
90 | $msg = WC_Coupon_Restrictions_Validation::message( 'existing-customer', $coupon );
91 | $this->remove_coupon( $coupon, $code, $msg );
92 | }
93 | }
94 |
95 | /**
96 | * Validates role restriction on checkout.
97 | *
98 | * @param WC_Coupon $coupon
99 | * @param string $code
100 | * @param array $posted
101 | * @return void
102 | */
103 | public function validate_role_restriction( $coupon, $code, $posted ) {
104 | $email = strtolower( $posted['billing_email'] );
105 | $valid = WC_Coupon_Restrictions_Validation::role_restriction( $coupon, $email );
106 |
107 | if ( false === $valid ) {
108 | $msg = WC_Coupon_Restrictions_Validation::message( 'role-restriction', $coupon );
109 | $this->remove_coupon( $coupon, $code, $msg );
110 | }
111 | }
112 |
113 | /**
114 | * Validates location restrictions.
115 | * Returns true if customer meets $coupon criteria.
116 | *
117 | * @param WC_Coupon $coupon
118 | * @param string $code
119 | * @param array $posted
120 | * @return void
121 | */
122 | public function validate_location_restrictions( $coupon, $code, $posted ) {
123 | // If location restrictions aren't set, coupon is valid.
124 | if ( 'yes' !== $coupon->get_meta( 'location_restrictions' ) ) {
125 | return true;
126 | }
127 |
128 | // Get the address type used for location restrictions (billing or shipping).
129 | $address = WC_Coupon_Restrictions_Validation::get_address_type_for_restriction( $coupon );
130 |
131 | // Defaults in case no conditions are met.
132 | $country_validation = true;
133 | $state_validation = true;
134 | $zipcode_validation = true;
135 |
136 | if ( 'shipping' === $address && isset( $posted['shipping_country'] ) ) {
137 | $country_validation = WC_Coupon_Restrictions_Validation::country_restriction( $coupon, $posted['shipping_country'] );
138 | } elseif ( 'shipping' === $address && isset( $posted['shipping_state'] ) ) {
139 | $state_validation = WC_Coupon_Restrictions_Validation::state_restriction( $coupon, $posted['shipping_state'] );
140 | }
141 |
142 | if ( 'shipping' === $address && isset( $posted['shipping_postcode'] ) ) {
143 | $zipcode_validation = WC_Coupon_Restrictions_Validation::postcode_restriction( $coupon, $posted['shipping_postcode'] );
144 | }
145 |
146 | if ( 'billing' === $address && isset( $posted['billing_country'] ) ) {
147 | $country_validation = WC_Coupon_Restrictions_Validation::country_restriction( $coupon, $posted['billing_country'] );
148 | }
149 |
150 | if ( 'billing' === $address && isset( $posted['billing_state'] ) ) {
151 | $state_validation = WC_Coupon_Restrictions_Validation::state_restriction( $coupon, $posted['billing_state'] );
152 | }
153 |
154 | if ( 'billing' === $address && isset( $posted['billing_postcode'] ) ) {
155 | $zipcode_validation = WC_Coupon_Restrictions_Validation::postcode_restriction( $coupon, $posted['billing_postcode'] );
156 | }
157 |
158 | if ( false === $country_validation ) {
159 | $msg = WC_Coupon_Restrictions_Validation::message( 'country', $coupon );
160 | $this->remove_coupon( $coupon, $code, $msg );
161 | }
162 |
163 | if ( false === $state_validation ) {
164 | $msg = WC_Coupon_Restrictions_Validation::message( 'state', $coupon );
165 | $this->remove_coupon( $coupon, $code, $msg );
166 | }
167 |
168 | if ( false === $zipcode_validation ) {
169 | $msg = WC_Coupon_Restrictions_Validation::message( 'zipcode', $coupon );
170 | $this->remove_coupon( $coupon, $code, $msg );
171 | }
172 | }
173 |
174 | /**
175 | * Validates similar emails restriction.
176 | *
177 | * @param WC_Coupon $coupon
178 | * @param string $code
179 | * @param array $posted
180 | * @return void
181 | */
182 | public function validate_similar_emails_restriction( $coupon, $code, $posted ) {
183 | $coupon_usage_limit = $coupon->get_usage_limit_per_user();
184 | if ( ! $coupon_usage_limit ) {
185 | return;
186 | }
187 |
188 | if ( 'yes' !== $coupon->get_meta( 'prevent_similar_emails' ) ) {
189 | return;
190 | }
191 |
192 | $email = $posted['billing_email'];
193 | $count = WC_Coupon_Restrictions_Table::get_similar_email_usage( $code, $email );
194 |
195 | if ( $count >= $coupon_usage_limit ) {
196 | $msg = WC_Coupon_Restrictions_Validation::message( 'similar-email-usage', $coupon );
197 | $this->remove_coupon( $coupon, $code, $msg );
198 | }
199 | }
200 |
201 | /**
202 | * Validates usage limit per shipping address.
203 | *
204 | * @param WC_Coupon $coupon
205 | * @param string $code
206 | * @param array $posted
207 | * @return void
208 | */
209 | public function validate_usage_limit_per_shipping_address( $coupon, $code, $posted ) {
210 | $limit = $coupon->get_meta( 'usage_limit_per_shipping_address' );
211 | if ( ! $limit ) {
212 | return;
213 | }
214 |
215 | $count = WC_Coupon_Restrictions_Table::get_shipping_address_usage( $coupon, $code, $posted );
216 | if ( $count >= $limit ) {
217 | $msg = WC_Coupon_Restrictions_Validation::message( 'usage-limit-per-shipping-address', $coupon );
218 | $this->remove_coupon( $coupon, $code, $msg );
219 | }
220 | }
221 |
222 | /**
223 | * Validates usage limit per IP address.
224 | *
225 | * @param WC_Coupon $coupon
226 | * @param string $code
227 | * @return void
228 | */
229 | public function validate_usage_limit_per_ip( $coupon, $code ) {
230 | $limit = $coupon->get_meta( 'usage_limit_per_ip_address' );
231 | if ( ! $limit ) {
232 | return;
233 | }
234 |
235 | $count = WC_Coupon_Restrictions_Table::get_ip_address_usage( $coupon, $code );
236 | if ( $count >= $limit ) {
237 | $msg = WC_Coupon_Restrictions_Validation::message( 'usage-limit-per-ip-address', $coupon );
238 | $this->remove_coupon( $coupon, $code, $msg );
239 | }
240 | }
241 |
242 | /**
243 | * Removes coupon and displays a validation message.
244 | *
245 | * @param WC_Coupon $coupon
246 | * @param string $code
247 | * @param string $msg
248 | * @return void
249 | */
250 | public function remove_coupon( $coupon, $code, $msg ) {
251 | // Filter to change validation text.
252 | $msg = apply_filters( 'woocommerce_coupon_restrictions_removed_message_with_code', $msg, $code, $coupon );
253 |
254 | // Remove the coupon.
255 | WC()->cart->remove_coupon( $code );
256 |
257 | // Throw a notice to stop checkout.
258 | wc_add_notice( $msg, 'error' );
259 |
260 | // Flag totals for refresh.
261 | WC()->session->set( 'refresh_totals', true );
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/includes/class-wc-coupon-restrictions-validation.php:
--------------------------------------------------------------------------------
1 | ID );
26 | if ( $customer->get_is_paying_customer() ) {
27 | return true;
28 | }
29 | }
30 |
31 | // If there isn't a user account or user account ! is_paying_customer
32 | // we can check against previous guest orders.
33 | // Store admin must opt-in to this because of performance concerns.
34 | $option = get_option( 'coupon_restrictions_customer_query', 'accounts' );
35 | if ( 'accounts-orders' === $option ) {
36 |
37 | // This query can be slow on sites with a lot of orders.
38 | // @todo Check if 'customer' => '' improves performance.
39 | $customer_orders = wc_get_orders(
40 | array(
41 | 'status' => array( 'wc-processing', 'wc-completed' ),
42 | 'email' => $email,
43 | 'limit' => 1,
44 | 'return' => 'ids',
45 | )
46 | );
47 |
48 | // If there is at least one order, customer is returning.
49 | if ( 1 === count( $customer_orders ) ) {
50 | return true;
51 | }
52 | }
53 |
54 | // If we've gotten to this point, the customer must be new.
55 | return false;
56 | }
57 |
58 | /**
59 | * Validates new customer restriction.
60 | * Returns true if customer meets $coupon criteria.
61 | *
62 | * @param WC_Coupon $coupon
63 | * @param string $email
64 | * @return boolean
65 | */
66 | public static function new_customer_restriction( $coupon, $email ) {
67 | $customer_restriction_type = $coupon->get_meta( 'customer_restriction_type', true );
68 |
69 | if ( 'new' === $customer_restriction_type ) {
70 | // If customer has purchases, coupon is not valid.
71 | if ( self::is_returning_customer( $email ) ) {
72 | return false;
73 | }
74 | }
75 |
76 | return true;
77 | }
78 |
79 |
80 | /**
81 | * Returns whether coupon address restriction applies to 'shipping' or 'billing'.
82 | *
83 | * @param WC_Coupon $coupon
84 | * @return string
85 | */
86 | public static function get_address_type_for_restriction( $coupon ) {
87 | $address_type = $coupon->get_meta( 'address_for_location_restrictions', true );
88 | if ( 'billing' === $address_type ) {
89 | return 'billing';
90 | }
91 |
92 | return 'shipping';
93 | }
94 |
95 | /**
96 | * Validates existing customer restriction.
97 | * Returns true if customer meets $coupon criteria.
98 | *
99 | * @param WC_Coupon $coupon
100 | * @param string $email
101 | * @return boolean
102 | */
103 | public static function existing_customer_restriction( $coupon, $email ) {
104 | $customer_restriction_type = $coupon->get_meta( 'customer_restriction_type', true );
105 |
106 | // If customer has purchases, coupon is valid.
107 | if ( 'existing' === $customer_restriction_type ) {
108 | if ( ! self::is_returning_customer( $email ) ) {
109 | return false;
110 | }
111 | }
112 |
113 | return true;
114 | }
115 |
116 | /**
117 | * Validates role restrictions.
118 | * Returns true if customer meets $coupon criteria.
119 | *
120 | * @param WC_Coupon $coupon
121 | * @param string $email
122 | * @return boolean
123 | */
124 | public static function role_restriction( $coupon, $email ) {
125 | // Returns an array with all the restricted roles.
126 | $restricted_roles = $coupon->get_meta( 'role_restriction', true );
127 |
128 | // If there are no restricted roles, coupon is valid.
129 | if ( ! $restricted_roles ) {
130 | return true;
131 | }
132 |
133 | // Checks if there is an account associated with the $email.
134 | $user = get_user_by( 'email', $email );
135 |
136 | // If user account does not exist and guest role is permitted, return true.
137 | if ( ! $user && in_array( 'woocommerce-coupon-restrictions-guest', $restricted_roles ) ) {
138 | return true;
139 | }
140 |
141 | // If user account does not exist and guest role not permitted, coupon is invalid.
142 | if ( ! $user ) {
143 | return false;
144 | }
145 |
146 | $user_meta = get_userdata( $user->ID );
147 | $user_roles = $user_meta->roles;
148 |
149 | // If any the user roles do not match the restricted roles, coupon is invalid.
150 | if ( ! array_intersect( $user_roles, $restricted_roles ) ) {
151 | return false;
152 | }
153 |
154 | return true;
155 | }
156 |
157 | /**
158 | * Validates state restriction.
159 | * Returns true if customer meets $coupon criteria.
160 | *
161 | * @param WC_Coupon $coupon
162 | * @param string $state
163 | * @return boolean
164 | */
165 | public static function state_restriction( $coupon, $state ) {
166 | // Get the allowed states from coupon meta.
167 | $state_restriction = $coupon->get_meta( 'state_restriction', true );
168 |
169 | // If $state_restriction has not been set, coupon remains valid.
170 | if ( ! $state_restriction ) {
171 | return true;
172 | }
173 |
174 | $state_array = self::comma_seperated_string_to_array( $state_restriction );
175 |
176 | if ( ! in_array( strtoupper( $state ), $state_array ) ) {
177 | return false;
178 | }
179 |
180 | return true;
181 | }
182 |
183 | /**
184 | * Validates postcode restriction.
185 | * Returns true if customer meets $coupon criteria.
186 | *
187 | * @param WC_Coupon $coupon
188 | * @param string $postcode
189 | * @return boolean
190 | */
191 | public static function postcode_restriction( $coupon, $postcode ) {
192 | // Get the allowed postcodes from coupon meta.
193 | $postcode_restriction = $coupon->get_meta( 'postcode_restriction', true );
194 |
195 | // If $postcode_restriction has not been set, coupon remains valid.
196 | if ( ! $postcode_restriction ) {
197 | return true;
198 | }
199 |
200 | $postcode_array = self::comma_seperated_string_to_array( $postcode_restriction );
201 |
202 | // Wildcard check.
203 | if ( strpos( $postcode_restriction, '*' ) !== false ) {
204 | foreach ( $postcode_array as $restricted_postcode ) {
205 | if ( strpos( $restricted_postcode, '*' ) !== false ) {
206 | if ( fnmatch( $restricted_postcode, $postcode ) ) {
207 | return true;
208 | }
209 | }
210 | }
211 | }
212 |
213 | // Standard check.
214 | if ( ! in_array( strtoupper( $postcode ), $postcode_array ) ) {
215 | return false;
216 | }
217 |
218 | return true;
219 | }
220 |
221 | /**
222 | * Validates country restriction.
223 | * Returns true if customer meets $coupon criteria.
224 | *
225 | * @param WC_Coupon $coupon
226 | * @param string $country
227 | * @return boolean
228 | */
229 | public static function country_restriction( $coupon, $country ) {
230 | // Get the allowed countries from coupon meta.
231 | $allowed_countries = $coupon->get_meta( 'country_restriction', true );
232 |
233 | // If $allowed_countries has not been set, coupon remains valid.
234 | if ( ! $allowed_countries ) {
235 | return true;
236 | }
237 |
238 | // If the customer country is not in allowed countries, return false.
239 | if ( ! in_array( $country, $allowed_countries ) ) {
240 | return false;
241 | }
242 |
243 | return true;
244 | }
245 |
246 | /**
247 | * Checks if coupon has enhanced usage restrictions set.
248 | *
249 | * @param WC_Coupon $coupon
250 | * @return boolean
251 | */
252 | public static function has_enhanced_usage_restrictions( $coupon ) {
253 | $meta = array(
254 | 'prevent_similar_emails',
255 | 'usage_limit_per_shipping_address',
256 | 'usage_limit_per_ip_address',
257 | );
258 |
259 | foreach ( $meta as $key ) {
260 | if ( $coupon->get_meta( $key ) ) {
261 | return true;
262 | }
263 | }
264 |
265 | return false;
266 | }
267 |
268 | /**
269 | * Convert string textarea to normalized array with uppercase.
270 | *
271 | * @param string $string
272 | * @return array $values
273 | */
274 | public static function comma_seperated_string_to_array( $string ) {
275 | // Converts string to array.
276 | $values = explode( ',', $string );
277 | $values = array_map( 'trim', $values );
278 |
279 | // Converts values to uppercase so comparison is not case sensitive.
280 | $values = array_map( 'strtoupper', $values );
281 |
282 | return $values;
283 | }
284 |
285 | /**
286 | * Returns the validation message.
287 | *
288 | * @param string $key
289 | * @param WC_Coupon $coupon
290 | * @return string
291 | */
292 | public static function message( $key, $coupon ) {
293 | $i8n_address = array(
294 | 'shipping' => __( 'shipping', 'woocommerce-coupon-restrictions' ),
295 | 'billing' => __( 'billing', 'woocommerce-coupon-restrictions' ),
296 | );
297 |
298 | if ( $key === 'new-customer' ) {
299 | return sprintf( __( 'Sorry, coupon code "%s" is only valid for new customers.', 'woocommerce-coupon-restrictions' ), $coupon->get_code() );
300 | }
301 |
302 | if ( $key === 'existing-customer' ) {
303 | return sprintf( __( 'Sorry, coupon code "%s" is only valid for existing customers.', 'woocommerce-coupon-restrictions' ), $coupon->get_code() );
304 | }
305 |
306 | if ( $key === 'role-restriction' ) {
307 | return sprintf( __( 'Sorry, coupon code "%s" is not valid with your customer role.', 'woocommerce-coupon-restrictions' ), $coupon->get_code() );
308 | }
309 |
310 | if ( $key === 'country' ) {
311 | $address_type = self::get_address_type_for_restriction( $coupon );
312 | $i8n_address_type = $i8n_address[ $address_type ];
313 | return sprintf( __( 'Sorry, coupon code "%1$s" is not valid in your %2$s country.', 'woocommerce-coupon-restrictions' ), $coupon->get_code(), $i8n_address_type );
314 | }
315 |
316 | if ( $key === 'state' ) {
317 | $address_type = self::get_address_type_for_restriction( $coupon );
318 | $i8n_address_type = $i8n_address[ $address_type ];
319 | return sprintf( __( 'Sorry, coupon code "%1$s" is not valid in your %2$s state.', 'woocommerce-coupon-restrictions' ), $coupon->get_code(), $i8n_address_type );
320 | }
321 |
322 | if ( $key === 'zipcode' ) {
323 | $address_type = self::get_address_type_for_restriction( $coupon );
324 | $i8n_address_type = $i8n_address[ $address_type ];
325 | return sprintf( __( 'Sorry, coupon code "%1$s" is not valid in your %2$s zip code.', 'woocommerce-coupon-restrictions' ), $coupon->get_code(), $i8n_address_type );
326 | }
327 |
328 | if ( $key === 'similar-email-usage' ||
329 | $key === 'usage-limit-per-shipping-address' ||
330 | $key === 'usage-limit-per-ip-address' ) {
331 | return sprintf( __( 'Sorry, coupon code "%s" usage limit exceeded.', 'woocommerce-coupon-restrictions' ), $coupon->get_code() );
332 | }
333 |
334 | // The $key should always find a match.
335 | // But we'll return a default message just in case.
336 | return sprintf( __( 'Sorry, coupon code "%s" is not valid.', 'woocommerce-coupon-restrictions' ), $coupon->get_code() );
337 | }
338 |
339 | }
340 |
--------------------------------------------------------------------------------
/languages/woocommerce-coupon-restrictions.pot:
--------------------------------------------------------------------------------
1 | # Copyright (C) 2025 WooCommerce
2 | # This file is distributed under the GNU General Public License v3.0.
3 | msgid ""
4 | msgstr ""
5 | "Project-Id-Version: WooCommerce Coupon Restrictions 2.2.3\n"
6 | "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/woocommerce-coupon-restrictions\n"
7 | "Last-Translator: FULL NAME \n"
8 | "Language-Team: LANGUAGE \n"
9 | "MIME-Version: 1.0\n"
10 | "Content-Type: text/plain; charset=UTF-8\n"
11 | "Content-Transfer-Encoding: 8bit\n"
12 | "POT-Creation-Date: 2025-03-24T22:10:04+00:00\n"
13 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
14 | "X-Generator: WP-CLI 2.11.0\n"
15 | "X-Domain: woocommerce-coupon-restrictions\n"
16 |
17 | #. Plugin Name of the plugin
18 | #: woocommerce-coupon-restrictions.php
19 | msgid "WooCommerce Coupon Restrictions"
20 | msgstr ""
21 |
22 | #. Plugin URI of the plugin
23 | #: woocommerce-coupon-restrictions.php
24 | msgid "http://woocommerce.com/products/woocommerce-coupon-restrictions/"
25 | msgstr ""
26 |
27 | #. Description of the plugin
28 | #: woocommerce-coupon-restrictions.php
29 | msgid "Create targeted coupons for new customers, user roles, countries or zip codes. Prevent coupon abuse with enhanced usage limits."
30 | msgstr ""
31 |
32 | #. Author of the plugin
33 | #: woocommerce-coupon-restrictions.php
34 | msgid "WooCommerce"
35 | msgstr ""
36 |
37 | #. Author URI of the plugin
38 | #: woocommerce-coupon-restrictions.php
39 | msgid "http://woocommerce.com/"
40 | msgstr ""
41 |
42 | #: includes/class-wc-coupon-restrictions-admin.php:51
43 | msgid "Customer restrictions"
44 | msgstr ""
45 |
46 | #: includes/class-wc-coupon-restrictions-admin.php:52
47 | msgid "Restricts coupon to specific customers based on purchase history."
48 | msgstr ""
49 |
50 | #: includes/class-wc-coupon-restrictions-admin.php:56
51 | msgid "Default (no restriction)"
52 | msgstr ""
53 |
54 | #: includes/class-wc-coupon-restrictions-admin.php:57
55 | msgid "New customers only"
56 | msgstr ""
57 |
58 | #: includes/class-wc-coupon-restrictions-admin.php:58
59 | msgid "Existing customers only"
60 | msgstr ""
61 |
62 | #: includes/class-wc-coupon-restrictions-admin.php:78
63 | msgid "User role restriction"
64 | msgstr ""
65 |
66 | #: includes/class-wc-coupon-restrictions-admin.php:92
67 | msgid "Guest (No User Account)"
68 | msgstr ""
69 |
70 | #: includes/class-wc-coupon-restrictions-admin.php:100
71 | msgid "Choose roles…"
72 | msgstr ""
73 |
74 | #: includes/class-wc-coupon-restrictions-admin.php:100
75 | msgid "Role"
76 | msgstr ""
77 |
78 | #: includes/class-wc-coupon-restrictions-admin.php:130
79 | msgid "Use location restrictions"
80 | msgstr ""
81 |
82 | #: includes/class-wc-coupon-restrictions-admin.php:131
83 | msgid "Display and enable location restriction options."
84 | msgstr ""
85 |
86 | #: includes/class-wc-coupon-restrictions-admin.php:142
87 | msgid "Address for location restrictions"
88 | msgstr ""
89 |
90 | #: includes/class-wc-coupon-restrictions-admin.php:145
91 | msgid "Shipping"
92 | msgstr ""
93 |
94 | #: includes/class-wc-coupon-restrictions-admin.php:146
95 | msgid "Billing"
96 | msgstr ""
97 |
98 | #: includes/class-wc-coupon-restrictions-admin.php:153
99 | msgid "Restrict to specific countries"
100 | msgstr ""
101 |
102 | #: includes/class-wc-coupon-restrictions-admin.php:172
103 | msgid "Choose countries…"
104 | msgstr ""
105 |
106 | #: includes/class-wc-coupon-restrictions-admin.php:172
107 | msgid "Country"
108 | msgstr ""
109 |
110 | #: includes/class-wc-coupon-restrictions-admin.php:189
111 | msgid "Select any country that your store currently sells to."
112 | msgstr ""
113 |
114 | #: includes/class-wc-coupon-restrictions-admin.php:191
115 | msgid "Adds all the countries that the store sells to in the restricted field."
116 | msgstr ""
117 |
118 | #: includes/class-wc-coupon-restrictions-admin.php:192
119 | msgid "Add All Countries"
120 | msgstr ""
121 |
122 | #: includes/class-wc-coupon-restrictions-admin.php:194
123 | msgid "Clears all restricted country selections."
124 | msgstr ""
125 |
126 | #: includes/class-wc-coupon-restrictions-admin.php:195
127 | msgid "Clear"
128 | msgstr ""
129 |
130 | #: includes/class-wc-coupon-restrictions-admin.php:204
131 | msgid "Restrict to specific states"
132 | msgstr ""
133 |
134 | #: includes/class-wc-coupon-restrictions-admin.php:205
135 | msgid "Use the two digit state codes. Comma separate to specify multiple states."
136 | msgstr ""
137 |
138 | #: includes/class-wc-coupon-restrictions-admin.php:215
139 | msgid "Restrict to specific zip codes"
140 | msgstr ""
141 |
142 | #: includes/class-wc-coupon-restrictions-admin.php:216
143 | msgid "Comma separate to list multiple zip codes. Wildcards (*) can be used to match portions of zip codes."
144 | msgstr ""
145 |
146 | #: includes/class-wc-coupon-restrictions-admin.php:240
147 | msgid "Enhanced Usage Limits"
148 | msgstr ""
149 |
150 | #: includes/class-wc-coupon-restrictions-admin.php:241
151 | msgid "Enhanced usage limits should be set when the coupon is first created. WooCommerce will verify against previous orders made with a coupon that has enhanced usage restrictions."
152 | msgstr ""
153 |
154 | #. translators: %s: link to WooCommerce Coupon Restrictions documentation.
155 | #: includes/class-wc-coupon-restrictions-admin.php:243
156 | msgid "Please read the documentation for more information."
157 | msgstr ""
158 |
159 | #: includes/class-wc-coupon-restrictions-admin.php:254
160 | msgid "Prevent similar emails"
161 | msgstr ""
162 |
163 | #: includes/class-wc-coupon-restrictions-admin.php:255
164 | msgid "Many email services ignore periods and anything after a \"+\". Check this box to prevent customers from using a similar email address to exceed the usage limit per user."
165 | msgstr ""
166 |
167 | #: includes/class-wc-coupon-restrictions-admin.php:265
168 | msgid "Usage limit per shipping address"
169 | msgstr ""
170 |
171 | #: includes/class-wc-coupon-restrictions-admin.php:266
172 | #: includes/class-wc-coupon-restrictions-admin.php:285
173 | msgid "Unlimited usage"
174 | msgstr ""
175 |
176 | #: includes/class-wc-coupon-restrictions-admin.php:267
177 | msgid "How many times this coupon can be used with the same shipping address."
178 | msgstr ""
179 |
180 | #: includes/class-wc-coupon-restrictions-admin.php:284
181 | msgid "Usage limit per IP address"
182 | msgstr ""
183 |
184 | #: includes/class-wc-coupon-restrictions-admin.php:286
185 | msgid "How many times this coupon can be used with the same IP address."
186 | msgstr ""
187 |
188 | #: includes/class-wc-coupon-restrictions-cli.php:18
189 | msgid "Coupon code to update data for:"
190 | msgstr ""
191 |
192 | #: includes/class-wc-coupon-restrictions-cli.php:22
193 | msgid "Coupon not found."
194 | msgstr ""
195 |
196 | #: includes/class-wc-coupon-restrictions-cli.php:28
197 | msgid "Coupon has not been used for any orders."
198 | msgstr ""
199 |
200 | #: includes/class-wc-coupon-restrictions-cli.php:33
201 | msgid "Coupon does not have any enhanced usage restrictions set."
202 | msgstr ""
203 |
204 | #. translators: %s: usage count of coupon
205 | #: includes/class-wc-coupon-restrictions-cli.php:38
206 | msgid "Coupon has been used %d times."
207 | msgstr ""
208 |
209 | #. translators: %s: last order processed for WP CLI command.
210 | #: includes/class-wc-coupon-restrictions-cli.php:51
211 | msgid "An update has already been started. The last order id processed was: %d."
212 | msgstr ""
213 |
214 | #: includes/class-wc-coupon-restrictions-cli.php:52
215 | msgid "Would you like to continue processing from order id %d? [yes/no]"
216 | msgstr ""
217 |
218 | #: includes/class-wc-coupon-restrictions-cli.php:57
219 | msgid "Data update will be restarted. All order data for coupon will be refreshed."
220 | msgstr ""
221 |
222 | #: includes/class-wc-coupon-restrictions-cli.php:62
223 | msgid "Orders will be checked in batches."
224 | msgstr ""
225 |
226 | #: includes/class-wc-coupon-restrictions-cli.php:63
227 | msgid "How many orders would you like to process per batch? [100]:"
228 | msgstr ""
229 |
230 | #: includes/class-wc-coupon-restrictions-cli.php:70
231 | msgid "Querying order batch starting at order id: %d"
232 | msgstr ""
233 |
234 | #: includes/class-wc-coupon-restrictions-cli.php:73
235 | msgid "No orders available to process."
236 | msgstr ""
237 |
238 | #: includes/class-wc-coupon-restrictions-cli.php:92
239 | msgid "Finished updating verification table."
240 | msgstr ""
241 |
242 | #: includes/class-wc-coupon-restrictions-cli.php:157
243 | msgid "This command updates the coupon restrictions verification table."
244 | msgstr ""
245 |
246 | #: includes/class-wc-coupon-restrictions-cli.php:158
247 | msgid "This can be run if enhanced usage limits have been added to an existing coupon."
248 | msgstr ""
249 |
250 | #: includes/class-wc-coupon-restrictions-cli.php:159
251 | msgid "After the update, enhanced usage restriction verifications will work for future checkouts."
252 | msgstr ""
253 |
254 | #: includes/class-wc-coupon-restrictions-onboarding.php:76
255 | msgid "New Coupon"
256 | msgstr ""
257 |
258 | #: includes/class-wc-coupon-restrictions-onboarding.php:77
259 | msgid "Settings"
260 | msgstr ""
261 |
262 | #: includes/class-wc-coupon-restrictions-onboarding.php:78
263 | msgid "Docs"
264 | msgstr ""
265 |
266 | #: includes/class-wc-coupon-restrictions-onboarding.php:130
267 | msgid "WooCommerce Coupon Restrictions plugin activated."
268 | msgstr ""
269 |
270 | #: includes/class-wc-coupon-restrictions-onboarding.php:131
271 | msgid "See how it works."
272 | msgstr ""
273 |
274 | #: includes/class-wc-coupon-restrictions-onboarding.php:204
275 | msgid "Usage Restrictions"
276 | msgstr ""
277 |
278 | #: includes/class-wc-coupon-restrictions-onboarding.php:205
279 | msgid "Coupon restrictions can be found in this panel."
280 | msgstr ""
281 |
282 | #: includes/class-wc-coupon-restrictions-onboarding.php:220
283 | msgid "Customer Restrictions"
284 | msgstr ""
285 |
286 | #: includes/class-wc-coupon-restrictions-onboarding.php:221
287 | msgid "You now have the option to restrict coupons to new customers or existing customers."
288 | msgstr ""
289 |
290 | #: includes/class-wc-coupon-restrictions-onboarding.php:222
291 | msgid "Customers are considered \"new\" until they complete a purchase."
292 | msgstr ""
293 |
294 | #: includes/class-wc-coupon-restrictions-onboarding.php:237
295 | msgid "Limit User Tip"
296 | msgstr ""
297 |
298 | #: includes/class-wc-coupon-restrictions-onboarding.php:238
299 | msgid "If you are using a new customer restriction, you may also want to limit the coupon to 1 use."
300 | msgstr ""
301 |
302 | #: includes/class-wc-coupon-restrictions-onboarding.php:239
303 | msgid "Payments can take a few minutes to process, and it is possible for a customer to place multiple orders in that time if a coupon does not have a 1 use limit."
304 | msgstr ""
305 |
306 | #: includes/class-wc-coupon-restrictions-onboarding.php:254
307 | msgid "Role Restrictions"
308 | msgstr ""
309 |
310 | #: includes/class-wc-coupon-restrictions-onboarding.php:255
311 | msgid "Coupons can be restricted to specific user roles. Customer must have an account for the coupon to apply."
312 | msgstr ""
313 |
314 | #: includes/class-wc-coupon-restrictions-onboarding.php:270
315 | msgid "Location Restrictions"
316 | msgstr ""
317 |
318 | #: includes/class-wc-coupon-restrictions-onboarding.php:271
319 | msgid "Checking this box displays options for country and/or zip code restrictions."
320 | msgstr ""
321 |
322 | #: includes/class-wc-coupon-restrictions-onboarding.php:281
323 | msgid "Multiple Restrictions"
324 | msgstr ""
325 |
326 | #: includes/class-wc-coupon-restrictions-onboarding.php:282
327 | msgid "If multiple coupon restrictions are set, the customer must meet all restrictions."
328 | msgstr ""
329 |
330 | #: includes/class-wc-coupon-restrictions-onboarding.php:320
331 | msgid "Dismiss"
332 | msgstr ""
333 |
334 | #: includes/class-wc-coupon-restrictions-onboarding.php:321
335 | msgid "Next"
336 | msgstr ""
337 |
338 | #: includes/class-wc-coupon-restrictions-onboarding.php:322
339 | msgid "Enjoy!"
340 | msgstr ""
341 |
342 | #: includes/class-wc-coupon-restrictions-validation.php:294
343 | msgid "shipping"
344 | msgstr ""
345 |
346 | #: includes/class-wc-coupon-restrictions-validation.php:295
347 | msgid "billing"
348 | msgstr ""
349 |
350 | #: includes/class-wc-coupon-restrictions-validation.php:299
351 | msgid "Sorry, coupon code \"%s\" is only valid for new customers."
352 | msgstr ""
353 |
354 | #: includes/class-wc-coupon-restrictions-validation.php:303
355 | msgid "Sorry, coupon code \"%s\" is only valid for existing customers."
356 | msgstr ""
357 |
358 | #: includes/class-wc-coupon-restrictions-validation.php:307
359 | msgid "Sorry, coupon code \"%s\" is not valid with your customer role."
360 | msgstr ""
361 |
362 | #: includes/class-wc-coupon-restrictions-validation.php:313
363 | msgid "Sorry, coupon code \"%1$s\" is not valid in your %2$s country."
364 | msgstr ""
365 |
366 | #: includes/class-wc-coupon-restrictions-validation.php:319
367 | msgid "Sorry, coupon code \"%1$s\" is not valid in your %2$s state."
368 | msgstr ""
369 |
370 | #: includes/class-wc-coupon-restrictions-validation.php:325
371 | msgid "Sorry, coupon code \"%1$s\" is not valid in your %2$s zip code."
372 | msgstr ""
373 |
374 | #: includes/class-wc-coupon-restrictions-validation.php:331
375 | msgid "Sorry, coupon code \"%s\" usage limit exceeded."
376 | msgstr ""
377 |
378 | #: includes/class-wc-coupon-restrictions-validation.php:336
379 | msgid "Sorry, coupon code \"%s\" is not valid."
380 | msgstr ""
381 |
382 | #: woocommerce-coupon-restrictions.php:113
383 | msgid "%1$s requires at least %2$s v%3$s in order to function."
384 | msgstr ""
385 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | */.git/*
12 | .github/
13 | */vendor/*
14 | */node_modules/*
15 | */tests/*
16 | */bin/*
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | .
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | includes
6 | woocommerce-coupon-restrictions.php
7 |
8 |
9 |
10 |
11 | ./tests/phpunit/Unit
12 |
13 |
14 | ./tests/phpunit/Integration
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # WooCommerce Coupon Restrictions 
2 |
3 | - Requires PHP: 8.0
4 | - WP requires at least: 6.2
5 | - WP tested up to: 6.7
6 | - WC requires at least: 8.6.1
7 | - WC tested up to: 9.7.1
8 | - Stable tag: 2.2.3
9 | - License: [GPLv3 or later License](http://www.gnu.org/licenses/gpl-3.0.html)
10 |
11 | ## Description
12 |
13 | Create targeted coupons for new customers, user roles, countries or zip codes. Prevent coupon abuse with enhanced usage limits.
14 |
15 | **New customer restriction**: Verifies that the customer does not have an account with completed purchases before applying the coupon. The extension can also verify that a customer doesn't have completed guest orders if when that setting is selected.
16 |
17 | **Existing customer restriction**: Verifies that the custom does have an account with completed purchases before applying a coupon.
18 |
19 | **User role restriction**: Limits a coupon to specific user roles. If you have custom roles for wholesale customers, affiliates, or vips, you can provide coupons that will only work for them.
20 |
21 | **Country restriction**: Allows you to restrict a coupon to specific countries. Restriction can be applied to shipping or billing country.
22 |
23 | **State restriction**: Allows you to restrict a coupon to specific states. Restriction can be applied to shipping or billing country.
24 |
25 | **Zip code restriction**: Allows you to restrict a coupon to specific zip codes or postal codes. Can be applied to the shipping or the billing address.
26 |
27 | **Similar emails usage limit**: Sometimes customers use different email addresses in order to avoid coupon usage limits. This option does basic checks to ensure a customer is not using an email alias with periods or a "+" to exceed the coupon usage limit.
28 |
29 | **Shipping address usage limit**: Allows you to limit the amount of a times a coupon can be used with a specific shipping address.
30 |
31 | **IP address usage limit**: Allows you to limit the amount of times a coupon can be used with a specific IP address.
32 |
33 | The plugin is fully translatable. Developers can also use filters to modify any notices displayed during coupon validation.
34 |
35 | ## Additional Notes
36 |
37 | Customers are considered "new customers" if they do not have a user account with completed purchases.
38 |
39 | If your site allows guest checkouts, you can also verify if a customer has completed a guest checkout order previously. To enable this, select "Verify new customers by checking against user accounts and all guest orders" under "WooCommerce > Settings > General". However, this setting is not recommended for sites with more than 10,000 orders as this verification query takes additional time to run. Instead, it's recommended to create customer accounts in the background during checkout.
40 |
41 | E-mail addresses, zip code, and state restrictions are not case sensitive.
42 |
43 | A customer must meet all requirements if multiple restrictions are set. For instance, if a "New Customer" and "Country Restriction" are set, a customer must meet both restrictions in order to checkout with the coupon.
44 |
45 | ## Unit Tests
46 |
47 | 
48 |
49 | PHPUnit, Composer and WP-CLI are required for running unit tests.
50 |
51 | Local testing set up instructions:
52 | https://github.com/devinsays/woocommerce-coupon-restrictions//blob/main/tests/readme.md
53 |
54 | ## Code Standards
55 |
56 | The WordPress VIP minimum standards and WordPress-Extra code standards are used for this project. They will be installed via composer.
57 |
58 | To run the code checks from the command line run: `vendor/bin/phpcs`
59 |
60 | ## Translations
61 |
62 | - Run `composer make-pot` to update the translation file.
63 |
64 | ## Changelog
65 |
66 | **2.2.3 (2025-04-25)**
67 |
68 | - Update: Declare compatibility with latest version of WooCommerce.
69 | - Update: Fix deprecated call to is_coupon_valid.
70 | - Update: Improve docblock documentation.
71 | - Update: Improve automated testing suite.
72 |
73 | **2.2.2 (2024-02-05)**
74 |
75 | - Bugfix: Add explicit check for array type for meta values that require it.
76 |
77 | **2.2.1 (2023-04-07)**
78 |
79 | - Bugfix: Fatal error on subscription renewal with coupon applied due to missing class.
80 |
81 | **2.2.0 (2023-03-09)**
82 |
83 | - Feature: WP CLI command to add historic order data to verification table.
84 | - Update: Improve documentation around enhanced usage limits.
85 | - Update: Declare compatibility for High Performance Order Storage.
86 |
87 | **2.1.0 (2023-02-10)**
88 |
89 | - Bugfix: Coupon verification records were not storing properly on checkouts with payment.
90 | - Bugfix: Show compatibility notice if WooCommerce is not active.
91 |
92 | **2.0.0 (2023-01-02)**
93 |
94 | - Update: Major plugin rewrite.
95 | - Enhancement: Enhanced usage limits to help reduce coupon fraud.
96 |
97 | **1.8.6 (2022-11-09)**
98 |
99 | - Update: Tested to WooCommerce 7.1.0.
100 | - Update: Migrate inline admin javascript to enqueued file.
101 |
102 | **1.8.5 (2022-03-06)**
103 |
104 | - Bugfix: Rename translation file.
105 | - Update: Migrate inline javascript to file.
106 |
107 | **1.8.4 (2022-01-12)**
108 |
109 | - Update: Tested to WooCommerce 6.1.0.
110 | - Update: WooCommerce 4.8.1 or higher required.
111 | - Bugfix: Display specific coupon validation messages during ajax checkout validation.
112 |
113 | **1.8.3 (2021-03-28)**
114 |
115 | - Enhancement: Adds "Guest (No Account)" option to the roles restriction.
116 |
117 | **1.8.2 (2021-01-12)**
118 |
119 | - Enhancement: Reduces use of coupon meta by only storing non-default values.
120 | - Update: Tested to WooCommerce 4.8.0.
121 | - Update: PHP 7.0 or higher required.
122 | - Update: WooCommerce 3.9.0 or higher required.
123 |
124 | **1.8.1 (2020-06-14)**
125 |
126 | - Enhancement: Add all countries easily for country restriction.
127 | - Enhancement: Improved automated testing suite.
128 | - Update: Tested to WooCommerce 4.2.0.
129 |
130 | **1.8.0 (2019-09-25)**
131 |
132 | - Enhancement: Adds feature to restrict coupon by user role.
133 | - Enhancement: Zip code restrictions now allow wildcard matches.
134 | - Enhancement: Filter to skip pre-checkout validation.
135 | - Bugfix: If user is logged in and has no orders associated with their account but does have previous guest orders, those guest orders will now be checked to verify if customer is a new customer when "check guest orders" is selected.
136 |
137 | **1.7.2 (2019-03-12)**
138 |
139 | - Bugfix: Fixes 500 when saving coupons in specific server environments.
140 |
141 | **1.7.1 (2019-03-03)**
142 |
143 | - Enhancement: Adds feature to restrict coupon by state.
144 |
145 | **1.7.0 (2019-02-11)**
146 |
147 | - Update: New setting for new/existing customer verification method. Defaults to account check.
148 | - Bugfix: Resolves bug applying coupon when there is no session (subscription renewals). Props @giantpeach.
149 |
150 | **1.6.2 (2018-07-17)**
151 |
152 | - Bugfix: PHP5.6 compatibility fixes for onboarding script.
153 |
154 | **1.6.1 (2018-06-21)**
155 |
156 | - Update: Use WooCommerce data store methods for saving and reading coupon meta.
157 | - Update: WC_Coupon_Restrictions() now returns shared instance of class rather than singleton.
158 | - Bugfix: Display onboarding notice on initial activation.
159 | - Bugfix: If the session data is blank for country or zipcode, a coupon with location restrictions will now apply until session or checkout has data to validate it.
160 |
161 | **1.6.0 (2018-06-15)**
162 |
163 | - Enhancement: Coupon validation now uses stored session data.
164 | - Enhancement: Checkout validation now uses $posted data.
165 | - Update: Additional unit and integration tests.
166 | - Update: Returns a main instance of the class to avoid the need for globals.
167 |
168 | **1.5.0 (2018-05-17)**
169 |
170 | - Update: Improve coupon validation messages.
171 | - Update: Use "Zip code" as default label.
172 | - Update: Improve customer restriction UX. Use radio buttons rather than select.
173 | - Update: Adds "Location Restrictions" checkbox. Additional options display when checked.
174 | - Update: Country restriction only permits selection of countries that shop currently sells to.
175 | - Update: New onboarding flow that shows where the new coupon options are located.
176 | - Bugfix: Bug with new customer coupon validation at checkout.
177 |
178 | **1.4.1 (2018-02-15)**
179 |
180 | - Update: Remove upgrade routine.
181 |
182 | **1.4.0 (2017-12-27)**
183 |
184 | - Enhancement: Adds option to restrict location based on shipping or billing address.
185 | - Enhancement: Adds option to restrict to postal code or zip code.
186 | - Update: Use WooCommerce order wrapper to fetch orders.
187 | - Update: Organize plugin into multiple classes.
188 | - Update: Upgrade script for sites using earlier version of plugin.
189 | - Update: Unit test framework added.
190 |
191 | **1.3.0 (2017-01-31)**
192 |
193 | - Enhancement: Adds option to restrict to existing customers.
194 | - Enhancement: Adds option to restrict by shipping country.
195 | - Update: Compatibility updates for WooCommerce 2.7.0.
196 |
197 | **1.2.0 (2016-11-25)**
198 |
199 | - Update: Compatibility updates for WooCommerce 2.6.8.
200 |
201 | **1.1.0 (2015-12-28)**
202 |
203 | - Bugfix: Coupons without the new customer restriction were improperly marked invalid for logged in users.
204 | - Bugfix: Filter woocommerce_coupon_is_valid in addition to woocommerce_coupon_error.
205 |
206 | **1.0.0 (2015-06-18)**
207 |
208 | - Initial release.
209 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === WooCommerce Coupon Restrictions ===
2 |
3 | Contributors: @downstairsdev
4 | Tags: woocommerce, coupon
5 | Requires at least: 6.2
6 | Tested up to: 6.7.2
7 | Requires PHP: 8.0
8 | Stable tag: 2.2.3
9 | License: GPLv3 or later License
10 | License URI: http://www.gnu.org/licenses/gpl-3.0.html
11 | WC requires at least: 8.6.1
12 | WC tested up to: 9.7.1
13 | Woo: 3200406:6d7b7aa4f9565b8f7cbd2fe10d4f119a
14 |
--------------------------------------------------------------------------------
/tests/bin/install-woocommerce.sh:
--------------------------------------------------------------------------------
1 | TMPDIR="tests/tmp"
2 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
3 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
4 |
5 | download() {
6 | if [ `which curl` ]; then
7 | curl -s "$1" > "$2";
8 | elif [ `which wget` ]; then
9 | wget -nv -O "$2" "$1"
10 | fi
11 | }
12 |
13 | install_woocommerce() {
14 | # Check if WP_CORE_DIR exists
15 | if [ ! -d "$WP_CORE_DIR" ]; then
16 | echo "Error: WordPress has not been installed yet. Please run 'bash tests/bin/install-wp-tests.sh'."
17 | return
18 | fi
19 |
20 | # Check if a version is provided
21 | WC_VERSION=${1:-"latest"}
22 |
23 | # Plugin directory with version suffix
24 | WC_PLUGIN_DIR="$WP_CORE_DIR/wp-content/plugins/woocommerce-$WC_VERSION"
25 |
26 | # Check if WooCommerce with the specific version is already installed
27 | if [ -d "$WC_PLUGIN_DIR" ]; then
28 | echo "WooCommerce $WC_VERSION is already installed."
29 | return
30 | fi
31 |
32 | # Set download URL based on version
33 | if [ "$WC_VERSION" = "latest" ]; then
34 | DOWNLOAD_URL="https://downloads.wordpress.org/plugin/woocommerce.zip"
35 | else
36 | DOWNLOAD_URL="https://downloads.wordpress.org/plugin/woocommerce.$WC_VERSION.zip"
37 | fi
38 |
39 | echo "Downloading WooCommerce version: $WC_VERSION..."
40 | download $DOWNLOAD_URL $TMPDIR/woocommerce.zip
41 | if [ ! -f $TMPDIR/woocommerce.zip ]; then
42 | echo "Error: Failed to download WooCommerce version $WC_VERSION."
43 | exit 1
44 | fi
45 |
46 | echo "Extracting WooCommerce zip file..."
47 | unzip -q $TMPDIR/woocommerce.zip -d $TMPDIR/
48 |
49 | # Check if the extracted directory exists
50 | if [ -d "$TMPDIR/woocommerce" ]; then
51 | # Rename the WooCommerce directory to include the version number
52 | mv "$TMPDIR/woocommerce" "$TMPDIR/woocommerce-$WC_VERSION"
53 |
54 | # Move the renamed directory to the plugin directory
55 | echo "Moving WooCommerce $WC_VERSION to plugins directory..."
56 | mv "$TMPDIR/woocommerce-$WC_VERSION" "$WP_CORE_DIR/wp-content/plugins/"
57 | echo "WooCommerce version $WC_VERSION installed successfully as 'woocommerce-$WC_VERSION'."
58 | else
59 | echo "Error: Extracted WooCommerce directory not found."
60 | exit 1
61 | fi
62 | }
63 |
64 | # Run the function
65 | install_woocommerce "$1"
66 |
--------------------------------------------------------------------------------
/tests/bin/install-wp-tests.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | # Notice: This file is pulled from the WP-CLI scaffold command,
3 | # and should not be modified directly.
4 |
5 | if [ $# -lt 3 ]; then
6 | echo "usage: $0 [db-host] [wp-version] [skip-database-creation]"
7 | exit 1
8 | fi
9 |
10 | DB_NAME=$1
11 | DB_USER=$2
12 | DB_PASS=$3
13 | DB_HOST=${4-localhost}
14 | WP_VERSION=${5-latest}
15 | SKIP_DB_CREATE=${6-false}
16 |
17 | TMPDIR="tests/tmp"
18 | TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
19 | WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
20 | WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
21 |
22 | download() {
23 | if [ `which curl` ]; then
24 | curl -s "$1" > "$2";
25 | elif [ `which wget` ]; then
26 | wget -nv -O "$2" "$1"
27 | fi
28 | }
29 |
30 | if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
31 | WP_BRANCH=${WP_VERSION%\-*}
32 | WP_TESTS_TAG="branches/$WP_BRANCH"
33 |
34 | elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
35 | WP_TESTS_TAG="branches/$WP_VERSION"
36 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
37 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
38 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
39 | WP_TESTS_TAG="tags/${WP_VERSION%??}"
40 | else
41 | WP_TESTS_TAG="tags/$WP_VERSION"
42 | fi
43 | elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
44 | WP_TESTS_TAG="trunk"
45 | else
46 | # http serves a single offer, whereas https serves multiple. we only want one
47 | download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
48 | grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
49 | LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
50 | if [[ -z "$LATEST_VERSION" ]]; then
51 | echo "Latest WordPress version could not be found"
52 | exit 1
53 | fi
54 | WP_TESTS_TAG="tags/$LATEST_VERSION"
55 | fi
56 | set -ex
57 |
58 | install_wp() {
59 | if [ -d $WP_CORE_DIR ]; then
60 | return;
61 | fi
62 |
63 | mkdir -p $WP_CORE_DIR
64 |
65 | if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
66 | mkdir -p $TMPDIR/wordpress-nightly
67 | download https://wordpress.org/nightly-builds/wordpress-latest.zip $TMPDIR/wordpress-nightly/wordpress-nightly.zip
68 | unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/
69 | mv $TMPDIR/wordpress-nightly/wordpress/* $WP_CORE_DIR
70 | else
71 | if [ $WP_VERSION == 'latest' ]; then
72 | local ARCHIVE_NAME='latest'
73 | elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
74 | # https serves multiple offers, whereas http serves single.
75 | download https://api.wordpress.org/core/version-check/1.7/ $TMPDIR/wp-latest.json
76 | if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
77 | # version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
78 | LATEST_VERSION=${WP_VERSION%??}
79 | else
80 | # otherwise, scan the releases and get the most up to date minor version of the major release
81 | local VERSION_ESCAPED=`echo $WP_VERSION | sed 's/\./\\\\./g'`
82 | LATEST_VERSION=$(grep -o '"version":"'$VERSION_ESCAPED'[^"]*' $TMPDIR/wp-latest.json | sed 's/"version":"//' | head -1)
83 | fi
84 | if [[ -z "$LATEST_VERSION" ]]; then
85 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
86 | else
87 | local ARCHIVE_NAME="wordpress-$LATEST_VERSION"
88 | fi
89 | else
90 | local ARCHIVE_NAME="wordpress-$WP_VERSION"
91 | fi
92 | download https://wordpress.org/${ARCHIVE_NAME}.tar.gz $TMPDIR/wordpress.tar.gz
93 | tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
94 | mkdir -p $WP_CORE_DIR/wp-content/uploads
95 | fi
96 |
97 | download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
98 | }
99 |
100 | install_test_suite() {
101 | # portable in-place argument for both GNU sed and Mac OSX sed
102 | if [[ $(uname -s) == 'Darwin' ]]; then
103 | local ioption='-i.bak'
104 | else
105 | local ioption='-i'
106 | fi
107 |
108 | # set up testing suite if it doesn't yet exist
109 | if [ ! -d $WP_TESTS_DIR ]; then
110 | # set up testing suite
111 | mkdir -p $WP_TESTS_DIR
112 | svn co --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
113 | svn co --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
114 | fi
115 |
116 | if [ ! -f wp-tests-config.php ]; then
117 | download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
118 | # remove all forward slashes in the end
119 | WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
120 | sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
121 | sed $ioption "s:__DIR__ . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
122 | sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
123 | sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
124 | sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
125 | sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
126 |
127 | # Copy our custom config file
128 | tail -n +6 tests/wp-tests-config.php >> "$WP_TESTS_DIR"/wp-tests-config.php
129 | fi
130 | }
131 |
132 | recreate_db() {
133 | shopt -s nocasematch
134 | if [[ $1 =~ ^(y|yes)$ ]]
135 | then
136 | mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
137 | create_db
138 | echo "Recreated the database ($DB_NAME)."
139 | else
140 | echo "Leaving the existing database ($DB_NAME) in place."
141 | fi
142 | shopt -u nocasematch
143 | }
144 |
145 | create_db() {
146 | mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
147 | }
148 |
149 | install_db() {
150 | if [ ${SKIP_DB_CREATE} = "true" ]; then
151 | return 0
152 | fi
153 |
154 | # parse DB_HOST for port or socket references
155 | local PARTS=(${DB_HOST//\:/ })
156 | local DB_HOSTNAME=${PARTS[0]};
157 | local DB_SOCK_OR_PORT=${PARTS[1]};
158 | local EXTRA=""
159 |
160 | if ! [ -z $DB_HOSTNAME ] ; then
161 | if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
162 | EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
163 | elif ! [ -z $DB_SOCK_OR_PORT ] ; then
164 | EXTRA=" --socket=$DB_SOCK_OR_PORT"
165 | elif ! [ -z $DB_HOSTNAME ] ; then
166 | EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
167 | fi
168 | fi
169 |
170 | # create database
171 | # Overwritten to incorporate https://github.com/wp-cli/scaffold-command/pull/255.
172 | if [ $(mysql --user="$DB_USER" --password="$DB_PASS" --execute='show databases;' | grep ^$DB_NAME$) ]
173 | then
174 | echo "Reinstalling will delete the existing test database ($DB_NAME)"
175 | read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB
176 | recreate_db $DELETE_EXISTING_DB
177 | else
178 | create_db
179 | fi
180 | }
181 |
182 | install_wp
183 | install_test_suite
184 | install_db
185 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ApplyCountryRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | set_billing_country( 'US' );
20 | $customer->set_shipping_country( 'US' );
21 | $customer->save();
22 | $this->customer = $customer;
23 |
24 | // Sets the customer session.
25 | $session = array(
26 | 'country' => $customer->get_billing_country(),
27 | 'state' => $customer->get_billing_state(),
28 | 'postcode' => $customer->get_billing_postcode(),
29 | 'city' => $customer->get_billing_city(),
30 | 'address' => $customer->get_billing_address(),
31 | 'shipping_country' => $customer->get_shipping_country(),
32 | 'shipping_state' => $customer->get_shipping_state(),
33 | 'shipping_postcode' => $customer->get_shipping_postcode(),
34 | 'shipping_city' => $customer->get_shipping_city(),
35 | 'shipping_address' => $customer->get_shipping_address(),
36 | );
37 | WC_Helper_Customer::set_customer_details( $session );
38 |
39 | // Creates a coupon.
40 | $coupon = WC_Helper_Coupon::create_coupon();
41 | $coupon->update_meta_data( 'location_restrictions', 'yes' );
42 | $coupon->update_meta_data( 'address_for_location_restrictions', 'billing' );
43 | $coupon->save();
44 |
45 | $this->coupon = $coupon;
46 |
47 | // Set the current customer.
48 | wp_set_current_user( $customer->get_id() );
49 | }
50 |
51 | /**
52 | * Tests applying a coupon with country restriction and valid customer.
53 | */
54 | public function test_coupon_country_restriction_with_valid_customer() {
55 | $coupon = $this->coupon;
56 |
57 | // Apply country restriction to single country "US"
58 | $coupon->update_meta_data( 'country_restriction', array( 'US' ) );
59 | $coupon->save();
60 |
61 | // Adds a country restricted coupon.
62 | // This should return true because customer billing is in US.
63 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
64 |
65 | // Verifies 1 coupon has been applied to cart.
66 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
67 | }
68 |
69 | /**
70 | * Tests applying a coupon with a two country restriction and valid customer.
71 | */
72 | public function test_coupon_two_country_restriction_with_valid_customer() {
73 | $coupon = $this->coupon;
74 |
75 | // Apply country restiction to two countries "US" and "CA"
76 | $coupon->update_meta_data( 'country_restriction', array( 'US', 'CA' ) );
77 | $coupon->save();
78 |
79 | // This should return true because customer billing is in US.
80 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
81 |
82 | // Verifies 1 coupon has been applied to cart.
83 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
84 | }
85 |
86 | /**
87 | * Tests applying a coupon with a two country restriction and non-valid customer.
88 | */
89 | public function test_coupon_country_restriction_with_nonvalid_customer() {
90 | $coupon = $this->coupon;
91 |
92 | // Apply country restriction single country "CA".
93 | $coupon->update_meta_data( 'country_restriction', array( 'CA' ) );
94 | $coupon->save();
95 |
96 | // Adds a country restricted coupon.
97 | // This should return false because customer billing is in US.
98 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
99 |
100 | // Verifies 0 coupons have been applied to cart.
101 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
102 | }
103 |
104 | /**
105 | * Tests applying a coupon with a country location requirement.
106 | *
107 | * The customer doesn't meet coupon location requirements,
108 | * but the location restriction option is not checked,
109 | * so coupon should be valid.
110 | */
111 | public function test_location_restrictions_should_apply() {
112 | // Coupon can only be used in CA.
113 | $coupon = $this->coupon;
114 | $coupon->update_meta_data( 'country_restriction', array( 'CA' ) );
115 |
116 | // Location restriction is not checked.
117 | $coupon->update_meta_data( 'location_restrictions', 'no' );
118 |
119 | $coupon->save();
120 |
121 | // Adds a country restricted coupon.
122 | // This should be valid since location restrictions are not being checked.
123 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
124 |
125 | // Verifies the coupon has not been added to cart.
126 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
127 | }
128 |
129 | /**
130 | * Tests applying a coupon with a country location requirement.
131 | *
132 | * locale_filter_matches customer doesn't meet coupon location requirements,
133 | * and location restriction option is checked,
134 | * so the coupon should not apply.
135 | */
136 | public function test_location_restrictions_should_not_apply() {
137 | // Coupon can only be used in CA.
138 | $coupon = $this->coupon;
139 | $coupon->update_meta_data( 'country_restriction', array( 'CA' ) );
140 |
141 | // Location restriction is checked.
142 | $coupon->update_meta_data( 'location_restrictions', 'yes' );
143 | $coupon->save();
144 |
145 | // Adds a country restricted coupon.
146 | // This should fail because customer doesn't meet requirements,
147 | // and location restrictions are checked.
148 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
149 |
150 | // Verifies the coupon has not been added to cart.
151 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
152 | }
153 |
154 | public function tear_down() {
155 | // Reset the customer session data.
156 | WC()->session->set( 'customer', array() );
157 |
158 | // Remove the coupons from the cart.
159 | WC()->cart->empty_cart();
160 | WC()->cart->remove_coupons();
161 |
162 | $this->customer->delete();
163 | $this->coupon->delete();
164 |
165 | parent::tear_down();
166 | }
167 | }
168 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ApplyExistingCustomerCouponTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'customer_restriction_type', 'existing' );
19 | $coupon->save();
20 |
21 | $this->coupon = $coupon;
22 | }
23 |
24 | /**
25 | * Coupon will apply because no session has been set yet.
26 | */
27 | public function test_existing_customer_restriction_coupon_applies_with_no_session() {
28 | $coupon = $this->coupon;
29 |
30 | // Adds a coupon restricted to existing customers.
31 | // This should return false because customer hasn't yet purchased.
32 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
33 |
34 | // Verifies 0 coupons have been applied to cart.
35 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
36 | }
37 |
38 | /**
39 | * Coupon will not apply once a session with email has been created,
40 | * and email does not match existing customer.
41 | */
42 | public function test_existing_customer_restriction_with_session_not_valid() {
43 | $coupon = $this->coupon;
44 |
45 | // Crate a mock customer session.
46 | $session = array(
47 | 'email' => 'new@woo.com'
48 | );
49 | WC_Helper_Customer::set_customer_details( $session );
50 |
51 | // Adds a coupon restricted to existing customers.
52 | // This should return false because customer hasn't yet purchased.
53 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
54 |
55 | // Verifies 0 coupons have been applied to cart.
56 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
57 | }
58 |
59 | /**
60 | * Coupon will not apply once a session with email has been created,
61 | * and email does not match existing customer.
62 | */
63 | public function test_existing_customer_restriction_with_session_valid() {
64 | $coupon = $this->coupon;
65 |
66 | // Create a customer.
67 | $customer = WC_Helper_Customer::create_customer();
68 | $this->customer = $customer;
69 |
70 | // Creates an order and applies it to new customer.
71 | $order = WC_Helper_Order::create_order( $customer->get_id() );
72 | $order->set_billing_email( $customer->get_email() );
73 | $order->set_status( 'completed' );
74 | $order->save();
75 |
76 | // Crate a mock customer session.
77 | $session = array(
78 | 'email' => $customer->get_email()
79 | );
80 | WC_Helper_Customer::set_customer_details( $session );
81 |
82 | // Adds a coupon restricted to existing customers.
83 | // This should return true because customer has purchased.
84 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
85 |
86 | // Verifies 0 coupons have been applied to cart.
87 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
88 |
89 | $order->delete();
90 | }
91 |
92 |
93 | public function tear_down() {
94 | // Reset the customer session data.
95 | WC()->session->set( 'customer', array() );
96 |
97 | // Removes the coupons from the cart.
98 | WC()->cart->empty_cart();
99 | WC()->cart->remove_coupons();
100 |
101 | // Deletes the coupon.
102 | $this->coupon->delete();
103 |
104 | parent::tear_down();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ApplyGenericCouponTest.php:
--------------------------------------------------------------------------------
1 | get_id();
17 | wp_set_current_user( $customer_id );
18 |
19 | // Creates a coupon.
20 | $coupon = WC_Helper_Coupon::create_coupon();
21 |
22 | // Adds a coupon, test return statement.
23 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
24 |
25 | // Test if total amount of coupons is 1.
26 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
27 |
28 | // Clean up.
29 | WC()->cart->empty_cart();
30 | WC()->cart->remove_coupons();
31 |
32 | $coupon->delete();
33 | $customer->delete();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ApplyNewCustomerCouponTest.php:
--------------------------------------------------------------------------------
1 | get_id(), 'customer_restriction_type', 'new' );
20 | $this->coupon = $coupon;
21 |
22 | // Create a customer.
23 | $customer = WC_Helper_Customer::create_customer(
24 | 'customer',
25 | 'password',
26 | 'customer@woo.com'
27 | );
28 | $this->customer = $customer;
29 |
30 | // Creates an order and applies it to new customer.
31 | $order = WC_Helper_Order::create_order( $customer->get_id() );
32 | $order->set_billing_email( $customer->get_email() );
33 | $order->set_status( 'completed' );
34 | $order->save();
35 | $this->order = $order;
36 |
37 | }
38 |
39 | /**
40 | * Coupon will apply once a session with email has been created,
41 | * and email does not match existing customer.
42 | */
43 | public function test_new_customer_restriction_valid() {
44 | $coupon = $this->coupon;
45 |
46 | // Create a mock customer session.
47 | $session = array(
48 | 'id' => 0,
49 | 'email' => 'new@woo.com'
50 | );
51 | WC_Helper_Customer::set_customer_details( $session );
52 |
53 | // Adds a coupon restricted to new customers.
54 | // This should return true because customer hasn't yet purchased.
55 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
56 |
57 | // Verifies 1 coupons have been applied to cart.
58 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
59 | }
60 |
61 | /**
62 | * Coupon will not apply once a session with email has been created,
63 | * and email matches existing customer with purchases.
64 | */
65 | public function new_customer_restriction_invalid_has_account() {
66 | $coupon = $this->coupon;
67 | $customer = $this->customer;
68 |
69 | // Create a mock customer session.
70 | $session = array(
71 | 'id' => 0,
72 | 'email' => $customer->get_email()
73 | );
74 | WC_Helper_Customer::set_customer_details( $session );
75 |
76 | // Adds a coupon restricted to new customers.
77 | // This should return false because customer has purchased.
78 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
79 |
80 | // Verifies 0 coupons have been applied to cart.
81 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
82 | }
83 |
84 | /**
85 | * Checks that "coupon_restrictions_customer_query" setting is working.
86 | */
87 | public function test_coupon_restrictions_customer_query_setting() {
88 | // Get data from setup.
89 | $coupon = $this->coupon;
90 |
91 | // Create new order.
92 | $guest_email = 'guest@woo.com';
93 | $order = WC_Helper_Order::create_order();
94 | $order->set_billing_email( $guest_email );
95 | $order->set_status( 'completed' );
96 | $order->save();
97 |
98 | // Create a mock customer session.
99 | $session = array(
100 | 'email' => $guest_email
101 | );
102 | WC_Helper_Customer::set_customer_details( $session );
103 |
104 | // Sets coupon_restrictions_customer_query to search accounts only.
105 | update_option( 'coupon_restrictions_customer_query', 'accounts' );
106 |
107 | // Adds a coupon restricted to new customers.
108 | // This should return true because customer has a previous guest order
109 | // but no account.
110 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
111 |
112 | // Verifies 1 coupon has been applied to cart.
113 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
114 |
115 | // Sets coupon_restrictions_customer_query to now search guest orders.
116 | update_option( 'coupon_restrictions_customer_query', 'accounts-orders' );
117 |
118 | // Remove coupon that had just been set.
119 | WC()->cart->remove_coupons();
120 |
121 | // Adds a coupon restricted to new customers.
122 | // This should return false because customer has a previous guest order.
123 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
124 |
125 | // Verifies 0 coupons have been applied to cart.
126 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
127 |
128 | delete_option( 'coupon_restrictions_customer_query' );
129 | }
130 |
131 | public function tear_down() {
132 | // Reset the customer session data.
133 | WC()->session->set( 'customer', array() );
134 |
135 | // Removes the coupons from the cart.
136 | WC()->cart->empty_cart();
137 | WC()->cart->remove_coupons();
138 |
139 | // Deletes objects.
140 | $this->coupon->delete();
141 | $this->customer->delete();
142 | $this->order->delete();
143 |
144 | parent::tear_down();
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ApplyPostcodeRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | set_billing_postcode( '78703' );
21 | $customer->set_shipping_postcode( '78703' );
22 | $customer->save();
23 | $this->customer = $customer;
24 |
25 | // Sets the customer session.
26 | $session = array(
27 | 'country' => $customer->get_billing_country(),
28 | 'state' => $customer->get_billing_state(),
29 | 'postcode' => $customer->get_billing_postcode(),
30 | 'city' => $customer->get_billing_city(),
31 | 'address' => $customer->get_billing_address(),
32 | 'shipping_country' => $customer->get_shipping_country(),
33 | 'shipping_state' => $customer->get_shipping_state(),
34 | 'shipping_postcode' => $customer->get_shipping_postcode(),
35 | 'shipping_city' => $customer->get_shipping_city(),
36 | 'shipping_address' => $customer->get_shipping_address(),
37 | );
38 | WC_Helper_Customer::set_customer_details( $session );
39 |
40 | // Creates a coupon.
41 | $coupon = WC_Helper_Coupon::create_coupon();
42 | $coupon->update_meta_data( 'location_restrictions', 'yes' );
43 | $coupon->update_meta_data( 'address_for_location_restrictions', 'billing' );
44 | $coupon->save();
45 | $this->coupon = $coupon;
46 |
47 | // Set the current customer.
48 | wp_set_current_user( $customer->get_id() );
49 | }
50 |
51 | /**
52 | * Tests applying a coupon with postcode restriction and valid customer.
53 | */
54 | public function test_postcode_restriction_with_valid_customer() {
55 | $coupon = $this->coupon;
56 | $coupon->update_meta_data( 'postcode_restriction', '78703' );
57 | $coupon->save();
58 |
59 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
60 |
61 | // Verifies 1 coupon has been applied to cart.
62 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
63 | }
64 |
65 | /**
66 | * Tests applying a postcode restriction and non-valid customer.
67 | */
68 | public function test_coupon_country_restriction_with_nonvalid_customer() {
69 | $coupon = $this->coupon;
70 | $coupon->update_meta_data( 'postcode_restriction', '000000' );
71 | $coupon->save();
72 |
73 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
74 |
75 | // Verifies 0 coupons have been applied to cart.
76 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
77 | }
78 |
79 | /**
80 | * Tests applying a coupon with postcode restriction and valid customer.
81 | */
82 | public function test_valid_postcode_restriction_wildcard() {
83 | $coupon = $this->coupon;
84 | $coupon->update_meta_data( 'postcode_restriction', '00000,787*,ALPHAZIP' );
85 | $coupon->save();
86 |
87 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
88 |
89 | // Verifies 1 coupon has been applied to cart.
90 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
91 | }
92 |
93 | public function tear_down() {
94 | // Reset the customer session data.
95 | WC()->session->set( 'customer', array() );
96 |
97 | // Remove the coupons from the cart.
98 | WC()->cart->empty_cart();
99 | WC()->cart->remove_coupons();
100 |
101 | $this->customer->delete();
102 | $this->coupon->delete();
103 |
104 | parent::tear_down();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ApplyRoleRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'role_restriction', ['administrator'] );
20 | $coupon->save();
21 |
22 | $this->coupon = $coupon;
23 |
24 | // Creates a customer.
25 | $customer = WC_Helper_Customer::create_customer();
26 | $this->customer = $customer;
27 | }
28 |
29 | /**
30 | * Coupon will apply because no session has been set yet.
31 | */
32 | public function test_coupon_applies_with_no_session() {
33 | $coupon = $this->coupon;
34 |
35 | // Adds a role restricted coupon.
36 | // This should apply because no session has been set.
37 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
38 |
39 | // Verifies 1 coupons have been applied to cart.
40 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
41 | }
42 |
43 | /**
44 | * Coupon will fail if restriction does not match.
45 | */
46 | public function test_coupon_fails_if_restrictions_do_not_match() {
47 | $coupon = $this->coupon;
48 | $customer = $this->customer;
49 |
50 | // Set role that does not match coupon.
51 | $user = new \WP_User( $customer->get_id() );
52 | $user->set_role('subscriber');
53 |
54 | // Create a mock customer session.
55 | $session = array(
56 | 'email' => $customer->get_email()
57 | );
58 | WC_Helper_Customer::set_customer_details( $session );
59 |
60 | // Coupon should not apply because custom role and restriction do not match.
61 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
62 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
63 | }
64 |
65 | /**
66 | * Coupon will apply if restrictions match.
67 | */
68 | public function test_coupon_success_if_restrictions_match() {
69 | $coupon = $this->coupon;
70 | $customer = $this->customer;
71 |
72 | // Set role that does match coupon.
73 | $user = new \WP_User( $customer->get_id() );
74 | $user->set_role('administrator');
75 |
76 | // Create a mock customer session.
77 | $session = array(
78 | 'email' => $customer->get_email()
79 | );
80 | WC_Helper_Customer::set_customer_details( $session );
81 |
82 | // Coupon should apply because customer role and restriction match.
83 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
84 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
85 | }
86 |
87 | /**
88 | * Coupon will apply because customer is guest and guest role is permitted.
89 | */
90 | public function test_coupon_success_if_guest_and_guest_role_set() {
91 | // Creates a coupon.
92 | $coupon = WC_Helper_Coupon::create_coupon();
93 | $coupon->update_meta_data( 'role_restriction', ['woocommerce-coupon-restrictions-guest'] );
94 | $coupon->save();
95 |
96 | // Create a mock customer session.
97 | $session = array(
98 | 'email' => 'guest@testing.dev'
99 | );
100 | WC_Helper_Customer::set_customer_details( $session );
101 |
102 | // Coupon should apply because customer does not have an account
103 | // and the role restriction allows guests.
104 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
105 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
106 | }
107 |
108 | /**
109 | * Coupon will not apply because customer is guest and guest role is not permitted.
110 | */
111 | public function test_coupon_fails_if_guest_and_guest_role_not_set() {
112 | // Get data from setup for coupon restricted to administrators (no guest role).
113 | $coupon = $this->coupon;
114 |
115 | // Create a mock customer session.
116 | $session = array(
117 | 'email' => 'guest@testing.dev'
118 | );
119 | WC_Helper_Customer::set_customer_details( $session );
120 |
121 | // Coupon should not apply because customer does not have an account
122 | // and the role restriction does not allow guests.
123 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
124 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
125 | }
126 |
127 |
128 | public function tear_down() {
129 | // Reset the customer session data.
130 | WC()->session->set( 'customer', array() );
131 |
132 | // Removes the coupons from the cart.
133 | WC()->cart->empty_cart();
134 | WC()->cart->remove_coupons();
135 |
136 | // Deletes objects.
137 | $this->coupon->delete();
138 | $this->customer->delete();
139 |
140 | parent::tear_down();
141 | }
142 |
143 | }
144 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ApplyStateRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | set_billing_state( 'TX' );
20 | $customer->set_shipping_state( 'TX' );
21 | $customer->save();
22 | $this->customer = $customer;
23 |
24 | // Sets the customer session.
25 | $session = array(
26 | 'country' => $customer->get_billing_country(),
27 | 'state' => $customer->get_billing_state(),
28 | 'postcode' => $customer->get_billing_postcode(),
29 | 'city' => $customer->get_billing_city(),
30 | 'address' => $customer->get_billing_address(),
31 | 'shipping_country' => $customer->get_shipping_country(),
32 | 'shipping_state' => $customer->get_shipping_state(),
33 | 'shipping_postcode' => $customer->get_shipping_postcode(),
34 | 'shipping_city' => $customer->get_shipping_city(),
35 | 'shipping_address' => $customer->get_shipping_address(),
36 | );
37 | WC_Helper_Customer::set_customer_details( $session );
38 |
39 | // Creates a coupon.
40 | $coupon = WC_Helper_Coupon::create_coupon();
41 | update_post_meta( $coupon->get_id(), 'location_restrictions', 'yes' );
42 | update_post_meta( $coupon->get_id(), 'address_for_location_restrictions', 'billing' );
43 | $this->coupon = $coupon;
44 |
45 | // Set the current customer.
46 | wp_set_current_user( $customer->get_id() );
47 | }
48 |
49 | /**
50 | * Tests applying a coupon with country restriction and valid customer.
51 | */
52 | public function test_coupon_state_restriction_with_valid_customer() {
53 | $coupon = $this->coupon;
54 |
55 | // Apply state restriction to single state "TX"
56 | update_post_meta( $coupon->get_id(), 'state_restriction', 'TX' );
57 |
58 | // Adds a state restricted coupon.
59 | // This should return true because customer billing is in TX.
60 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
61 |
62 | // Verifies 1 coupon has been applied to cart.
63 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
64 | }
65 |
66 | /**
67 | * Tests applying a coupon with a two state restriction and valid customer.
68 | */
69 | public function test_coupon_two_state_restriction_with_valid_customer() {
70 | $coupon = $this->coupon;
71 |
72 | // Apply country restiction to two states "TX" and "CA"
73 | update_post_meta( $coupon->get_id(), 'state_restriction', "ca,tx" );
74 |
75 | // This should return true because customer billing is in tx.
76 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
77 |
78 | // Verifies 1 coupon has been applied to cart.
79 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
80 | }
81 |
82 | /**
83 | * Tests applying a coupon with a two country restriction and non-valid customer.
84 | */
85 | public function test_coupon_state_restriction_with_nonvalid_customer() {
86 | $coupon = $this->coupon;
87 |
88 | // Apply state restriction single state "CA".
89 | update_post_meta( $coupon->get_id(), 'state_restriction', 'CA' );
90 |
91 | // Adds a state restricted coupon.
92 | // This should return false because customer billing is in TX.
93 | $this->assertFalse( WC()->cart->apply_coupon( $coupon->get_code() ) );
94 |
95 | // Verifies 0 coupons have been applied to cart.
96 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
97 | }
98 |
99 | public function tear_down() {
100 | // Reset the customer session data.
101 | WC()->session->set( 'customer', array() );
102 |
103 | // Remove the coupons from the cart.
104 | WC()->cart->empty_cart();
105 | WC()->cart->remove_coupons();
106 |
107 | $this->customer->delete();
108 | $this->coupon->delete();
109 |
110 | parent::tear_down();
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutCountryRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'location_restrictions', 'yes' );
18 | $coupon->update_meta_data( 'address_for_location_restrictions', 'billing' );
19 | $coupon->save();
20 | $this->coupon = $coupon;
21 | }
22 |
23 | /**
24 | * Coupon is valid because coupon is restricted to US,
25 | * and customer billing_country is US.
26 | */
27 | public function test_checkout_country_restriction_with_valid_customer() {
28 | $coupon = $this->coupon;
29 |
30 | // Apply country restriction to single country "US".
31 | $coupon->update_meta_data( 'country_restriction', array( 'US' ) );
32 | $coupon->save();
33 |
34 | // Mock post data.
35 | $posted = array(
36 | 'billing_country' => 'US'
37 | );
38 |
39 | // Adds a country restricted coupon.
40 | // This will apply because no validation runs if a session is not set.
41 | WC()->cart->apply_coupon( $coupon->get_code() );
42 |
43 | // Run the post checkout validation.
44 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
45 | $validation->validate_coupons_after_checkout( $posted );
46 |
47 | // Verifies 1 coupon is still in cart after checkout validation.
48 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
49 | }
50 |
51 | /**
52 | * Coupon is not valid because coupon is restricted to US,
53 | * and customer billing_country is CA.
54 | */
55 | public function test_checkout_country_restriction_with_not_valid_customer() {
56 | $coupon = $this->coupon;
57 |
58 | // Apply country restriction to single country "US"
59 | $coupon->update_meta_data( 'country_restriction', array( 'US' ) );
60 | $coupon->save();
61 |
62 | // Mock post data.
63 | $posted = array(
64 | 'billing_email' => 'customer@woo.com',
65 | 'billing_country' => 'CA'
66 | );
67 |
68 | // Adds a country restricted coupon.
69 | // This will apply because no validation runs if a session is not set.
70 | WC()->cart->apply_coupon( $coupon->get_code() );
71 |
72 | // Run the post checkout validation.
73 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
74 | $validation->validate_coupons_after_checkout( $posted );
75 |
76 | // Verifies 0 coupons are still in cart after checkout validation.
77 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
78 | }
79 |
80 | public function tear_down() {
81 | WC()->cart->empty_cart();
82 | WC()->cart->remove_coupons();
83 | $this->coupon->delete();
84 |
85 | parent::tear_down();
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutExistingCustomerCouponTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'customer_restriction_type', 'existing' );
20 | $coupon->save();
21 | $this->coupon = $coupon;
22 | }
23 |
24 | /**
25 | * Coupon will not apply because $posted data contains a new customer.
26 | */
27 | public function test_existing_customer_restriction_with_checkout_not_valid() {
28 | $coupon = $this->coupon;
29 |
30 | // Applies the coupon. This should apply since no session is set.
31 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
32 |
33 | // Mock the posted data.
34 | $posted = array(
35 | 'billing_email' => 'new@woo.com'
36 | );
37 |
38 | // Run the post checkout validation.
39 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
40 | $validation->validate_coupons_after_checkout( $posted );
41 |
42 | // Verifies 0 coupons are still in cart after checkout validation.
43 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
44 | }
45 |
46 | /**
47 | * Coupon will be valid because $posted data contains an existing customer.
48 | */
49 | public function test_existing_customer_restriction_checkout_not_valid() {
50 | // Create a customer.
51 | $customer = WC_Helper_Customer::create_customer();
52 |
53 | // Creates an order and applies it to new customer.
54 | $order = WC_Helper_Order::create_order( $customer->get_id() );
55 | $order->set_billing_email( $customer->get_email() );
56 | $order->set_status( 'completed' );
57 | $order->save();
58 |
59 | // Applies the coupon. This should apply since no session is set.
60 | // Creates a coupon.
61 | $coupon = $this->coupon;
62 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
63 |
64 | // Mock the posted data.
65 | $posted = array(
66 | 'billing_email' => $customer->get_email()
67 | );
68 |
69 | // Run the post checkout validation.
70 | // Coupon will be removed from cart because customer has previous purchases.
71 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
72 | $validation->validate_coupons_after_checkout( $posted );
73 |
74 | // Verifies 1 coupon has been applied to cart.
75 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
76 |
77 | $order->delete();
78 | }
79 |
80 |
81 | public function tear_down() {
82 | // Removes the coupons from the cart.
83 | WC()->cart->empty_cart();
84 | WC()->cart->remove_coupons();
85 |
86 | // Deletes the coupon.
87 | $this->coupon->delete();
88 |
89 | parent::tear_down();
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutLimitPerIPTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'usage_limit_per_ip_address', 1 );
25 | $coupon->save();
26 | $this->coupon = $coupon;
27 |
28 | // Create an order.
29 | $order = WC_Helper_Order::create_order();
30 | $order->set_status( 'processing' );
31 | $order->apply_coupon( $coupon );
32 | $order->calculate_totals();
33 | $this->order = $order;
34 |
35 | // Validation object.
36 | $this->validation = new WC_Coupon_Restrictions_Validation_Checkout();
37 |
38 | // Custom table.
39 | WC_Coupon_Restrictions_Table::maybe_create_table();
40 | }
41 |
42 | /**
43 | * Validate IP usage.
44 | */
45 | public function test_ip_limit() {
46 | $ip = '208.67.220.220';
47 | $coupon = $this->coupon;
48 | $order = $this->order;
49 | $order->set_customer_ip_address( $ip );
50 | $order->save();
51 |
52 | // Set IP address.
53 | $_SERVER['HTTP_X_REAL_IP'] = $ip;
54 |
55 | $posted = array(
56 | 'billing_email' => 'customer@test.com'
57 | );
58 |
59 | // Run the post checkout validation with new customer.
60 | WC()->cart->apply_coupon( $coupon->get_code() );
61 | $this->validation->validate_coupons_after_checkout( $posted );
62 |
63 | // Verifies coupon is still applied.
64 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
65 |
66 | // Mimic the hook that gets triggered once the order is created.
67 | do_action( 'woocommerce_pre_payment_complete', $order->get_id() );
68 |
69 | // Run the post checkout validation.
70 | WC()->cart->apply_coupon( $coupon->get_code() );
71 | $this->validation->validate_coupons_after_checkout( $posted );
72 |
73 | // Verifies coupon has been removed.
74 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
75 |
76 | // Update the coupon to permit 2 usages of same IP.
77 | $coupon->update_meta_data( 'usage_limit_per_ip_address', 2 );
78 | $coupon->save();
79 |
80 | // Run the post checkout validation.
81 | WC()->cart->apply_coupon( $coupon->get_code() );
82 | $this->validation->validate_coupons_after_checkout( $posted );
83 |
84 | // Verifies coupon is still applied.
85 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
86 |
87 | // Unset IP used for test.
88 | unset( $_SERVER['HTTP_X_REAL_IP'] );
89 | }
90 |
91 | public function tear_down() {
92 | $this->coupon->delete();
93 | $this->order->delete();
94 |
95 | // Deletes the custom table if it has been created.
96 | WC_Coupon_Restrictions_Table::delete_table();
97 |
98 | parent::tear_down();
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutLimitPerShippingAddressTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'usage_limit_per_shipping_address', 1 );
25 | $coupon->save();
26 | $this->coupon = $coupon;
27 |
28 | // Create an order.
29 | $order = WC_Helper_Order::create_order();
30 | $order->set_status( 'processing' );
31 | $order->apply_coupon( $coupon );
32 | $order->calculate_totals();
33 | $this->order = $order;
34 |
35 | // Validation object.
36 | $this->validation = new WC_Coupon_Restrictions_Validation_Checkout();
37 |
38 | // Custom table.
39 | WC_Coupon_Restrictions_Table::maybe_create_table();
40 | }
41 |
42 | /**
43 | * Validate per address.
44 | */
45 | public function test_usage_limit_per_shipping_address() {
46 | $address = [
47 | 'shipping_address_1' => '123 Main St',
48 | 'shipping_address_2' => 'Apt 1',
49 | 'shipping_city' => 'Test City',
50 | 'shipping_postcode' => '12345',
51 | ];
52 |
53 | $coupon = $this->coupon;
54 | $order = $this->order;
55 |
56 | $order->set_shipping_address_1( $address['shipping_address_1'] );
57 | $order->set_shipping_address_2( $address['shipping_address_2'] );
58 | $order->set_shipping_city( $address['shipping_city'] );
59 | $order->set_shipping_postcode( $address['shipping_postcode'] );
60 | $order->save();
61 |
62 | // Mock post data.
63 | $posted = $address;
64 |
65 | // Run the post checkout validation with new customer.
66 | WC()->cart->apply_coupon( $coupon->get_code() );
67 | $this->validation->validate_coupons_after_checkout( $posted );
68 |
69 | // Verifies coupon is still applied.
70 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
71 |
72 | // Mimic the hook that gets triggered once the order is created.
73 | do_action( 'woocommerce_pre_payment_complete', $order->get_id() );
74 |
75 | // Run the post checkout validation.
76 | WC()->cart->apply_coupon( $coupon->get_code() );
77 | $this->validation->validate_coupons_after_checkout( $posted );
78 |
79 | // Verifies coupon has been removed.
80 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
81 |
82 | // Update the coupon to permit 2 usages of same shipping address.
83 | $coupon->update_meta_data( 'usage_limit_per_shipping_address', 2 );
84 | $coupon->save();
85 |
86 | // Run the post checkout validation.
87 | WC()->cart->apply_coupon( $coupon->get_code() );
88 | $this->validation->validate_coupons_after_checkout( $posted );
89 |
90 | // Verifies coupon is still applied.
91 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
92 | }
93 |
94 | public function tear_down() {
95 | $this->coupon->delete();
96 | $this->order->delete();
97 |
98 | // Deletes the custom table if it has been created.
99 | WC_Coupon_Restrictions_Table::delete_table();
100 |
101 | parent::tear_down();
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutLimitSimilarEmailsTest.php:
--------------------------------------------------------------------------------
1 | set_usage_limit_per_user( 1 );
25 | $coupon->update_meta_data( 'prevent_similar_emails', 'yes' );
26 | $coupon->save();
27 | $this->coupon = $coupon;
28 |
29 | // Create an order.
30 | $order = WC_Helper_Order::create_order();
31 | $order->set_status( 'processing' );
32 | $order->apply_coupon( $coupon );
33 | $order->calculate_totals();
34 | $this->order = $order;
35 |
36 | // Validation object.
37 | $this->validation = new WC_Coupon_Restrictions_Validation_Checkout();
38 |
39 | // Create table.
40 | WC_Coupon_Restrictions_Table::maybe_create_table();
41 | }
42 |
43 | /**
44 | * Validate basic similar emails restriction.
45 | */
46 | public function test_email_usage_restriction() {
47 | $coupon = $this->coupon;
48 | $order = $this->order;
49 |
50 | $email = 'customer1@gmail.com';
51 | $order->set_billing_email( $email );
52 | $order->save();
53 |
54 | // Mock post data.
55 | $posted = array(
56 | 'billing_email' => $email,
57 | );
58 |
59 | // Run the post checkout validation with new customer.
60 | WC()->cart->apply_coupon( $coupon->get_code() );
61 | $this->validation->validate_coupons_after_checkout( $posted );
62 |
63 | // Verifies 1 coupon is still in cart after checkout validation.
64 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
65 |
66 | // Mimic the hook that gets triggered once the order is created.
67 | do_action( 'woocommerce_pre_payment_complete', $order->get_id() );
68 |
69 | // Run the post checkout validation.
70 | WC()->cart->apply_coupon( $coupon->get_code() );
71 | $this->validation->validate_coupons_after_checkout( $posted );
72 |
73 | // Verifies coupon has been removed.
74 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
75 | }
76 |
77 | /**
78 | * Validate similar emails restriction.
79 | *
80 | * @TODO This test runs fine on its own, but fails when running all tests in this class.
81 | * // i.e. phpunit --filter=CheckoutLimitSimilarEmailsTest
82 | * // Leaving this as an item to debug later.
83 | * // Prefix this method with 'test_' to add it back to the test suite.
84 | */
85 | public function similar_email_usage_restriction() {
86 | $coupon = $this->coupon;
87 | $order = $this->order;
88 |
89 | $email = 'customer2@gmail.com';
90 | $order->set_billing_email( $email );
91 | $order->save();
92 |
93 | // Mimic the hook that gets triggered once the order is created.
94 | do_action( 'woocommerce_pre_payment_complete', $order->get_id() );
95 |
96 | // Test a similar email (not exact match).
97 | $posted = array(
98 | 'billing_email' => 'customer2+test@gmail.com'
99 | );
100 |
101 | // Apply the coupon.
102 | WC()->cart->apply_coupon( $coupon->get_code() );
103 |
104 | // Verify the coupon is removed because it is a similar email.
105 | $this->validation->validate_coupons_after_checkout( $posted );
106 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
107 |
108 | // Update the usage limit to 2.
109 | $coupon->set_usage_limit_per_user( 2 );
110 | $coupon->save();
111 |
112 | // Run the post checkout validation.
113 | WC()->cart->apply_coupon( $coupon->get_code() );
114 | $this->validation->validate_coupons_after_checkout( $posted );
115 |
116 | // Verifies coupon now applies.
117 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
118 | }
119 |
120 | public function tear_down() {
121 | $this->coupon->delete();
122 | $this->order->delete();
123 |
124 | // Deletes the custom table if it has been created.
125 | WC_Coupon_Restrictions_Table::delete_table();
126 |
127 | parent::tear_down();
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutNewCustomerCouponTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'customer_restriction_type', 'new' );
20 | $coupon->save();
21 | $this->coupon = $coupon;
22 |
23 | }
24 |
25 | /**
26 | * Coupon will apply because $posted data contains a new customer.
27 | */
28 | public function test_new_customer_restriction_with_checkout_valid() {
29 | $coupon = $this->coupon;
30 |
31 | // Applies the coupon. This should apply since no session is set.
32 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
33 |
34 | // Mock the posted data.
35 | $posted = array(
36 | 'billing_email' => 'new@woo.com'
37 | );
38 |
39 | // Run the post checkout validation.
40 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
41 | $validation->validate_coupons_after_checkout( $posted );
42 |
43 | // Verifies 1 coupon is still in cart after checkout validation.
44 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
45 |
46 | }
47 |
48 | /**
49 | * Coupon will be removed because $posted data contains an existing customer.
50 | */
51 | public function test_new_customer_restriction_with_checkout_not_valid() {
52 | // Create a customer.
53 | $customer = WC_Helper_Customer::create_customer();
54 |
55 | // Creates an order and applies it to new customer.
56 | $order = WC_Helper_Order::create_order( $customer->get_id() );
57 | $order->set_billing_email( $customer->get_email() );
58 | $order->set_status( 'completed' );
59 | $order->save();
60 |
61 | // Applies the coupon. This should apply since no session is set.
62 | // Creates a coupon.
63 | $coupon = $this->coupon;
64 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
65 |
66 | // Mock the posted data.
67 | $posted = array(
68 | 'billing_email' => $customer->get_email()
69 | );
70 |
71 | // Run the post checkout validation.
72 | // Coupon will be removed from cart because customer has previous purchases.
73 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
74 | $validation->validate_coupons_after_checkout( $posted );
75 |
76 | // Verifies 0 coupons have been applied to cart.
77 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
78 | }
79 |
80 | /**
81 | * If customer has previous guest order and coupon_restrictions_customer_query
82 | * is set to 'accounts-orders', checkout should fail.
83 | */
84 | public function test_customer_has_previous_guest_order() {
85 | $coupon = $this->coupon;
86 |
87 | // Email to use for this test.
88 | $email = 'customer@woo.com';
89 |
90 | // Creates a new guest order.
91 | $order = WC_Helper_Order::create_order();
92 | $order->set_billing_email( $email );
93 | $order->set_status( 'completed' );
94 | $order->save();
95 |
96 | // Create a customer.
97 | WC_Helper_Customer::create_customer( 'customer', 'password', $email );
98 |
99 | // Adds a coupon restricted to new customers.
100 | // This should return true because customer doesn't have any purchases applied to their account.
101 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
102 |
103 | // Mock the posted data.
104 | $posted = array(
105 | 'billing_email' => $email
106 | );
107 |
108 | // Run the post checkout validation.
109 | // Coupon will not be removed because coupon_restrictions_customer_query
110 | // is set to 'acccounts' by default.
111 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
112 | $validation->validate_coupons_after_checkout( $posted );
113 |
114 | // Verifies 1 coupons have been applied to cart.
115 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
116 |
117 | update_option( 'coupon_restrictions_customer_query', 'accounts-orders' );
118 |
119 | // Run the post checkout validation now with
120 | // coupon_restrictions_customer_query set to 'accounts-orders'.
121 | // Coupon will be removed this time.
122 | $validation->validate_coupons_after_checkout( $posted );
123 |
124 | delete_option( 'coupon_restrictions_customer_query' );
125 | $order->delete();
126 | }
127 |
128 |
129 | public function tear_down() {
130 | WC()->cart->empty_cart();
131 | WC()->cart->remove_coupons();
132 | $this->coupon->delete();
133 |
134 | parent::tear_down();
135 | }
136 |
137 | }
138 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutPostcodeRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'location_restrictions', 'yes' );
22 | $coupon->update_meta_data( 'address_for_location_restrictions', 'billing' );
23 | $coupon->save();
24 |
25 | $this->coupon = $coupon;
26 |
27 | // Inits the checkout validation class
28 | $this->validation = new WC_Coupon_Restrictions_Validation_Checkout();
29 | }
30 |
31 | /**
32 | * Test checkout with single zipcode.
33 | */
34 | public function test_checkout_zipcode_restriction_valid() {
35 | $coupon = $this->coupon;
36 |
37 | // Apply postcode restriction to zipcode 78703.
38 | $coupon->update_meta_data( 'postcode_restriction', '78703' );
39 | $coupon->save();
40 |
41 | // Mock post data.
42 | $posted = array(
43 | 'billing_postcode' => '78703'
44 | );
45 |
46 | WC()->cart->apply_coupon( $coupon->get_code() );
47 |
48 | // Run the post checkout validation.
49 | $this->validation->validate_coupons_after_checkout( $posted );
50 |
51 | // Verifies 1 coupon is still in cart after checkout validation.
52 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
53 |
54 | $posted = array(
55 | 'billing_postcode' => '000000'
56 | );
57 |
58 | // Run the post checkout validation.
59 | $this->validation->validate_coupons_after_checkout( $posted );
60 |
61 | // Verifies 0 coupons in cart with invalid zipcode.
62 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
63 | }
64 |
65 | /**
66 | * Test checkout with multiple zipcodes (comma seperated).
67 | */
68 | public function test_checkout_multiple_zipcode_restriction_valid() {
69 | $coupon = $this->coupon;
70 |
71 | // Apply postcode restriction to zipcode 78703.
72 | $coupon->update_meta_data( 'postcode_restriction', '78702,78703,78704' );
73 | $coupon->save();
74 |
75 | // Mock post data.
76 | $posted = array(
77 | 'billing_postcode' => '78703'
78 | );
79 |
80 | WC()->cart->apply_coupon( $coupon->get_code() );
81 |
82 | // Run the post checkout validation.
83 | $this->validation->validate_coupons_after_checkout( $posted );
84 |
85 | // Verifies 1 coupon is still in cart after checkout validation.
86 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
87 | }
88 |
89 | /**
90 | * Test checkout with wildcard zipcode restriction.
91 | */
92 | public function test_checkout_wildcode_zipcode_restriction_valid() {
93 | $coupon = $this->coupon;
94 |
95 | // Apply postcode restriction to zipcode 787*.
96 | $coupon->update_meta_data( 'postcode_restriction', '787*' );
97 | $coupon->save();
98 |
99 | // Mock post data.
100 | $posted = array(
101 | 'billing_postcode' => '78703'
102 | );
103 |
104 | WC()->cart->apply_coupon( $coupon->get_code() );
105 |
106 | // Run the post checkout validation.
107 | $this->validation->validate_coupons_after_checkout( $posted );
108 |
109 | // Verifies 1 coupon is still in cart after checkout validation.
110 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
111 |
112 | $posted = array(
113 | 'billing_postcode' => '000000'
114 | );
115 |
116 | // Run the post checkout validation.
117 | $this->validation->validate_coupons_after_checkout( $posted );
118 |
119 | // Verifies 0 coupons in cart with invalid zipcode.
120 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
121 | }
122 |
123 | /**
124 | * Test checkout with multiple wildcard zipcode restriction.
125 | */
126 | public function test_checkout_multiple_wildcode_zipcode_restriction_invalid() {
127 | $coupon = $this->coupon;
128 |
129 | // Apply postcode restriction multiple wildcards.
130 | $coupon->update_meta_data( 'postcode_restriction', '787*,zip*' );
131 | $coupon->save();
132 |
133 | // Mock post data.
134 | $posted = array(
135 | 'billing_postcode' => 'ZIPCODE'
136 | );
137 |
138 | WC()->cart->apply_coupon( $coupon->get_code() );
139 |
140 | // Run the post checkout validation.
141 | $this->validation->validate_coupons_after_checkout( $posted );
142 |
143 | // Verifies 1 coupon is still in cart after checkout validation.
144 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
145 |
146 | $posted = array(
147 | 'billing_postcode' => '000000'
148 | );
149 |
150 | // Run the post checkout validation.
151 | $this->validation->validate_coupons_after_checkout( $posted );
152 |
153 | // Verifies 0 coupons in cart with invalid zipcode.
154 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
155 | }
156 |
157 | public function tear_down() {
158 | WC()->cart->empty_cart();
159 | WC()->cart->remove_coupons();
160 | $this->coupon->delete();
161 |
162 | parent::tear_down();
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutRoleRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | get_id(), 'role_restriction', ['administrator'] );
21 | $this->coupon = $coupon;
22 |
23 | // Creates a customer.
24 | $customer = WC_Helper_Customer::create_customer();
25 | $this->customer = $customer;
26 | }
27 |
28 | /**
29 | * Coupon will be removed because user role does not match restriction.
30 | */
31 | public function test_coupon_removed_if_role_restriction_invalid() {
32 | $coupon = $this->coupon;
33 | $customer = $this->customer;
34 |
35 | // Applies the coupon. This should apply since no session is set.
36 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
37 |
38 | // Set role that does not match coupon.
39 | $user = new \WP_User( $customer->get_id() );
40 | $user->set_role('subscriber');
41 |
42 | // Creates mock checkout data.
43 | $posted = array(
44 | 'billing_email' => $customer->get_email()
45 | );
46 |
47 | // Run the post checkout validation.
48 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
49 | $validation->validate_coupons_after_checkout( $posted );
50 |
51 | // Verifies coupon has been removed.
52 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
53 | }
54 |
55 | /**
56 | * Coupon will remain applied because user role matches restriction.
57 | */
58 | public function test_coupon_applies_if_role_restriction_valid() {
59 | $coupon = $this->coupon;
60 | $customer = $this->customer;
61 |
62 | // Applies the coupon. This should apply since no session is set.
63 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
64 |
65 | // Set role that does not match coupon.
66 | $user = new \WP_User( $customer->get_id() );
67 | $user->set_role('administrator');
68 |
69 | // Creates mock checkout data.
70 | $posted = array(
71 | 'billing_email' => $customer->get_email()
72 | );
73 |
74 | // Run the post checkout validation.
75 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
76 | $validation->validate_coupons_after_checkout( $posted );
77 |
78 | // Verifies coupon remains applied.
79 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
80 | }
81 |
82 | /**
83 | * Coupon will apply because customer is guest and guest role is permitted.
84 | */
85 | public function test_coupon_success_if_guest_and_guest_role_set() {
86 | // Creates a coupon.
87 | $coupon = WC_Helper_Coupon::create_coupon();
88 | $coupon->update_meta_data( 'role_restriction', ['woocommerce-coupon-restrictions-guest'] );
89 | $coupon->save();
90 |
91 | // Applies the coupon. This should apply since no session is set.
92 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
93 |
94 | // Creates mock checkout data.
95 | $posted = array(
96 | 'email' => 'guest@testing.dev'
97 | );
98 |
99 | // Run the post checkout validation.
100 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
101 | $validation->validate_coupons_after_checkout( $posted );
102 |
103 | // Verifies coupon remains applied.
104 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
105 | }
106 |
107 | /**
108 | * Coupon will not apply because customer is guest and guest role is not permitted.
109 | */
110 | public function test_coupon_fails_if_guest_and_guest_role_not_set() {
111 | // Get data from setup for coupon restricted to administrators (no guest role).
112 | $coupon = $this->coupon;
113 |
114 | // Applies the coupon. This should apply since no session is set.
115 | $this->assertTrue( WC()->cart->apply_coupon( $coupon->get_code() ) );
116 |
117 | // Creates mock checkout data.
118 | $posted = array(
119 | 'email' => 'guest@testing.dev'
120 | );
121 |
122 | // Run the post checkout validation.
123 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
124 | $validation->validate_coupons_after_checkout( $posted );
125 |
126 | // Verifies coupon removed because customer does not meet restriction.
127 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
128 | }
129 |
130 | public function tear_down() {
131 | WC()->cart->empty_cart();
132 | WC()->cart->remove_coupons();
133 | $this->coupon->delete();
134 | $this->customer->delete();
135 |
136 | parent::tear_down();
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/CheckoutStateRestrictionCouponTest.php:
--------------------------------------------------------------------------------
1 | update_meta_data( 'location_restrictions', 'yes' );
18 | $coupon->update_meta_data( 'address_for_location_restrictions', 'billing' );
19 | $coupon->save();
20 |
21 | $this->coupon = $coupon;
22 | }
23 |
24 | /**
25 | * Coupon is valid because coupon is restricted to TX,
26 | * and customer billing_state is TX.
27 | */
28 | public function test_checkout_state_restriction_with_valid_customer() {
29 | $coupon = $this->coupon;
30 |
31 | // Apply state restriction to single state "US"
32 | $coupon->update_meta_data( 'state_restriction', 'TX' );
33 | $coupon->save();
34 |
35 | // Mock post data.
36 | $posted = array(
37 | 'billing_state' => 'TX'
38 | );
39 |
40 | // Adds a state restricted coupon.
41 | // This will apply because no validation runs if a session is not set.
42 | WC()->cart->apply_coupon( $coupon->get_code() );
43 |
44 | // Run the post checkout validation.
45 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
46 | $validation->validate_coupons_after_checkout( $posted );
47 |
48 | // Verifies 1 coupon is still in cart after checkout validation.
49 | $this->assertEquals( 1, count( WC()->cart->get_applied_coupons() ) );
50 | }
51 |
52 | /**
53 | * Coupon is not valid because coupon is restricted to US,
54 | * and customer billing_state is CA.
55 | */
56 | public function test_checkout_state_restriction_with_not_valid_customer() {
57 | $coupon = $this->coupon;
58 |
59 | // Apply state restriction to single state "TX"
60 | $coupon->update_meta_data( 'state_restriction', 'TX' );
61 | $coupon->save();
62 |
63 | // Mock post data.
64 | $posted = array(
65 | 'billing_email' => 'customer@woo.com',
66 | 'billing_state' => 'CA'
67 | );
68 |
69 | // Adds a state restricted coupon.
70 | // This will apply because no validation runs if a session is not set.
71 | WC()->cart->apply_coupon( $coupon->get_code() );
72 |
73 | // Run the post checkout validation.
74 | $validation = new WC_Coupon_Restrictions_Validation_Checkout();
75 | $validation->validate_coupons_after_checkout( $posted );
76 |
77 | // Verifies 0 coupons are still in cart after checkout validation.
78 | $this->assertEquals( 0, count( WC()->cart->get_applied_coupons() ) );
79 | }
80 |
81 | public function tear_down() {
82 | WC()->cart->empty_cart();
83 | WC()->cart->remove_coupons();
84 | $this->coupon->delete();
85 |
86 | parent::tear_down();
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/OnboardingSetupTest.php:
--------------------------------------------------------------------------------
1 | assertEquals( $plugin->version, $option['version'] );
14 | }
15 |
16 | /**
17 | * Checks that onboarding transient is set.
18 | */
19 | public function test_onboarding_transient_set() {
20 | WC_Coupon_Restrictions();
21 | $transient = get_transient( 'woocommerce-coupon-restrictions-activated' );
22 | $this->assertEquals( 1, $transient );
23 | }
24 |
25 | /**
26 | * Checks upgrade routine from <= 1.6.2 to current.
27 | */
28 | public function test_upgrade_routine() {
29 | $option['version'] = '1.6.2';
30 | update_option( 'woocommerce-coupon-restrictions', $option );
31 |
32 | // This will kick off the upgrade routine.
33 | $plugin = WC_Coupon_Restrictions();
34 | $plugin->init();
35 |
36 | $query_type = get_option( 'coupon_restrictions_customer_query', 'account' );
37 | $this->assertEquals( $query_type, 'accounts-orders' );
38 |
39 | delete_option( 'woocommerce-coupon-restrictions' );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/RestrictionsTableTest.php:
--------------------------------------------------------------------------------
1 | assertFalse( WC_Coupon_Restrictions_Table::table_exists() );
19 |
20 | // Create table.
21 | WC_Coupon_Restrictions_Table::maybe_create_table();
22 |
23 | // Table should exist now.
24 | $this->assertTrue( WC_Coupon_Restrictions_Table::table_exists() );
25 | }
26 |
27 | /**
28 | * Test that format_address is correct.
29 | */
30 | public function test_format_address() {
31 | $address = [
32 | 'address_1' => '123 Main St',
33 | 'address_2' => 'Apt 1',
34 | 'city' => 'Test City',
35 | 'postcode' => '12345',
36 | ];
37 | $formatted_address = WC_Coupon_Restrictions_Table::format_address(
38 | $address['address_1'],
39 | $address['address_2'],
40 | $address['city'],
41 | $address['postcode']
42 | );
43 | $this->assertEquals( $formatted_address, '123MAINSTAPT1TESTCITY12345' );
44 | }
45 |
46 | /**
47 | * Test that get_scrubbed_email is correct.
48 | */
49 | public function test_get_scrubbed_email() {
50 | // Same email address.
51 | $email1 = 'customer@example.com';
52 | $email1scrubbed = WC_Coupon_Restrictions_Table::get_scrubbed_email( 'customer@example.com' );
53 | $this->assertEquals( $email1, $email1scrubbed );
54 |
55 | // Different capitalization in submitted address.
56 | $email2 = 'customer@example.com';
57 | $email2scrubbed = WC_Coupon_Restrictions_Table::get_scrubbed_email( 'Customer@Example.com' );
58 | $this->assertEquals( $email2, $email2scrubbed );
59 |
60 | // Test alias.
61 | $email3 = 'customer@example.com';
62 | $email3scrubbed = WC_Coupon_Restrictions_Table::get_scrubbed_email( 'customer+test@example.com' );
63 | $this->assertEquals( $email3, $email3scrubbed );
64 |
65 | // Test periods in email.
66 | $email3 = 'firstlast@gmail.com';
67 | $email3scrubbed = WC_Coupon_Restrictions_Table::get_scrubbed_email( 'first.last@gmail.com' );
68 | $this->assertEquals( $email3, $email3scrubbed );
69 | }
70 |
71 | public function tear_down() {
72 | // Deletes the custom table if it has been created.
73 | WC_Coupon_Restrictions_Table::delete_table();
74 |
75 | parent::tear_down();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ReturningCustomerTest.php:
--------------------------------------------------------------------------------
1 | customer = $customer;
22 |
23 | // Creates an order and applies it to new customer.
24 | $order = WC_Helper_Order::create_order( $customer->get_id() );
25 | $order->set_billing_email( $customer->get_email() );
26 | $order->set_status( 'completed' );
27 | $order->save();
28 | $this->order = $order;
29 | }
30 |
31 | /**
32 | * Test returning customer function.
33 | */
34 | public function test_is_returning_customer() {
35 | // Test should return false because customer hasn't purchased.
36 | $this->assertFalse( WC_Coupon_Restrictions_Validation::is_returning_customer( 'not@woo.com' ) );
37 |
38 | // Get data from setup.
39 | $customer = $this->customer;
40 | wp_set_current_user( $customer->get_id() );
41 |
42 | // Test should return true because customer has a completed order.
43 | $this->assertTrue( WC_Coupon_Restrictions_Validation::is_returning_customer( $customer->get_email() ) );
44 | }
45 |
46 | public function tear_down() {
47 | $this->order->delete();
48 | $this->customer->delete();
49 |
50 | parent::tear_down();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/phpunit/Integration/ValidationsTest.php:
--------------------------------------------------------------------------------
1 | assertFalse( $result );
20 |
21 | $coupon->update_meta_data( 'prevent_similar_emails', 'yes' );
22 | $coupon->save();
23 |
24 | // Test with one enhanced usage restriction.
25 | $result = WC_Coupon_Restrictions_Validation::has_enhanced_usage_restrictions( $coupon );
26 | $this->assertTrue( $result );
27 | }
28 |
29 | public function tear_down() {
30 | // Deletes the custom table if it has been created.
31 | WC_Coupon_Restrictions_Table::delete_table();
32 |
33 | parent::tear_down();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/phpunit/Unit/ValidationTest.php:
--------------------------------------------------------------------------------
1 | assertEquals( $result, ['TEST','TEST2','TEST3'] );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/phpunit/bootstrap.php:
--------------------------------------------------------------------------------
1 | $coupon_code,
25 | 'post_type' => 'shop_coupon',
26 | 'post_status' => 'publish',
27 | 'post_excerpt' => 'This is a dummy coupon',
28 | )
29 | );
30 |
31 | $meta = wp_parse_args(
32 | $meta,
33 | array(
34 | 'discount_type' => 'fixed_cart',
35 | 'coupon_amount' => '1',
36 | 'individual_use' => 'no',
37 | 'product_ids' => '',
38 | 'exclude_product_ids' => '',
39 | 'usage_limit' => '',
40 | 'usage_limit_per_user' => '',
41 | 'limit_usage_to_x_items' => '',
42 | 'expiry_date' => '',
43 | 'free_shipping' => 'no',
44 | 'exclude_sale_items' => 'no',
45 | 'product_categories' => array(),
46 | 'exclude_product_categories' => array(),
47 | 'minimum_amount' => '',
48 | 'maximum_amount' => '',
49 | 'customer_email' => array(),
50 | 'usage_count' => '0',
51 | )
52 | );
53 |
54 | // Update meta.
55 | foreach ( $meta as $key => $value ) {
56 | update_post_meta( $coupon_id, $key, $value );
57 | }
58 |
59 | return new WC_Coupon( $coupon_code );
60 | }
61 |
62 | /**
63 | * Delete a coupon.
64 | *
65 | * @param $coupon_id
66 | *
67 | * @return bool
68 | */
69 | public static function delete_coupon( $coupon_id ) {
70 | wp_delete_post( $coupon_id, true );
71 |
72 | return true;
73 | }
74 |
75 | /**
76 | * Register a custom coupon type.
77 | *
78 | * @param string $coupon_type
79 | */
80 | public static function register_custom_type( $coupon_type ) {
81 | static $filters_added = false;
82 | if ( isset( self::$custom_types[ $coupon_type ] ) ) {
83 | return;
84 | }
85 |
86 | self::$custom_types[ $coupon_type ] = "Testing custom type {$coupon_type}";
87 |
88 | if ( ! $filters_added ) {
89 | add_filter( 'woocommerce_coupon_discount_types', array( __CLASS__, 'filter_discount_types' ) );
90 | add_filter( 'woocommerce_coupon_get_discount_amount', array( __CLASS__, 'filter_get_discount_amount' ), 10, 5 );
91 | add_filter( 'woocommerce_coupon_is_valid_for_product', '__return_true' );
92 | $filters_added = true;
93 | }
94 | }
95 |
96 | /**
97 | * Unregister custom coupon type.
98 | *
99 | * @param $coupon_type
100 | */
101 | public static function unregister_custom_type( $coupon_type ) {
102 | unset( self::$custom_types[ $coupon_type ] );
103 | if ( empty( self::$custom_types ) ) {
104 | remove_filter( 'woocommerce_coupon_discount_types', array( __CLASS__, 'filter_discount_types' ) );
105 | remove_filter( 'woocommerce_coupon_get_discount_amount', array( __CLASS__, 'filter_get_discount_amount' ) );
106 | remove_filter( 'woocommerce_coupon_is_valid_for_product', '__return_true' );
107 | }
108 | }
109 |
110 | /**
111 | * Register custom discount types.
112 | *
113 | * @param array $discount_types
114 | * @return array
115 | */
116 | public static function filter_discount_types( $discount_types ) {
117 | return array_merge( $discount_types, self::$custom_types );
118 | }
119 |
120 | /**
121 | * Get custom discount type amount. Works like 'percent' type.
122 | *
123 | * @param float $discount
124 | * @param float $discounting_amount
125 | * @param array|null $item
126 | * @param bool $single
127 | * @param WC_Coupon $coupon
128 | *
129 | * @return float
130 | */
131 | public static function filter_get_discount_amount( $discount, $discounting_amount, $item, $single, $coupon ) {
132 | if ( ! isset( self::$custom_types [ $coupon->get_discount_type() ] ) ) {
133 | return $discount;
134 | }
135 |
136 | return (float) $coupon->get_amount() * ( $discounting_amount / 100 );
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/tests/phpunit/helpers/class-wc-helper-customer.php:
--------------------------------------------------------------------------------
1 | 0,
18 | 'date_modified' => null,
19 | 'country' => 'US',
20 | 'state' => 'CA',
21 | 'postcode' => '94110',
22 | 'city' => 'San Francisco',
23 | 'address' => '123 South Street',
24 | 'address_2' => 'Apt 1',
25 | 'shipping_country' => 'US',
26 | 'shipping_state' => 'CA',
27 | 'shipping_postcode' => '94110',
28 | 'shipping_city' => 'San Francisco',
29 | 'shipping_address' => '123 South Street',
30 | 'shipping_address_2' => 'Apt 1',
31 | 'is_vat_exempt' => false,
32 | 'calculated_shipping' => false,
33 | );
34 |
35 | self::set_customer_details( $customer_data );
36 |
37 | $customer = new WC_Customer( 0, true );
38 |
39 | return $customer;
40 | }
41 |
42 | /**
43 | * Creates a customer in the tests DB.
44 | */
45 | public static function create_customer( $username = 'testcustomer', $password = 'hunter2', $email = 'test@woo.local' ) {
46 | $customer = new WC_Customer();
47 | $customer->set_billing_country( 'US' );
48 | $customer->set_first_name( 'Justin' );
49 | $customer->set_billing_state( 'CA' );
50 | $customer->set_billing_postcode( '94110' );
51 | $customer->set_billing_city( 'San Francisco' );
52 | $customer->set_billing_address( '123 South Street' );
53 | $customer->set_billing_address_2( 'Apt 1' );
54 | $customer->set_shipping_country( 'US' );
55 | $customer->set_shipping_state( 'CA' );
56 | $customer->set_shipping_postcode( '94110' );
57 | $customer->set_shipping_city( 'San Francisco' );
58 | $customer->set_shipping_address( '123 South Street' );
59 | $customer->set_shipping_address_2( 'Apt 1' );
60 | $customer->set_username( $username );
61 | $customer->set_password( $password );
62 | $customer->set_email( $email );
63 | $customer->save();
64 | return $customer;
65 | }
66 |
67 | /**
68 | * Get the expected output for the store's base location settings.
69 | *
70 | * @return array
71 | */
72 | public static function get_expected_store_location() {
73 | return array( 'US', 'CA', '', '' );
74 | }
75 |
76 | /**
77 | * Get the customer's shipping and billing info from the session.
78 | *
79 | * @return array
80 | */
81 | public static function get_customer_details() {
82 | return WC()->session->get( 'customer' );
83 | }
84 |
85 | /**
86 | * Get the user's chosen shipping method.
87 | *
88 | * @return array
89 | */
90 | public static function get_chosen_shipping_methods() {
91 | return WC()->session->get( 'chosen_shipping_methods' );
92 | }
93 |
94 | /**
95 | * Get the "Tax Based On" WooCommerce option.
96 | *
97 | * @return string base or billing
98 | */
99 | public static function get_tax_based_on() {
100 | return get_option( 'woocommerce_tax_based_on' );
101 | }
102 |
103 | /**
104 | * Set the current customer's billing details in the session.
105 | *
106 | * @param string $default_shipping_method Shipping Method slug
107 | */
108 | public static function set_customer_details( $customer_details ) {
109 | WC()->session->set( 'customer', array_map( 'strval', $customer_details ) );
110 | }
111 |
112 | /**
113 | * Set the user's chosen shipping method.
114 | *
115 | * @param string $chosen_shipping_method Shipping Method slug
116 | */
117 | public static function set_chosen_shipping_methods( $chosen_shipping_methods ) {
118 | WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
119 | }
120 |
121 | /**
122 | * Set the "Tax Based On" WooCommerce option.
123 | *
124 | * @param string $default_shipping_method Shipping Method slug
125 | */
126 | public static function set_tax_based_on( $default_shipping_method ) {
127 | update_option( 'woocommerce_tax_based_on', $default_shipping_method );
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/tests/phpunit/helpers/class-wc-helper-order.php:
--------------------------------------------------------------------------------
1 | get_items() as $item ) {
26 | WC_Helper_Product::delete_product( $item['product_id'] );
27 | }
28 |
29 | WC_Helper_Shipping::delete_simple_flat_rate();
30 |
31 | // Delete the order post.
32 | $order->delete( true );
33 | }
34 |
35 | /**
36 | * Create a order.
37 | *
38 | * @since 2.4
39 | * @version 3.0 New parameter $product.
40 | *
41 | * @param int $customer_id The ID of the customer the order is for.
42 | * @param WC_Product $product The product to add to the order.
43 | * @param array $order_props Order properties.
44 | *
45 | * @return WC_Order
46 | */
47 | public static function create_order( $customer_id = 1, $product = null, $order_props = [] ) {
48 |
49 | if ( ! is_a( $product, 'WC_Product' ) ) {
50 | $product = WC_Helper_Product::create_simple_product();
51 | }
52 |
53 | WC_Helper_Shipping::create_simple_flat_rate();
54 |
55 | $order_data = [
56 | 'status' => 'pending',
57 | 'customer_id' => $customer_id,
58 | 'customer_note' => '',
59 | 'total' => '',
60 | ];
61 |
62 | $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; // Required, else wc_create_order throws an exception.
63 | $order = wc_create_order( $order_data );
64 |
65 | // Add order products.
66 | $item = new WC_Order_Item_Product();
67 | $item->set_props(
68 | [
69 | 'product' => $product,
70 | 'quantity' => 4,
71 | 'subtotal' => wc_get_price_excluding_tax( $product, [ 'qty' => 4 ] ),
72 | 'total' => wc_get_price_excluding_tax( $product, [ 'qty' => 4 ] ),
73 | ]
74 | );
75 | $item->save();
76 | $order->add_item( $item );
77 |
78 | // Set billing address.
79 | $order->set_billing_first_name( 'Jeroen' );
80 | $order->set_billing_last_name( 'Sormani' );
81 | $order->set_billing_company( 'WooCompany' );
82 | $order->set_billing_address_1( 'WooAddress' );
83 | $order->set_billing_address_2( '' );
84 | $order->set_billing_city( 'WooCity' );
85 | $order->set_billing_state( 'NY' );
86 | $order->set_billing_postcode( '12345' );
87 | $order->set_billing_country( 'US' );
88 | $order->set_billing_email( 'admin@example.org' );
89 | $order->set_billing_phone( '555-32123' );
90 |
91 | // Add shipping costs.
92 | $shipping_taxes = WC_Tax::calc_shipping_tax( '10', WC_Tax::get_shipping_tax_rates() );
93 | $rate = new WC_Shipping_Rate( 'flat_rate_shipping', 'Flat rate shipping', '10', $shipping_taxes, 'flat_rate' );
94 | $item = new WC_Order_Item_Shipping();
95 | $item->set_props(
96 | [
97 | 'method_title' => $rate->label,
98 | 'method_id' => $rate->id,
99 | 'total' => wc_format_decimal( $rate->cost ),
100 | 'taxes' => $rate->taxes,
101 | ]
102 | );
103 | foreach ( $rate->get_meta_data() as $key => $value ) {
104 | $item->add_meta_data( $key, $value, true );
105 | }
106 | $order->add_item( $item );
107 |
108 | // Set payment gateway.
109 | $payment_gateways = WC()->payment_gateways->payment_gateways();
110 | $order->set_payment_method( $payment_gateways['bacs'] );
111 |
112 | // Set totals.
113 | $order->set_shipping_total( 10 );
114 | $order->set_discount_total( 0 );
115 | $order->set_discount_tax( 0 );
116 | $order->set_cart_tax( 0 );
117 | $order->set_shipping_tax( 0 );
118 | $order->set_total( 50 ); // 4 x $10 simple helper product
119 |
120 | // Additional order properties.
121 | foreach ( $order_props as $key => $value ) {
122 | $order->{"set_$key"}( $value );
123 | }
124 |
125 | $order->save();
126 |
127 | return $order;
128 | }
129 |
130 | /**
131 | * Helper function to create order with fees and shipping objects.
132 | *
133 | * @param int $customer_id The ID of the customer the order is for.
134 | * @param WC_Product $product The product to add to the order.
135 | *
136 | * @return WC_Order
137 | */
138 | public static function create_order_with_fees_and_shipping( $customer_id = 1, $product = null ) {
139 | $order = self::create_order( $customer_id, $product );
140 |
141 | $fee_item = new WC_Order_Item_Fee();
142 | $fee_item->set_order_id( $order->get_id() );
143 | $fee_item->set_name( 'Testing fees' );
144 | $fee_item->set_total( 100 );
145 |
146 | $shipping_item = new WC_Order_Item_Shipping();
147 | $shipping_item->set_order_id( $order->get_id() );
148 | $shipping_item->set_name( 'Flat shipping' );
149 | $shipping_item->set_total( 25 );
150 |
151 | $order->add_item( $fee_item );
152 | $order->add_item( $shipping_item );
153 | $order->save();
154 | $order->calculate_totals( true );
155 |
156 | return $order;
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/tests/phpunit/helpers/class-wc-helper-product.php:
--------------------------------------------------------------------------------
1 | delete( true );
24 | }
25 | }
26 |
27 | /**
28 | * Create simple product.
29 | *
30 | * @since 2.3
31 | * @param bool $save Save or return object.
32 | * @param array $props Properties to be set in the new product, as an associative array.
33 | * @return WC_Product_Simple
34 | */
35 | public static function create_simple_product( $save = true, $props = [] ) {
36 | $product = new WC_Product_Simple();
37 | $default_props =
38 | [
39 | 'name' => 'Dummy Product',
40 | 'regular_price' => 10,
41 | 'price' => 10,
42 | 'sku' => 'DUMMY SKU',
43 | 'manage_stock' => false,
44 | 'tax_status' => 'taxable',
45 | 'downloadable' => false,
46 | 'virtual' => false,
47 | 'stock_status' => 'instock',
48 | 'weight' => '1.1',
49 | ];
50 |
51 | $product->set_props( array_merge( $default_props, $props ) );
52 |
53 | if ( $save ) {
54 | $product->save();
55 | return wc_get_product( $product->get_id() );
56 | } else {
57 | return $product;
58 | }
59 | }
60 |
61 | /**
62 | * Create external product.
63 | *
64 | * @since 3.0.0
65 | * @return WC_Product_External
66 | */
67 | public static function create_external_product() {
68 | $product = new WC_Product_External();
69 | $product->set_props(
70 | [
71 | 'name' => 'Dummy External Product',
72 | 'regular_price' => 10,
73 | 'sku' => 'DUMMY EXTERNAL SKU',
74 | 'product_url' => 'http://woocommerce.com',
75 | 'button_text' => 'Buy external product',
76 | ]
77 | );
78 | $product->save();
79 |
80 | return wc_get_product( $product->get_id() );
81 | }
82 |
83 | /**
84 | * Create grouped product.
85 | *
86 | * @since 3.0.0
87 | * @return WC_Product_Grouped
88 | */
89 | public static function create_grouped_product() {
90 | $simple_product_1 = self::create_simple_product();
91 | $simple_product_2 = self::create_simple_product();
92 | $product = new WC_Product_Grouped();
93 | $product->set_props(
94 | [
95 | 'name' => 'Dummy Grouped Product',
96 | 'sku' => 'DUMMY GROUPED SKU',
97 | ]
98 | );
99 | $product->set_children( [ $simple_product_1->get_id(), $simple_product_2->get_id() ] );
100 | $product->save();
101 |
102 | return wc_get_product( $product->get_id() );
103 | }
104 |
105 | /**
106 | * Create a dummy variation product or configure an existing product object with dummy data.
107 | *
108 | *
109 | * @since 2.3
110 | * @param WC_Product_Variable|null $product Product object to configure, or null to create a new one.
111 | * @return WC_Product_Variable
112 | */
113 | public static function create_variation_product( $product = null ) {
114 | $is_new_product = is_null( $product );
115 | if ( $is_new_product ) {
116 | $product = new WC_Product_Variable();
117 | }
118 |
119 | $product->set_props(
120 | [
121 | 'name' => 'Dummy Variable Product',
122 | 'sku' => 'DUMMY VARIABLE SKU',
123 | ]
124 | );
125 |
126 | $attributes = [];
127 |
128 | $attributes[] = self::create_product_attribute_object( 'size', [ 'small', 'large', 'huge' ] );
129 | $attributes[] = self::create_product_attribute_object( 'colour', [ 'red', 'blue' ] );
130 | $attributes[] = self::create_product_attribute_object( 'number', [ '0', '1', '2' ] );
131 |
132 | $product->set_attributes( $attributes );
133 | $product->save();
134 |
135 | $variations = [];
136 |
137 | $variations[] = self::create_product_variation_object(
138 | $product->get_id(),
139 | 'DUMMY SKU VARIABLE SMALL',
140 | 10,
141 | [ 'pa_size' => 'small' ]
142 | );
143 |
144 | $variations[] = self::create_product_variation_object(
145 | $product->get_id(),
146 | 'DUMMY SKU VARIABLE LARGE',
147 | 15,
148 | [ 'pa_size' => 'large' ]
149 | );
150 |
151 | $variations[] = self::create_product_variation_object(
152 | $product->get_id(),
153 | 'DUMMY SKU VARIABLE HUGE RED 0',
154 | 16,
155 | [
156 | 'pa_size' => 'huge',
157 | 'pa_colour' => 'red',
158 | 'pa_number' => '0',
159 | ]
160 | );
161 |
162 | $variations[] = self::create_product_variation_object(
163 | $product->get_id(),
164 | 'DUMMY SKU VARIABLE HUGE RED 2',
165 | 17,
166 | [
167 | 'pa_size' => 'huge',
168 | 'pa_colour' => 'red',
169 | 'pa_number' => '2',
170 | ]
171 | );
172 |
173 | $variations[] = self::create_product_variation_object(
174 | $product->get_id(),
175 | 'DUMMY SKU VARIABLE HUGE BLUE 2',
176 | 18,
177 | [
178 | 'pa_size' => 'huge',
179 | 'pa_colour' => 'blue',
180 | 'pa_number' => '2',
181 | ]
182 | );
183 |
184 | $variations[] = self::create_product_variation_object(
185 | $product->get_id(),
186 | 'DUMMY SKU VARIABLE HUGE BLUE ANY NUMBER',
187 | 19,
188 | [
189 | 'pa_size' => 'huge',
190 | 'pa_colour' => 'blue',
191 | 'pa_number' => '',
192 | ]
193 | );
194 |
195 | if ( $is_new_product ) {
196 | return wc_get_product( $product->get_id() );
197 | }
198 |
199 | $variation_ids = array_map(
200 | function( $variation ) {
201 | return $variation->get_id();
202 | },
203 | $variations
204 | );
205 | $product->set_children( $variation_ids );
206 | return $product;
207 | }
208 |
209 | /**
210 | * Creates an instance of WC_Product_Variation with the supplied parameters, optionally persisting it to the database.
211 | *
212 | * @param string $parent_id Parent product id.
213 | * @param string $sku SKU for the variation.
214 | * @param int $price Price of the variation.
215 | * @param array $attributes Attributes that define the variation, e.g. ['pa_color'=>'red'].
216 | * @param bool $save If true, the object will be saved to the database after being created and configured.
217 | *
218 | * @return WC_Product_Variation The created object.
219 | */
220 | public static function create_product_variation_object( $parent_id, $sku, $price, $attributes, $save = true ) {
221 | $variation = new WC_Product_Variation();
222 | $variation->set_props(
223 | [
224 | 'parent_id' => $parent_id,
225 | 'sku' => $sku,
226 | 'regular_price' => $price,
227 | ]
228 | );
229 | $variation->set_attributes( $attributes );
230 | if ( $save ) {
231 | $variation->save();
232 | }
233 | return $variation;
234 | }
235 |
236 | /**
237 | * Creates an instance of WC_Product_Attribute with the supplied parameters.
238 | *
239 | * @param string $raw_name Attribute raw name (without 'pa_' prefix).
240 | * @param array $terms Possible values for the attribute.
241 | *
242 | * @return WC_Product_Attribute The created attribute object.
243 | */
244 | public static function create_product_attribute_object( $raw_name = 'size', $terms = [ 'small' ] ) {
245 | $attribute = new WC_Product_Attribute();
246 | $attribute_data = self::create_attribute( $raw_name, $terms );
247 | $attribute->set_id( $attribute_data['attribute_id'] );
248 | $attribute->set_name( $attribute_data['attribute_taxonomy'] );
249 | $attribute->set_options( $attribute_data['term_ids'] );
250 | $attribute->set_position( 1 );
251 | $attribute->set_visible( true );
252 | $attribute->set_variation( true );
253 | return $attribute;
254 | }
255 |
256 | /**
257 | * Create a dummy attribute.
258 | *
259 | * @since 2.3
260 | *
261 | * @param string $raw_name Name of attribute to create.
262 | * @param array(string) $terms Terms to create for the attribute.
263 | * @return array
264 | */
265 | public static function create_attribute( $raw_name = 'size', $terms = [ 'small' ] ) {
266 | global $wpdb, $wc_product_attributes;
267 |
268 | // Make sure caches are clean.
269 | delete_transient( 'wc_attribute_taxonomies' );
270 | if ( is_callable( [ 'WC_Cache_Helper', 'invalidate_cache_group' ] ) ) {
271 | WC_Cache_Helper::invalidate_cache_group( 'woocommerce-attributes' );
272 | }
273 |
274 | // These are exported as labels, so convert the label to a name if possible first.
275 | $attribute_labels = wp_list_pluck( wc_get_attribute_taxonomies(), 'attribute_label', 'attribute_name' );
276 | $attribute_name = array_search( $raw_name, $attribute_labels, true );
277 |
278 | if ( ! $attribute_name ) {
279 | $attribute_name = wc_sanitize_taxonomy_name( $raw_name );
280 | }
281 |
282 | $attribute_id = wc_attribute_taxonomy_id_by_name( $attribute_name );
283 |
284 | if ( ! $attribute_id ) {
285 | $taxonomy_name = wc_attribute_taxonomy_name( $attribute_name );
286 |
287 | // Degister taxonomy which other tests may have created...
288 | unregister_taxonomy( $taxonomy_name );
289 |
290 | $attribute_id = wc_create_attribute(
291 | [
292 | 'name' => $raw_name,
293 | 'slug' => $attribute_name,
294 | 'type' => 'select',
295 | 'order_by' => 'menu_order',
296 | 'has_archives' => 0,
297 | ]
298 | );
299 |
300 | // Register as taxonomy.
301 | register_taxonomy(
302 | $taxonomy_name,
303 | apply_filters( 'woocommerce_taxonomy_objects_' . $taxonomy_name, [ 'product' ] ),
304 | apply_filters(
305 | 'woocommerce_taxonomy_args_' . $taxonomy_name,
306 | [
307 | 'labels' => [
308 | 'name' => $raw_name,
309 | ],
310 | 'hierarchical' => false,
311 | 'show_ui' => false,
312 | 'query_var' => true,
313 | 'rewrite' => false,
314 | ]
315 | )
316 | );
317 |
318 | // Set product attributes global.
319 | $wc_product_attributes = [];
320 |
321 | foreach ( wc_get_attribute_taxonomies() as $taxonomy ) {
322 | $wc_product_attributes[ wc_attribute_taxonomy_name( $taxonomy->attribute_name ) ] = $taxonomy;
323 | }
324 | }
325 |
326 | $attribute = wc_get_attribute( $attribute_id );
327 | $return = [
328 | 'attribute_name' => $attribute->name,
329 | 'attribute_taxonomy' => $attribute->slug,
330 | 'attribute_id' => $attribute_id,
331 | 'term_ids' => [],
332 | ];
333 |
334 | foreach ( $terms as $term ) {
335 | $result = term_exists( $term, $attribute->slug );
336 |
337 | if ( ! $result ) {
338 | $result = wp_insert_term( $term, $attribute->slug );
339 | $return['term_ids'][] = $result['term_id'];
340 | } else {
341 | $return['term_ids'][] = $result['term_id'];
342 | }
343 | }
344 |
345 | return $return;
346 | }
347 |
348 | /**
349 | * Creates a new product review on a specific product.
350 | *
351 | * @since 3.0
352 | * @param int $product_id integer Product ID that the review is for.
353 | * @param string $review_content string Content to use for the product review.
354 | * @return integer Product Review ID.
355 | */
356 | public static function create_product_review( $product_id, $review_content = 'Review content here' ) {
357 | $data = [
358 | 'comment_post_ID' => $product_id,
359 | 'comment_author' => 'admin',
360 | 'comment_author_email' => 'woo@woo.local',
361 | 'comment_author_url' => '',
362 | 'comment_date' => '2016-01-01T11:11:11',
363 | 'comment_content' => $review_content,
364 | 'comment_approved' => 1,
365 | 'comment_type' => 'review',
366 | ];
367 | return wp_insert_comment( $data );
368 | }
369 | }
370 |
--------------------------------------------------------------------------------
/tests/phpunit/helpers/class-wc-helper-shipping.php:
--------------------------------------------------------------------------------
1 | 'yes',
25 | 'title' => 'Flat rate',
26 | 'availability' => 'all',
27 | 'countries' => '',
28 | 'tax_status' => 'taxable',
29 | 'cost' => $cost,
30 | ];
31 |
32 | update_option( 'woocommerce_flat_rate_settings', $flat_rate_settings );
33 | update_option( 'woocommerce_flat_rate', [] );
34 | WC_Cache_Helper::get_transient_version( 'shipping', true );
35 | WC()->shipping()->load_shipping_methods();
36 | }
37 |
38 | /**
39 | * Helper function to set customer address so that shipping can be calculated.
40 | */
41 | public static function force_customer_us_address() {
42 | add_filter( 'woocommerce_customer_get_shipping_country', [ self::class, 'force_customer_us_country' ] );
43 | add_filter( 'woocommerce_customer_get_shipping_state', [ self::class, 'force_customer_us_state' ] );
44 | add_filter( 'woocommerce_customer_get_shipping_postcode', [ self::class, 'force_customer_us_postcode' ] );
45 | }
46 |
47 | /**
48 | * Helper that can be hooked to a filter to force the customer's shipping state to be NY.
49 | *
50 | * @since 4.4.0
51 | * @param string $state State code.
52 | * @return string
53 | */
54 | public static function force_customer_us_state( $state ) {
55 | return 'NY';
56 | }
57 |
58 | /**
59 | * Helper that can be hooked to a filter to force the customer's shipping country to be US.
60 | *
61 | * @since 4.4.0
62 | * @param string $country Country code.
63 | * @return string
64 | */
65 | public static function force_customer_us_country( $country ) {
66 | return 'US';
67 | }
68 |
69 | /**
70 | * Helper that can be hooked to a filter to force the customer's shipping postal code to be 12345.
71 | *
72 | * @since 4.4.0
73 | * @param string $postcode Postal code.
74 | * @return string
75 | */
76 | public static function force_customer_us_postcode( $postcode ) {
77 | return '12345';
78 | }
79 |
80 | /**
81 | * Delete the simple flat rate.
82 | *
83 | * @since 2.3
84 | */
85 | public static function delete_simple_flat_rate() {
86 | delete_option( 'woocommerce_flat_rate_settings' );
87 | delete_option( 'woocommerce_flat_rate' );
88 | WC_Cache_Helper::get_transient_version( 'shipping', true );
89 | WC()->shipping()->unregister_shipping_methods();
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/readme.md:
--------------------------------------------------------------------------------
1 | Running tests requires composer, wp-cli and phpunit to be installed.
2 |
3 | # About the tests
4 |
5 | You can install the testing directory anywhere on your system. You don't need to set up a site, though that can be helpful if you're making changes to the plugin and want to test actual functionality.
6 |
7 | If you already have a working WordPress install and just want to install the plugin and unit tests, skip to step #2.
8 |
9 | ### 1. Create a WordPress install
10 |
11 | ```
12 | mkdir woocommerce
13 | cd woocommerce
14 | wp core download
15 | ```
16 |
17 | ### 2. Install the plugin files
18 |
19 | Run this from the base directory of your WordPress install.
20 |
21 | ```
22 | git clone https://github.com/devinsays/woocommerce-coupon-restrictions wp-content/plugins/woocommerce-coupon-restrictions
23 | cd wp-content/plugins/woocommerce-coupon-restrictions
24 | ```
25 |
26 | ### 3. Create a database for running tests and install core WordPress unit tests
27 |
28 | You'll be creating a new database to runs the tests. The contents of the database will be deleted after each run. Use your local database username/password to set up the database.
29 |
30 | ```
31 | bash tests/bin/install-wp-tests.sh {dbname} {dbuser} {dbpass}
32 | ```
33 |
34 | Example:
35 |
36 | ```
37 | bash tests/bin/install-wp-tests.sh woocommerce_test root root
38 | ```
39 |
40 | ### 4. Install the test dependencies
41 |
42 | ```
43 | composer install
44 | ```
45 |
46 | ### 5. Run the tests
47 |
48 | If you have phpunit installed globally, you can just run it from the directory:
49 |
50 | ```
51 | phpunit
52 | ```
53 |
54 | Otherwise, you can run the version of phpunit that composer installs:
55 |
56 | ```
57 | vendor/phpunit/phpunit/phpunit -c phpunit.xml
58 | ```
59 |
60 | Once the test runs the terminal output how many tests passed/failed. If you have no output, it means the test didn't run properly. In that case, check your PHP logs.
61 |
62 | ### 6. Test against specific versions of WooCommerce
63 |
64 | Tests run by default against the latest version of WooCommerce. To test a specific version of WooCommerce, set an environment variable before running your test:
65 |
66 | ```
67 | WC_VERSION=9.4.0 phpunit
68 | ```
69 |
--------------------------------------------------------------------------------
/tests/wp-tests-config.php:
--------------------------------------------------------------------------------
1 | plugin_path = untrailingslashit( plugin_dir_path( __FILE__ ) );
66 |
67 | // Declares compatibility with High Performance Order Storage.
68 | add_action( 'before_woocommerce_init', array( $this, 'declare_custom_order_table_compatibility' ) );
69 |
70 | // Init hooks runs after plugins_loaded and woocommerce_loaded hooks.
71 | add_action( 'init', array( $this, 'init' ) );
72 | }
73 |
74 | /**
75 | * Declares compatibility with High Performance Order Storage.
76 | * https://github.com/woocommerce/woocommerce/wiki/High-Performance-Order-Storage-Upgrade-Recipe-Book
77 | *
78 | * @since 2.2.0
79 | */
80 | public function declare_custom_order_table_compatibility() {
81 | if ( ! class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
82 | return;
83 | }
84 | \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
85 | }
86 |
87 | /**
88 | * Plugin base file.
89 | * Used for activation hook and plugin links.
90 | *
91 | * @since 1.5.0
92 | */
93 | public static function plugin_base() {
94 | return plugin_basename( __FILE__ );
95 | }
96 |
97 | /**
98 | * Plugin asset path.
99 | *
100 | * @since 1.8.5
101 | */
102 | public static function plugin_asset_path() {
103 | return plugin_dir_url( __FILE__ );
104 | }
105 |
106 | /**
107 | * Display a warning message if minimum version of WooCommerce check fails.
108 | *
109 | * @since 1.3.0
110 | * @return void
111 | */
112 | public function woocommerce_compatibility_notice() {
113 | echo '' . sprintf( __( '%1$s requires at least %2$s v%3$s in order to function.', 'woocommerce-coupon-restrictions' ), 'WooCommerce Coupon Restrictions', 'WooCommerce', $this->required_woo ) . '
';
114 | }
115 |
116 | /**
117 | * Initialize the plugin.
118 | *
119 | * @since 1.3.0
120 | * @return void
121 | */
122 | public function init() {
123 | // Check if we're running the required version of WooCommerce.
124 | if ( ! defined( 'WC_VERSION' ) || version_compare( WC_VERSION, $this->required_woo, '<' ) ) {
125 | add_action( 'admin_notices', array( $this, 'woocommerce_compatibility_notice' ) );
126 | return false;
127 | }
128 |
129 | // Load translations.
130 | load_plugin_textdomain(
131 | 'woocommerce-coupon-restrictions',
132 | false,
133 | dirname( plugin_basename( __FILE__ ) ) . '/languages/'
134 | );
135 |
136 | // Upgrade routine.
137 | $this->upgrade_routine();
138 |
139 | // Stores coupon use in a custom table if required by restrictions.
140 | require_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-table.php';
141 | new WC_Coupon_Restrictions_Table();
142 |
143 | // WP CLI command to populate restrictions table with already completed orders
144 | // that have enhanced usage limits.
145 | if ( defined( 'WP_CLI' ) && WP_CLI ) {
146 | include_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-cli.php';
147 | }
148 |
149 | // These classes are only needed in the admin.
150 | if ( is_admin() ) {
151 | // Onboarding actions when plugin is first installed.
152 | require_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-onboarding.php';
153 | new WC_Coupon_Restrictions_Onboarding();
154 |
155 | // Adds fields and metadata for coupons in admin screen.
156 | require_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-admin.php';
157 | new WC_Coupon_Restrictions_Admin();
158 |
159 | // Adds coupon meta fields.
160 | require_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-settings.php';
161 | new WC_Coupon_Restrictions_Settings();
162 | }
163 |
164 | // Validation methods used for both cart and checkout validation.
165 | require_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-validation.php';
166 | new WC_Coupon_Restrictions_Validation();
167 |
168 | // Validates coupons added to the cart.
169 | require_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-validation-cart.php';
170 | new WC_Coupon_Restrictions_Validation_Cart();
171 |
172 | // Validates coupons on checkout.
173 | require_once $this->plugin_path . '/includes/class-wc-coupon-restrictions-validation-checkout.php';
174 | new WC_Coupon_Restrictions_Validation_Checkout();
175 | }
176 |
177 | /**
178 | * Runs an upgrade routine.
179 | *
180 | * @since 1.3.0
181 | * @return void
182 | */
183 | public function upgrade_routine() {
184 | $option = get_option( 'woocommerce-coupon-restrictions', false );
185 |
186 | // If a previous version was installed, run any required updates.
187 | if ( isset( $option['version'] ) ) {
188 | if ( version_compare( $option['version'], '1.6.2', '<=' ) ) {
189 | // This setting determines how to verify new/existing customers.
190 | // In v1.6.2 and before the default was to check against accounts and orders.
191 | // In new installs, the default is to check against accounts only.
192 | update_option( 'coupon_restrictions_customer_query', 'accounts-orders' );
193 | }
194 | }
195 |
196 | // Sets a transient that triggers the onboarding notice.
197 | // Notice expires after one week.
198 | if ( false === $option ) {
199 | set_transient( 'woocommerce-coupon-restrictions-activated', 1, WEEK_IN_SECONDS );
200 | }
201 |
202 | // Sets the plugin version number in database.
203 | if ( false === $option || $this->version !== $option['version'] ) {
204 | update_option( 'woocommerce-coupon-restrictions', array( 'version' => $this->version ) );
205 | }
206 | }
207 | }
208 | }
209 |
210 | /**
211 | * Public function to access the shared instance of WC_Coupon_Restrictions.
212 | *
213 | * @since 1.5.0
214 | * @return class WC_Coupon_Restrictions
215 | */
216 | function WC_Coupon_Restrictions() {
217 | return WC_Coupon_Restrictions::instance();
218 | }
219 | add_action( 'plugins_loaded', 'WC_Coupon_Restrictions' );
220 |
--------------------------------------------------------------------------------