Files
wordpress-theme-aksrt/tests/widget-system/vip-settings-structured-harness.php
T
kate522 af8dccdc45 feat(vip): restructure membership settings into structured groups
Replace the pipe-delimited VIP configuration with structured groups and
accordions, matching the reference theme (Zibll) 1:1.

Settings:
- 会员等级 keeps only the nav buttons and the two level switches, plus the
  "must enable level 1 first" guard. Level identity fields (display name,
  intro, benefits HTML, icon) and 会员商品 move into the 一级会员/二级会员
  tabs of 会员购买选项, so each level is configured in one place.
- Level tab titles bind to the configured display name via a new
  titleKey/titleFallback hook in AksFormRenderer.
- 会员商品 is a repeater with exactly the reference fields: 执行价/原价/
  促销标签/标签颜色/会员有效时间/时长单位. The tag colour uses the full
  b+jb palette and now reaches the product card instead of a hardcoded
  jb-yellow.
- 会员续费 gains the 自定义商品 type backed by per-level renew-product
  groups; the discount becomes a 0.1-9.9 slider with float sanitizing.
- 会员升级 becomes the reference three-block accordion (month-to-month
  day-rate, jump to permanent, permanent to permanent), each with its own
  price, strikethrough price, promo tag and tag colour.
- 积分兑换明细 is a repeater (points/level/duration/unit) where the
  duration accepts the literal Permanent.

Runtime:
- PaymentRuntime normalizes the structured groups into purchasable rows
  (money normalization, palette validation, zero-price and out-of-range
  rows dropped) and no longer emits any *_raw strings.
- The catalog builder carries tag_class through and routes renewals to the
  renew-product group under 自定义商品.
- The points-exchange order note carries time/unit so month durations use
  calendar math instead of a 30-day approximation.

Dead code removed: parse_level_products, parse_packages,
parse_upgrade_product_line and their pipe-splitting, plus nine zero-call
PaymentRuntime proxy methods in HomeCms. No dual-format or migration path
was added; SchemaProvider already falls back to defaults when a stored
group value is not an array, so existing pipe strings degrade safely.

Verified: php -l on every changed PHP file, 655 Node contract tests passing
(14 new ones in vip-settings-structured-contract.test.cjs exercising the
real schema build and runtime normalization via a PHP harness), and the
admin SPA builds.
2026-10-18 17:14:00 +08:00

208 lines
8.2 KiB
PHP

