├── .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 ![Testing status](https://github.com/devinsays/woocommerce-coupon-restrictions/actions/workflows/php-tests.yml/badge.svg?branch=main) 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 | ![Testing status](https://github.com/devinsays/woocommerce-coupon-restrictions/actions/workflows/php-tests.yml/badge.svg?branch=main) 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 | --------------------------------------------------------------------------------