get_meta('_refref_request') && $coupon->get_meta('_refref_account') !== RefRef_Config::identity()['id']) { throw new Exception(esc_html__('This coupon belongs to another store installation.', 'refref')); } $owner = (int) $coupon->get_meta('_refref_owner'); if ($owner && $owner !== get_current_user_id()) { throw new Exception(esc_html__('Sign in to the account that earned this reward.', 'refref')); } $handle = $coupon->get_meta('_refref_handle'); if ($handle && (!WC()->session || !hash_equals($handle, RefRef_Storefront::handle_id()))) { throw new Exception(esc_html__('This offer belongs to another referral visit.', 'refref')); } if ($coupon->get_meta('_refref_request') && !RefRef_Config::connection()) { throw new Exception(esc_html__('This referral offer is not currently available.', 'refref')); } return $valid; }, 10, 2); } public static function deactivate(): void { // Native coupon validity must not outlive this plugin's account/handle checks. $offset = 0; do { $ids = get_posts(['post_type' => 'shop_coupon', 'post_status' => 'any', 'fields' => 'ids', 'posts_per_page' => 100, 'offset' => $offset, 'orderby' => 'ID', 'order' => 'ASC', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Deactivation only; bounded pages find all owned coupons through the WordPress API. 'meta_query' => [['key' => '_refref_request', 'compare' => 'EXISTS']]]); foreach ($ids as $id) { $coupon = new WC_Coupon($id); $coupon->set_date_expires(time() - 1); $coupon->save(); } $offset += count($ids); } while (count($ids) === 100); } public static function decimal(string $minor): string { if (!preg_match('/^\d{1,15}$/', $minor)) { throw new RuntimeException(esc_html__('Invalid coupon amount.', 'refref')); } $places = wc_get_price_decimals(); if ($places === 0) { return $minor; } $digits = str_pad($minor, $places + 1, '0', STR_PAD_LEFT); return substr($digits, 0, -$places) . '.' . substr($digits, -$places); } public static function issue(array $input): array { global $wpdb; $code = strtolower((string) ($input['code'] ?? '')); if (!preg_match('/^[a-z0-9_-]{8,100}$/', $code)) { throw new RuntimeException(esc_html__('Invalid coupon code.', 'refref')); } $lock = 'refref:' . hash('sha256', $wpdb->prefix . $code); $lock = substr($lock, 0, 64); // Connection-scoped database mutex: a cached result cannot acquire or release a lock. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- No WordPress locking API provides this cross-worker guarantee. if ((int) $wpdb->get_var($wpdb->prepare('SELECT GET_LOCK(%s, 5)', $lock)) !== 1) { throw new RuntimeException(esc_html__('Coupon creation is busy.', 'refref')); } try { $fingerprint = hash('sha256', wp_json_encode($input)); $existing = wc_get_coupon_id_by_code($code); if ($existing) { $coupon = new WC_Coupon($existing); if (!hash_equals((string) $coupon->get_meta('_refref_request'), $fingerprint)) { throw new RuntimeException(esc_html__('Coupon request conflicts with the saved coupon.', 'refref')); } return ['reference' => (string) $coupon->get_id(), 'code' => $coupon->get_code()]; } if (($input['currency'] ?? get_woocommerce_currency()) !== get_woocommerce_currency()) { throw new RuntimeException(esc_html__('Coupon currency does not match the store.', 'refref')); } $owner = (int) ($input['customerId'] ?? 0); if ($owner && !get_user_by('id', $owner)) { throw new RuntimeException(esc_html__('Coupon account not found.', 'refref')); } $coupon = new WC_Coupon(); $coupon->set_code($code); $coupon->set_usage_limit(1); $coupon->set_individual_use(true); $rate = $input['rateBps'] ?? null; if ($rate !== null) { if (!is_int($rate) || $rate <= 0 || $rate > 10000) { throw new RuntimeException(esc_html__('Invalid discount rate.', 'refref')); } $coupon->set_discount_type('percent'); $coupon->set_amount(number_format($rate / 100, 2, '.', '')); } else { $coupon->set_discount_type('fixed_cart'); $coupon->set_amount(self::decimal((string) ($input['amountMinor'] ?? ''))); } if (!empty($input['minimumSubtotalMinor'])) { $coupon->set_minimum_amount(self::decimal($input['minimumSubtotalMinor'])); } if (!empty($input['expiresAt'])) { $expires = strtotime($input['expiresAt']); if (!$expires || $expires <= time()) { throw new RuntimeException(esc_html__('Coupon has expired.', 'refref')); } $coupon->set_date_expires($expires); } $coupon->update_meta_data('_refref_account', RefRef_Config::identity()['id']); $coupon->update_meta_data('_refref_request', $fingerprint); $coupon->update_meta_data('_refref_owner', $owner); $coupon->update_meta_data('_refref_handle', $input['handleId'] ?? ''); $coupon->save(); return ['reference' => (string) $coupon->get_id(), 'code' => $coupon->get_code()]; } finally { // Connection-scoped database mutex: a cached result cannot acquire or release a lock. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- No WordPress locking API provides this cross-worker guarantee. $wpdb->get_var($wpdb->prepare('SELECT RELEASE_LOCK(%s)', $lock)); } } public static function read(array $input): array { $code = (string) ($input['code'] ?? ''); $owner = (string) ($input['customerId'] ?? ''); $rate = $input['rateBps'] ?? null; if (!preg_match('/^rr-[a-f0-9]{32}$/D', $code) || !preg_match('/^[1-9]\d*$/D', $owner) || !is_int($rate) || $rate <= 0 || $rate > 10000) { throw new RuntimeException(esc_html__('Invalid coupon receipt.', 'refref')); } $coupon = new WC_Coupon(wc_get_coupon_id_by_code($code)); $account = RefRef_Config::identity()['id']; $expires = $coupon->get_date_expires(); $expected_expiry = empty($input['expiresAt']) ? null : strtotime($input['expiresAt']); $active = $coupon->get_id() && get_post_status($coupon->get_id()) === 'publish' && $coupon->get_code() === $code && $coupon->get_meta('_refref_account') === $account && hash_equals((string) $coupon->get_meta('_refref_request'), hash('sha256', wp_json_encode($input))) && (string) $coupon->get_meta('_refref_owner') === $owner && !$coupon->get_meta('_refref_handle') && $coupon->get_discount_type() === 'percent' && wc_format_decimal($coupon->get_amount(), 2) === number_format($rate / 100, 2, '.', '') && $coupon->get_individual_use() && $coupon->get_usage_limit() === 1 && $coupon->get_usage_count() === 0 && ($expires ? $expires->getTimestamp() : null) === $expected_expiry && (!$expires || $expires->getTimestamp() > time()) && (bool) RefRef_Config::connection(); return ['accountId' => $account, 'mode' => RefRef_Config::mode(), 'code' => $code, 'customerId' => $owner, 'rateBps' => $rate, 'expiresAt' => $input['expiresAt'] ?? null, 'active' => (bool) $active]; } public static function revoke(string $reference): array { $coupon = new WC_Coupon((int) $reference); if (!$coupon->get_id() || !$coupon->get_meta('_refref_request')) { throw new RuntimeException(esc_html__('Coupon not found.', 'refref')); } if ($coupon->get_usage_count() > 0) { throw new RuntimeException(esc_html__('The coupon has already been redeemed.', 'refref')); } $coupon->set_date_expires(time() - 1); $coupon->save(); return ['reference' => $reference]; } }