PluginProbe ʕ •ᴥ•ʔ
MailPoet – Newsletters, Email Marketing, and Automation / 5.35.1
MailPoet – Newsletters, Email Marketing, and Automation v5.35.1
5.35.1 5.35.0 5.34.3 5.34.2 5.34.1 5.34.0 5.33.1 5.33.0 5.32.0 5.31.0 5.30.0 5.29.0 5.28.1 5.28.0 5.27.0 5.26.0 5.26.1 5.25.0 5.24.0 4.43.0 4.43.1 4.44.0 4.44.1 4.45.0 4.46.0 4.47.0 4.48.0 4.48.1 4.48.2 4.49.0 4.49.1 4.5.0 4.5.1 4.5.2 4.50.0 4.50.1 4.51.0 4.51.1 4.51.2 4.52.0 4.53.0 4.54.0 4.55.0 4.56.0 4.57.0 4.58.0 4.58.1 4.58.2 4.6.0 4.6.1 4.6.2 4.7.0 4.7.1 4.8.0 4.8.1 4.9.0 5.0.0 5.0.1 5.0.2 5.1.0 5.1.1 5.10.0 5.10.1 5.11.0 5.12.0 5.12.1 5.12.10 5.12.11 5.12.12 5.12.13 5.12.2 5.12.3 5.12.4 5.12.5 5.12.6 5.12.7 5.12.8 5.12.9 5.13.0 5.13.1 5.13.2 5.14.0 5.14.1 5.14.2 5.14.3 5.15.0 5.15.1 5.16.0 5.16.1 5.16.2 5.16.3 5.16.4 5.17.0 5.17.1 5.17.2 5.17.3 5.17.4 5.17.5 5.17.6 5.18.0 5.19.0 5.2.0 5.2.1 5.2.2 5.2.3 5.20.0 5.21.0 5.21.1 5.21.2 5.21.3 5.22.0 5.22.1 5.22.2 5.22.3 5.22.4 5.23.0 5.23.1 5.23.2 5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.4.0 5.4.1 5.4.2 5.5.0 5.5.1 5.5.2 5.6.0 5.6.1 5.6.2 5.6.3 5.6.4 5.7.0 5.7.1 5.8.0 5.8.1 5.9.0 3.0.0-beta.15 3.7.1 3.0.0-beta.16 3.7.2 3.0.0-beta.17 3.7.3 3.0.0-beta.18 3.7.4 3.0.0-beta.19 3.7.5 3.0.0-beta.2 3.7.6 3.0.0-beta.20 3.7.8 3.0.0-beta.21 3.70.0 3.0.0-beta.22 3.71.0 3.0.0-beta.23 3.71.1 3.0.0-beta.23.1 3.71.2 3.0.0-beta.23.2 3.71.3 3.0.0-beta.24 3.72.0 3.0.0-beta.25 3.73.0 3.0.0-beta.26 3.73.1 3.0.0-beta.27 3.73.2 3.0.0-beta.28 3.74.0 3.0.0-beta.29 3.74.1 3.0.0-beta.3 3.74.2 3.0.0-beta.30 3.74.3 3.0.0-beta.31 3.75.0 3.0.0-beta.32 3.75.1 3.0.0-beta.33 3.76.0 3.0.0-beta.33.1 3.77.0 3.0.0-beta.34.0.0 3.77.1 3.0.0-beta.36.0.0 3.78.0 3.0.0-beta.36.0.1 3.79.0 3.0.0-beta.36.2.0 3.8 3.0.0-beta.36.3.0 3.8.1 3.0.0-beta.36.3.1 3.8.2 3.0.0-beta.37.0.0 3.8.3 3.0.0-beta.4 3.8.4 3.0.0-beta.5 3.8.5 3.0.0-beta.6 3.8.6 3.0.0-beta.7 3.80.0 3.0.0-beta.7.1 3.81.0 3.0.0-beta.8 3.82.0 3.0.0-beta.9 3.83.0 3.0.0-rc.1.0.0 3.84.0 3.0.0-rc.1.0.1 3.84.1 3.0.0-rc.1.0.2 3.85.0 3.0.0-rc.1.0.3 3.85.1 3.0.0-rc.1.0.4 3.86.0 3.0.0-rc.2.0.0 3.87.0 3.0.0-rc.2.0.1 3.87.1 3.0.0-rc.2.0.2 3.87.2 3.0.0-rc.2.0.3 3.88.0 3.0.1 3.88.1 3.0.2 3.88.2 3.0.3 3.89.0 3.0.4 3.89.1 3.0.5 3.89.2 3.0.6 3.89.3 3.0.7 3.89.4 3.0.8 3.9.0 3.0.9 3.9.1 3.1.0 3.90.0 3.10 3.90.1 3.10.1 3.90.2 3.100.0 3.91.0 3.100.1 3.91.1 3.100.2 3.92.0 3.101.0 3.92.1 3.101.1 3.93.0 3.102.0 3.93.1 3.102.1 3.94.0 3.103.0 3.95.0 3.103.1 3.95.1 3.11.0 3.96.0 3.11.1 3.96.1 3.11.2 3.97.0 3.11.3 3.98.0 3.11.4 3.98.1 3.11.5 3.99.0 3.12.0 3.99.1 3.12.1 4.0.0 3.13.0 4.0.1 3.14.0 4.1.0 3.14.1 4.1.1 3.15.0 4.10.0 3.16.0 4.11.0 3.16.1 4.11.1 3.16.2 4.12.0 3.16.3 4.12.1 3.17.0 4.12.2 3.17.1 4.13.0 3.17.2 4.14.0 3.18.0 4.15.0 3.18.1 4.16.0 3.18.2 4.17.0 3.19.0 4.17.1 3.19.1 4.18.0 3.19.2 4.18.1 3.19.3 4.19.0 3.2.0 4.2.0 3.2.1 4.20.0 3.2.2 4.20.1 3.2.3 4.20.2 3.2.4 4.21.0 3.2.5 4.22.0 3.20.0 4.22.1 3.21.0 4.22.2 3.21.1 4.23.0 3.22.0 4.24.0 3.23.0 4.25.0 3.23.1 4.26.0 3.23.2 4.26.1 3.24.0 4.27.0 3.25.0 4.28.0 3.25.1 4.29.0 3.26.0 4.3.0 3.26.1 4.3.1 3.27.0 4.30.0 3.28.0 4.31.0 3.29.0 4.31.1 3.3.0 4.32.0 3.3.1 4.33.0 3.3.2 4.34.0 3.3.3 4.35.0 3.3.4 4.35.1 3.3.5 4.36.0 3.3.6 4.37.0 3.30.0 4.38.0 3.31.0 4.39.0 3.31.1 4.4.0 3.32.0 4.40.0 3.32.1 4.41.0 3.32.2 4.41.1 3.33.0 4.41.2 3.34.0 4.41.3 3.34.1 4.42.0 3.34.2 4.42.1 3.34.3 3.34.4 3.35.0 3.35.1 3.35.3 3.35.4 3.36.0 3.37.0 3.37.1 3.37.2 3.37.3 3.38.0 3.38.1 3.39.0 3.39.1 3.39.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.40.0 3.40.1 3.41.0 3.41.1 3.41.2 3.42.0 3.42.1 3.42.2 3.42.3 3.43.0 3.43.1 3.44.0 3.45.0 3.45.1 3.46.0 3.46.1 3.46.10 3.46.11 3.46.12 3.46.13 3.46.14 3.46.2 3.46.3 3.46.4 3.46.5 3.46.6 3.46.7 3.46.8 3.46.9 3.47.0 3.47.1 3.47.10 3.47.11 3.47.2 3.47.3 3.47.5 3.47.6 3.47.7 3.47.9 3.48.0 3.48.1 3.49.0 3.49.1 3.5.0 3.5.1 3.50.0 3.51.0 3.51.1 3.51.2 3.52.0 3.53.0 3.54.0 3.54.1 3.54.2 3.54.3 3.55.0 3.55.1 3.56.0 3.56.1 3.56.2 3.57.0 3.57.1 3.58.0 3.59.0 3.59.1 3.59.2 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.6 3.6.7 3.60.0 3.60.1 3.60.10 3.60.11 3.60.12 3.60.2 3.60.3 3.60.4 3.60.6 3.60.7 3.60.8 3.60.9 3.61.0 3.62.0 3.62.1 3.63.0 3.64.0 3.64.1 3.64.2 3.64.3 3.65.0 trunk 3.65.1 3.0.0 3.66.0 3.0.0-beta.1 3.67.0 3.0.0-beta.10 3.67.1 3.0.0-beta.11 3.68.0 3.0.0-beta.12 3.69.0 3.0.0-beta.13 3.69.1 3.0.0-beta.14 3.7.0
mailpoet / lib / EmailEditor / Integrations / MailPoet / Coupons / CouponBlockGenerator.php
mailpoet / lib / EmailEditor / Integrations / MailPoet / Coupons Last commit date
CouponBlock.php 3 months ago CouponBlockAttributeParser.php 3 months ago CouponBlockDetector.php 3 months ago CouponBlockFailureTranslator.php 3 months ago CouponBlockGenerationFailureCollector.php 3 months ago CouponBlockGenerator.php 3 months ago CouponBlockValidationException.php 3 months ago CouponBlockValidator.php 3 months ago EmailContextBuilder.php 1 month ago index.php 3 months ago
CouponBlockGenerator.php
274 lines
1 <?php declare(strict_types = 1);
2
3 namespace MailPoet\EmailEditor\Integrations\MailPoet\Coupons;
4
5 if (!defined('ABSPATH')) exit;
6
7
8 use Automattic\WooCommerce\EmailEditor\Engine\Renderer\ContentRenderer\Rendering_Context;
9 use MailPoet\Entities\NewsletterEntity;
10 use MailPoet\WooCommerce\Helper;
11 use MailPoet\WooCommerce\RandomCouponCodeGenerator;
12 use MailPoet\WP\Functions as WPFunctions;
13
14 /**
15 * Generates coupon codes only while WooCommerce renders a MailPoet block email.
16 *
17 * Renderer clears the failure collector before each render and reads it
18 * immediately after rendering, so failures recorded here stop that send without
19 * leaking details into later renders.
20 */
21 class CouponBlockGenerator {
22 const SAFE_PLACEHOLDER = CouponBlock::SAFE_PLACEHOLDER;
23 const MAX_CODE_RETRIES = 5;
24
25 /** @var Helper */
26 private $wcHelper;
27
28 /** @var CouponBlockValidator */
29 private $validator;
30
31 /** @var CouponBlockGenerationFailureCollector */
32 private $failureCollector;
33
34 /** @var WPFunctions */
35 private $wp;
36
37 private RandomCouponCodeGenerator $randomCouponCodeGenerator;
38
39 public function __construct(
40 Helper $wcHelper,
41 CouponBlockValidator $validator,
42 CouponBlockGenerationFailureCollector $failureCollector,
43 WPFunctions $wp,
44 RandomCouponCodeGenerator $randomCouponCodeGenerator
45 ) {
46 $this->wcHelper = $wcHelper;
47 $this->validator = $validator;
48 $this->failureCollector = $failureCollector;
49 $this->wp = $wp;
50 $this->randomCouponCodeGenerator = $randomCouponCodeGenerator;
51 }
52
53 public function init(): void {
54 if (!class_exists(Rendering_Context::class)) {
55 return;
56 }
57
58 $this->wp->addFilter(
59 'woocommerce_coupon_code_block_auto_generate',
60 [$this, 'generate'],
61 5,
62 3
63 );
64 $this->wp->addAction('woocommerce_email_editor_render_start', [$this, 'registerEmailRenderer']);
65 }
66
67 public function generate($couponCode, array $attrs, Rendering_Context $renderingContext): string {
68 $couponCode = (string)$couponCode;
69 if ($couponCode !== '') {
70 return $couponCode;
71 }
72
73 $context = $renderingContext->get_email_context();
74 if (($context['integration'] ?? null) !== 'mailpoet') {
75 return $couponCode;
76 }
77
78 if (!CouponBlock::isCreateNew($attrs)) {
79 return self::SAFE_PLACEHOLDER;
80 }
81 $attrs = CouponBlock::withCreateNewDefaults($attrs);
82
83 if (!($context['is_real_send'] ?? false)) {
84 return self::SAFE_PLACEHOLDER;
85 }
86
87 $unsupportedContextMessage = $this->getUnsupportedRealSendContextMessage($attrs, $renderingContext);
88 if ($unsupportedContextMessage !== null) {
89 $this->recordFailure('invalid_real_send_context', $unsupportedContextMessage, $attrs, $context);
90 return self::SAFE_PLACEHOLDER;
91 }
92
93 if (!$this->wcHelper->isWooCommerceActive()) {
94 $this->recordFailure('woocommerce_unavailable', 'WooCommerce is not available.', $attrs, $context);
95 return self::SAFE_PLACEHOLDER;
96 }
97
98 try {
99 $recipientEmail = (string)$renderingContext->get_recipient_email();
100 $validatedAttrs = $this->validator->validate($attrs, $recipientEmail);
101 $code = $this->generateUniqueCode();
102 $coupon = $this->wcHelper->createWcCoupon('');
103 $coupon->set_code($code);
104 $coupon->set_description($this->getCouponDescription($context));
105 $coupon->set_discount_type($validatedAttrs['discountType']);
106 $coupon->set_amount($validatedAttrs['amount']);
107
108 if ($validatedAttrs['expiryDay'] > 0) {
109 $coupon->set_date_expires(time() + ($validatedAttrs['expiryDay'] * (defined('DAY_IN_SECONDS') ? DAY_IN_SECONDS : 86400)));
110 }
111
112 $coupon->set_free_shipping($validatedAttrs['freeShipping']);
113 $coupon->set_minimum_amount($validatedAttrs['minimumAmount']);
114 $coupon->set_maximum_amount($validatedAttrs['maximumAmount']);
115 $coupon->set_individual_use($validatedAttrs['individualUse']);
116 $coupon->set_exclude_sale_items($validatedAttrs['excludeSaleItems']);
117 $coupon->set_product_ids($validatedAttrs['productIds']);
118 $coupon->set_excluded_product_ids($validatedAttrs['excludedProductIds']);
119 $coupon->set_product_categories($validatedAttrs['productCategoryIds']);
120 $coupon->set_excluded_product_categories($validatedAttrs['excludedProductCategoryIds']);
121 $coupon->set_email_restrictions($validatedAttrs['emailRestrictions']);
122 $coupon->set_usage_limit($validatedAttrs['usageLimit']);
123 $coupon->set_usage_limit_per_user($validatedAttrs['usageLimitPerUser']);
124
125 $couponId = $coupon->save();
126 if (!$couponId) {
127 throw new \RuntimeException('WooCommerce did not save the generated coupon.');
128 }
129
130 return $code;
131 } catch (\Throwable $e) {
132 $this->recordFailure('generation_failed', $e->getMessage(), $attrs, $context);
133 return self::SAFE_PLACEHOLDER;
134 }
135 }
136
137 public function registerEmailRenderer(): void {
138 if (!class_exists('\WP_Block_Type_Registry')) {
139 return;
140 }
141
142 $blockType = \WP_Block_Type_Registry::get_instance()->get_registered(CouponBlock::NAME);
143 $renderEmailCallbackProperty = 'render_email_callback';
144 $currentCallback = $blockType ? (get_object_vars($blockType)[$renderEmailCallbackProperty] ?? null) : null;
145 if (!$blockType || $this->usesWooCommerceEmailEditorRenderer($currentCallback)) {
146 return;
147 }
148
149 // WooCommerce 10.8 development builds can expose the block attributes
150 // without wiring the matching email renderer. Patch only that gap.
151 // @phpstan-ignore-next-line -- WooCommerce email editor reads this dynamic block setting.
152 $blockType->{$renderEmailCallbackProperty} = [$this, 'renderEmailCouponBlock'];
153 }
154
155 public function renderEmailCouponBlock(string $blockContent, array $parsedBlock, Rendering_Context $renderingContext): string {
156 $attrs = $parsedBlock['attrs'] ?? [];
157 if (!is_array($attrs)) {
158 $attrs = [];
159 }
160
161 if (!CouponBlock::isCreateNew($attrs, strpos($blockContent, self::SAFE_PLACEHOLDER) !== false)) {
162 return $blockContent;
163 }
164
165 $couponCode = $this->wp->applyFilters(
166 'woocommerce_coupon_code_block_auto_generate',
167 '',
168 $attrs,
169 $renderingContext
170 );
171 $couponCode = is_scalar($couponCode) ? (string)$couponCode : '';
172
173 if ($couponCode === '') {
174 return '';
175 }
176
177 return str_replace(self::SAFE_PLACEHOLDER, $this->wp->escHtml($couponCode), $blockContent);
178 }
179
180 private function usesWooCommerceEmailEditorRenderer($callback): bool {
181 if (!is_array($callback) || !isset($callback[0]) || !is_object($callback[0])) {
182 return false;
183 }
184
185 return strpos(get_class($callback[0]), 'Automattic\\WooCommerce\\EmailEditor\\Integrations\\WooCommerce\\') === 0;
186 }
187
188 private function getUnsupportedRealSendContextMessage(array $attrs, Rendering_Context $renderingContext): ?string {
189 $requiresRecipientEmail = CouponBlockAttributeParser::toBoolean($attrs['restrictToSubscriber'] ?? false);
190 if ($this->isSupportedAutomationContext($renderingContext, $requiresRecipientEmail)) {
191 return null;
192 }
193
194 if ($requiresRecipientEmail) {
195 return 'Recipient-restricted generated coupons are only supported in automation emails sent to one subscriber at a time.';
196 }
197
198 if ($this->isSupportedStandardContext($renderingContext)) {
199 return null;
200 }
201
202 return 'Auto-generated coupon codes are only supported in regular newsletters and automation emails sent to one subscriber at a time.';
203 }
204
205 private function isSupportedAutomationContext(Rendering_Context $renderingContext, bool $requiresRecipientEmail): bool {
206 $context = $renderingContext->get_email_context();
207 $recipientEmail = $renderingContext->get_recipient_email();
208 $automationTypes = [
209 NewsletterEntity::TYPE_AUTOMATION,
210 NewsletterEntity::TYPE_AUTOMATION_NOTIFICATION,
211 NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL,
212 ];
213
214 return ($context['integration'] ?? null) === 'mailpoet'
215 && ($context['is_real_send'] ?? null) === true
216 && ($context['is_preview'] ?? null) === false
217 && ($context['is_single_recipient'] ?? null) === true
218 && is_numeric($context['subscriber_count'] ?? null)
219 && (int)$context['subscriber_count'] === 1
220 && ($context['mailpoet_is_automation'] ?? null) === true
221 && in_array($context['email_type'] ?? '', $automationTypes, true)
222 && (
223 !$requiresRecipientEmail
224 || (is_string($recipientEmail) && (bool)$this->wp->isEmail($recipientEmail))
225 );
226 }
227
228 private function isSupportedStandardContext(Rendering_Context $renderingContext): bool {
229 $context = $renderingContext->get_email_context();
230 return ($context['integration'] ?? null) === 'mailpoet'
231 && ($context['is_real_send'] ?? null) === true
232 && ($context['is_preview'] ?? null) === false
233 && ($context['email_type'] ?? '') === NewsletterEntity::TYPE_STANDARD
234 && ($context['mailpoet_is_automation'] ?? null) === false
235 && is_numeric($context['subscriber_count'] ?? null)
236 && (int)$context['subscriber_count'] >= 1;
237 }
238
239 private function generateUniqueCode(): string {
240 for ($i = 0; $i < self::MAX_CODE_RETRIES; $i++) {
241 $code = $this->randomCouponCodeGenerator->generate();
242 if (!$this->wcHelper->wcGetCouponIdByCode($code)) {
243 return $code;
244 }
245 }
246
247 throw new \RuntimeException('Failed to generate a unique coupon code.');
248 }
249
250 private function getCouponDescription(array $context): string {
251 return sprintf(
252 // translators: %1$d is the MailPoet email ID.
253 _x('Auto-generated coupon by MailPoet for email: %1$d', 'Coupon block code generation', 'mailpoet'),
254 (int)($context['newsletter_id'] ?? 0)
255 );
256 }
257
258 private function recordFailure(string $code, string $message, array $attrs, array $context): void {
259 $safeContext = array_intersect_key($context, array_flip([
260 'integration',
261 'newsletter_id',
262 'queue_id',
263 'email_type',
264 'is_real_send',
265 'is_preview',
266 'is_single_recipient',
267 'subscriber_count',
268 'mailpoet_is_automation',
269 ]));
270
271 $this->failureCollector->record($code, $message, $attrs, $safeContext);
272 }
273 }
274