<?php
/**
* Executable VIP structured-settings contract harness.
*
* Builds the payment schema with WordPress stubs, normalizes a stored
* `aksrt_settings` snapshot through PaymentRuntime, and prints the result as
* JSON so the Node contract test can assert on the real shapes.
*/
declare(strict_types=1);
namespace {
define('ABSPATH', __DIR__ . '/');
define('DAY_IN_SECONDS', 86400);
function __(string $text, string $domain = ''): string { return $text; }
function _x(string $text, string $context, string $domain = ''): string { return $text; }
function esc_html($value): string { return (string) $value; }
function esc_attr($value): string { return (string) $value; }
function esc_url($value): string { return (string) $value; }
function esc_url_raw($value): string { return trim((string) $value); }
function sanitize_key($value): string { return preg_replace('/[^a-z0-9_\-]/', '', strtolower((string) $value)) ?? ''; }
function sanitize_text_field($value): string { return trim(strip_tags((string) $value)); }
function sanitize_textarea_field($value): string { return trim((string) $value); }
function wp_kses_post($value): string { return (string) $value; }
function wp_kses($value, $allowed = null): string { return (string) $value; }
function trailingslashit($value): string { return rtrim((string) $value, '/\\') . '/'; }
function get_template_directory_uri(): string { return 'https://example.test/wp-content/themes/aksrt'; }
function absint($value): int { return max(0, (int) $value); }
function wp_json_encode($value, int $flags = 0): string { return json_encode($value, $flags | JSON_UNESCAPED_SLASHES); }
function is_multisite(): bool { return false; }
function get_option(string $key, $default = false) { return $default; }
function did_action(string $hook): int { return 0; }
function apply_filters(string $hook, $value, ...$args) { return $value; }
function add_action(...$args): void {}
function add_filter(...$args): void {}
}
namespace AKSRT\Settings {
/** Test double: PaymentRuntime reads every raw settings key through this. */
final class Settings {
/** @var array<string,mixed> */
public static array $snapshot = [];
public static function get_all(): array {
return self::$snapshot;
}
public static function get_paid_content_settings(): array {
return ['display' => ['currency_symbol' => '¥']];
}
public static function reset(): void {
self::$snapshot = [];
}
}
}
namespace {
require dirname(__DIR__, 2) . '/src/Ledger/AssetType.php';
require dirname(__DIR__, 2) . '/src/Ledger/AssetAmount.php';
require dirname(__DIR__, 2) . '/src/Settings/PaymentRuntime.php';
require dirname(__DIR__, 2) . '/src/Admin/Schema/SchemaPayment.php';
require dirname(__DIR__, 2) . '/src/Content/VipServicePackages.php';
/** Exposes the trait's private renew-pricing math for contract assertions. */
final class VipPricingProbe {
use \AKSRT\Content\VipServicePackages;
/** @param array<string,mixed> $pkg */
public function price(array $pkg, string $action, array $renew, int $days): ?array {
return $this->apply_action_price($pkg, $action, $renew, [], 1, 1, 0, false, $days);
}
}
use AKSRT\Admin\Schema\SchemaPayment;
use AKSRT\Settings\PaymentRuntime;
use AKSRT\Settings\Settings;
$vip = SchemaPayment::build()['sections']['vip'];
$groups = [];
foreach ($vip['groups'] as $group) {
$groups[$group['key']] = $group;
}
$opt_tabs = [];
foreach ($groups['opt']['fields'][0]['tabs'] as $tab) {
$fields = [];
foreach ($tab['fields'] as $field) {
$fields[$field['key']] = $field;
}
$opt_tabs[$tab['key']] = [
'title' => $tab['title'],
'titleKey' => $tab['titleKey'] ?? null,
'titleFallback' => $tab['titleFallback'] ?? null,
'fields' => $fields,
];
}
$benefit_tabs = [];
foreach ($groups['benefit']['fields'][0]['tabs'] as $tab) {
$benefit_tabs[$tab['key']] = [
'title' => $tab['title'],
'titleKey' => $tab['titleKey'] ?? null,
'titleFallback' => $tab['titleFallback'] ?? null,
];
}
// ---- runtime: defaults (nothing stored) -------------------------------
Settings::reset();
$defaults = PaymentRuntime::get_payment_vip_settings();
// ---- runtime: fully customised stored snapshot -------------------------
Settings::$snapshot = [
'pay_user_vip_1_s' => true,
'pay_user_vip_2_s' => true,
'pay_user_vip_1_name' => '青铜会员',
'pay_user_vip_2_name' => '王者会员',
'vip_renew' => true,
'vip_renew_desc' => '续费有礼',
'vip_renew_price_type' => 'customize',
'vip_renew_discount' => 7.5,
'vip_renew_reduce' => '15.00',
'vip_upgrade' => true,
'vip_upgrade_desc' => '升级王者',
'vip_upgrade_product' => [
'month_to_month_s' => true,
'unit_price' => '12.50',
'unit_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 按天升级',
'unit_tag_class' => 'jb-blue',
'jump_s' => true,
'jump_price' => '259.00',
'jump_show_price' => '399.00',
'jump_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 直升永久',
'jump_tag_class' => 'jb-red',
'permanent_price' => '159.00',
'permanent_show_price' => '259.00',
'permanent_tag' => '<i class="fa fas fa-fw fa-bolt"></i> 王者特惠',
'permanent_tag_class' => 'jb-vip2',
],
'vip_1_product' => [
['price' => '88.80', 'show_price' => '188.00', 'tag' => '<i class="fa fas fa-fw fa-bolt"></i> 新客', 'tag_class' => 'jb-green', 'time' => 1, 'unit' => 'month'],
['price' => '0', 'show_price' => '', 'tag' => '', 'time' => 0, 'unit' => 'day'],
['price' => '10', 'show_price' => '20', 'tag' => '', 'time' => 2, 'unit' => 'month'],
],
'vip_2_product' => [
['price' => '300', 'show_price' => '600', 'tag' => 'x', 'tag_class' => 'not-a-real-class', 'time' => 12, 'unit' => 'month'],
],
'vip_1_renew_product' => [
['price' => '60', 'show_price' => '99', 'tag' => '续费专享', 'tag_class' => 'b-red', 'time' => 1, 'unit' => 'month'],
],
'vip_2_renew_product' => [],
'pay_vip_points_exchange_s' => true,
'pay_vip_points_exchange_product' => [
['points' => 800, 'level' => 1, 'time' => 30, 'unit' => 'day'],
['points' => 9000, 'level' => 2, 'time' => 'Permanent'],
['points' => 0, 'level' => 1, 'time' => 10, 'unit' => 'day'],
['points' => 100, 'level' => 9, 'time' => 10, 'unit' => 'day'],
['points' => 100, 'level' => 1, 'time' => '', 'unit' => 'day'],
['points' => 120, 'level' => 2, 'time' => 3, 'unit' => 'month'],
],
];
$custom = PaymentRuntime::get_payment_vip_settings();
// ---- legacy scalar payloads must fall back to defaults -----------------
Settings::$snapshot = [
'pay_user_vip_1_s' => true,
'vip_1_product' => "30|59|1|month|限时|月卡",
'pay_vip_points_exchange_product' => "1|500|30|积分月卡",
'vip_upgrade_product' => "unit|1|按剩余天数升级",
];
$legacy = PaymentRuntime::get_payment_vip_settings();
echo wp_json_encode([
'schema' => [
'version' => $vip['version'],
'group_keys' => array_column($vip['groups'], 'key'),
'basic_field_keys' => array_column($groups['basic']['fields'], 'key'),
'benefit_tabs' => $benefit_tabs,
'opt_tabs' => $opt_tabs,
'points_fields' => array_column($groups['points-exchange']['fields'], 'key'),
'points_group' => $groups['points-exchange']['fields'][1],
'tag_palette' => $opt_tabs['vip1']['fields']['vip_1_product']['fields'][3]['options'],
],
'runtime' => [
'defaults' => $defaults,
'custom' => $custom,
'legacy' => $legacy,
],
'pricing' => (static function (): array {
$probe = new VipPricingProbe();
$base = [
'price' => '100.00', 'show_price' => '200.00', 'tag' => '限时',
'tag_class' => 'jb-red', 'time' => 1, 'unit' => 'month',
];
return [
'same' => $probe->price($base, 'renew', ['price_type' => 'same'], 30),
'discount_8' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 8], 30),
'discount_7_5' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 7.5], 30),
'discount_10_clamped' => $probe->price($base, 'renew', ['price_type' => 'discount', 'discount' => 10], 30),
'reduce_20' => $probe->price($base, 'renew', ['price_type' => 'reduce', 'reduce' => '20.00'], 30),
'customize' => $probe->price($base, 'renew', ['price_type' => 'customize'], 30),
'pay' => $probe->price($base, 'pay', ['price_type' => 'discount'], 30),
];
})(),
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), PHP_EOL;
}