Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
322 lines
12 KiB
PHP
322 lines
12 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_strip_all_tags($value): string { return trim(strip_tags((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/Admin/SchemaProvider.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' => '<strong>青铜会员</strong>',
|
|
'pay_user_vip_2_name' => '<span>王者会员</span>',
|
|
'pay_user_vip_1_desc' => '<em>专属权益</em>',
|
|
'pay_user_vip_1_equity' => '<p>一对一支持</p>',
|
|
'vip_renew' => true,
|
|
'vip_renew_desc' => '<b>续费有礼</b>',
|
|
'vip_renew_price_type' => 'customize',
|
|
'vip_renew_discount' => 7.5,
|
|
'vip_renew_reduce' => '15.00',
|
|
'vip_upgrade' => true,
|
|
'vip_upgrade_desc' => '<i>升级王者</i>',
|
|
'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();
|
|
|
|
// ---- sanitize -> validate round-trip -----------------------------------
|
|
// 复现并锁定:积分兑换明细.level 的选项值为整数,而 SPA 提交的可能是数字
|
|
// 或字符串;净化必须把值收敛回选项声明的类型,否则严格 in_array 校验会以
|
|
// "has an invalid value" 拒绝一次完全合法的保存。
|
|
$sanitize = (static function (): array {
|
|
$provider = \AKSRT\Admin\SchemaProvider::get_instance();
|
|
$cases = [
|
|
'vip_html' => [
|
|
'pay_user_vip_1_name' => '<b>会员</b>',
|
|
'pay_user_vip_1_desc' => '<em>简介</em>',
|
|
'pay_user_vip_1_equity' => '<p>权益</p>',
|
|
'pay_user_vip_desc' => '<span>会员介绍</span>',
|
|
'vip_renew_desc' => '<strong>续费</strong>',
|
|
'vip_upgrade_desc' => '<i>升级</i>',
|
|
'vip_1_product' => [
|
|
['price' => '88.80', 'show_price' => '99.00', 'tag' => '<b>推荐</b>', 'tag_class' => 'jb-yellow', 'time' => 1, 'unit' => 'month'],
|
|
],
|
|
],
|
|
'default_rows' => [
|
|
'pay_vip_points_exchange_s' => true,
|
|
'pay_vip_points_exchange_product' => [
|
|
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'day'],
|
|
['points' => 5000, 'level' => 1, 'time' => 'Permanent'],
|
|
],
|
|
],
|
|
'string_level' => [
|
|
'pay_vip_points_exchange_s' => true,
|
|
'pay_vip_points_exchange_product' => [
|
|
['points' => 800, 'level' => '2', 'time' => 3, 'unit' => 'month'],
|
|
],
|
|
],
|
|
'invalid_level' => [
|
|
'pay_vip_points_exchange_s' => true,
|
|
'pay_vip_points_exchange_product' => [
|
|
['points' => 500, 'level' => 9, 'time' => 30, 'unit' => 'day'],
|
|
],
|
|
],
|
|
'invalid_unit' => [
|
|
'pay_vip_points_exchange_s' => true,
|
|
'pay_vip_points_exchange_product' => [
|
|
['points' => 500, 'level' => 1, 'time' => 30, 'unit' => 'year'],
|
|
],
|
|
],
|
|
'invalid_tag_class' => [
|
|
'vip_1_product' => [
|
|
['price' => '88.80', 'show_price' => '99', 'tag' => 'x', 'tag_class' => 'nope', 'time' => 1, 'unit' => 'month'],
|
|
],
|
|
],
|
|
];
|
|
$out = [];
|
|
foreach ($cases as $name => $payload) {
|
|
$clean = $provider->sanitize_payload('payment', $payload, 'vip');
|
|
$result = $provider->validate('payment', $clean, 'vip');
|
|
$out[$name] = [
|
|
'fields' => $clean,
|
|
'points_rows' => $clean['pay_vip_points_exchange_product'] ?? null,
|
|
'valid' => $result['valid'],
|
|
'errors' => $result['errors'],
|
|
];
|
|
}
|
|
return $out;
|
|
})();
|
|
|
|
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,
|
|
],
|
|
'sanitize' => $sanitize,
|
|
'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;
|
|
}
|