The structured groups introduced in the previous commit rendered every row fully expanded, which buried the fields an admin actually came to edit. They now behave like accordions and start collapsed. - 会员商品, 一级/二级会员续费 and 积分兑换明细 opt in via `collapsible` on the group schema. Each row shows only its header, carrying a summary line built from `listPreview` (价格:99.00 · 3 个月 / 积分:500 · 30 天), and expands on click. `defaultCollapsed` keeps the collapsed default; setting it to false restores the previous always-open behaviour. - 升级价格 splits into its three reference-theme blocks (月费升月费 / 月费升永久 / 永久升永久) through a new `sections` option on the accordion type, each block independently collapsible. - AksField keeps the existing open state keyed by index so adding or removing a row does not disturb the others, and the drag handle, delete button and chevron are excluded from the header toggle. - Non-collapsible groups are untouched: the accordion markup only renders when the schema opts in. Verified: php -l on the changed PHP, 656 Node contract tests passing (including new assertions that every collapsible group is opted in, that the upgrade sections cover exactly the declared fields, and that the SPA mirror matches), and the admin SPA builds.
254 lines
9.7 KiB
PHP
254 lines
9.7 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,
|
|
],
|
|
'accordion' => (static function (array $groups): array {
|
|
$out = ['groups' => [], 'sections' => []];
|
|
foreach ($groups as $group) {
|
|
foreach ($group['fields'] as $top) {
|
|
if (($top['type'] ?? '') !== 'tabbed') {
|
|
continue;
|
|
}
|
|
foreach ($top['tabs'] as $tab) {
|
|
foreach ($tab['fields'] as $field) {
|
|
if (($field['type'] ?? '') === 'group') {
|
|
$out['groups'][$tab['key'] . '/' . $field['key']] = [
|
|
'collapsible' => $field['collapsible'] ?? null,
|
|
'defaultCollapsed' => $field['defaultCollapsed'] ?? null,
|
|
'listPreview' => $field['listPreview'] ?? null,
|
|
];
|
|
}
|
|
if (($field['type'] ?? '') === 'accordion') {
|
|
$out['sections'][$tab['key'] . '/' . $field['key']] = array_map(
|
|
static fn(array $section): array => [
|
|
'title' => $section['title'],
|
|
'keys' => $section['keys'],
|
|
],
|
|
$field['sections'] ?? []
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
foreach ($groups as $group) {
|
|
if ($group['key'] !== 'points-exchange') {
|
|
continue;
|
|
}
|
|
foreach ($group['fields'] as $field) {
|
|
if (($field['type'] ?? '') !== 'group') {
|
|
continue;
|
|
}
|
|
$out['groups']['points-exchange/' . $field['key']] = [
|
|
'collapsible' => $field['collapsible'] ?? null,
|
|
'defaultCollapsed' => $field['defaultCollapsed'] ?? null,
|
|
'listPreview' => $field['listPreview'] ?? null,
|
|
];
|
|
}
|
|
}
|
|
return $out;
|
|
})($groups),
|
|
'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;
|
|
}
|