├── .codecov.yml ├── composer.json ├── tests ├── mocks │ ├── class-wc-data.php │ ├── function-mocks.php │ └── class-wc-order_item_product.php ├── bin │ └── travis.sh ├── framework │ └── class-wcsr-unit-testcase.php ├── bootstrap.php └── unit │ ├── class-wcsr-renewal-functions-test.php │ ├── class-wcsr-time-functions-test.php │ └── class-wcsr-resource-test.php ├── .travis.yml ├── includes ├── wcsr-functions.php ├── class-wcsr-data-store.php ├── wcsr-renewal-functions.php ├── wcsr-time-functions.php ├── class-wcsr-resource-manager.php ├── class-wcsr-resource-data-store-cpt.php └── class-wcsr-resource.php ├── phpunit.xml ├── woocommerce-subscriptions-resource.php ├── README.md └── license.txt /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - tests/.* 4 | - i18n/.* 5 | - apigen/.* 6 | - tmp/.* 7 | status: 8 | project: false 9 | patch: false 10 | changes: false 11 | 12 | comment: false -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "prospress/woocommerce-subscriptions-resource", 3 | "version": "1.5.3", 4 | "description": "A generic resource class to track and prorate payments for a subscription using WooCommerce Subscriptions.", 5 | "type": "wordpress-plugin", 6 | "license": "GPL-3.0", 7 | "minimum-stability": "dev", 8 | "require": {} 9 | } 10 | -------------------------------------------------------------------------------- /tests/mocks/class-wc-data.php: -------------------------------------------------------------------------------- 1 | getMethod( $method_name ); 14 | $reflected_method->setAccessible( true ); 15 | return $reflected_method; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | ./ 14 | 15 | ./tests 16 | 17 | 18 | 19 | 20 | 21 | ./tests/unit 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/mocks/function-mocks.php: -------------------------------------------------------------------------------- 1 | name = $name; 34 | } 35 | 36 | /** 37 | * Stub method for set_props 38 | */ 39 | public function set_props( $props ) { 40 | $this->props = $props; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /includes/class-wcsr-data-store.php: -------------------------------------------------------------------------------- 1 | init(); 33 | } 34 | 35 | /** 36 | * Register the resource data store with WooCommerce 37 | * 38 | * @param array 39 | * @return array 40 | */ 41 | public static function add_data_store( $data_stores ) { 42 | 43 | $data_stores[ self::get_object_type() ] = apply_filters( 'wcsr_resource_data_store_class', 'WCSR_Resource_Data_Store_CPT' ); 44 | 45 | return $data_stores; 46 | } 47 | 48 | /** 49 | * Wrapper for getting an instance of the resource data store 50 | * 51 | * @return WCSR_Resource_Data_Store_CPT 52 | */ 53 | public static function store() { 54 | return WC_Data_Store::load( self::get_object_type() ); 55 | } 56 | 57 | /** 58 | * Get the object type used to identify the resource data store 59 | * 60 | * @return string 61 | */ 62 | protected static function get_object_type() { 63 | return 'subscription-resource'; 64 | } 65 | } 66 | WCSR_Data_Store::init(); 67 | -------------------------------------------------------------------------------- /woocommerce-subscriptions-resource.php: -------------------------------------------------------------------------------- 1 | . 30 | * 31 | * @package WooCommerce Subscriptions Resource 32 | * @author Prospress Inc. 33 | * @since 1.0.0 34 | */ 35 | 36 | /** 37 | * Loads library if we know WooCommerce is at a valid version and the library hasn't already been loaded. 38 | * 39 | * @since 1.0.0 40 | */ 41 | function wcsr_init() { 42 | if ( defined( 'WC_VERSION' ) && version_compare( WC_VERSION, '3.0.0', '>=' ) && ! class_exists( 'WCSR_Resource' ) ) { 43 | require_once( 'includes/wcsr-functions.php' ); 44 | require_once( 'includes/wcsr-time-functions.php' ); 45 | require_once( 'includes/wcsr-renewal-functions.php' ); 46 | require_once( 'includes/class-wcsr-resource.php' ); 47 | require_once( 'includes/class-wcsr-data-store.php' ); 48 | require_once( 'includes/class-wcsr-resource-data-store-cpt.php' ); 49 | require_once( 'includes/class-wcsr-resource-manager.php' ); 50 | } 51 | } 52 | add_action( 'plugins_loaded', 'wcsr_init' ); 53 | -------------------------------------------------------------------------------- /includes/wcsr-renewal-functions.php: -------------------------------------------------------------------------------- 1 | get_name(), $days_active, $days_in_period ) : $line_item->get_name(); 29 | 30 | return apply_filters( 'wcsr_renewal_line_item_name', $line_item_name, $resource, $line_item, $days_active, $days_in_period, $from_timestamp, $to_timestamp ); 31 | } 32 | 33 | /** 34 | * Returns the new line item for the resource with updated the line item totals if prorating is required 35 | * 36 | * @since 1.1.0 37 | * @param WC_Order_Item_Product $line_item The existing line item on the renewal order 38 | * @param float $days_active_ratio The ratio of days active to days in the billing period 39 | * @return WC_Order_Item_Product 40 | */ 41 | function wcsr_get_prorated_line_item( $line_item, $days_active_ratio ) { 42 | $new_item = new WC_Order_Item_Product(); 43 | wcs_copy_order_item( $line_item, $new_item ); 44 | 45 | // If the $days_in_period != $days_active or in other words if the ratio is not 1 is to 1, prorate the line item totals 46 | if ( $days_active_ratio !== 1 ) { 47 | $taxes = $line_item->get_taxes(); 48 | 49 | foreach( $taxes as $total_type => $tax_values ) { 50 | foreach( $tax_values as $tax_id => $tax_value ) { 51 | $taxes[ $total_type ][ $tax_id ] = $tax_value * $days_active_ratio; 52 | } 53 | } 54 | 55 | $new_item->set_props( array( 56 | 'subtotal' => $line_item->get_subtotal() * $days_active_ratio, 57 | 'total' => $line_item->get_total() * $days_active_ratio, 58 | 'subtotal_tax' => $line_item->get_subtotal_tax() * $days_active_ratio, 59 | 'total_tax' => $line_item->get_total_tax() * $days_active_ratio, 60 | 'taxes' => $taxes, 61 | ) ); 62 | } 63 | 64 | return $new_item; 65 | } 66 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | tests_dir = dirname( __FILE__ ); 48 | $this->plugin_dir = dirname( $this->tests_dir ); 49 | $this->modules_dir = dirname( dirname( $this->tests_dir ) ); 50 | 51 | // Define a mock WC_Data object rather than requiring WooCommerce (we shouldn't be relying on or testing any WC_Data methods anyway) 52 | require_once( 'mocks/class-wc-data.php' ); 53 | require_once( 'mocks/function-mocks.php' ); 54 | require_once( 'mocks/class-wc-order_item_product.php' ); 55 | 56 | // Load relevant Resource plugin files 57 | require_once( $this->plugin_dir . '/includes/wcsr-time-functions.php' ); 58 | require_once( $this->plugin_dir . '/includes/wcsr-renewal-functions.php' ); 59 | require_once( $this->plugin_dir . '/includes/class-wcsr-resource.php' ); 60 | require_once( $this->plugin_dir . '/includes/class-wcsr-resource-manager.php' ); 61 | 62 | // Load WooCommerce Subscription files 63 | require_once( $this->modules_dir . '/woocommerce-subscriptions/includes/wcs-time-functions.php' ); 64 | 65 | // Manually Load WC_DateTime during test suite 66 | if ( ! class_exists( 'WC_DateTime' ) ) { 67 | require_once( $this->modules_dir . '/woocommerce-subscriptions/includes/libraries/class-wc-datetime.php' ); 68 | } 69 | 70 | // Load relevant class aliases for PHPUnit 6 (ran on PHP v7.0+ in Travis) 71 | if ( class_exists( 'PHPUnit\Runner\Version' ) && version_compare( PHPUnit\Runner\Version::id(), '6.0', '>=' ) ) { 72 | class_alias( 'PHPUnit\Framework\TestCase', 'PHPUnit_Framework_TestCase' ); 73 | // class_alias( 'PHPUnit\Framework\Exception', 'PHPUnit_Framework_Exception' ); 74 | // class_alias( 'PHPUnit\Framework\ExpectationFailedException', 'PHPUnit_Framework_ExpectationFailedException' ); 75 | // class_alias( 'PHPUnit\Framework\Error\Notice', 'PHPUnit_Framework_Error_Notice' ); 76 | // class_alias( 'PHPUnit\Framework\Test', 'PHPUnit_Framework_Test' ); 77 | // class_alias( 'PHPUnit\Framework\Warning', 'PHPUnit_Framework_Warning' ); 78 | // class_alias( 'PHPUnit\Framework\AssertionFailedError', 'PHPUnit_Framework_AssertionFailedError' ); 79 | // class_alias( 'PHPUnit\Framework\TestSuite', 'PHPUnit_Framework_TestSuite' ); 80 | // class_alias( 'PHPUnit\Framework\TestListener', 'PHPUnit_Framework_TestListener' ); 81 | // class_alias( 'PHPUnit\Util\GlobalState', 'PHPUnit_Util_GlobalState' ); 82 | // class_alias( 'PHPUnit\Util\Getopt', 'PHPUnit_Util_Getopt' ); 83 | } 84 | 85 | require_once( 'framework/class-wcsr-unit-testcase.php' ); 86 | } 87 | 88 | /** 89 | * Get the single class instance 90 | * 91 | * @since 2.0 92 | * @return WCS_Unit_Tests_Bootstrap 93 | */ 94 | public static function instance() { 95 | 96 | if ( is_null( self::$instance ) ) { 97 | self::$instance = new self(); 98 | } 99 | 100 | return self::$instance; 101 | } 102 | } 103 | WCSR_Unit_Tests_Bootstrap::instance(); 104 | -------------------------------------------------------------------------------- /tests/unit/class-wcsr-renewal-functions-test.php: -------------------------------------------------------------------------------- 1 | array( 19 | 'days_active_ratio' => .8, 20 | 'expected_data' => array ( 21 | 'subtotal' => 7.20, 22 | 'total' => 7.20, 23 | 'subtotal_tax' => 0, 24 | 'total_tax' => 0, 25 | 'taxes' => array(), 26 | ), 27 | ), 28 | 29 | 1 => array( 30 | 'days_active_ratio' => 1, 31 | 'expected_data' => array() // uses the existing line item totals from the produce (i.e. no proration required) 32 | ), 33 | 34 | 2 => array( 35 | 'days_active_ratio' => 1, 36 | 'expected_data' => array() // uses the existing line item totals from the produce (i.e. no proration required) 37 | ), 38 | 39 | 3 => array( 40 | 'days_active_ratio' => 0.03, 41 | 'expected_data' => array( 42 | 'subtotal' => 0.27, 43 | 'total' => 0.27, 44 | 'subtotal_tax' => 0, 45 | 'total_tax' => 0, 46 | 'taxes' => array(), 47 | ), 48 | ) 49 | ); 50 | } 51 | 52 | /** 53 | * Make sure get_prorated_resource_line_item() correctly calculates the line item totals 54 | * 55 | * @dataProvider provider_get_prorated_resource_line_item 56 | * @group prorated_line_items 57 | */ 58 | public function test_get_prorated_resource_line_item( $days_active_ratio, $expected_data ) { 59 | // create a copy of WC_Order_Item_Product and set args and line item name to be used as the expected result 60 | $expected_result = new WC_Order_Item_Product(); 61 | $expected_result->props = $expected_data; 62 | 63 | // Mock the line item object and line item methods used within WCSR_Resource_Manager::get_prorated_resource_line_item() function 64 | $line_item_mock = $this->getMockBuilder( 'WC_Order_Item_Product' )->setMethods( array( 'set_props', 'get_name', 'get_taxes', 'get_subtotal', 'get_total', 'get_subtotal_tax', 'get_total_tax' ) )->disableOriginalConstructor()->getMock(); 65 | $line_item_mock->expects( $this->any() )->method( 'get_taxes' )->will( $this->returnValue( array() ) ); 66 | $line_item_mock->expects( $this->any() )->method( 'get_subtotal' )->will( $this->returnValue( self::$product_total ) ); 67 | $line_item_mock->expects( $this->any() )->method( 'get_total' )->will( $this->returnValue( self::$product_total ) ); 68 | $line_item_mock->expects( $this->any() )->method( 'get_subtotal_tax' )->will( $this->returnValue( self::$product_total_tax ) ); 69 | $line_item_mock->expects( $this->any() )->method( 'get_total_tax' )->will( $this->returnValue( self::$product_total_tax ) ); 70 | $line_item_mock->expects( $this->any() )->method( 'get_name' )->will( $this->returnValue( self::$product_name ) ); 71 | 72 | // mock the resource manager class and a resource object for sending to get_prorated_resource_line_item 73 | 74 | $this->assertEquals( $expected_result, wcsr_get_prorated_line_item( $line_item_mock, $days_active_ratio ) ); 75 | } 76 | 77 | public function provider_get_line_item_name() { 78 | return array ( 79 | // prorated line item name 80 | 0 => array( 81 | 'days_active' => 20, 82 | 'days_in_period' => 30, 83 | 'expected' => 'Robot Ninja usage for 20 of 30 days.', 84 | ), 85 | 86 | // non-prorated line item name 87 | 1 => array( 88 | 'days_active' => 30, 89 | 'days_in_period' => 30, 90 | 'expected' => 'Robot Ninja', 91 | ), 92 | 93 | // impossible case 94 | 2 => array( 95 | 'days_active' => 31, 96 | 'days_in_period' => 30, 97 | 'expected' => 'Robot Ninja usage for 31 of 30 days.', 98 | ), 99 | ); 100 | } 101 | 102 | /** 103 | * Test the line_item_name string returned from wcsr_get_line_item_name() 104 | * 105 | * @dataProvider provider_get_line_item_name 106 | * @group prorated_line_items 107 | */ 108 | public function test_get_line_item_name( $days_active, $days_in_period, $expected ) { 109 | // Mock the line item object and line item methods used within WCSR_Resource_Manager::get_prorated_resource_line_item() function 110 | $line_item_mock = $this->getMockBuilder( 'WC_Order_Item_Product' )->setMethods( array( 'get_name' ) )->disableOriginalConstructor()->getMock(); 111 | $line_item_mock->expects( $this->any() )->method( 'get_name' )->will( $this->returnValue( self::$product_name ) ); 112 | 113 | $this->assertEquals( $expected, wcsr_get_line_item_name( $line_item_mock, $days_active, $days_in_period ) ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /includes/wcsr-time-functions.php: -------------------------------------------------------------------------------- 1 | 0 && $counter < 50 ) { 40 | $next_timestamp = wcs_add_time( $billing_interval, $billing_period, $from_timestamp ); // use the same function used to calculate subscription billing periods to work out the exact billing 41 | $days_in_billing_period = wcsr_get_days_in_period( $from_timestamp, $next_timestamp ); 42 | $days_in_billing_cycle += $days_in_billing_period; 43 | 44 | if ( $days_left_in_period >= $days_in_billing_period ) { 45 | $number_of_billing_periods++; 46 | } elseif ( $days_left_in_period < $days_in_billing_period ) { // if the days left in the period are less then the days in the billing cycle - find the ratio of days left and days in billing period 47 | $number_of_billing_periods += round( $days_left_in_period / $days_in_billing_period, 2 ); 48 | break; 49 | } 50 | 51 | $counter++; 52 | $days_left_in_period -= $days_in_billing_period; 53 | $from_timestamp = $next_timestamp; 54 | } 55 | 56 | // if the number of days active is more than or equal to the days in the period - return the exact number of billing periods (remember this can be a float or whole number i.e. 15 day period of a monthly subscription is only 0.5) 57 | if ( $days_active >= $days_in_period ) { 58 | $active_days_ratio = $number_of_billing_periods; 59 | } else { 60 | // calculate the active days ratio for days active and days in period, then multiply that by the number of billing periods found between the from and to timestamp. This replaces the extra days in cycle logic because instead of finding the extra days and minusing that off the days in period, we can just multiply the result by the number of cycles. 61 | $active_days_ratio = round( ( $days_active / $days_in_period ) * $number_of_billing_periods, 2 ); 62 | } 63 | 64 | return $active_days_ratio; 65 | } 66 | 67 | /** 68 | * Calculates the number of whole (i.e. using floor) days between two timestamps. 69 | * Uses the same logic in @see wcs_estimate_periods_between() but this function exclusively calculates the number of days 70 | * 71 | * @since 1.1.0 72 | * @param int $start_timestamp The starting timestamp to calculate the number of days from 73 | * @param int $end_timestamp The end timestamp to calculate the number of days to 74 | * @return int 75 | */ 76 | function wcsr_get_days_in_period( $start_timestamp, $end_timestamp ) { 77 | // return 0 if the start timestamp is after the end timestamp 78 | $periods_until = 0; 79 | 80 | if ( $end_timestamp > $start_timestamp ) { 81 | $periods_until = floor( ( $end_timestamp - $start_timestamp ) / DAY_IN_SECONDS ); 82 | } 83 | 84 | return $periods_until; 85 | } 86 | 87 | /** 88 | * Find all the timestamps from a given array that fall within a from/to timestamp range. 89 | * 90 | * @param array $timestamps_to_check 91 | * @param int $from_timestamp 92 | * @param int $to_timestamp 93 | * @return array 94 | */ 95 | function wcsr_get_timestamps_between( $timestamps_to_check, $from_timestamp, $to_timestamp ) { 96 | 97 | $times = array(); 98 | 99 | foreach ( $timestamps_to_check as $i => $timestamp ) { 100 | if ( $timestamp >= $from_timestamp && $timestamp <= $to_timestamp ) { 101 | $times[] = $timestamp; 102 | } 103 | } 104 | 105 | return $times; 106 | } 107 | 108 | 109 | /** 110 | * Conditional check for whether a timestamp is on the same 24 hour block as another timestamp 111 | * 112 | * The catch is the "day" is not typical calendar day - it based on a 24 hour block from the $start_timestamp 113 | * 114 | * Uses the $start_timestamp to loop over and add DAY_IN_SECONDS to the time until it reaches the same 24 hour block as the $compare_timestamp 115 | * This function then checks whether the $current_timestamp and the $compare_timestamp are within the same 24 hour block 116 | * 117 | * @param int $current_timestamp The current timestamp being checked 118 | * @param int $compare_timestamp The timestamp used to check if the $current_timestamp is on the same 24 hour block 119 | * @param int $start_timestamp The start timestamp of the period (to calculate when the 24 hour blocks start) 120 | * @return boolean true on same 24 hour block | false if not 121 | */ 122 | function wcsr_is_on_same_day( $current_timestamp, $compare_timestamp, $start_timestamp ) { 123 | $start_of_the_day = null; 124 | 125 | for ( $end_of_the_day = $start_timestamp; $end_of_the_day <= $compare_timestamp; $end_of_the_day += DAY_IN_SECONDS ) { 126 | // The loop controls take care of incrementing the end day (3rd expression) until the day after the compare date (2nd expression), but we also want to set the start date so we do that here using the current value of end day (which will be the start day in the final iteration as the 3rd expression in the loop hasn't run yet) 127 | $start_of_the_day = $end_of_the_day; 128 | } 129 | 130 | // Compare timestamp to see if it is within our ranges 131 | if ( ( $current_timestamp >= $start_of_the_day ) && ( $current_timestamp < $end_of_the_day ) ) { 132 | return true; 133 | } else { 134 | return false; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /includes/class-wcsr-resource-manager.php: -------------------------------------------------------------------------------- 1 | true, 48 | 'is_prorated' => false, 49 | 'date_created' => gmdate( 'U' ), 50 | ) 51 | ); 52 | 53 | $resource_id = 0; 54 | $resource_class = self::get_resource_class( $resource_id ); 55 | $resource = new $resource_class( $resource_id ); 56 | 57 | $resource->set_external_id( $external_id ); 58 | $resource->set_subscription_id( $subscription_id ); 59 | 60 | $resource->set_is_pre_paid( $args['is_pre_paid'] ); 61 | $resource->set_is_prorated( $args['is_prorated'] ); 62 | $resource->set_date_created( $args['date_created'] ); 63 | 64 | // If the resource is being created as an active resource, make sure its creation time is included in the activation timestamps 65 | if ( 'active' === $status ) { 66 | $resource->set_activation_timestamps( array( $args['date_created'] ) ); 67 | } 68 | 69 | WCSR_Data_Store::store()->create( $resource ); 70 | 71 | do_action( 'wcsr_created_resource', $resource, $status, $external_id, $subscription_id, $args ); 72 | 73 | return $resource; 74 | } 75 | 76 | /** 77 | * Activate a resource linked to an external object, specified by ID. 78 | * 79 | * @param int 80 | * @return null 81 | */ 82 | public static function activate_resource( $external_id ) { 83 | if ( $resource_id = WCSR_Data_Store::store()->get_resource_id_by_external_id( $external_id ) ) { 84 | $resource = self::get_resource( $resource_id ); 85 | $resource->activate(); 86 | $resource->save(); 87 | } 88 | } 89 | 90 | /** 91 | * Deactivate a resource linked to an external object, specified by ID. 92 | * 93 | * @param int 94 | * @return null 95 | */ 96 | public static function deactivate_resource( $external_id ) { 97 | if ( $resource_id = WCSR_Data_Store::store()->get_resource_id_by_external_id( $external_id ) ) { 98 | $resource = self::get_resource( $resource_id ); 99 | $resource->deactivate(); 100 | $resource->save(); 101 | } 102 | } 103 | 104 | /** 105 | * When a renewal order is created, make sure line items for all resources that are post-paid and prorated reflect 106 | * the prorated amounts. 107 | * 108 | * @param WC_Order 109 | * @param WC_Subscription 110 | */ 111 | public static function maybe_prorate_renewal( $renewal_order, $subscription ) { 112 | 113 | $is_prorated = false; 114 | $resource_ids = WCSR_Data_Store::store()->get_resource_ids_for_subscription( $subscription->get_id(), 'wcsr-unended' ); 115 | 116 | if ( ! empty( $resource_ids ) ) { 117 | 118 | // First, get the line items representing the resource so we can figure out things like cost for it 119 | $line_items = $renewal_order->get_items(); 120 | 121 | foreach ( $resource_ids as $resource_id ) { 122 | 123 | $resource = self::get_resource( $resource_id ); 124 | 125 | if ( ! empty( $resource ) && false === $resource->get_is_pre_paid() && $resource->get_is_prorated() && $resource->has_been_activated() ) { 126 | 127 | // Calculate prorated payments from paid date to match how Subscriptions determine next payment dates. 128 | if ( $subscription->get_time( 'date_paid' ) > 0 ) { 129 | $from_timestamp = $subscription->get_time( 'date_paid' ); 130 | } elseif ( $subscription->get_time( 'date_completed' ) > 0 ) { 131 | $from_timestamp = $subscription->get_time( 'date_completed' ); 132 | } else { 133 | // We can't use last order date created, because that will be the renewal order just created, so go straight to the subscrition start time 134 | $from_timestamp = $subscription->get_time( 'date_created' ); 135 | } 136 | 137 | $from_timestamp = apply_filters( 'wcsr_renewal_proration_from_timetamp', $from_timestamp, $subscription, $renewal_order, $resource ); 138 | $to_timestamp = $renewal_order->get_date_created()->getTimestamp(); 139 | 140 | // Calculate the usage and the ratio of active days vs days in the period 141 | $days_active = $resource->get_days_active( $from_timestamp, $to_timestamp ); 142 | $days_in_period = wcsr_get_days_in_period( $from_timestamp, $to_timestamp ); 143 | 144 | // make sure the days active doesn't go over the amount of days in the period 145 | if ( $days_active > $days_in_period ) { 146 | $days_active = $days_in_period; 147 | } 148 | 149 | $days_active_ratio = wcsr_get_active_days_ratio( $from_timestamp, $days_in_period, $days_active, $subscription->get_billing_period(), $subscription->get_billing_interval() ); 150 | 151 | foreach ( $line_items as $line_item ) { 152 | 153 | // Now add a prorated line item for the resource based on the resource's usage for this period 154 | $new_item = wcsr_get_prorated_line_item( $line_item, $days_active_ratio ); 155 | $new_item->set_name( wcsr_get_line_item_name( $new_item, $days_active, $days_in_period, $resource, $from_timestamp, $to_timestamp ) ); 156 | 157 | $new_item = apply_filters( 'wcsr_prorated_line_item_for_resource', $new_item, $resource ); 158 | 159 | // Add item to order 160 | $renewal_order->add_item( $new_item ); 161 | 162 | $is_prorated = true; 163 | } 164 | } 165 | } 166 | 167 | if ( $is_prorated ) { 168 | 169 | // Delete the existing items as they've been replaced by the new, prorated items 170 | foreach ( $line_items as $line_item_id => $line_item ) { 171 | $renewal_order->remove_item( $line_item_id ); 172 | } 173 | 174 | $renewal_order->calculate_totals(); // also saves the order 175 | } 176 | } 177 | 178 | // Allow 3rd party code to perform their own proration or other logic just after we have prorate 179 | if ( $is_prorated ) { 180 | $renewal_order = apply_filters( 'wcsr_after_renewal_order_prorated', $renewal_order, $resource_ids, $subscription ); 181 | } 182 | 183 | return $renewal_order; 184 | } 185 | 186 | /** 187 | * Get an instance of a resource specified by ID 188 | * 189 | * @param int 190 | * @return WCSR_Resource 191 | */ 192 | public static function get_resource( $resource_id ) { 193 | 194 | if ( ! $resource_id ) { 195 | return false; 196 | } 197 | 198 | $resource_class = self::get_resource_class( $resource_id ); 199 | 200 | return new $resource_class( $resource_id ); 201 | } 202 | 203 | /** 204 | * Get the class used to instantiate resources or a given resource. 205 | * 206 | * @param int 207 | * @return string 208 | */ 209 | protected static function get_resource_class( $resource_id ) { 210 | return apply_filters( 'wcsr_resource_class', 'WCSR_Resource', $resource_id ); 211 | } 212 | } 213 | WCSR_Resource_Manager::init(); 214 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WooCommerce Subscriptions Resource [![Build Status](https://travis-ci.org/Prospress/woocommerce-subscriptions-resource.svg?branch=master)](https://travis-ci.org/Prospress/woocommerce-subscriptions-resource) [![license: GPL v2](https://img.shields.io/badge/license-GPLv2-blue.svg)](http://opensource.org/licenses/GPL-2.0) 2 | 3 | 4 | A library to track and prorate payments for a subscription using WooCommerce Subscriptions based on the status of an external resource. 5 | 6 | A "_resource_" is deliberately generic to allow for proration of anything. For example, it is implemented on [Robot Ninja](http://robotninja.com/) to prorate payments for stores similar to [Slack's billing](https://get.slack.help/hc/en-us/articles/218915077). In this case, the _store_ is the resource. But other resources could be a seat in a club membership, a video library for an education site, or a server for a web hosting service. 7 | 8 | Resources can be pre-paid or post-paid. They can be prorated to daily rates, or charged in full for each billing period. 9 | 10 | ## Proration Behaviour 11 | 12 | By default, proration for resources works very similar to [Slack's billing](https://get.slack.help/hc/en-us/articles/218915077), because we thought it was cool, and very fair, so wanted to use it for Robot Ninja. 13 | 14 | Specifically, any changes to the number of active resources will be reflected in the next renewal order, prorated daily. 15 | 16 | Here’s an example: 17 | 18 | > Let’s suppose your customer is on a subscription which costs $8 per resource per month. You add a new resource 10 days into your billing period, leaving 20 days remaining in the month. 19 | 20 | > The prorated renewal cost is calculated by dividing the cost per resource ($8) by the number of days in the month (30) and multiplying it by the number of days remaining (20), which gives the prorated cost for the remainder of that billing period: $5.33 21 | 22 | > That amount will be set as a line item for that resource 23 | 24 | If your subscription bills annually, it works the same way. The prorated cost for the prior year will be calculated and billed at the next renewal. 25 | 26 | ## Usage 27 | 28 | The code in this library will take care of creating and linking a resource. All your code needs to do is tell it when to create or update a resource and the IDs of objects to link it to. 29 | 30 | This can be done with the following hooks: 31 | 32 | * `wcsr_create_resource` 33 | * `wcsr_activate_resource` 34 | * `wcsr_deactivate_resource` 35 | 36 | ### Create a Resource 37 | 38 | To create a new resource, trigger the `wcsr_create_resource` hook via `do_action()` and pass the following parameters: 39 | 40 | 1. `$status` (`string`): the status for the resource at the time of creation. Should be either 'active' or 'inactive', unless using a custom resource class which can handle other statuses. 41 | 1. `$external_id` (`int`): the ID of the external object to link this resource to. For example, to link it to a _store_ on [Robot Ninja](http://robotninja.com/), the store's ID is passed as the `$external_id`. 42 | 1. `$subscription_id` (`int`): the ID of the subscription in WooCommerce to link this resource to. 43 | 1. `$args` (`array`): A set of params to customise the behaviour of the resource, especially for proration and pre-pay vs. post pay. Default value: `array ( 'is_pre_paid' => true, 'is_prorated' => false )`. 44 | 45 | #### Resource Creation Example Code 46 | 47 | To create a active resource linked to a store with ID 23 and subscription with ID 159, the following code can be used: 48 | 49 | ``` 50 | do_action( 'wcsr_create_resource', 'active', 23, 159 ); 51 | ``` 52 | 53 | ### Activate a Resource 54 | 55 | To record the activation of an existing resource, trigger the `wcsr_activate_resource` hook via `do_action()` and pass the following parameter: 56 | 57 | 1. `$external_id` (`int`): the ID of the external object to link this resource to. For example, to link it to a _store_ on [Robot Ninja](http://robotninja.com/), the store's ID is passed as the `$external_id`. 58 | 59 | This will record the resource's activation timestamp against the resource so that it can later be used for prorating that period's payment, if required. 60 | 61 | #### Resource Activation Example Code 62 | 63 | To active a resource linked to a store with ID 23, the following code can be used: 64 | 65 | ``` 66 | do_action( 'wcsr_activate_resource', 23 ); 67 | ``` 68 | 69 | ### Deactivate a Resource 70 | 71 | To record the deactivation of an existing resource, trigger the `wcsr_deactivate_resource` hook via `do_action()` and pass the following parameter: 72 | 73 | 1. `$external_id` (`int`): the ID of the external object to link this resource to. For example, to link it to a _store_ on [Robot Ninja](http://robotninja.com/), the store's ID is passed as the `$external_id`. 74 | 75 | This will record the resource's deactivation timestamp against the resource so that it can later be used for prorating that period's payment, if required. 76 | 77 | #### Resource Deactivation Example Code 78 | 79 | To deactive a resource linked to a store with ID 23, the following code can be used: 80 | 81 | ``` 82 | do_action( 'wcsr_deactivate_resource', 23 ); 83 | ``` 84 | 85 | ### Add custom line items or modify prorated totals 86 | 87 | If you want to customize the prorated amounts, or apply other logic to orders that have been prorated, like adding other line items, the `'wcsr_after_renewal_order_prorated'` hook is triggered after a renewal order's totals have been prorated. 88 | 89 | For example, we use this on Robot Ninja to implmement a minimum amount for renewal orders of $9 by adding custom fee line items to orders with a prorated total of less than $9. 90 | 91 | Callbacks on the `wcsr_after_renewal_order_prorated` filter receive: 92 | 93 | 1. an `WC_Order` object representing the prorated renewal order as the first param. 94 | 2. an array of resource IDs for the matching subscription as the 2nd param. 95 | 96 | > Note: this filter is only trigger for orders with prorated amounts, making it easier to use then existing, more generic hooks, like `'wcs_renewal_order_created'`. 97 | 98 | #### Custom Resource Line Item Name Example 99 | 100 | The following snippet shows code similar to that used on Robot Ninja to enforce a minimum monthly access fee of $9. If the prorated renewal order is less than $9 based on the resource usage during the prior month, then a new fee line item is added to the renewal order for difference. 101 | 102 | ```php 103 | public function eg_add_minimum_fee( $renewal_order, $resource_ids ) { 104 | 105 | if ( $renewal_order->get_total() < 9 ) { 106 | 107 | $fee_item = new WC_Order_Item_Fee(); 108 | 109 | $fee_item->set_props( array( 110 | 'name' => 'Robot Ninja Gap Fee for Monthly Minimum', 111 | 'tax_class' => '', 112 | 'total' => wc_format_decimal( 9.00 - $renewal_order->get_total() ), 113 | 'total_tax' => '', 114 | 'taxes' => array( 115 | 'total' => 0, 116 | ), 117 | 'order_id' => $renewal_order->get_id(), 118 | ) ); 119 | 120 | $fee_item->save(); 121 | 122 | $renewal_order->add_item( $fee_item ); 123 | 124 | $renewal_order->calculate_totals(); // also saves the order 125 | } 126 | 127 | return $renewal_order; 128 | } 129 | add_filter( 'wcsr_after_renewal_order_prorated', ''eg_add_minimum_fee, 10, 2 );_ 130 | ``` 131 | 132 | ### Link a Resource to Line Item Name on Renewal Order 133 | 134 | Each resource will be set as a separate line item on the renewal orders (using the correct product IDs to make sure reports are accurate). 135 | 136 | This has the added advantage of being able to identify each resource on each line item by filtering the line item name for that resource. 137 | 138 | To do this, add a callback to the `wcsr_renewal_line_item_name` filter. You can use the `WCSR_Resource` object passed to your callback to derive information about the resource. 139 | 140 | #### Custom Resource Line Item Name Example 141 | 142 | To add the _store_ on [Robot Ninja](http://robotninja.com/) for a resource linked to a store with ID 23, the following code can be used: 143 | 144 | ``` 145 | function eg_add_store_to_line_item( $line_item_name, $resource, $line_item, $days_active ) { 146 | 147 | // Get the Robot Ninja Store 148 | $store = get_store( $resource->get_external_id() ); 149 | 150 | $line_item_name = sprintf( '%s (%s) %s usage for %d days.', $store['name'], $store['url'], $line_item->get_name(), $days_active ); 151 | 152 | return $line_item_name; 153 | } 154 | add_filter( 'wcsr_renewal_line_item_name', 'eg_add_store_to_line_item', 10, 4 ); 155 | ``` 156 | 157 | ## FAQ 158 | 159 | ### Do the subscription's line item totals display updated amounts each day to account for proration? 160 | 161 | No. For now, on the customer facing **My Account > View Subscription** and admin facing **WooCommerce > Edit Subscription** screens, the subscription line item totals will only display whatever line items were set on it at the time of sign-up. 162 | 163 | These totals are then used at the time of renewal to determine the prorated amount. 164 | 165 | The renewal order's line item totals will then display the prorated amounts for the prior billing period. 166 | 167 | ### Can a resource be linked to a specific line item on the subscription? 168 | 169 | No. For now, it can only be linked to a subscription as a whole, and whatever product line items are set on that subscription will be prorated. 170 | 171 | This makes is simpler to keep resources and subscriptions in sync, because it means your customers can switch line items on a subscription without the resource needing to be updated. 172 | 173 | It also makes it possible for multiple line items to be prorated for a given resource. For example, you might rent a VPS Server with an optional 1TB Storage addon. These can be included as separate line items on the subscription and renewal order, which helps keep track of revenue from each. But both can be prorated based on the resource's usage. 174 | 175 | ### How is the number of days active determined? 176 | 177 | If a resource is active for more than 1 second on a given day, it will be considered to have been "active" on that day, and a prorated charge for that day will be included on the renewal. 178 | 179 | When prorating renewal amounts, the resource object will check for the number of days the resource was active for more than one second between the date the subscription's last order was _paid_ (i.e. not created) and when the renewal was was created. This matches Subscriptions default next payment calculations, which base the next payment date based on the last payment date. 180 | 181 | If the subscription has no date for the last paid order, then the from date will attempt to use the last order's creation date, and if that does not exist, the subscription's creation creation. 182 | 183 | If you are continuing to provide access to the resource even between when a payment is due and when it is processed, by using a plugin like [WooCommerce Subscriptions - Preserve Billing Schedule](https://github.com/woocommerce/woocommerce-subscriptions-preserve-billing-schedule), this default behaviour will result in incorrectly prorated amounts. You would instead wish for proration to always be based on the date the last order was created. 184 | 185 | To achieve this, you can use the `'wcsr_renewal_proration_from_timetamp'` filter with a callback like this: 186 | 187 | ``` 188 | function eg_use_last_order_date_created_for_resource_proration( $from_timestamp, $subscription, $renewal_order, $resource ) { 189 | 190 | $from_timestamp = eg_function_to_get_second_last_orders_creation_time(); 191 | 192 | return $from_timestamp; 193 | } 194 | add_filter( 'wcsr_renewal_proration_from_timetamp', 'eg_use_last_order_date_created_for_resource_proration', 10, 4 ); 195 | ``` 196 | 197 | ### Are shipping, fee or other line items prorated? 198 | 199 | No. Only product line item amounts will be prorated. 200 | 201 | ## Installation 202 | 203 | This isn't a plugin, installing it as a plugin won't do anything. 204 | 205 | It's a class that can be extended from your code to implement your own resource type. 206 | 207 | You can include it via Git Subtree merge, or using Composer. 208 | 209 | **Requires WooCommerce 3.0 or newer.** 210 | 211 | ## Reporting Issues 212 | 213 | If you find an problem or would like to request this library be extended, please [open a new Issue](https://github.com/woocommerce/woocommerce-subscriptions-resource/issues/new). 214 | 215 | -------------------------------------------------------------------------------- /includes/class-wcsr-resource-data-store-cpt.php: -------------------------------------------------------------------------------- 1 | post_type ) ) { 37 | return; 38 | } 39 | 40 | do_action( 'wcsr_register_post_types' ); 41 | 42 | register_post_type( $this->post_type, 43 | apply_filters( 'woocommerce_register_post_type_' . $this->post_type, 44 | array( 45 | 'labels' => array( 46 | 'name' => __( 'Resources', 'woocommerce-subscriptions-resource' ), 47 | 'singular_name' => __( 'Resource', 'woocommerce-subscriptions-resource' ), 48 | 'all_items' => __( 'All Resources', 'woocommerce-subscriptions-resource' ), 49 | 'menu_name' => _x( 'Resources', 'Admin menu name', 'woocommerce-subscriptions-resource' ), 50 | 'add_new' => __( 'Add New', 'woocommerce-subscriptions-resource' ), 51 | 'add_new_item' => __( 'Add new product', 'woocommerce-subscriptions-resource' ), 52 | 'edit' => __( 'Edit', 'woocommerce-subscriptions-resource' ), 53 | 'edit_item' => __( 'Edit resource', 'woocommerce-subscriptions-resource' ), 54 | 'new_item' => __( 'New resource', 'woocommerce-subscriptions-resource' ), 55 | 'view' => __( 'View resource', 'woocommerce-subscriptions-resource' ), 56 | 'view_item' => __( 'View resource', 'woocommerce-subscriptions-resource' ), 57 | 'search_items' => __( 'Search resources', 'woocommerce-subscriptions-resource' ), 58 | 'not_found' => __( 'No resources found', 'woocommerce-subscriptions-resource' ), 59 | 'not_found_in_trash' => __( 'No resources found in trash', 'woocommerce-subscriptions-resource' ), 60 | 'parent' => __( 'Parent resource', 'woocommerce-subscriptions-resource' ), 61 | 'featured_image' => __( 'Resource image', 'woocommerce-subscriptions-resource' ), 62 | 'set_featured_image' => __( 'Set resource image', 'woocommerce-subscriptions-resource' ), 63 | 'remove_featured_image' => __( 'Remove resource image', 'woocommerce-subscriptions-resource' ), 64 | 'use_featured_image' => __( 'Use as resource image', 'woocommerce-subscriptions-resource' ), 65 | 'insert_into_item' => __( 'Insert into resource', 'woocommerce-subscriptions-resource' ), 66 | 'uploaded_to_this_item' => __( 'Uploaded to this resource', 'woocommerce-subscriptions-resource' ), 67 | 'filter_items_list' => __( 'Filter resources', 'woocommerce-subscriptions-resource' ), 68 | 'items_list_navigation' => __( 'Resources navigation', 'woocommerce-subscriptions-resource' ), 69 | 'items_list' => __( 'Resources list', 'woocommerce-subscriptions-resource' ), 70 | ), 71 | 'description' => __( 'This is where you can add new resources to your store.', 'woocommerce-subscriptions-resource' ), 72 | 'public' => false, 73 | 'show_ui' => false, 74 | 'capability_type' => 'shop_order', 75 | 'map_meta_cap' => true, 76 | 'publicly_queryable' => false, 77 | 'exclude_from_search' => true, 78 | 'hierarchical' => false, // Hierarchical causes memory issues - WP loads all records! 79 | 'show_in_nav_menus' => false, 80 | 'rewrite' => false, 81 | 'query_var' => false, 82 | 'supports' => array( 'title', 'custom-fields' ), 83 | 'has_archive' => false, 84 | 'show_in_rest' => true, 85 | 'can_export' => true, 86 | 'ep_mask' => EP_NONE, 87 | ) 88 | ) 89 | ); 90 | 91 | foreach ( array( 'wcsr-ended', 'wcsr-unended' ) as $status ) { 92 | register_post_status( $status, array( 93 | 'public' => false, 94 | 'exclude_from_search' => false, 95 | 'show_in_admin_all_list' => false, 96 | ) ); 97 | } 98 | } 99 | 100 | /** 101 | * Method to create a new record of a WC_Data based object. 102 | * 103 | * @param WCSR_Resource &$resource 104 | */ 105 | public function create( &$resource ) { 106 | 107 | if ( null === $resource->get_date_created( 'edit' ) ) { 108 | $resource->set_date_created( gmdate( 'U' ) ); 109 | } 110 | 111 | $resource_id = wp_insert_post( apply_filters( 'wcsr_new_resouce_data', array( 112 | 'post_type' => $this->post_type, 113 | 'post_status' => 'wcsr-unended', 114 | 'post_author' => 1, // Matches how Abstract_WC_Order_Data_Store_CPT works, using the default WP user 115 | 'post_date' => gmdate( 'Y-m-d H:i:s', $resource->get_date_created()->getOffsetTimestamp() ), 116 | 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $resource->get_date_created()->getTimestamp() ), 117 | 'post_title' => $this->get_post_title( $resource ), 118 | 'post_parent' => $resource->get_subscription_id( 'edit' ), 119 | 'post_excerpt' => '', 120 | 'post_content' => '', 121 | 'post_password' => uniqid( 'resource_' ), 122 | ) ), true ); 123 | 124 | if ( $resource_id ) { 125 | $resource->set_id( $resource_id ); 126 | $this->update_post_meta( $resource ); 127 | $resource->save_meta_data(); 128 | $resource->apply_changes(); 129 | do_action( 'wcsr_new_resource', $resource_id ); 130 | } 131 | } 132 | 133 | /** 134 | * Method to read a record. Creates a new WC_Data based object. 135 | * 136 | * @param WCSR_Resource &$resource 137 | */ 138 | public function read( &$resource ) { 139 | $resource->set_defaults(); 140 | 141 | if ( ! $resource->get_id() || ! ( $post_object = get_post( $resource->get_id() ) ) || $this->post_type !== $post_object->post_type ) { 142 | throw new Exception( __( 'Invalid resource.', 'woocommerce-subscriptions-resource' ) ); 143 | } 144 | 145 | $resource_id = $resource->get_id(); 146 | 147 | $resource->set_props( array( 148 | 'date_created' => 0 < $post_object->post_date_gmt ? wc_string_to_timestamp( $post_object->post_date_gmt ) : null, 149 | 'status' => $post_object->post_status, 150 | 'external_id' => get_post_meta( $resource_id, 'external_id', true ), 151 | 'subscription_id' => get_post_meta( $resource_id, 'subscription_id', true ), 152 | 153 | 'is_pre_paid' => wc_string_to_bool( get_post_meta( $resource_id, 'is_pre_paid', true ) ), 154 | 'is_prorated' => wc_string_to_bool( get_post_meta( $resource_id, 'is_prorated', true ) ), 155 | 156 | 'activation_timestamps' => array_filter( (array) get_post_meta( $resource_id, 'activation_timestamps', true ) ), 157 | 'deactivation_timestamps' => array_filter( (array) get_post_meta( $resource_id, 'deactivation_timestamps', true ) ), 158 | ) ); 159 | 160 | $resource->read_meta_data(); 161 | 162 | $resource->set_object_read( true ); 163 | 164 | do_action( 'wcsr_resource_loaded', $resource ); 165 | } 166 | 167 | /** 168 | * Updates a record in the database. 169 | * 170 | * @param WCSR_Resource &$resource 171 | */ 172 | public function update( &$resource ) { 173 | 174 | $resource->save_meta_data(); 175 | $changes = $resource->get_changes(); 176 | 177 | if ( array_intersect( array( 'date_created', 'status', 'subscription_id' ), array_keys( $changes ) ) ) { 178 | 179 | $post_data = array( 180 | 'post_date' => gmdate( 'Y-m-d H:i:s', $resource->get_date_created( 'edit' )->getOffsetTimestamp() ), 181 | 'post_date_gmt' => gmdate( 'Y-m-d H:i:s', $resource->get_date_created( 'edit' )->getTimestamp() ), 182 | 'post_parent' => $resource->get_subscription_id( 'edit' ), 183 | 'post_status' => $resource->get_status( 'edit' ) ? $resource->get_status( 'edit' ) : apply_filters( 'wcsr_default_resource_status', 'wcsr-unended' ), 184 | ); 185 | 186 | /** 187 | * When updating this object, to prevent infinite loops, use $wpdb 188 | * to update data, since wp_update_post spawns more calls to the 189 | * save_post action. 190 | * 191 | * This ensures hooks are fired by either WP itself (admin screen save), 192 | * or an update purely from CRUD. 193 | */ 194 | if ( doing_action( 'save_post' ) ) { 195 | $GLOBALS['wpdb']->update( $GLOBALS['wpdb']->posts, $post_data, array( 'ID' => $resource->get_id() ) ); 196 | clean_post_cache( $resource->get_id() ); 197 | } else { 198 | wp_update_post( array_merge( array( 'ID' => $resource->get_id() ), $post_data ) ); 199 | } 200 | $resource->read_meta_data( true ); // Refresh internal meta data, in case things were hooked into `save_post` or another WP hook. 201 | } 202 | $this->update_post_meta( $resource ); 203 | $resource->apply_changes(); 204 | 205 | do_action( 'wcsr_resouce_updated', $resource->get_id() ); 206 | } 207 | 208 | /** 209 | * Deletes a record from the database. 210 | * 211 | * @param WCSR_Resource &$resource 212 | * @param array $args Array of args determine whether to trash or permanently delete the record. 213 | * @return bool result 214 | */ 215 | public function delete( &$resource, $args = array() ) { 216 | $args = wp_parse_args( $args, array( 217 | 'force_delete' => false, 218 | ) ); 219 | 220 | $id = $resource->get_id(); 221 | 222 | if ( ! $id ) { 223 | return; 224 | } 225 | 226 | if ( $args['force_delete'] ) { 227 | wp_delete_post( $id ); 228 | $resource->set_id( 0 ); 229 | do_action( 'wcsr_resouce_deleted', $id ); 230 | } else { 231 | wp_trash_post( $id ); 232 | do_action( 'wcsr_resouce_trashed', $id ); 233 | } 234 | } 235 | 236 | /** 237 | * Get the IDs of all resources from the database for a given subscription/order 238 | * 239 | * @param int $order_id 240 | * @param string $status 241 | * @return array 242 | */ 243 | public function get_resource_ids_for_subscription( $subscription_id, $status = 'any' ) { 244 | 245 | if ( ! in_array( $status, wcsr_get_valid_statuses() ) && 'any' !== $status ) { 246 | throw new InvalidArgumentException( sprintf( '2nd parameter must be a valid resource status. One of: %s.', implode( ',', wcsr_get_valid_statuses() ) ) ); 247 | } 248 | 249 | $resource_post_ids = get_posts( array( 250 | 'posts_per_page' => -1, 251 | 'post_type' => $this->post_type, 252 | 'post_status' => $status, 253 | 'post_parent' => $subscription_id, 254 | 'fields' => 'ids', 255 | 'orderby' => 'ID', 256 | 'order' => 'ASC', 257 | ) ); 258 | 259 | return $resource_post_ids; 260 | } 261 | 262 | /** 263 | * Get the post ID of the resource from the database for a given resouce (using the ID of the resource in 264 | * the 3rd party system, not post ID for it) 265 | * 266 | * @param int $external_id 267 | * @param string $status 268 | * @return int 269 | */ 270 | public function get_resource_id_by_external_id( $external_id, $status = 'wcsr-unended' ) { 271 | $status = ( empty( $status ) || ! in_array( $status, wcsr_get_valid_statuses() ) ) ? 'any' : $status; 272 | 273 | $resource_post_ids = get_posts( array( 274 | 'posts_per_page' => 1, 275 | 'post_type' => $this->post_type, 276 | 'post_status' => $status, 277 | 'fields' => 'ids', 278 | 'orderby' => 'ID', 279 | 'order' => 'ASC', 280 | 'meta_query' => array( 281 | array( 282 | 'key' => 'external_id', 283 | 'value' => $external_id, 284 | ), 285 | ), 286 | ) ); 287 | 288 | $resource_post_id = empty( $resource_post_ids ) ? false : array_pop( $resource_post_ids ); 289 | 290 | return $resource_post_id; 291 | } 292 | 293 | /** 294 | * Helper method that updates all the post meta for a resource based on its settings in the WCSR_Resource class. 295 | * 296 | * @param WCSR_Resource &$resource 297 | * @since 1.0.0 298 | */ 299 | private function update_post_meta( &$resource ) { 300 | 301 | $updated_props = array(); 302 | $meta_key_to_props = array( 303 | 'external_id' => 'external_id', 304 | 'is_pre_paid' => 'is_pre_paid', 305 | 'is_prorated' => 'is_prorated', 306 | 'activation_timestamps' => 'activation_timestamps', 307 | 'deactivation_timestamps' => 'deactivation_timestamps', 308 | ); 309 | 310 | $props_to_update = $this->get_props_to_update( $resource, $meta_key_to_props ); 311 | 312 | foreach ( $props_to_update as $meta_key => $prop ) { 313 | 314 | $value = $resource->{"get_$prop"}( 'edit' ); 315 | 316 | switch ( $prop ) { 317 | 318 | case 'is_pre_paid' : 319 | case 'is_prorated' : 320 | $updated = update_post_meta( $resource->get_id(), $meta_key, wc_bool_to_string( $value ) ); 321 | break; 322 | 323 | // For now, we can keep this as a single row in the DB as it's much easier to manage changes, we may need to separate it later if it grows too long or is found to be corrupted too easily 324 | case 'activation_timestamps' : 325 | case 'deactivation_timestamps' : 326 | $updated = update_post_meta( $resource->get_id(), $meta_key, array_filter( array_map( 'intval', $value ) ) ); 327 | break; 328 | 329 | default : 330 | $updated = update_post_meta( $resource->get_id(), $meta_key, $value ); 331 | break; 332 | } 333 | 334 | if ( $updated ) { 335 | $updated_props[] = $prop; 336 | } 337 | } 338 | 339 | do_action( 'wcsr_resource_object_updated_props', $resource, $updated_props ); 340 | } 341 | 342 | /** 343 | * Get a title for the new post type. 344 | * 345 | * @param WCSR_Resource &$resource 346 | * @since 1.0.0 347 | * @return string 348 | */ 349 | protected function get_post_title( &$resource ) { 350 | /* translators: %s: Order date */ 351 | return sprintf( __( 'Resource – %s', 'woocommerce-subscriptions-resource' ), strftime( _x( '%b %d, %Y @ %I:%M %p', 'Resource date parsed by strftime', 'woocommerce-subscriptions-resource' ), $resource->get_date_created()->getTimestamp() ) ); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /includes/class-wcsr-resource.php: -------------------------------------------------------------------------------- 1 | null, 28 | 'status' => '', 29 | 'external_id' => 0, 30 | 'subscription_id' => 0, 31 | 'is_pre_paid' => true, 32 | 'is_prorated' => false, 33 | 'activation_timestamps' => array(), 34 | 'deactivation_timestamps' => array(), 35 | ); 36 | 37 | /** 38 | * Get an instance for a given resource 39 | * 40 | * @return null 41 | */ 42 | public function __construct( $resource ) { 43 | parent::__construct( $resource ); 44 | 45 | if ( is_numeric( $resource ) && $resource > 0 ) { 46 | $this->set_id( $resource ); 47 | } elseif ( $resource instanceof self ) { 48 | $this->set_id( $resource->get_id() ); 49 | } elseif ( ! empty( $resource->ID ) ) { 50 | $this->set_id( $resource->ID ); 51 | } else { 52 | $this->set_object_read( true ); 53 | } 54 | 55 | $this->data_store = WCSR_Data_Store::store(); 56 | 57 | if ( $this->get_id() > 0 ) { 58 | $this->data_store->read( $this ); 59 | } 60 | } 61 | 62 | /** 63 | * Check whether the resource is paid for before or after each billing period where the benefit of the resource has been consumed. 64 | * 65 | * By default, subscriptions with WooCommerce Subscriptions are always paid in advance; however, a resource can be paid after its benefit has 66 | * been consumed, like a Slack account. Amoung other reasons why this might be used, it allows for proration of the resource's cost to account 67 | * only for those days where it is actually used. 68 | * 69 | * By default, this flag applies to both the initial period in which the sign-up occurs, as well as successive billing periods. 70 | * 71 | * For example, consider a resource charged at $7 / week. If a customer is granted her initial access to the resource on the 4th day of the 72 | * billing schedule, if $is_post_pay is: 73 | * - true, the customer will be charged $3 at the time of sign-up to account for the remaining 3 days during the billing cycle. 74 | * - false, the customer will be charged nothing at the time of sign-up, and will then be charged $3 at the time of the next scheduled payment 75 | * to account for the 3 days the resource was used during the billing cycle. 76 | * 77 | * @return bool 78 | */ 79 | public function get_is_pre_paid( $context = 'view' ) { 80 | return $this->get_prop( 'is_pre_paid', $context ); 81 | } 82 | 83 | /** 84 | * Check whether the resource's cost is prorated to the daily rate of its usage during each billing period. 85 | * 86 | * By default, subscriptions with WooCommerce Subscriptions are always paid in advance in full; however, a resource can be paid after its benefit 87 | * has been consumed by setting the WCS_Resource::$is_pre_paid flag to false. Because this charges for the resource retrospectively, it allows 88 | * for proration of the resource's cost to account only for those days where it is actually used (or at least, active). 89 | * 90 | * @return bool 91 | */ 92 | public function get_is_prorated( $context = 'view' ) { 93 | return $this->get_prop( 'is_prorated', $context ); 94 | } 95 | 96 | /** 97 | * Record the resource's activation 98 | */ 99 | public function activate() { 100 | 101 | $activation_timestamps = $this->get_activation_timestamps(); 102 | $activation_timestamps[] = gmdate( 'U' ); 103 | 104 | $this->set_activation_timestamps( $activation_timestamps ); 105 | } 106 | 107 | /** 108 | * Record the resource's deactivation 109 | */ 110 | public function deactivate() { 111 | 112 | $deactivation_timestamps = $this->get_deactivation_timestamps(); 113 | $deactivation_timestamps[] = gmdate( 'U' ); 114 | 115 | $this->set_deactivation_timestamps( $deactivation_timestamps ); 116 | } 117 | 118 | /** 119 | * Update the resource's status 120 | * 121 | * @since 1.1.0 122 | */ 123 | public function get_status( $context = 'view' ) { 124 | $status = $this->get_prop( 'status', $context ); 125 | 126 | if ( empty( $status ) && 'view' === $context ) { 127 | // In view context, return the default status if no status has been set. 128 | $status = apply_filters( 'wcsr_default_resource_status', 'wcsr-unended', $this ); 129 | } 130 | 131 | return $status; 132 | } 133 | 134 | /** 135 | * Get date_created. 136 | * 137 | * @param string $context 138 | * @return WC_DateTime|NULL object if the date is set or null if there is no date. 139 | */ 140 | public function get_date_created( $context = 'view' ) { 141 | return $this->get_prop( 'date_created', $context ); 142 | } 143 | 144 | /** 145 | * The ID for the subscription this resource is linked to. 146 | * 147 | * @param string $context 148 | * @return int 149 | */ 150 | public function get_subscription_id( $context = 'view' ) { 151 | return $this->get_prop( 'subscription_id', $context ); 152 | } 153 | 154 | /** 155 | * Get ID for the object outside Subscriptions this resource is linked to. 156 | * 157 | * @param string $context 158 | * @return int 159 | */ 160 | public function get_external_id( $context = 'view' ) { 161 | return $this->get_prop( 'external_id', $context ); 162 | } 163 | 164 | /** 165 | * Get an array of timestamps on which this resource was activated 166 | * 167 | * @param string $context 168 | * @return array 169 | */ 170 | public function get_activation_timestamps( $context = 'view' ) { 171 | return $this->get_prop( 'activation_timestamps', $context ); 172 | } 173 | 174 | /** 175 | * Get an array of timestamps on which this resource was deactivated 176 | * 177 | * @param string $context 178 | * @return array 179 | */ 180 | public function get_deactivation_timestamps( $context = 'view' ) { 181 | return $this->get_prop( 'deactivation_timestamps', $context ); 182 | } 183 | 184 | /** 185 | * Determine the number of days between two timestamps where this resource was active 186 | * 187 | * We don't use DateTime::diff() here to avoid gotchas like https://stackoverflow.com/questions/2040560/finding-the-number-of-days-between-two-dates#comment36236581_16177475 188 | * 189 | * @param int $from_timestamp 190 | * @param int $to_timestamp 191 | * @return int 192 | */ 193 | public function get_days_active( $from_timestamp, $to_timestamp = null ) { 194 | $days_active = 0; 195 | 196 | if ( false === $this->has_been_activated() ) { 197 | return $days_active; 198 | } 199 | 200 | if ( is_null( $to_timestamp ) ) { 201 | $to_timestamp = gmdate( 'U' ); 202 | } 203 | 204 | // Find all the activation and deactivation timestamps between the given timestamps 205 | $activation_times = wcsr_get_timestamps_between( $this->get_activation_timestamps(), $from_timestamp, $to_timestamp ); 206 | $deactivation_times = wcsr_get_timestamps_between( $this->get_deactivation_timestamps(), $from_timestamp, $to_timestamp ); 207 | 208 | // if the the first activation time isset and comes after the first deactivation time, make sure we prepend the start timestamp to act as the first "activated" date for the resource 209 | // if the resource was active at $from_timestamp but doesn't have a first activation time, make sure we prepend the start timestamp to act as the first "activated" date for the resource 210 | if ( ( $this->is_active( $from_timestamp ) && ! isset( $activation_times[0] ) || ( isset( $activation_times[0] ) && isset( $deactivation_times[0] ) && $activation_times[0] > $deactivation_times[0] ) ) ) { 211 | $start_timestamp = ( $this->get_date_created()->getTimestamp() > $from_timestamp ) ? $this->get_date_created()->getTimestamp() : $from_timestamp; 212 | 213 | // before setting the start timestamp as the created time or the $from_timestamp make sure the deactivation date doesn't come before it 214 | if ( isset( $deactivation_times[0] ) && $start_timestamp > $deactivation_times[0] ) { 215 | throw new Exception( 'The resource first deactivation date in the period comes before the resource start time or before the beginning of the period. This is invalid.' ); 216 | } 217 | 218 | array_unshift( $activation_times, $start_timestamp ); 219 | } 220 | 221 | foreach ( $activation_times as $i => $activation_time ) { 222 | // If there is corresponding deactivation timestamp, the resource has deactivated before the end of the period so that's the time we want, otherwise, use the end of the period as the resource was still active at end of the period 223 | $deactivation_time = isset( $deactivation_times[ $i ] ) ? $deactivation_times[ $i ] : $to_timestamp; 224 | 225 | // skip over any days that are activated/deactivated on the same 24 hour block and have already been accounted for 226 | if ( $i !== 0 && isset( $deactivation_times[ $i - 1 ] ) && wcsr_is_on_same_day( $deactivation_time, $deactivation_times[ $i - 1 ], $from_timestamp ) ) { 227 | continue; 228 | } 229 | 230 | // Calculate days based on time between 231 | $days_by_time = intval( ceil( ( $deactivation_time - $activation_time ) / DAY_IN_SECONDS ) ); 232 | 233 | // Increase our tally 234 | $days_active += $days_by_time; 235 | 236 | // If days based on time is only 1 but it was "across a 24 hour block" we may need to adjust IF NOT accounted for already 237 | if ( $days_by_time == 1 && ! wcsr_is_on_same_day( $activation_time, $deactivation_time, $from_timestamp ) ) { 238 | 239 | // handle situation if first activation crosses a 24 hour block 240 | if ( $i == 0 && ! wcsr_is_on_same_day( $activation_time, $deactivation_time, $from_timestamp ) ) { 241 | $days_active += 1; 242 | } 243 | 244 | // if this activation didn't start on the same 24 hour block as previous activation it is safe to add an extra day 245 | if ( $i !== 0 && ! wcsr_is_on_same_day( $activation_time, $deactivation_times[ $i - 1 ], $from_timestamp ) ) { 246 | $days_active += 1; 247 | } 248 | } 249 | } 250 | 251 | return $days_active; 252 | } 253 | 254 | /** 255 | * Determine if the resource has ever been activated by checking whether it has at least one activation timestamp 256 | * 257 | * @return bool 258 | */ 259 | public function has_been_activated() { 260 | 261 | $activation_timestamps = $this->get_activation_timestamps(); 262 | 263 | return empty( $activation_timestamps ) ? false : true; 264 | } 265 | 266 | /** 267 | * Based on a resource's activation and deactivation timestamps, determine if the resource is active. 268 | * 269 | * If a timestamp is given, this function will determine if the resource was active at a given time. 270 | * 271 | * @param int $at_timestamp 272 | * @return bool 273 | */ 274 | public function is_active( $at_timestamp = 0 ) { 275 | $is_active = false; 276 | $timestamp = ( empty( $at_timestamp ) ) ? time() : $at_timestamp; 277 | 278 | if ( empty( $timestamp ) || false === $this->has_been_activated() ) { 279 | return $is_active; 280 | } 281 | 282 | $activation_times = $this->get_activation_timestamps(); 283 | $deactivation_times = $this->get_deactivation_timestamps(); 284 | 285 | foreach ( $activation_times as $i => $activation_time ) { 286 | $deactivation_time = isset( $deactivation_times[ $i ] ) ? $deactivation_times[ $i ] : null; 287 | 288 | if ( ! empty( $deactivation_time ) && ( $timestamp >= $activation_time ) && ( $timestamp < $deactivation_time ) ) { 289 | $is_active = true; 290 | break; 291 | } 292 | 293 | if ( empty( $deactivation_time ) && ( $timestamp >= $activation_time ) ) { 294 | $is_active = true; 295 | break; 296 | } 297 | } 298 | 299 | return $is_active; 300 | } 301 | 302 | /** 303 | * Setters 304 | */ 305 | 306 | /** 307 | * The ID of the object in the external system (i.e. system outside Subscriptions) this resource is linked to. 308 | * 309 | * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if there is no date. 310 | * @throws WC_Data_Exception 311 | */ 312 | public function set_date_created( $date ) { 313 | $this->set_date_prop( 'date_created', $date ); 314 | } 315 | 316 | /** 317 | * The ID of the object in the external system (i.e. system outside Subscriptions) this resource is linked to. 318 | * 319 | * @param int|string 320 | */ 321 | public function set_external_id( $external_id ) { 322 | $this->set_prop( 'external_id', $external_id ); 323 | } 324 | 325 | /** 326 | * The ID of the subscription this resource is linked to. 327 | * 328 | * @param int 329 | */ 330 | public function set_subscription_id( $subscription_id ) { 331 | $this->set_prop( 'subscription_id', $subscription_id ); 332 | } 333 | 334 | /** 335 | * Set whether the resource is paid for before or after each billing period. 336 | * 337 | * @param bool 338 | */ 339 | public function set_is_pre_paid( $is_pre_paid ) { 340 | $this->set_prop( 'is_pre_paid', (bool) $is_pre_paid ); 341 | } 342 | 343 | /** 344 | * Set whether the resource's cost is prorated to the daily rate of its usage during each billing period. 345 | * 346 | * @param bool 347 | */ 348 | public function set_is_prorated( $is_prorated ) { 349 | $this->set_prop( 'is_prorated', (bool) $is_prorated ); 350 | } 351 | 352 | /** 353 | * Set the array of timestamps to record all occasions when this resource was activated 354 | * 355 | * @param array $timestamps 356 | */ 357 | public function set_activation_timestamps( $timestamps ) { 358 | $this->set_prop( 'activation_timestamps', $timestamps ); 359 | } 360 | 361 | /** 362 | * Set the array of timestamps to record all occasions when this resource was deactivated 363 | * 364 | * @param array $timestamps 365 | */ 366 | public function set_deactivation_timestamps( $timestamps ) { 367 | $this->set_prop( 'deactivation_timestamps', $timestamps ); 368 | } 369 | 370 | /** 371 | * Set resource status. 372 | * 373 | * @since 1.1.0 374 | * @param string $new_status Status to change the resource to. Either 'wcsr-unended' or 'wcsr-ended'. 375 | * @return array details of change 376 | */ 377 | public function set_status( $new_status ) { 378 | $old_status = $this->get_status(); 379 | 380 | // If setting the status, ensure it's set to a valid status. 381 | if ( true === $this->object_read ) { 382 | // Only allow valid new status 383 | if ( ! in_array( $new_status, wcsr_get_valid_statuses() ) && 'trash' !== $new_status ) { 384 | $new_status = 'wcsr-unended'; 385 | } 386 | 387 | // If the old status is set but unknown (e.g. draft) assume its pending for action usage. 388 | if ( $old_status && ! in_array( $old_status, wcsr_get_valid_statuses() ) && 'trash' !== $old_status ) { 389 | $old_status = 'wcsr-unended'; 390 | } 391 | } 392 | 393 | $this->set_prop( 'status', $new_status ); 394 | 395 | return array( 396 | 'from' => $old_status, 397 | 'to' => $new_status, 398 | ); 399 | } 400 | } 401 | -------------------------------------------------------------------------------- /tests/unit/class-wcsr-time-functions-test.php: -------------------------------------------------------------------------------- 1 | array( 11 | 'from_timestamp' => '2017-09-14 14:21:40', 12 | 'days_in_period' => 30, 13 | 'days_active' => 30, 14 | 'billing_period' => 'month', 15 | 'billing_interval' => 1, 16 | 'expected_ratio' => 1 17 | ), 18 | 19 | // days in period was 61 day, but the billing period is only 1 month (i.e. the last renewal failed, but the subscription was manually activated) 20 | 1 => array( 21 | 'from_timestamp' => '2017-09-14 14:21:40', 22 | 'days_in_period' => 61, 23 | 'days_active' => 61, 24 | 'billing_period' => 'month', 25 | 'billing_interval' => 1, 26 | 'expected_ratio' => 2 27 | ), 28 | 29 | // Resource was active for 45 days in 61 days (i.e. the last renewal failed, but the subscription was manually activated) 30 | 2 => array( 31 | 'from_timestamp' => '2017-09-14 14:21:40', 32 | 'days_in_period' => 61, 33 | 'days_active' => 45, 34 | 'billing_period' => 'month', 35 | 'billing_interval' => 1, 36 | 'expected_ratio' => 1.48 37 | ), 38 | 39 | // if the last payment failed for a monthly subscription and was manually reactivated - resource was active for a total of 45 out of the 61 days 40 | 3 => array( 41 | 'from_timestamp' => '2017-09-14 14:21:40', 42 | 'days_in_period' => 61, 43 | 'days_active' => 45, 44 | 'billing_period' => 'month', 45 | 'billing_interval' => 1, 46 | 'expected_ratio' => 1.48 47 | ), 48 | 49 | // if the last payment failed for a monthly subscription and was manually reactivated - resource remained active for the entire time 50 | 4 => array( 51 | 'from_timestamp' => '2017-09-14 14:21:40', 52 | 'days_in_period' => 61, 53 | 'days_active' => 61, 54 | 'billing_period' => 'month', 55 | 'billing_interval' => 2, 56 | 'expected_ratio' => 1 57 | ), 58 | 59 | // this tests the case where a subscription was cancelled mid way through the month or if the subscription was renewed manually early (for whatever reason) 60 | 5 => array( 61 | 'from_timestamp' => '2017-09-14 14:21:40', 62 | 'days_in_period' => 15, 63 | 'days_active' => 15, 64 | 'billing_period' => 'month', 65 | 'billing_interval' => 1, 66 | 'expected_ratio' => 0.5 67 | ), 68 | 69 | // Months July/August both have 31 days.. so there's a possibility that the total days in period could be 62 70 | 6 => array( 71 | 'from_timestamp' => '2017-07-14 14:21:40', 72 | 'days_in_period' => 62, 73 | 'days_active' => 62, 74 | 'billing_period' => 'month', 75 | 'billing_interval' => 1, 76 | 'expected_ratio' => 2 77 | ), 78 | 79 | // Same test as above but this time the billing cycle of the subscription is every 2 months 80 | 7 => array( 81 | 'from_timestamp' => '2017-07-14 14:21:40', 82 | 'days_in_period' => 62, 83 | 'days_active' => 62, 84 | 'billing_period' => 'month', 85 | 'billing_interval' => 2, 86 | 'expected_ratio' => 1 87 | ), 88 | 89 | // test a monthly subscription renewing across Feb in a non-leap year (28 days) 90 | 8 => array( 91 | 'from_timestamp' => '2017-02-01 14:21:40', 92 | 'days_in_period' => 28, 93 | 'days_active' => 28, 94 | 'billing_period' => 'month', 95 | 'billing_interval' => 1, 96 | 'expected_ratio' => 1 97 | ), 98 | 99 | 9 => array( 100 | 'from_timestamp' => '2017-09-14 14:21:40', 101 | 'days_in_period' => 61, 102 | 'days_active' => 1, 103 | 'billing_period' => 'month', 104 | 'billing_interval' => 1, 105 | 'expected_ratio' => 0.03 106 | ), 107 | 108 | // simple test for 20 active days out of the full month in September 109 | 10 => array( 110 | 'from_timestamp' => '2017-09-14 14:21:40', 111 | 'days_in_period' => 30, 112 | 'days_active' => 20, 113 | 'billing_period' => 'month', 114 | 'billing_interval' => 1, 115 | 'expected_ratio' => 0.67 116 | ), 117 | 118 | 11 => array( 119 | 'from_timestamp' => '2017-09-14 14:21:40', 120 | 'days_in_period' => 61, 121 | 'days_active' => 40, 122 | 'billing_period' => 'month', 123 | 'billing_interval' => 1, 124 | 'expected_ratio' => 1.31 125 | ), 126 | 127 | // if the subscription was active for all of Sept and then deactiveated it for October they should pay $9.00 for Sept, so the ratio should be 1 128 | 12 => array( 129 | 'from_timestamp' => '2017-09-14 14:21:40', 130 | 'days_in_period' => 61, 131 | 'days_active' => 30, 132 | 'billing_period' => 'month', 133 | 'billing_interval' => 1, 134 | 'expected_ratio' => 0.98 // i feel like this should be 1, not .98. i.e. the 30 active days could've been all of September so that should be $9.00 they get charged.. not $8.82 135 | ), 136 | 137 | // If somehow the days active is 31 (this calculastion uses ceil) but only 30 days in the period (floor is used in this calculation), make sure we return 1 138 | 13 => array( 139 | 'from_timestamp' => '2017-09-14 14:21:40', 140 | 'days_in_period' => 30, 141 | 'days_active' => 31, 142 | 'billing_period' => 'month', 143 | 'billing_interval' => 1, 144 | 'expected_ratio' => 1 145 | ), 146 | 147 | // if the monthly subscription renews but only has 15 days active 148 | 14 => array( 149 | 'from_timestamp' => '2017-09-14 14:21:40', 150 | 'days_in_period' => 30, 151 | 'days_active' => 15, 152 | 'billing_period' => 'month', 153 | 'billing_interval' => 1, 154 | 'expected_ratio' => 0.5 155 | ), 156 | 157 | // 31 days in the period from August (full month) 158 | 15 => array( 159 | 'from_timestamp' => '2017-08-14 14:21:40', 160 | 'days_in_period' => 31, 161 | 'days_active' => 31, 162 | 'billing_period' => 'month', 163 | 'billing_interval' => 1, 164 | 'expected_ratio' => 1 165 | ), 166 | 167 | // 31 days in the period from September (full month + 1 day (i.e. maybe the renewal was a day late)) 168 | 16 => array( 169 | 'from_timestamp' => '2017-09-14 14:21:40', 170 | 'days_in_period' => 31, 171 | 'days_active' => 31, 172 | 'billing_period' => 'month', 173 | 'billing_interval' => 1, 174 | 'expected_ratio' => 1.03 // the renewal came late so they should be charged 1 month + 1 day. 175 | ), 176 | 177 | // 0 case - no days in the period and 0 days active (only feasible scenario i can see this ever happening is if the renewal was manually triggered and days in period ends up being less than 1 day (i.e. floor is used so it would be 0), but because we use CEIL when calculating the active days that value could be 1) 178 | 17 => array( 179 | 'from_timestamp' => '2017-09-14 14:21:40', 180 | 'days_in_period' => 0, 181 | 'days_active' => 1, 182 | 'billing_period' => 'month', 183 | 'billing_interval' => 1, 184 | 'expected_ratio' => 0 185 | ), 186 | 187 | // 0 case 188 | 17 => array( 189 | 'from_timestamp' => '2017-09-14 14:21:40', 190 | 'days_in_period' => 0, 191 | 'days_active' => 0, 192 | 'billing_period' => 'month', 193 | 'billing_interval' => 1, 194 | 'expected_ratio' => 0 195 | ), 196 | 197 | // no active days in the period 198 | 18 => array( 199 | 'from_timestamp' => '2017-09-14 14:21:40', 200 | 'days_in_period' => 30, 201 | 'days_active' => 0, 202 | 'billing_period' => 'month', 203 | 'billing_interval' => 1, 204 | 'expected_ratio' => 0 // the renewal came late so they should be charged 1 month + 1 day. 205 | ), 206 | 207 | // monthly subscription, 17 day in period, 15 days active 208 | 19 => array( 209 | 'from_timestamp' => '2017-11-11 10:24:40', 210 | 'days_in_period' => 17, 211 | 'days_active' => 15, 212 | 'billing_period' => 'month', 213 | 'billing_interval' => 1, 214 | 'expected_ratio' => 0.5 // For Jasons recent renewal and prior the latest changes, this value was returning 0.88 as the days active ratio and the result from the line item 0.88 * $9.00 = $7.92 215 | ), 216 | 217 | // a simple test to confirm the logic for minusing off the extra days can be replaced by: ( $days_active / $days_in_period ) * $number_of_billing_periods 218 | 20 => array( 219 | 'from_timestamp' => '2017-09-14 10:24:40', 220 | 'days_in_period' => 170, // just short of 6 months - extra days would've been ~140 221 | 'days_active' => 30, 222 | 'billing_period' => 'month', 223 | 'billing_interval' => 1, 224 | 'expected_ratio' => 0.99 225 | ), 226 | 227 | // Jason's renewal test case - 12 day period, 13 days active, the ratio should account for 12 days because thats the days in period 228 | 21 => array( 229 | 'from_timestamp' => '2017-09-14 14:21:40', 230 | 'days_in_period' => 12, 231 | 'days_active' => 13, 232 | 'billing_period' => 'month', 233 | 'billing_interval' => 1, 234 | 'expected_ratio' => 0.4 235 | ), 236 | 237 | // 32 days active in a 31 days period from August (full month) - this test is not possible now that we have a safe guard so that the active days is never more than the days in period 238 | 22 => array( 239 | 'from_timestamp' => '2017-08-14 14:21:40', 240 | 'days_in_period' => 31, 241 | 'days_active' => 32, 242 | 'billing_period' => 'month', 243 | 'billing_interval' => 1, 244 | 'expected_ratio' => 1 245 | ), 246 | 247 | // 0 days active days active 248 | 23 => array( 249 | 'from_timestamp' => '2017-08-14 14:21:40', 250 | 'days_in_period' => 31, 251 | 'days_active' => 0, 252 | 'billing_period' => 'month', 253 | 'billing_interval' => 1, 254 | 'expected_ratio' => 0 255 | ), 256 | ); 257 | } 258 | 259 | /** 260 | * Make sure get_days_in_period() is calculating the number of days properly 261 | * 262 | * @group days_ratio 263 | * @dataProvider provider_get_active_days_ratio 264 | */ 265 | public function test_get_active_days_ratio( $from_timestamp, $days_in_period, $days_active, $billing_period, $billing_interval, $expected_ratio ) { 266 | $this->assertEquals( $expected_ratio, wcsr_get_active_days_ratio( strtotime( $from_timestamp ), $days_in_period, $days_active, $billing_period, $billing_interval ) ); 267 | } 268 | 269 | /** 270 | * Procide data to test days in period 271 | */ 272 | public function provider_get_days_in_period() { 273 | return array( 274 | // end comes before start 275 | 0 => array( 276 | 'start_timestamp' => strtotime( '2017-09-14 14:21:40' ), 277 | 'end_timestamp' => strtotime( '2017-09-13 14:21:40' ), 278 | 'expected_result' => 0, 279 | ), 280 | 281 | // exactly 1 day 282 | 1 => array( 283 | 'start_timestamp' => strtotime( '2017-09-14 14:21:40' ), 284 | 'end_timestamp' => strtotime( '2017-09-15 14:21:40' ), 285 | 'expected_result' => 1, 286 | ), 287 | 288 | // just before 1 day 289 | 2 => array( 290 | 'start_timestamp' => strtotime( '2017-09-14 14:21:40' ), 291 | 'end_timestamp' => strtotime( '2017-09-15 14:20:40' ), 292 | 'expected_result' => 0, 293 | ), 294 | 295 | // just after 1 day 296 | 3 => array( 297 | 'start_timestamp' => strtotime( '2017-09-14 14:21:40' ), 298 | 'end_timestamp' => strtotime( '2017-09-15 14:22:40' ), 299 | 'expected_result' => 1, 300 | ), 301 | 302 | // standard 1 month renewal period 303 | 4 => array( 304 | 'start_timestamp' => strtotime( '2017-09-14 14:21:40' ), 305 | 'end_timestamp' => strtotime( '2017-10-14 14:26:30' ), 306 | 'expected_result' => 30, 307 | ), 308 | 309 | 5 => array( 310 | 'start_timestamp' => strtotime( '2017-09-14 14:21:40' ), 311 | 'end_timestamp' => strtotime( '2017-09-14 14:21:40' ), 312 | 'expected_result' => 0, 313 | ), 314 | ); 315 | } 316 | 317 | /** 318 | * Make sure get_days_in_period() is calculating the number of days properly 319 | * 320 | * 321 | * @dataProvider provider_get_days_in_period 322 | * @group days_in_period 323 | */ 324 | public function test_get_days_in_period( $start_timestamp, $end_timestamp, $expected_result ) { 325 | $this->assertEquals( $expected_result, wcsr_get_days_in_period( $start_timestamp, $end_timestamp ) ); 326 | } 327 | 328 | /** 329 | * Provide data to whether timestamp is on same day 330 | */ 331 | public function provider_is_on_same_day() { 332 | return array( 333 | 334 | // Exactly start of same day 00:00:00 335 | 0 => array( 336 | 'current_timestamp' => strtotime( '2017-09-14 09:13:14' ), 337 | 'compare_timestamp' => strtotime( '2017-09-14 09:13:14' ), 338 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 339 | 'expected_result' => true, 340 | ), 341 | 342 | // Just within the same day 23:59:59 343 | 1 => array( 344 | 'current_timestamp' => strtotime( '2017-09-15 09:13:13' ), 345 | 'compare_timestamp' => strtotime( '2017-09-14 09:13:14' ), 346 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 347 | 'expected_result' => true, 348 | ), 349 | 350 | // start of the next day 00:00:00 351 | 2 => array( 352 | 'current_timestamp' => strtotime( '2017-09-15 09:13:14' ), 353 | 'compare_timestamp' => strtotime( '2017-09-14 09:13:14' ), 354 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 355 | 'expected_result' => false, 356 | ), 357 | 358 | // move comparison date and increase current second 359 | 3 => array( 360 | 'current_timestamp' => strtotime( '2017-09-20 09:13:14' ), 361 | 'compare_timestamp' => strtotime( '2017-09-19 09:13:14' ), 362 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 363 | 'expected_result' => false, 364 | ), 365 | 366 | // move comparison date and decrease current second 367 | 4 => array( 368 | 'current_timestamp' => strtotime( '2017-09-20 09:13:13' ), 369 | 'compare_timestamp' => strtotime( '2017-09-19 09:13:14' ), 370 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 371 | 'expected_result' => true, 372 | ), 373 | 374 | // move comparison date and increase current minute 375 | 5 => array( 376 | 'current_timestamp' => strtotime( '2017-09-20 09:15:14' ), 377 | 'compare_timestamp' => strtotime( '2017-09-19 09:13:14' ), 378 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 379 | 'expected_result' => false, 380 | ), 381 | 382 | // move comparison date and decrease current minute 383 | 6 => array( 384 | 'current_timestamp' => strtotime( '2017-09-20 09:12:14' ), 385 | 'compare_timestamp' => strtotime( '2017-09-19 09:13:14' ), 386 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 387 | 'expected_result' => true, 388 | ), 389 | 390 | // move comparison date and increase current hour 391 | 7 => array( 392 | 'current_timestamp' => strtotime( '2017-09-20 10:13:14' ), 393 | 'compare_timestamp' => strtotime( '2017-09-19 09:13:14' ), 394 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 395 | 'expected_result' => false, 396 | ), 397 | 398 | // move comparison date and decrease current hour 399 | 8 => array( 400 | 'current_timestamp' => strtotime( '2017-09-20 08:13:14' ), 401 | 'compare_timestamp' => strtotime( '2017-09-19 09:13:14' ), 402 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 403 | 'expected_result' => true, 404 | ), 405 | 406 | // move comparison date, decrease hour and increase current second 407 | 9 => array( 408 | 'current_timestamp' => strtotime( '2017-09-20 09:13:14' ), 409 | 'compare_timestamp' => strtotime( '2017-09-19 08:13:14' ), 410 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 411 | 'expected_result' => false, 412 | ), 413 | 414 | // move comparison date, decrease hour and and decrease current second 415 | 10 => array( 416 | 'current_timestamp' => strtotime( '2017-09-20 09:13:13' ), 417 | 'compare_timestamp' => strtotime( '2017-09-19 08:13:14' ), 418 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 419 | 'expected_result' => false, 420 | ), 421 | 422 | // move comparison date, decrease hour and and increase current minute 423 | 11 => array( 424 | 'current_timestamp' => strtotime( '2017-09-20 09:15:14' ), 425 | 'compare_timestamp' => strtotime( '2017-09-19 08:13:14' ), 426 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 427 | 'expected_result' => false, 428 | ), 429 | 430 | // move comparison date, decrease hour and and decrease current minute 431 | 12 => array( 432 | 'current_timestamp' => strtotime( '2017-09-20 09:12:14' ), 433 | 'compare_timestamp' => strtotime( '2017-09-19 08:13:14' ), 434 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 435 | 'expected_result' => false, 436 | ), 437 | 438 | // move comparison date, decrease hour and and increase current hour 439 | 13 => array( 440 | 'current_timestamp' => strtotime( '2017-09-20 10:13:14' ), 441 | 'compare_timestamp' => strtotime( '2017-09-19 08:13:14' ), 442 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 443 | 'expected_result' => false, 444 | ), 445 | 446 | // move comparison date, decrease hour and set hour to just outside of 1 day (00:00:00) 447 | 14 => array( 448 | 'current_timestamp' => strtotime( '2017-09-20 08:13:14' ), 449 | 'compare_timestamp' => strtotime( '2017-09-19 08:13:14' ), 450 | 'start_timestamp' => strtotime( '2017-09-14 09:13:14' ), 451 | 'expected_result' => false, 452 | ), 453 | ); 454 | } 455 | 456 | /** 457 | * Make sure is_on_same_day() works 458 | * 459 | * @dataProvider provider_is_on_same_day 460 | * @group is_on_same_day 461 | */ 462 | public function test_is_on_same_day( $current_timestamp, $compare_timestamp, $start_timestamp, $expected_result ) { 463 | $this->assertEquals( $expected_result, wcsr_is_on_same_day( $current_timestamp, $compare_timestamp, $start_timestamp ) ); 464 | } 465 | } 466 | -------------------------------------------------------------------------------- /tests/unit/class-wcsr-resource-test.php: -------------------------------------------------------------------------------- 1 | array( 33 | 'date_created' => '2017-09-14 09:13:14', // same as $from_timestamp 34 | 'activation_times' => array( '2017-09-14 09:13:14' ), // same as $from_timestamp 35 | 'deactivation_times' => array( '2017-09-20 11:01:40' ), 36 | 'expected_days_active' => 7, 37 | ), 38 | 39 | /* 40 | * Simulate an existing active resource that is active for 10 days at the start of its 2nd cycle. 41 | * 42 | * To test this requires a resource that is: 43 | * 0. created prior to the start of the period being checked ($creation_time < $from_timestamp) 44 | * 1. activate at the start of the period being checked 45 | * 2. active for at least 1 second more than 9 * 24 * 60 during the period then deactivated 46 | */ 47 | 1 => array( 48 | 'date_created' => '2017-08-14 09:13:14', // 1 month prior to $from_timestamp 49 | 'activation_times' => array( '2017-08-14 09:13:14' ), 50 | 'deactivation_times' => array( '2017-09-23 11:13:40' ), 51 | 'expected_days_active' => 10, 52 | ), 53 | 54 | /* 55 | * Simulate an existing inactive resource that is active for 10 days in the middle of its 2nd cycle. 56 | * 57 | * To test this requires a resource that is: 58 | * 0. created prior to the start of the period being checked ($creation_time < $from_timestamp) 59 | * 1. first activation timestamp after the start time of the period being checked ($activation_times[0] > $from_timestamp) 60 | * 2. active for at least 1 second more than 9 * 24 * 60 during the period then deactivated 61 | */ 62 | 2 => array( 63 | 'date_created' => '2017-08-14 09:13:14', // 1 month prior to $from_timestamp 64 | 'activation_times' => array( '2017-09-24 09:13:14' ), // 10 days after $from_timestamp 65 | 'deactivation_times' => array( '2017-10-03 11:13:40' ), // 9 days, 2 hours, 26 seconds after activation timestamp 66 | 'expected_days_active' => 10, 67 | ), 68 | 69 | /* 70 | * Simulate a new resource that is active for multiple different periods during its first cycle with a total of 10 days. 71 | * 72 | * To test this requires a resource that is: 73 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 74 | * 1. activate at the time it is created 75 | * 2. active for 2 days, then deactivated for 2 days 76 | * 3. active for 2 days, then deactivated for 2 days 77 | * 4. active for 2 days, then deactivated for 2 days 78 | * 5. active for 2 days, then deactivated for 2 days 79 | * 6. activated again for the rest of the cycle 80 | */ 81 | 3 => array( 82 | 'date_created' => '2017-09-14 09:13:14', 83 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-18 09:13:14', '2017-09-22 09:13:14', '2017-10-26 09:13:14', '2017-10-30 09:14:02' ), 84 | 'deactivation_times' => array( '2017-09-16 09:13:13', '2017-09-20 09:13:13', '2017-09-24 09:13:13', '2017-10-28 09:13:13' ), 85 | 'expected_days_active' => 6, 86 | ), 87 | 88 | /* 89 | * Simulate a new resource that is active for multiple different periods during its first cycle with a total of 10 days. 90 | * 91 | * To test this requires a resource that is: 92 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 93 | * 1. activate at the time it is created 94 | * 2. active for at least 1 second more than 4 * 24 * 60, then deactivated 95 | * 3. actived again for at least 1 second more than 4 * 24 * 60, then deactivated before the end of the cycle ($to_timestamp) 96 | */ 97 | 4 => array( 98 | 'date_created' => '2017-09-14 09:13:14', 99 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-20 09:13:14' ), 100 | 'deactivation_times' => array( '2017-09-18 09:13:15', '2017-09-24 09:13:15' ), 101 | 'expected_days_active' => 10, 102 | ), 103 | 104 | /* 105 | * Simulate an existing active resource that is active for multiple different occasions during its 2nd cycle for a total of 10 days. 106 | * 107 | * To test this requires a resource that is: 108 | * 0. created prior to the start of the period being checked ($creation_time < $from_timestamp) 109 | * 1. active at the start of the period being checked 110 | * 2. active for at least 1 second more than 4 * 24 * 60, then deactivated 111 | * 3. activated again for at least 1 second more than 4 * 24 * 60, then deactivated before the end of the cycle ($to_timestamp) 112 | */ 113 | 5 => array( 114 | 'date_created' => '2017-08-14 09:13:14', // 1 month prior to $from_timestamp 115 | 'activation_times' => array( '2017-09-30 09:13:14' ), // previously activated in the last cycle 116 | 'deactivation_times' => array( '2017-09-18 10:14:15', '2017-10-04 12:24:10' ), 117 | 'expected_days_active' => 10, 118 | ), 119 | 120 | /* 121 | * Simulate an existing inactive resource that is actived for multiple different occasions during its 2nd cycle for a total of 10 days. 122 | * 123 | * To test this requires a resource that is: 124 | * 0. created prior to the start of the period being checked ($creation_time < $from_timestamp) 125 | * 1. inactive at the start of the period being checked 126 | * 2. activated for at least 1 second more than 4 * 24 * 60, then deactivated more than 5 * 24 * 60 before the end of the cycle ($to_timestamp) 127 | * 3. activated again for at least 1 second more than 4 * 24 * 60, then deactivated before the end of the cycle ($to_timestamp) 128 | */ 129 | 6 => array( 130 | 'date_created' => '2017-08-14 09:13:14', // 1 month prior to $from_timestamp 131 | 'activation_times' => array( '2017-09-26 09:13:14', '2017-10-05 09:13:14' ), // previously activated in the last cycle 132 | 'deactivation_times' => array( '2017-09-30 15:35:43', '2017-10-09 12:24:10' ), 133 | 'expected_days_active' => 10, 134 | ), 135 | 136 | /* 137 | * Simulate a new active resource that is activated and deactivated for multiple occasions on the same day. 138 | * 139 | * To test this requires a resource that is: 140 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 141 | * 1. activate at the time it is created 142 | * 2. activated for just over an hour, then deactivated for 2'ish hours 143 | * 3. activated 4 hours then deactivated for the rest of the cycle 144 | */ 145 | 7 => array( 146 | 'date_created' => '2017-09-14 09:13:14', 147 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-14 13:13:14' ), 148 | 'deactivation_times' => array( '2017-09-14 10:35:43', '2017-09-14 17:24:10' ), 149 | 'expected_days_active' => 1, 150 | ), 151 | 152 | /* 153 | * Simulate a new active resource that is active for the full cycle 154 | * 155 | * To test this requires a resource that is: 156 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 157 | */ 158 | 8 => array( 159 | 'date_created' => '2017-09-14 09:13:14', // same as $from_timestamp 160 | 'activation_times' => array( '2017-09-14 09:13:14' ), 161 | 'deactivation_times' => array(), 162 | 'expected_days_active' => 31, 163 | ), 164 | 165 | /* 166 | * Simulate a resource that has never been activated or deactivated 167 | * 168 | * To test this requires a resource that is: 169 | * 0. created prior to the start of the period being checked ($creation_time < $from_timestamp) 170 | * 1. active before the start of the period being checked 171 | */ 172 | 9 => array( 173 | 'date_created' => '2017-08-14 09:13:14', 174 | 'activation_times' => array(), 175 | 'deactivation_times' => array(), 176 | 'expected_days_active' => 0, 177 | ), 178 | 179 | /* 180 | * Simulate a new active resource that is activated and deactivated for multiple occasions on the same day and then left active for 2 days. 181 | * 182 | * To test this requires a resource that is: 183 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 184 | * 1. activate at the time it is created 185 | * 2. activated for just over an hour, then deactivated for 2'ish hours 186 | * 3. activated 4 hours then deactivated for the rest of the cycle 187 | */ 188 | 10 => array( 189 | 'date_created' => '2017-09-14 09:13:14', 190 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-14 13:13:14', '2017-09-14 20:00:03' ), 191 | 'deactivation_times' => array( '2017-09-14 10:35:43', '2017-09-14 17:24:10', '2017-09-16 17:24:10' ), 192 | 'expected_days_active' => 3, 193 | ), 194 | 195 | /* 196 | * Simulate a new active resource that is activated and deactivated for multiple occasions on the same day and then left active for the rest of the month. 197 | * 198 | * To test this requires a resource that is: 199 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 200 | * 1. activate at the time it is created 201 | * 2. activated for just over an hour, then deactivated for 2'ish hours 202 | * 3. activated for the rest of the cycle 203 | */ 204 | 11 => array( 205 | 'date_created' => '2017-09-14 09:13:14', 206 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-14 13:13:14', '2017-09-14 20:00:03' ), 207 | 'deactivation_times' => array( '2017-09-14 10:35:43', '2017-09-14 17:24:10' ), 208 | 'expected_days_active' => 31, 209 | ), 210 | 211 | /* 212 | * Simulate a new active resource that is activated and deactivated for multiple occasions on the same day and then left inactive for the rest of the month. 213 | * 214 | * To test this requires a resource that is: 215 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 216 | * 1. activate at the time it is created 217 | * 2. activated for just over an hour, then deactivated for 2'ish hours 218 | * 3. activated 4 hours then deactivated for the rest of the period 219 | */ 220 | 12 => array( 221 | 'date_created' => '2017-09-14 09:13:14', 222 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-14 13:13:14' ), 223 | 'deactivation_times' => array( '2017-09-14 10:35:43', '2017-09-14 17:24:10' ), 224 | 'expected_days_active' => 1, 225 | ), 226 | 227 | /* 228 | * Simulate a new active resource that is activated and deactivated for multiple occasions everyday for 3 days and then left inactive for the rest of the month. 229 | * 230 | * To test this requires a resource that is: 231 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 232 | * 1. activate at the time it is created 233 | * 2. activated for just over an hour, then deactivated for 2'ish hours 234 | * 3. activated 4 hours then deactivated for the rest of the period 235 | */ 236 | 13 => array( 237 | 'date_created' => '2017-09-14 09:13:14', 238 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-14 13:13:14', '2017-09-15 09:13:14', '2017-09-15 13:13:14', '2017-09-16 09:13:14', '2017-09-16 13:13:14' ), 239 | 'deactivation_times' => array( '2017-09-14 10:35:43', '2017-09-14 17:24:10', '2017-09-15 10:35:43', '2017-09-15 17:24:10', '2017-09-16 10:35:43', '2017-09-16 17:24:10' ), 240 | 'expected_days_active' => 3, 241 | ), 242 | 243 | /* 244 | * Simulate an existing resource that is activated roughly 12 hours into the first day then left activated 245 | * 246 | * To test this requires a resource that is: 247 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 248 | * 1. activate at the time it is created 249 | * 2. activated for just over an hour, then deactivated for 2'ish hours 250 | * 3. activated for the rest of the cycle 251 | */ 252 | 14 => array( 253 | 'date_created' => '2017-08-14 09:13:14', 254 | 'activation_times' => array( '2017-09-14 20:00:03' ), 255 | 'deactivation_times' => array(), 256 | 'expected_days_active' => 30, 257 | ), 258 | 259 | /* 260 | * Simulate an existing resource that is activated for 1 hour into the first day then left activated 261 | * 262 | * To test this requires a resource that is: 263 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 264 | * 1. activate at the time it is created 265 | * 2. activated for just over an hour, then deactivated for 2'ish hours 266 | * 3. activated for the rest of the cycle 267 | */ 268 | 15 => array( 269 | 'date_created' => '2017-06-14 09:13:14', 270 | 'activation_times' => array( '2017-09-14 20:00:03', '2017-09-14 22:00:03' ), 271 | 'deactivation_times' => array( '2017-09-14 21:00:03', '2017-09-15 01:15:11' ), 272 | 'expected_days_active' => 1, 273 | ), 274 | 275 | /* 276 | * Simulate an existing resource that is activated for 5ish hours crossing into the next day and then left inactive. 277 | * Same overall time as Test 15 (without the multiple activating and deactiving on the same day) 278 | * 279 | * To test this requires a resource that is: 280 | * 0. created prior to the start of the period being checked ($creation_time < $from_timestamp) 281 | * 1. activated for 5 hours, then deactivatd for the rest of the cycle 282 | */ 283 | 16 => array( 284 | 'date_created' => '2017-06-14 09:13:14', 285 | 'activation_times' => array( '2017-09-14 20:00:03' ), 286 | 'deactivation_times' => array( '2017-09-15 01:15:11' ), 287 | 'expected_days_active' => 1, 288 | ), 289 | 290 | /* 291 | * Simulate an new resource that is activated for 1 day at the start, then deactivated until the end of the month, then activated and deactivated across a 4hour period. 292 | * Similar to Test 15, but this test has multiple activations and deactivations at the end of the test and is also active at the beginning. 293 | * 294 | * To test this requires a resource that is: 295 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 296 | * 1. activate at the time it is created for 1 day, then deactivated 297 | * 2. activated for a just under 3 hours across a 4 hour period, then deactivatd for the rest of the cycle 298 | */ 299 | 17 => array( 300 | 'date_created' => '2017-09-14 09:13:14', 301 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-30 07:00:03', '2017-09-30 09:00:03' ), 302 | 'deactivation_times' => array( '2017-09-15 09:13:13', '2017-09-30 08:15:11', '2017-09-30 11:00:03' ), 303 | 'expected_days_active' => 3, 304 | ), 305 | 306 | /* 307 | * Simulate an new resource that is activated for 1 day, then deactivated until the end of the month for 4 hours. 308 | * This test is the same as Test 17 (without the multiple activating and deactiving on the same day) 309 | * 310 | * To test this requires a resource that is: 311 | * 0. created at the same time as the start of the period being checked ($from_timestamp) 312 | * 1. activate at the time it is created for 1 day, then deactivated 313 | * 2. activated for 4 hours at the end of the cycle, then deactivatd for the rest of the cycle 314 | */ 315 | 18 => array( 316 | 'date_created' => '2017-09-14 09:13:14', 317 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-30 07:00:03' ), 318 | 'deactivation_times' => array( '2017-09-15 09:13:13', '2017-09-30 11:00:03' ), 319 | 'expected_days_active' => 3, 320 | ), 321 | 322 | 19 => array( 323 | 'date_created' => '2017-09-14 09:13:14', 324 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-15 09:12:03', '2017-09-30 09:00:03' ), 325 | 'deactivation_times' => array( '2017-09-15 09:00:13', '2017-09-30 08:15:11', '2017-09-30 11:00:03' ), 326 | 'expected_days_active' => 17, 327 | ), 328 | 329 | 20 => array( 330 | 'date_created' => '2017-09-14 09:13:14', 331 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-15 09:12:03', '2017-09-30 09:00:03', '2017-10-01 08:00:03' ), 332 | 'deactivation_times' => array( '2017-09-15 09:00:13', '2017-09-30 08:15:11', '2017-09-30 11:00:03', '2017-10-01 10:00:03' ), 333 | 'expected_days_active' => 18, 334 | ), 335 | 336 | 21 => array( 337 | 'date_created' => '2017-09-14 09:13:14', 338 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-15 09:12:03', '2017-09-30 09:00:03', '2017-10-02 08:00:03' ), 339 | 'deactivation_times' => array( '2017-09-15 09:00:13', '2017-09-30 08:15:11', '2017-09-30 11:00:03', '2017-10-02 10:00:03' ), 340 | 'expected_days_active' => 19, 341 | ), 342 | 343 | 344 | 22 => array( 345 | 'date_created' => '2017-09-14 09:13:14', 346 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-15 09:12:03', '2017-09-30 09:00:03', '2017-10-02 09:30:03' ), 347 | 'deactivation_times' => array( '2017-09-15 09:00:13', '2017-09-30 08:15:11', '2017-09-30 11:00:03', '2017-10-02 10:00:03' ), 348 | 'expected_days_active' => 18, 349 | ), 350 | 351 | 23 => array( 352 | 'date_created' => '2017-09-14 09:13:14', 353 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-16 09:16:03', '2017-09-30 09:00:03', '2017-10-02 09:30:03' ), 354 | 'deactivation_times' => array( '2017-09-15 09:00:13', '2017-09-30 08:15:11', '2017-09-30 11:00:03', '2017-10-02 10:00:03' ), 355 | 'expected_days_active' => 17, 356 | ), 357 | 358 | // Demonstrates most basic difference between comparing same day based on calendar days vs 24 hour periods 359 | // Branch issue_11 picks this up as 2 days 360 | // This Branch picks this up as 1 day 361 | 24 => array( 362 | 'date_created' => '2017-09-14 09:13:14', 363 | 'activation_times' => array( '2017-09-14 09:13:14', '2017-09-15 01:00:00', ), 364 | 'deactivation_times' => array( '2017-09-14 21:13:13', '2017-09-15 03:00:00', ), 365 | 'expected_days_active' => 1, 366 | ), 367 | 368 | // First activation is on a different day to start AND crosses a "day" 369 | 25 => array( 370 | 'date_created' => '2017-09-16 08:13:14', 371 | 'activation_times' => array( '2017-09-16 08:13:14' ), 372 | 'deactivation_times' => array( '2017-09-16 10:13:14' ), 373 | 'expected_days_active' => 2, 374 | ), 375 | 376 | // First activation is on a different day to start AND crosses a "day", plus another activation that crosses same day 377 | 26 => array( 378 | 'date_created' => '2017-09-16 08:13:14', 379 | 'activation_times' => array( '2017-09-16 08:13:14', '2017-09-17 08:13:14' ), 380 | 'deactivation_times' => array( '2017-09-16 10:13:14', '2017-09-18 08:13:14' ), 381 | 'expected_days_active' => 3, 382 | ), 383 | 384 | // First activation is on a different day to start AND crosses a "day", plus activation that doesn't cross 2 days 385 | 27 => array( 386 | 'date_created' => '2017-09-16 08:13:14', 387 | 'activation_times' => array( '2017-09-16 08:13:14', '2017-09-17 10:13:14' ), 388 | 'deactivation_times' => array( '2017-09-16 10:13:14', '2017-09-18 08:13:14' ), 389 | 'expected_days_active' => 3, 390 | ), 391 | 392 | // First activation is on a different day to start AND crosses a "day", plus more activations which also crosses 2 days 393 | 28 => array( 394 | 'date_created' => '2017-09-16 08:13:14', 395 | 'activation_times' => array( '2017-09-16 08:13:14', '2017-09-18 08:13:14' ), 396 | 'deactivation_times' => array( '2017-09-16 10:13:14', '2017-09-18 10:13:14' ), 397 | 'expected_days_active' => 4, 398 | ), 399 | 400 | // First activation timestamp in array falls out of current renewal period. This is testing the wcsr_get_timestamps_between() function returns `2017-09-15 09:13:14` as the 0th key 401 | 29 => array( 402 | 'date_created' => '2017-07-14 09:13:14', 403 | 'activation_times' => array( '2017-07-14 08:13:14', '2017-09-15 09:13:14' ), 404 | 'deactivation_times' => array( '2017-10-14 09:14:02' ), 405 | 'expected_days_active' => 30, 406 | ), 407 | 408 | // Test for an empty deactivated_timestamps[$i -1] value - this is an impossible case without a bug being in place, i.e you need activation_times[1] to be set with no deactivated_timestamps[0]. This should be impossible because activation_times[1] should never exist without at least deactivation_times[0] between two activation_times 409 | 30 => array( 410 | 'date_created' => '2017-07-14 09:13:14', 411 | 'activation_times' => array( 1 => '2017-09-15 09:13:14' ), 412 | 'deactivation_times' => array(), 413 | 'expected_days_active' => 30, 414 | ), 415 | 416 | // A test case testing a 6month old subscription with a store that has multiple activations/deactivations at the beginning then left active 417 | 31 => array( 418 | 'date_created' => '2017-03-14 09:13:14', 419 | 'activation_times' => array( 420 | '2017-03-14 09:13:14', // 14th MAR 421 | '2017-03-30 20:13:14', // 30th MAR 422 | '2017-05-05 20:13:14', // 5th MAY 423 | ), 424 | 'deactivation_times' => array( 425 | '2017-03-20 08:13:50', // 20th MAR 426 | '2017-04-01 20:13:14', // 1st APR 427 | ), 428 | 'expected_days_active' => 31, 429 | ), 430 | 431 | // A test case testing a 6month old subscription with a store that has multiple activations/deactivations thoughout, including the current period 432 | 32 => array( 433 | 'date_created' => '2017-03-14 09:13:14', 434 | 'activation_times' => array( 435 | '2017-03-14 09:13:14', // 14th MAR 436 | '2017-03-30 20:13:14', // 30th MAR 437 | '2017-05-05 20:13:14', // 5th MAY 438 | '2017-07-01 14:19:40', // 1st JUL 439 | '2017-07-05 14:19:40', // 5th JUL 440 | '2017-08-17 14:19:40', // 17th AUG 441 | '2017-10-10 14:19:41', // 10th OCT (activated for 3 days) 442 | ), 443 | 'deactivation_times' => array( 444 | '2017-03-20 08:13:50', // 20th MAR 445 | '2017-04-01 20:13:14', // 1st APR 446 | '2017-06-30 20:13:14', // 30th JUN 447 | '2017-07-02 14:19:40', // 2nd JUL 448 | '2017-08-15 14:19:40', // 15th AUG 449 | '2017-09-19 14:19:40', // 19th SEPT (deactivated for the first time 5/6 days into current period) 450 | '2017-10-13 14:19:40', // 13th OCT 451 | ), 452 | 'expected_days_active' => 9, 453 | ), 454 | 455 | // Tests for having 0 deactivation times between the from and to timestamps, and the second activation timestamp (at index 1) being the only timestamp within the from and to timestamps. 456 | 33 => array( 457 | 'date_created' => '2017-08-14 09:13:14', 458 | 'activation_times' => array( '2017-08-14 09:13:14', '2017-09-20 09:13:14' ), 459 | 'deactivation_times' => array( '2017-09-01 09:13:14' ), 460 | 'expected_days_active' => 25, 461 | ), 462 | 463 | // Tests for having 0 days active 464 | 34 => array( 465 | 'date_created' => '2017-08-14 09:13:14', 466 | 'activation_times' => array( '2017-08-14 09:13:14' ), 467 | 'deactivation_times' => array( '2017-08-15 09:13:14' ), 468 | 'expected_days_active' => 0, 469 | ), 470 | ); 471 | } 472 | 473 | /** 474 | * Make sure get_days_active() handles all calculation scenarios 475 | * 476 | * @dataProvider provider_get_days_active 477 | */ 478 | public function test_get_days_active( $date_created_string, $activation_times, $deactivation_times, $expected_days_active ) { 479 | 480 | $date_created = new DateTime(); 481 | $date_created->setTimestamp( strtotime( $date_created_string ) ); 482 | 483 | // Convert activation/deactivate dates to timestamps 484 | $activation_times = array_map( 'strtotime', $activation_times ); 485 | $deactivation_times = array_map( 'strtotime', $deactivation_times ); 486 | 487 | $resource_mock = $this->getMockBuilder( 'WCSR_Resource' )->setMethods( array( 'get_date_created', 'has_been_activated', 'get_activation_timestamps', 'get_deactivation_timestamps' ) )->disableOriginalConstructor()->getMock(); 488 | $resource_mock->expects( $this->any() )->method( 'get_date_created' )->will( $this->returnValue( $date_created ) ); 489 | $resource_mock->expects( $this->any() )->method( 'has_been_activated' )->will( $this->returnValue( true ) ); 490 | $resource_mock->expects( $this->any() )->method( 'get_activation_timestamps' )->will( $this->returnValue( $activation_times ) ); 491 | $resource_mock->expects( $this->any() )->method( 'get_deactivation_timestamps' )->will( $this->returnValue( $deactivation_times ) ); 492 | 493 | $this->assertEquals( $expected_days_active, $resource_mock->get_days_active( self::$from_timestamp, self::$to_timestamp ) ); 494 | } 495 | 496 | /** 497 | * Provide data to test is active 498 | */ 499 | public function provider_is_active() { 500 | return array( 501 | // test the first renewal case.. i.e check that the $at_timestamp is the same as the first activation timestamp 502 | 0 => array( 503 | 'activation_times' => array( '2017-09-14 09:13:14' ), 504 | 'deactivation_times' => array( '2017-10-14 11:01:40' ), 505 | 'at_timestamp' => strtotime( '2017-09-14 09:13:14' ), 506 | 'expected_is_active' => true, 507 | ), 508 | 509 | // active at time, but deactivate one day after $at_timestamp 510 | 1 => array( 511 | 'activation_times' => array( '2018-07-20 09:13:14' ), 512 | 'deactivation_times' => array( '2018-08-19 11:01:40' ), 513 | 'at_timestamp' => strtotime( '2018-08-18 09:13:14' ), 514 | 'expected_is_active' => true, 515 | ), 516 | 517 | // Active for a day then deactivated for three months, then activated again. This test is checking in the middle of the two active periods whether 518 | 2 => array( 519 | 'activation_times' => array( '2018-07-10 09:13:14', '2018-10-20 10:20:20' ), 520 | 'deactivation_times' => array( '2018-07-11 11:01:40', '2018-10-30 10:30:12' ), 521 | 'at_timestamp' => strtotime( '2018-09-18 09:13:14' ), 522 | 'expected_is_active' => false, 523 | ), 524 | 525 | // Active for a day then deactivated for three months, then activated again. This test is checking in the middle of the two active periods whether 526 | 3 => array( 527 | 'activation_times' => array( '2018-07-20 09:13:14', '2018-10-20 10:20:20' ), 528 | 'deactivation_times' => array( '2018-07-21 11:01:40' ), 529 | 'at_timestamp' => strtotime( '2018-08-18 09:13:14' ), // not active now, but will be active in two days 530 | 'expected_is_active' => false, 531 | ), 532 | 533 | // Check if a resource is active at a specific time, when the resource has never been activated. 534 | 4 => array( 535 | 'activation_times' => array(), 536 | 'deactivation_times' => array(), 537 | 'at_timestamp' => strtotime( '2018-08-18 09:13:14' ), // not active now, but will be active in two days 538 | 'expected_is_active' => false, 539 | ), 540 | 541 | // Check if a resource is active at the current time, when the resource has never been activated. 542 | 5 => array( 543 | 'activation_times' => array(), 544 | 'deactivation_times' => array(), 545 | 'at_timestamp' => null, 546 | 'expected_is_active' => false, 547 | ), 548 | 549 | // Check if a resource is active at the current time, when the resource has never been deactivated. 550 | 6 => array( 551 | 'activation_times' => array( '2017-07-20 09:13:14' ), 552 | 'deactivation_times' => array(), 553 | 'at_timestamp' => null, 554 | 'expected_is_active' => true, 555 | ), 556 | 557 | // Check if a resource is active at the current time, when the resource was activated for 1 minute in the past 558 | 7 => array( 559 | 'activation_times' => array( '2017-07-20 09:13:14' ), 560 | 'deactivation_times' => array( '2017-07-20 09:14:50' ), 561 | 'at_timestamp' => null, 562 | 'expected_is_active' => false, 563 | ), 564 | 565 | // Check if a resource is active at time that is before it was even first activated 566 | 8 => array( 567 | 'activation_times' => array( '2017-07-20 09:13:14' ), 568 | 'deactivation_times' => array( '2017-07-20 09:14:50' ), 569 | 'at_timestamp' => strtotime( '2017-06-20 09:14:50' ), 570 | 'expected_is_active' => false, 571 | ), 572 | 573 | // Check if a resource is active at some time in the future when the resource was only activated for 1 minute in the past 574 | 9 => array( 575 | 'activation_times' => array( '2017-07-20 09:13:14' ), 576 | 'deactivation_times' => array( '2017-07-20 09:14:50' ), 577 | 'at_timestamp' => strtotime( '2018-08-10 10:10:10' ), 578 | 'expected_is_active' => false, 579 | ), 580 | 581 | // Test if a resource was active at the exact same time that it was deactivated 582 | 10 => array( 583 | 'activation_times' => array( '2017-07-20 09:13:14' ), 584 | 'deactivation_times' => array( '2017-07-20 09:14:50' ), 585 | 'at_timestamp' => strtotime( '2017-07-20 09:14:50' ), 586 | 'expected_is_active' => false, 587 | ), 588 | 589 | // Test if a resource was active 1 second before it was deactivated 590 | 11 => array( 591 | 'activation_times' => array( '2017-07-20 09:13:14' ), 592 | 'deactivation_times' => array( '2017-07-20 09:14:50' ), 593 | 'at_timestamp' => strtotime( '2017-07-20 09:14:49' ), 594 | 'expected_is_active' => true, 595 | ), 596 | 597 | // Check if a resource was active at time that is before it was even first activated 598 | 12 => array( 599 | 'activation_times' => array( '2017-07-20 09:13:14' ), 600 | 'deactivation_times' => array(), 601 | 'at_timestamp' => strtotime( '2017-06-20 09:14:50' ), 602 | 'expected_is_active' => false, 603 | ), 604 | ); 605 | } 606 | 607 | /** 608 | * Test case for $resource->is_active() 609 | * 610 | * @dataProvider provider_is_active 611 | */ 612 | public function test_is_active( $activation_times, $deactivation_times, $at_timestamp, $expected_is_active ) { 613 | // Convert activation/deactivate dates to timestamps 614 | $activation_times = array_map( 'strtotime', $activation_times ); 615 | $deactivation_times = array_map( 'strtotime', $deactivation_times ); 616 | 617 | $resource_mock = $this->getMockBuilder( 'WCSR_Resource' )->setMethods( array( 'has_been_activated', 'get_activation_timestamps', 'get_deactivation_timestamps' ) )->disableOriginalConstructor()->getMock(); 618 | $resource_mock->expects( $this->any() )->method( 'has_been_activated' )->will( $this->returnValue( true ) ); 619 | $resource_mock->expects( $this->any() )->method( 'get_activation_timestamps' )->will( $this->returnValue( $activation_times ) ); 620 | $resource_mock->expects( $this->any() )->method( 'get_deactivation_timestamps' )->will( $this->returnValue( $deactivation_times ) ); 621 | 622 | $this->assertEquals( $expected_is_active, $resource_mock->is_active( $at_timestamp ) ); 623 | } 624 | } 625 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . --------------------------------------------------------------------------------