From cb4341236969f7588023e5f6f884300e3fc37d1c Mon Sep 17 00:00:00 2001
From: unknown
Date: Wed, 15 Jul 2026 13:21:48 +0800
Subject: [PATCH] feat(forum): plate follow cash coupons + free auto-order
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Meta _aksrt_follow_pay_coupon_s + plate admin toggle (money only)
- Plate_Follow_Pay soft/hard coupon apply, free-after-discount, burn via shared helper
- Cashier auto-submits free topic/plate orders; topic coupon label no longer "预留"
---
assets/js/cms-download.js | 21 +-
docs/zibll-gap-matrix.md | 2 +-
inc/classes/class-aksrt-plate-follow-pay.php | 275 +++++++++++++++---
.../forum/class-aksrt-plate-meta-box.php | 12 +-
.../forum/class-aksrt-topic-meta-box.php | 4 +-
inc/core-hooks/part-1.php | 1 +
6 files changed, 273 insertions(+), 42 deletions(-)
diff --git a/assets/js/cms-download.js b/assets/js/cms-download.js
index 38f73aa1..19d6205b 100644
--- a/assets/js/cms-download.js
+++ b/assets/js/cms-download.js
@@ -448,7 +448,7 @@
window.location.reload();
return;
}
- // Free VIP topic/plate_follow still need a free-order unlock POST; content free can reload.
+ // Content free: reload. Topic/plate free: auto free-order POST (VIP free / free-after-coupon).
if (preview.is_free && preview.pay_type !== 'topic' && preview.pay_type !== 'plate_follow') {
window.location.reload();
return;
@@ -458,6 +458,25 @@
const priceType = (preview._price_type || preview.price_type || '');
const apiKind = preview._api_kind
|| (preview.pay_type === 'topic' ? 'topic' : (preview.pay_type === 'plate_follow' ? 'plate_follow' : 'content'));
+
+ // Auto-submit free topic/plate orders so user doesn't click through empty cash UI.
+ if (preview.is_free && (apiKind === 'topic' || apiKind === 'plate_follow') && !isPoints) {
+ open(
+ '' + tt('confirm_buy', '确认购买') + '
' +
+ '' + tt('processing', '处理中…') + '
',
+ returnFocusEl
+ );
+ const freeBtn = document.createElement('button');
+ freeBtn.type = 'button';
+ freeBtn.style.display = 'none';
+ freeBtn.setAttribute('data-price-type', priceType || '');
+ document.body.appendChild(freeBtn);
+ const freeCode = String(preview.coupon_code || preview._coupon_code || '');
+ submitPurchase(preview.post_id, 'cash', 'balance', freeBtn, returnFocusEl, freeCode, apiKind);
+ try { freeBtn.remove(); } catch (e0) { /* ignore */ }
+ return;
+ }
+
let amountLabel = escapeHtml(preview.amount_label || '');
let baseLabel = escapeHtml(preview.base_label || '');
const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel);
diff --git a/docs/zibll-gap-matrix.md b/docs/zibll-gap-matrix.md
index 99692f72..39fb4870 100644
--- a/docs/zibll-gap-matrix.md
+++ b/docs/zibll-gap-matrix.md
@@ -100,7 +100,7 @@ Note: zibll「商品管理」`zibpay_product_page` is **paid article sales**, no
| 3 users.php submenus | done |
| 4 SPA strip | done (ops routes redirect to native) |
| 5+ behavior parity | done — native ops shell + plate follow-pay + topic pay matrix + multi-gateway topic cashier (`AKSRT_Topic_Pay` /pay/topic) + topic coupon burn (P1) + plate follow VIP1/2 + multi-gateway (`AKSRT_Plate_Follow_Pay` /pay/plate-follow) + cash creator revenue (content/topic/plate) + sales report/download logs + hidecontent + users commerce + order filters/clear-closed + post pay_type filter + bulk content-pay edit + income/rebate user filter |
-| 5 remaining | optional polish only (plate coupons, guest topic/plate purchase — out of scope); order note JSON preserve + shared coupon burn helper done |
+| 5 remaining | optional polish only (guest topic/plate purchase — out of scope); plate follow coupons + free auto-order + note JSON preserve + shared coupon burn done |
## Dual-UI rule
diff --git a/inc/classes/class-aksrt-plate-follow-pay.php b/inc/classes/class-aksrt-plate-follow-pay.php
index 6a67b122..b39ba640 100644
--- a/inc/classes/class-aksrt-plate-follow-pay.php
+++ b/inc/classes/class-aksrt-plate-follow-pay.php
@@ -7,7 +7,7 @@
* - product_type = plate_follow, product_id = plate_id
* - order note.plate_follow_pay = 1
* - paid → mark_plate_followed (idempotent)
- * - no coupons this phase
+ * - optional coupons when _aksrt_follow_pay_coupon_s=1 (cash only)
*
* @package AKSRT
*/
@@ -62,7 +62,8 @@ class AKSRT_Plate_Follow_Pay {
public function rest_preview(WP_REST_Request $request) {
$plate_id = (int) $request['id'];
$uid = get_current_user_id();
- $preview = $this->preview($plate_id, $uid);
+ $coupon_code = sanitize_text_field((string) ($request->get_param('coupon_code') ?? $request->get_param('coupon') ?? ''));
+ $preview = $this->preview($plate_id, $uid, $coupon_code);
if (is_wp_error($preview)) {
return $preview;
}
@@ -89,9 +90,10 @@ class AKSRT_Plate_Follow_Pay {
if ($payment_method === '' || $payment_method === 'cash') {
$payment_method = 'balance';
}
+ $coupon_code = sanitize_text_field((string) ($params['coupon_code'] ?? $params['coupon'] ?? ''));
return $this->rest_wrap_result(
- $this->create_cash_order($plate_id, $uid, $payment_method)
+ $this->create_cash_order($plate_id, $uid, $payment_method, $coupon_code)
);
}
@@ -121,7 +123,7 @@ class AKSRT_Plate_Follow_Pay {
/**
* @return array|WP_Error
*/
- public function preview(int $plate_id, int $user_id = 0) {
+ public function preview(int $plate_id, int $user_id = 0, string $coupon_code = '') {
$assert = $this->assert_plate($plate_id);
if (is_wp_error($assert)) {
return $assert;
@@ -129,6 +131,7 @@ class AKSRT_Plate_Follow_Pay {
/** @var WP_Post $post */
$post = $assert['post'];
$uid = $user_id > 0 ? $user_id : get_current_user_id();
+ $coupon_code = sanitize_text_field($coupon_code);
if ($uid <= 0) {
return [
@@ -179,9 +182,9 @@ class AKSRT_Plate_Follow_Pay {
return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
}
- $amount = round((float) ($priced['amount'] ?? 0), 2);
- $base = round((float) ($priced['base'] ?? $amount), 2);
- $is_free = !empty($priced['is_free']) || $amount <= 0;
+ $list_amount = round((float) ($priced['amount'] ?? 0), 2);
+ $base = round((float) ($priced['base'] ?? $list_amount), 2);
+ $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
$has_followed = $this->user_has_followed($uid, $plate_id);
$balance = 0;
@@ -193,9 +196,39 @@ class AKSRT_Plate_Follow_Pay {
? aksrt_list_cash_payment_methods(true)
: [['id' => 'balance', 'name' => __('站内余额', 'aksrt')]];
+ $coupon_allowed = $this->is_coupon_allowed($plate_id);
+ $coupon_error = '';
+ $coupon_discount = 0.0;
+ $coupon_meta = null;
+ $applied_code = '';
+ $payable = $list_amount;
+
+ if (!$vip_free && $list_amount > 0) {
+ $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, $coupon_code, true);
+ if (is_wp_error($discounted)) {
+ $coupon_error = $discounted->get_error_message();
+ $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $uid, '', true);
+ }
+ if (!is_wp_error($discounted)) {
+ $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
+ $payable = round((float) ($discounted['payable'] ?? $list_amount), 2);
+ $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
+ $coupon_meta = !empty($discounted['coupon']) ? $discounted['coupon'] : null;
+ $applied_code = (string) ($discounted['coupon_code'] ?? ($coupon_meta['code'] ?? ''));
+ if ($coupon_error === '' && !empty($discounted['coupon_error'])) {
+ $coupon_error = (string) $discounted['coupon_error'];
+ }
+ }
+ } elseif ($vip_free) {
+ $payable = 0.0;
+ $list_amount = 0.0;
+ }
+
+ $is_free = $vip_free || $payable <= 0;
$amount_label = $is_free
? __('免费', 'aksrt')
- : sprintf('¥%s', number_format($amount, 2, '.', ''));
+ : sprintf('¥%s', number_format($payable, 2, '.', ''));
+ $list_label = sprintf('¥%s', number_format($list_amount > 0 ? $list_amount : $base, 2, '.', ''));
$base_label = sprintf('¥%s', number_format($base, 2, '.', ''));
return [
@@ -205,11 +238,11 @@ class AKSRT_Plate_Follow_Pay {
'pay_modo' => '0',
'pay_type' => 'plate_follow',
'pay_type_label' => __('付费关注', 'aksrt'),
- 'amount' => $amount,
+ 'amount' => $payable,
'base' => $base,
- 'list_amount' => $amount,
- 'list_amount_label' => $amount_label,
- 'payable' => $amount,
+ 'list_amount' => $list_amount > 0 ? $list_amount : $base,
+ 'list_amount_label' => $list_label,
+ 'payable' => $payable,
'currency' => '¥',
'amount_label' => $amount_label,
'base_label' => $base_label,
@@ -228,13 +261,15 @@ class AKSRT_Plate_Follow_Pay {
'login_url' => wp_login_url(get_permalink($post)),
'vip_url' => home_url('/'),
'credits_url' => home_url('/'),
- 'coupon_allowed' => false,
- 'coupon_desc' => '',
- 'coupon_code' => '',
- 'coupon' => null,
- 'coupon_error' => '',
- 'coupon_discount' => 0,
- 'coupon_discount_label' => '',
+ 'coupon_allowed' => $coupon_allowed,
+ 'coupon_desc' => $coupon_allowed ? __('支持优惠码', 'aksrt') : '',
+ 'coupon_code' => $applied_code,
+ 'coupon' => $coupon_meta,
+ 'coupon_error' => $coupon_error,
+ 'coupon_discount' => $coupon_discount,
+ 'coupon_discount_label' => $coupon_discount > 0
+ ? ('-' . sprintf('¥%s', number_format($coupon_discount, 2, '.', '')))
+ : '',
'rebate_discount' => 0,
'rebate_discount_label' => '',
'points_deduction_allowed' => false,
@@ -248,7 +283,7 @@ class AKSRT_Plate_Follow_Pay {
/**
* @return array|WP_Error
*/
- public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance') {
+ public function create_cash_order(int $plate_id, int $user_id, string $payment_method = 'balance', string $coupon_code = '') {
if ($user_id <= 0) {
return new WP_Error('aksrt_need_login', __('请先登录后再关注。', 'aksrt'), ['status' => 401]);
}
@@ -269,18 +304,52 @@ class AKSRT_Plate_Follow_Pay {
return new WP_Error('aksrt_wrong_modo', __('该版块仅支持积分关注。', 'aksrt'), ['status' => 400]);
}
- $amount = round((float) ($priced['amount'] ?? 0), 2);
- $base = round((float) ($priced['base'] ?? $amount), 2);
- $is_free = !empty($priced['is_free']) || $amount <= 0;
+ $list_amount = round((float) ($priced['amount'] ?? 0), 2);
+ $base = round((float) ($priced['base'] ?? $list_amount), 2);
+ $vip_free = !empty($priced['is_free']) || ($list_amount <= 0 && !empty($priced['is_vip_price']));
+ $coupon_code = sanitize_text_field($coupon_code);
- if ($is_free) {
- return $this->create_free_paid_order($plate_id, $user_id, $post, $priced);
+ // VIP free — no coupon stack.
+ if ($vip_free) {
+ return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [
+ 'list_amount' => 0.0,
+ 'payable' => 0.0,
+ 'coupon_code' => '',
+ 'coupon_discount' => 0.0,
+ 'coupon' => [],
+ 'message' => __('VIP 免费关注成功', 'aksrt'),
+ 'paid_note' => __('VIP 免费关注版块。', 'aksrt'),
+ ]);
}
- if ($amount <= 0) {
+ if ($list_amount <= 0) {
return new WP_Error('aksrt_price_unset', __('价格未设置', 'aksrt'), ['status' => 400]);
}
+ $discounted = $this->apply_plate_cash_discounts($priced, $plate_id, $user_id, $coupon_code, false);
+ if (is_wp_error($discounted)) {
+ return $discounted;
+ }
+
+ $list_amount = round((float) ($discounted['list_amount'] ?? $list_amount), 2);
+ $amount = round((float) ($discounted['payable'] ?? $list_amount), 2);
+ $coupon_discount = round((float) ($discounted['coupon_discount'] ?? 0), 2);
+ $coupon_meta = is_array($discounted['coupon'] ?? null) ? $discounted['coupon'] : [];
+ $coupon_code_norm = (string) ($coupon_meta['code'] ?? $discounted['coupon_code'] ?? $coupon_code);
+
+ // Free after coupon.
+ if ($amount <= 0) {
+ return $this->create_free_paid_order($plate_id, $user_id, $post, $priced, [
+ 'list_amount' => $list_amount,
+ 'payable' => 0.0,
+ 'coupon_code' => $coupon_code_norm,
+ 'coupon_discount' => $coupon_discount,
+ 'coupon' => $coupon_meta,
+ 'message' => __('无需支付,已关注。', 'aksrt'),
+ 'paid_note' => __('优惠后无需支付。', 'aksrt'),
+ ]);
+ }
+
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
return new WP_Error('aksrt_pay_unavailable', __('支付系统不可用。', 'aksrt'), ['status' => 500]);
}
@@ -291,7 +360,17 @@ class AKSRT_Plate_Follow_Pay {
}
$pending = $this->find_pending_order($user_id, $plate_id);
- $can_resume = $pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001;
+ $can_resume = false;
+ if ($pending && abs((float) ($pending['amount'] ?? 0) - $amount) < 0.001) {
+ $pending_meta = $this->decode_order_note_meta($pending);
+ $pending_coupon = (string) ($pending_meta['coupon_code'] ?? '');
+ if (is_array($pending_meta['coupon'] ?? null)) {
+ $pending_coupon = (string) ($pending_meta['coupon']['code'] ?? $pending_coupon);
+ }
+ if ($pending_coupon === $coupon_code_norm) {
+ $can_resume = true;
+ }
+ }
if ($can_resume) {
$order_number = (string) ($pending['order_number'] ?? '');
@@ -303,9 +382,12 @@ class AKSRT_Plate_Follow_Pay {
'pay_modo' => '0',
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => $base,
- 'list_amount' => $amount,
+ 'list_amount' => $list_amount,
'amount' => $amount,
'payable' => $amount,
+ 'coupon_code' => $coupon_code_norm,
+ 'coupon_discount' => $coupon_discount,
+ 'coupon' => $coupon_meta,
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -433,6 +515,7 @@ class AKSRT_Plate_Follow_Pay {
}
$this->mark_followed($user_id, $plate_id);
+ $this->finalize_plate_order_side_effects($order_number, $paid_order);
}
/**
@@ -515,9 +598,28 @@ class AKSRT_Plate_Follow_Pay {
}
/**
+ * Free VIP or free-after-discount paid order.
+ *
+ * @param array{
+ * list_amount?:float,
+ * payable?:float,
+ * coupon_code?:string,
+ * coupon_discount?:float,
+ * coupon?:array,
+ * message?:string,
+ * paid_note?:string
+ * } $opts
* @return array|WP_Error
*/
- private function create_free_paid_order(int $plate_id, int $user_id, WP_Post $post, array $priced) {
+ private function create_free_paid_order(int $plate_id, int $user_id, WP_Post $post, array $priced, array $opts = []) {
+ $list_amount = round((float) ($opts['list_amount'] ?? 0), 2);
+ $payable = round((float) ($opts['payable'] ?? 0), 2);
+ $coupon_code = sanitize_text_field((string) ($opts['coupon_code'] ?? ''));
+ $coupon_discount = round((float) ($opts['coupon_discount'] ?? 0), 2);
+ $coupon_meta = is_array($opts['coupon'] ?? null) ? $opts['coupon'] : [];
+ $message = (string) ($opts['message'] ?? __('VIP 免费关注成功', 'aksrt'));
+ $paid_note = (string) ($opts['paid_note'] ?? __('VIP 免费关注版块。', 'aksrt'));
+
if (!class_exists('AKSRT_Order') || !class_exists('AKSRT_Order_Repository')) {
$this->mark_followed($user_id, $plate_id);
return [
@@ -527,7 +629,7 @@ class AKSRT_Plate_Follow_Pay {
'amount' => 0,
'pay_modo' => '0',
'free' => true,
- 'message' => __('VIP 免费关注成功', 'aksrt'),
+ 'message' => $message,
'reload' => true,
],
];
@@ -539,9 +641,12 @@ class AKSRT_Plate_Follow_Pay {
'pay_modo' => '0',
'vip_level' => (int) ($priced['vip_level'] ?? 0),
'base' => (float) ($priced['base'] ?? 0),
- 'list_amount' => 0,
- 'amount' => 0,
- 'payable' => 0,
+ 'list_amount' => $list_amount,
+ 'amount' => $payable,
+ 'payable' => $payable,
+ 'coupon_code' => $coupon_code,
+ 'coupon_discount' => $coupon_discount,
+ 'coupon' => $coupon_meta,
'free' => 1,
'ip_address' => function_exists('aksrt_pay_client_ip') ? aksrt_pay_client_ip() : (string) ($_SERVER['REMOTE_ADDR'] ?? ''),
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
@@ -566,17 +671,20 @@ class AKSRT_Plate_Follow_Pay {
'order_number' => '',
'amount' => 0,
'free' => true,
- 'message' => __('VIP 免费关注成功', 'aksrt'),
+ 'message' => $message,
'reload' => true,
],
];
}
- AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
+ $paid = AKSRT_Order::get_instance()->update_status($order_number, AKSRT_Order::STATUS_PAID, [
'payment_method' => 'free',
'payment_transaction_id' => 'free_' . $order_number,
- 'note' => __('VIP 免费关注版块。', 'aksrt'),
+ 'note' => $paid_note,
]);
+ if (!$paid) {
+ return new WP_Error('aksrt_order_confirm_failed', __('订单确认失败。', 'aksrt'), ['status' => 500]);
+ }
$this->mark_followed($user_id, $plate_id);
return [
@@ -586,12 +694,105 @@ class AKSRT_Plate_Follow_Pay {
'amount' => 0,
'pay_modo' => '0',
'free' => true,
- 'message' => __('VIP 免费关注成功', 'aksrt'),
+ 'message' => $message,
'reload' => true,
],
];
}
+ private function is_coupon_allowed(int $plate_id): bool {
+ return (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1;
+ }
+
+ /**
+ * VIP list price → coupon → payable.
+ *
+ * @param array $priced
+ * @return array|WP_Error
+ */
+ private function apply_plate_cash_discounts(array $priced, int $plate_id, int $user_id, string $coupon_code = '', bool $soft_coupon = false) {
+ $list_amount = round(max(0.0, (float) ($priced['amount'] ?? 0)), 2);
+ $pay = $list_amount;
+ $coupon_discount = 0.0;
+ $coupon_meta = [];
+ $coupon_error = '';
+ $coupon_code = sanitize_text_field($coupon_code);
+
+ if ($coupon_code !== '') {
+ if (!$this->is_coupon_allowed($plate_id)) {
+ $err = new WP_Error('aksrt_coupon_not_allowed', __('当前版块不支持优惠码。', 'aksrt'), ['status' => 400]);
+ if (!$soft_coupon) {
+ return $err;
+ }
+ $coupon_error = $err->get_error_message();
+ $coupon_code = '';
+ } elseif (!class_exists('AKSRT_Coupon')) {
+ $err = new WP_Error('aksrt_coupon_unavailable', __('优惠券系统不可用。', 'aksrt'), ['status' => 500]);
+ if (!$soft_coupon) {
+ return $err;
+ }
+ $coupon_error = $err->get_error_message();
+ $coupon_code = '';
+ } else {
+ $result = AKSRT_Coupon::get_instance()->validate($coupon_code, $pay, [
+ 'user_id' => max(0, $user_id),
+ 'product_ids' => [$plate_id],
+ 'context' => 'plate_follow',
+ ]);
+ if (empty($result['valid'])) {
+ $err = new WP_Error(
+ 'aksrt_coupon_error',
+ (string) ($result['message'] ?? __('优惠码不可用。', 'aksrt')),
+ ['status' => 400]
+ );
+ if (!$soft_coupon) {
+ return $err;
+ }
+ $coupon_error = $err->get_error_message();
+ $coupon_code = '';
+ } else {
+ $coupon_discount = round(max(0.0, (float) ($result['discount'] ?? 0)), 2);
+ if ($coupon_discount > $pay) {
+ $coupon_discount = $pay;
+ }
+ $pay = round(max(0.0, $pay - $coupon_discount), 2);
+ $c = is_array($result['coupon'] ?? null) ? $result['coupon'] : [];
+ $coupon_meta = [
+ 'id' => (int) ($c['id'] ?? 0),
+ 'code' => (string) ($c['code'] ?? $coupon_code),
+ 'type' => (string) ($c['type'] ?? ''),
+ 'value' => (float) ($c['value'] ?? 0),
+ 'discount' => $coupon_discount,
+ ];
+ $coupon_code = (string) $coupon_meta['code'];
+ }
+ }
+ }
+
+ return [
+ 'list_amount' => $list_amount,
+ 'coupon_discount' => $coupon_discount,
+ 'coupon' => $coupon_meta,
+ 'coupon_code' => $coupon_code,
+ 'coupon_error' => $coupon_error,
+ 'payable' => round(max(0.0, $pay), 2),
+ ];
+ }
+
+ private function finalize_plate_order_side_effects(string $order_number, array $order): void {
+ $meta = $this->decode_order_note_meta($order);
+ if (empty($meta['plate_follow_pay'])) {
+ return;
+ }
+ if (($meta['pay_modo'] ?? '') === 'points') {
+ return;
+ }
+ $buyer_id = (int) ($order['user_id'] ?? 0);
+ if (function_exists('aksrt_burn_order_coupon')) {
+ aksrt_burn_order_coupon($order_number, $meta, $buyer_id);
+ }
+ }
+
private function find_pending_order(int $user_id, int $plate_id): ?array {
if ($user_id <= 0 || $plate_id <= 0 || !class_exists('AKSRT_Order_Repository')) {
return null;
diff --git a/inc/classes/forum/class-aksrt-plate-meta-box.php b/inc/classes/forum/class-aksrt-plate-meta-box.php
index 004a8410..b133beda 100644
--- a/inc/classes/forum/class-aksrt-plate-meta-box.php
+++ b/inc/classes/forum/class-aksrt-plate-meta-box.php
@@ -190,6 +190,13 @@ class AKSRT_Plate_Meta_Box {
+
+
+
+
(int) get_post_meta($plate_id, '_aksrt_follow_pay_points', true),
'vip1_price' => $vip1_set ? (string) (float) $vip1_raw : '',
'vip2_price' => $vip2_set ? (string) (float) $vip2_raw : '',
+ 'coupon_s' => (int) get_post_meta($plate_id, '_aksrt_follow_pay_coupon_s', true) === 1,
];
}
diff --git a/inc/classes/forum/class-aksrt-topic-meta-box.php b/inc/classes/forum/class-aksrt-topic-meta-box.php
index 81379b95..aeee3f62 100644
--- a/inc/classes/forum/class-aksrt-topic-meta-box.php
+++ b/inc/classes/forum/class-aksrt-topic-meta-box.php
@@ -223,9 +223,9 @@ class AKSRT_Topic_Meta_Box {
-
+
['type' => 'number', 'default' => 0], // legacy read fallback → vip1
'_aksrt_follow_pay_vip1_price' => ['type' => 'number', 'default' => 0],
'_aksrt_follow_pay_vip2_price' => ['type' => 'number', 'default' => 0],
+ '_aksrt_follow_pay_coupon_s' => ['type' => 'integer', 'default' => 0],
];
foreach ($plate_meta as $key => $args) {