Files
wordpress-theme-aksrt/inc/classes/payment/class-aksrt-creator-revenue.php
T
kate522 ce1c13b947 refactor(classes): organize inc/classes into domain folders
Move ~94 root class shells and root *-traits into domain directories
(core, auth, user, credits, content, payment/gateways, forum, shop,
sms, ai, messages, extensions, widgets, settings, admin). Update
bootstrap require paths, co-locate traits with owners, fix forum/user
relative requires, drop duplicate shop controller requires, and wire
home-modules load. Path-only; class names and init order unchanged.
2026-07-17 11:11:57 +08:00

244 lines
8.4 KiB
PHP

<?php
/**
* AKSRT Creator Revenue Settlement.
*
* Wires order-paid into the pre-existing revenue ledger
* (`aksrt_revenue` table + AKSRT_Revenue service).
*
* Scope:
* - Shop product orders (line items via AKSRT_Shop::get_order_items)
* - Non-shop cash: content_pay / topic_pay / plate_follow_pay
* - Share ratio from AKSRT_Settings::get_payment_revenue_settings()
* - Buyer == author skipped; amount≤0 skipped; points cash skipped
* - Idempotent per order_number via type=order guard row
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
if (!class_exists('AKSRT_Creator_Revenue')) {
class AKSRT_Creator_Revenue {
private static ?AKSRT_Creator_Revenue $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('aksrt_order_paid', [$this, 'handle_order_paid'], 20, 2);
}
/**
* Whether creator revenue settlement is enabled.
*/
private function is_enabled(): bool {
if (!class_exists('AKSRT_Revenue') || !class_exists('AKSRT_Settings')) {
return false;
}
$settings = AKSRT_Settings::get_payment_revenue_settings();
return !empty($settings['enabled']);
}
/**
* Resolve share ratio (percentage 0-100) for a given product author.
*/
private function resolve_share_ratio(int $author_id): float {
$settings = AKSRT_Settings::get_payment_revenue_settings();
$ratio = (float) ($settings['share_ratio'] ?? 0);
// VIP-tier author overrides, if configured (>0 wins).
if ($author_id > 0 && function_exists('aksrt_get_user_vip_level')) {
$level = (int) aksrt_get_user_vip_level($author_id);
if ($level === 1 && (float) ($settings['ratio_vip1'] ?? 0) > 0) {
$ratio = (float) $settings['ratio_vip1'];
} elseif ($level >= 2 && (float) ($settings['ratio_vip2'] ?? 0) > 0) {
$ratio = (float) $settings['ratio_vip2'];
}
}
return max(0.0, min(100.0, $ratio));
}
/**
* Guard: has this order already been settled into revenue?
*/
private function already_settled(string $order_number): bool {
if ($order_number === '' || !class_exists('AKSRT_DB_Migrator')) {
return false;
}
global $wpdb;
$table = AKSRT_DB_Migrator::table('revenue');
if ($table === '') {
return false;
}
$count = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COUNT(*) FROM {$table} WHERE order_number = %s AND type = %s",
$order_number,
'order'
));
return $count > 0;
}
/**
* aksrt_order_paid listener: credit creators their share.
*
* @param string $order_number Paid order number.
* @param array $order Order row (status=paid).
*/
public function handle_order_paid(string $order_number, array $order): void {
if (!$this->is_enabled() || $order_number === '') {
return;
}
if ($this->already_settled($order_number)) {
return;
}
$buyer_id = (int) ($order['user_id'] ?? 0);
$items = [];
if (class_exists('AKSRT_Shop')) {
$shop = AKSRT_Shop::get_instance();
if (method_exists($shop, 'get_order_items')) {
$items = (array) $shop->get_order_items($order_number);
}
}
if (!empty($items)) {
$this->settle_shop_items($order_number, $buyer_id, $items);
return;
}
$this->settle_non_shop_cash_order($order_number, $order, $buyer_id);
}
/**
* @param list<array<string,mixed>> $items
*/
private function settle_shop_items(string $order_number, int $buyer_id, array $items): void {
$revenue = AKSRT_Revenue::get_instance();
foreach ($items as $item) {
$product_id = (int) ($item['product_id'] ?? 0);
$pay_price = (float) ($item['pay_price'] ?? $item['total_price'] ?? 0);
if ($product_id <= 0 || $pay_price <= 0) {
continue;
}
$author_id = (int) get_post_field('post_author', $product_id);
if ($author_id <= 0 || $author_id === $buyer_id) {
continue;
}
$ratio = $this->resolve_share_ratio($author_id);
if ($ratio <= 0) {
continue;
}
$amount = round($pay_price * $ratio / 100, 2);
if ($amount <= 0) {
continue;
}
$revenue->add($author_id, $amount, $order_number, 'order');
}
}
/**
* Content / topic / plate_follow cash orders (no shop line items).
*/
private function settle_non_shop_cash_order(string $order_number, array $order, int $buyer_id): void {
$meta = $this->decode_note($order);
if (($meta['pay_modo'] ?? '') === 'points') {
return;
}
$payment_method = sanitize_key((string) ($order['payment_method'] ?? ''));
$currency = sanitize_key((string) ($order['currency'] ?? ''));
if ($payment_method === 'points' || $currency === 'points') {
return;
}
$base_amount = round((float) ($order['amount'] ?? $meta['payable'] ?? 0), 2);
if ($base_amount <= 0) {
return;
}
$product_type = sanitize_key((string) ($order['product_type'] ?? ''));
$product_id = (int) ($order['product_id'] ?? 0);
if ($product_id <= 0) {
return;
}
$post = get_post($product_id);
if (!($post instanceof WP_Post)) {
return;
}
$is_plate = !empty($meta['plate_follow_pay'])
|| $product_type === AKSRT_Domain_Model::ORDER_PRODUCT_PLATE_FOLLOW;
$is_topic = !empty($meta['topic_pay'])
|| ($product_type === AKSRT_Domain_Model::ORDER_PRODUCT_RESOURCE
&& $post->post_type === 'aksrt_forum_topic');
$is_content = !empty($meta['content_pay'])
&& $post->post_type === 'post';
// Resource without content_pay/topic_pay and not a known cash kind: skip.
if (!$is_plate && !$is_topic && !$is_content) {
return;
}
// Guard: plate must be plate CPT; topic must be topic CPT.
if ($is_plate && $post->post_type !== 'aksrt_forum_plate') {
return;
}
if ($is_topic && $post->post_type !== 'aksrt_forum_topic') {
return;
}
$author_id = (int) $post->post_author;
if ($author_id <= 0 || $author_id === $buyer_id) {
return;
}
$ratio = $this->resolve_share_ratio($author_id);
if ($ratio <= 0) {
return;
}
$amount = round($base_amount * $ratio / 100, 2);
if ($amount <= 0) {
return;
}
// Re-check after shop-less path; shop path already returned.
if ($this->already_settled($order_number)) {
return;
}
AKSRT_Revenue::get_instance()->add($author_id, $amount, $order_number, 'order');
}
/**
* @param array<string,mixed> $order
* @return array<string,mixed>
*/
private function decode_note(array $order): array {
if (isset($order['meta_payload']) && is_array($order['meta_payload'])) {
return $order['meta_payload'];
}
$note = (string) ($order['note'] ?? '');
if ($note === '') {
return [];
}
$decoded = json_decode($note, true);
return is_array($decoded) ? $decoded : [];
}
}
}