PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / api / coupon.php

coupon.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/api/coupon.php

228 lines 7.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace StoreEngine\API;
4
5 use stdClass;
6 use StoreEngine\Classes\Exceptions\StoreEngineException;
7 use StoreEngine\Utils\Formatting;
8 use StoreEngine\Utils\Helper;
9 use WP_Error;
10 use WP_REST_Request;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit;
14 }
15
16 class Coupon {
17
18 /**
19 * Derived, queryable copy of the coupon end date/time as a single unix
20 * timestamp. The authoritative value (`_storeengine_coupon_end_date_time`)
21 * is a serialized `{date, time, timezone}` object, which cannot be compared
22 * in a meta_query — so we mirror it into this scalar on every save and
23 * filter the admin list against it. `0` means "no end date" (never expires).
24 */
25 const END_TS_META = '_storeengine_coupon_end_ts';
26
27 /**
28 * Meta storing which time mode a coupon uses. `set_time_limit` means it has a
29 * start/end window; anything else (default `forever_time`, or unset on legacy
30 * coupons) means it is open-ended.
31 */
32 const TIME_TYPE_META = '_storeengine_coupon_time_type';
33
34 public static function init() {
35 $self = new self();
36 add_filter( 'rest_pre_insert_storeengine_coupon', [ $self, 'validate_request_data' ], 10, 2 );
37
38 // Keep the queryable end-timestamp mirror in sync on create/update.
39 add_action( 'rest_after_insert_' . Helper::COUPON_POST_TYPE, [ $self, 'sync_end_timestamp' ], 10, 1 );
40
41 // Expose the `se_expiry` filter on the coupon REST collection and turn it
42 // into a meta_query against the mirrored timestamp.
43 add_filter( 'rest_' . Helper::COUPON_POST_TYPE . '_collection_params', [ $self, 'register_expiry_param' ] );
44 add_filter( 'rest_' . Helper::COUPON_POST_TYPE . '_query', [ $self, 'filter_by_expiry' ], 10, 2 );
45 }
46
47 /**
48 * Reduce the serialized end date/time meta to a single unix timestamp.
49 *
50 * Uses the same `strtotime( "date time" )` reading as the coupon-expiry
51 * validator (see Coupon::check_coupon_between_start_and_end_date) so the
52 * "expired" list filter and the real checkout expiry agree. Returns `0`
53 * when no end date is set, which is the sentinel for "never expires".
54 *
55 * @param mixed $end_date_time The `_storeengine_coupon_end_date_time` meta value.
56 */
57 public static function compute_end_timestamp( $end_date_time ): int {
58 if ( ! is_array( $end_date_time ) || empty( $end_date_time['date'] ) ) {
59 return 0;
60 }
61
62 $time = $end_date_time['time'] ?? '';
63 $ts = strtotime( trim( $end_date_time['date'] . ' ' . $time ) );
64
65 return $ts ? (int) $ts : 0;
66 }
67
68 /**
69 * Mirror the coupon end date/time into the queryable timestamp meta.
70 *
71 * @param \WP_Post $post The saved coupon post.
72 */
73 public function sync_end_timestamp( $post ) {
74 if ( ! $post || Helper::COUPON_POST_TYPE !== $post->post_type ) {
75 return;
76 }
77
78 $end_date_time = get_post_meta( $post->ID, '_storeengine_coupon_end_date_time', true );
79 update_post_meta( $post->ID, self::END_TS_META, self::compute_end_timestamp( $end_date_time ) );
80 }
81
82 /**
83 * Register the `se_expiry` query param on the coupon REST collection.
84 *
85 * @param array $params Collection params.
86 * @return array
87 */
88 public function register_expiry_param( array $params ): array {
89 $params['se_expiry'] = [
90 'description' => __( 'Limit coupons by expiry state: "expired" (past end date), "no_end" (time limit set but no end date) or "forever" (open-ended, no time limit).', 'storeengine' ),
91 'type' => 'string',
92 'enum' => [ 'expired', 'no_end', 'forever' ],
93 ];
94
95 return $params;
96 }
97
98 /**
99 * Translate the `se_expiry` param into a meta_query on the mirrored timestamp.
100 *
101 * @param array $args WP_Query args.
102 * @param WP_REST_Request $request REST request.
103 * @return array
104 */
105 public function filter_by_expiry( array $args, WP_REST_Request $request ): array {
106 $expiry = $request->get_param( 'se_expiry' );
107 if ( ! $expiry ) {
108 return $args;
109 }
110
111 $meta_query = $args['meta_query'] ?? [];
112
113 if ( 'expired' === $expiry ) {
114 // A set, in-the-past end date. Bounded below by 1 so "no end date"
115 // (stored as 0) is never counted as expired.
116 $meta_query[] = [
117 'key' => self::END_TS_META,
118 'value' => [ 1, time() ],
119 'compare' => 'BETWEEN',
120 'type' => 'NUMERIC',
121 ];
122 } elseif ( 'no_end' === $expiry ) {
123 // A time limit was chosen but the end date left blank — the risky
124 // case. Requires the `set_time_limit` mode so intentionally
125 // open-ended ("Forever") coupons fall under the `forever` filter
126 // instead. The mirror is 0, or absent on coupons predating it.
127 $meta_query[] = [
128 'relation' => 'AND',
129 [
130 'key' => self::TIME_TYPE_META,
131 'value' => 'set_time_limit',
132 'compare' => '=',
133 ],
134 [
135 'relation' => 'OR',
136 [
137 'key' => self::END_TS_META,
138 'compare' => 'NOT EXISTS',
139 ],
140 [
141 'key' => self::END_TS_META,
142 'value' => 0,
143 'compare' => '=',
144 'type' => 'NUMERIC',
145 ],
146 ],
147 ];
148 } elseif ( 'forever' === $expiry ) {
149 // Open-ended by choice: any mode that is not `set_time_limit`,
150 // including legacy coupons with the meta unset (default is
151 // `forever_time`).
152 $meta_query[] = [
153 'relation' => 'OR',
154 [
155 'key' => self::TIME_TYPE_META,
156 'compare' => 'NOT EXISTS',
157 ],
158 [
159 'key' => self::TIME_TYPE_META,
160 'value' => 'set_time_limit',
161 'compare' => '!=',
162 ],
163 ];
164 }
165
166 $args['meta_query'] = $meta_query;
167
168 return $args;
169 }
170
171 public function validate_request_data( stdClass $prepared_post, WP_REST_Request $request ) {
172 $meta = $request->get_param( 'meta' );
173
174 $customers = $meta['_storeengine_coupon_valid_customers'] ?? [];
175 foreach ( $customers as $customer_id ) {
176 if ( ! is_numeric( $customer_id ) || ! get_userdata( $customer_id ) ) {
177 return new WP_Error( 'invalid_customer_id', esc_html__( 'Invalid customer ID provided.', 'storeengine' ), [ 'status' => 400 ] );
178 }
179 }
180
181 $prices = $meta['_storeengine_coupon_valid_prices'] ?? [];
182
183 foreach ( $prices as $index => $price_data ) {
184 $product_id = absint( $price_data['product_id'] );
185 if ( ! $product_id || ! get_post( $product_id ) ) {
186 return new WP_Error( 'invalid_product_id', esc_html(
187 sprintf(
188 // translators: %s. Item index.
189 __( 'Invalid product ID provided at %s item', 'storeengine' ),
190 Formatting::append_numeral_suffix( $index + 1 )
191 )
192 ), [ 'status' => 400 ] );
193 }
194
195 $price_ids = array_map( 'absint', $price_data['price_ids'] );
196 foreach ( $price_ids as $price_id ) {
197 try {
198 $price = Helper::get_price( $price_id );
199 if ( $price->get_product_id() !== $product_id ) {
200 return new WP_Error(
201 'invalid_price_id',
202 esc_html(
203 sprintf(
204 // translators: %s. Item index.
205 __( 'Invalid product & price combination at %s item.', 'storeengine' ),
206 Formatting::append_numeral_suffix( $index + 1 )
207 )
208 ),
209 [ 'status' => 400 ]
210 );
211 }
212 } catch ( StoreEngineException $e ) {
213 return new WP_Error( 'invalid_price_id', esc_html(
214 sprintf(
215 // translators: %s. Item index.
216 __( 'Invalid price selected at %s item.', 'storeengine' ),
217 Formatting::append_numeral_suffix( $index + 1 )
218 )
219 ), [ 'status' => 400 ] );
220 }
221 }
222 }
223
224 return $prepared_post;
225 }
226
227 }
228