feat(payment): separate creator income settlement
This commit is contained in:
1 parent
cd93ea1656
commit
e20418e909
17 files changed
+451
-445
No files matched your search
+58
-120
@@ -66,7 +66,7 @@ class Revenue {
|
||||
$author_level = function_exists('aksrt_get_user_vip_level')
|
||||
? max(0, (int) aksrt_get_user_vip_level($author_id))
|
||||
: 0;
|
||||
$ratio = $this->resolve_share_ratio($author_level);
|
||||
$ratio = $this->resolve_share_ratio($currency, $author_level);
|
||||
if ($ratio <= 0) {
|
||||
return 0;
|
||||
}
|
||||
@@ -157,7 +157,7 @@ class Revenue {
|
||||
}
|
||||
|
||||
/**
|
||||
* Settle all due pending records in automatic mode.
|
||||
* Settle all pending records whose rolling earning cycle has ended.
|
||||
*
|
||||
* @return int Number of records settled.
|
||||
*/
|
||||
@@ -168,15 +168,15 @@ class Revenue {
|
||||
return 0;
|
||||
}
|
||||
$settings = \AKSRT\Settings\Settings::get_payment_revenue_settings();
|
||||
if (($settings['settlement_mode'] ?? 'automatic') !== 'automatic') {
|
||||
return 0;
|
||||
}
|
||||
$cycle_days = max(1, (int) ($settings['cycle_days'] ?? 30));
|
||||
$timezone = function_exists('wp_timezone') ? wp_timezone() : new \DateTimeZone('UTC');
|
||||
$cutoff = (new \DateTimeImmutable('now', $timezone))
|
||||
->modify('-' . $cycle_days . ' days')
|
||||
->format('Y-m-d H:i:s');
|
||||
|
||||
$limit = max(1, min(500, $limit));
|
||||
$delay_hours = max(0, (int) ($settings['settlement_delay_hours'] ?? 24));
|
||||
$cutoff = gmdate('Y-m-d H:i:s', time() - ($delay_hours * HOUR_IN_SECONDS));
|
||||
$ids = $wpdb->get_col($wpdb->prepare(
|
||||
"SELECT id FROM {$this->table()} WHERE status = %s AND created_at <= %s ORDER BY id ASC LIMIT %d",
|
||||
"SELECT id FROM {$this->table()} WHERE status = %s AND created_at < %s ORDER BY id ASC LIMIT %d",
|
||||
'pending',
|
||||
$cutoff,
|
||||
$limit
|
||||
@@ -191,100 +191,6 @@ class Revenue {
|
||||
return $settled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending record or reverse a settled record for a refunded order.
|
||||
*
|
||||
* @param string|int $order_id Order number or local order id.
|
||||
*/
|
||||
public function cancel_or_reverse($order_id, string $reason = ''): bool {
|
||||
global $wpdb;
|
||||
|
||||
$order_number = sanitize_text_field((string) $order_id);
|
||||
if ($order_number === '') {
|
||||
return false;
|
||||
}
|
||||
$reason = sanitize_textarea_field($reason);
|
||||
$wpdb->query('START TRANSACTION');
|
||||
try {
|
||||
$rows = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT * FROM {$this->table()} WHERE order_number = %s FOR UPDATE",
|
||||
$order_number
|
||||
), ARRAY_A);
|
||||
if (!is_array($rows) || !$rows) {
|
||||
$wpdb->query('COMMIT');
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$status = (string) ($row['status'] ?? '');
|
||||
$id = (int) ($row['id'] ?? 0);
|
||||
if (in_array($status, ['cancelled', 'reversed'], true)) {
|
||||
continue;
|
||||
}
|
||||
if ($status === 'pending') {
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, error_message = %s, audit_json = %s WHERE id = %d AND status = %s",
|
||||
'cancelled',
|
||||
$reason,
|
||||
$this->audit_json('cancelled', $reason),
|
||||
$id,
|
||||
'pending'
|
||||
));
|
||||
if ($updated !== 1) {
|
||||
throw new \RuntimeException('revenue_cancel_failed');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!in_array($status, ['settled', 'pending_recovery'], true)) {
|
||||
continue;
|
||||
}
|
||||
if ($status === 'pending_recovery') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$amount = round((float) ($row['amount'] ?? 0), 2);
|
||||
$user_id = (int) ($row['author_id'] ?? 0);
|
||||
$ref = ['type' => 'creator_income_reversal', 'id' => $id];
|
||||
$debited = (string) ($row['currency'] ?? '') === 'points'
|
||||
? (class_exists('\\AKSRT\\Credits\\Credits') ? \AKSRT\Credits\Credits::deduct($user_id, (int) ceil($amount), 'creator_income_reversal', $ref, $reason) : new \WP_Error('credits_unavailable'))
|
||||
: (class_exists('\\AKSRT\\Credits\\Balance') ? \AKSRT\Credits\Balance::deduct($user_id, $amount, 'creator_income_reversal', $ref, $reason) : new \WP_Error('balance_unavailable'));
|
||||
if (is_wp_error($debited)) {
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, reversal_amount = %f, error_message = %s, audit_json = %s WHERE id = %d AND status = %s",
|
||||
'pending_recovery',
|
||||
$amount,
|
||||
sanitize_textarea_field($debited->get_error_message()),
|
||||
$this->audit_json('pending_recovery', $debited->get_error_message()),
|
||||
$id,
|
||||
'settled'
|
||||
));
|
||||
if ($updated !== 1) {
|
||||
throw new \RuntimeException('revenue_recovery_failed');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$updated = $wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->table()} SET status = %s, reversal_amount = %f, reversed_at = %s, error_message = %s, audit_json = %s WHERE id = %d AND status = %s",
|
||||
'reversed',
|
||||
$amount,
|
||||
current_time('mysql'),
|
||||
$reason,
|
||||
$this->audit_json('reversed', $reason),
|
||||
$id,
|
||||
'settled'
|
||||
));
|
||||
if ($updated !== 1) {
|
||||
throw new \RuntimeException('revenue_reverse_failed');
|
||||
}
|
||||
}
|
||||
$wpdb->query('COMMIT');
|
||||
return true;
|
||||
} catch (\Throwable $e) {
|
||||
$wpdb->query('ROLLBACK');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function settle_one(int $revenue_id, int $actor_id): bool {
|
||||
global $wpdb;
|
||||
$wpdb->query('START TRANSACTION');
|
||||
@@ -370,10 +276,10 @@ class Revenue {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
$where = ['author_id = %d'];
|
||||
$where = ["author_id = %d", "status IN ('pending', 'settled')"];
|
||||
$params = [$user_id];
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if ($status !== '') {
|
||||
if (in_array($status, ['pending', 'settled'], true)) {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
@@ -416,15 +322,17 @@ class Revenue {
|
||||
$where .= ' AND currency = %s';
|
||||
$params[] = $currency;
|
||||
}
|
||||
$today = current_time('Y-m-d');
|
||||
$stats_params = array_merge($params, [$today]);
|
||||
$stats_row = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT currency,
|
||||
COALESCE(SUM(CASE WHEN status IN ('pending', 'settled') THEN amount ELSE 0 END), 0) total_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END), 0) pending_amount,
|
||||
COALESCE(SUM(CASE WHEN status = 'settled' THEN amount ELSE 0 END), 0) settled_amount,
|
||||
COALESCE(SUM(CASE WHEN status IN ('pending', 'settled') AND DATE(created_at) = CURDATE() THEN amount ELSE 0 END), 0) today_amount
|
||||
COALESCE(SUM(CASE WHEN status IN ('pending', 'settled') AND DATE(created_at) = %s THEN amount ELSE 0 END), 0) today_amount
|
||||
FROM {$this->table()} WHERE {$where}
|
||||
GROUP BY currency",
|
||||
$params
|
||||
$stats_params
|
||||
), ARRAY_A);
|
||||
foreach ((array) $stats_row as $row) {
|
||||
$key = (string) ($row['currency'] ?? '');
|
||||
@@ -438,11 +346,14 @@ class Revenue {
|
||||
}
|
||||
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if (!in_array($status, ['pending', 'settled'], true)) {
|
||||
$status = '';
|
||||
}
|
||||
$list_args = ['currency' => $currency, 'status' => $status, 'limit' => 20, 'offset' => 0];
|
||||
$page = max(1, min(100, (int) ($args['page'] ?? 1)));
|
||||
$list_args['offset'] = ($page - 1) * 20;
|
||||
$items = $this->get_user_revenues($user_id, $list_args);
|
||||
$count_where = $where . ($status !== '' ? ' AND status = %s' : '');
|
||||
$count_where = $where . " AND status IN ('pending', 'settled')" . ($status !== '' ? ' AND status = %s' : '');
|
||||
$count_params = $params;
|
||||
if ($status !== '') {
|
||||
$count_params[] = $status;
|
||||
@@ -462,9 +373,10 @@ class Revenue {
|
||||
global $wpdb;
|
||||
$limit = max(1, min(100, (int) ($args['limit'] ?? 20)));
|
||||
$offset = max(0, (int) ($args['offset'] ?? 0));
|
||||
$where = [];
|
||||
$where = ["status IN ('pending', 'settled')"];
|
||||
$params = [];
|
||||
if (($status = sanitize_key((string) ($args['status'] ?? ''))) !== '') {
|
||||
$status = sanitize_key((string) ($args['status'] ?? ''));
|
||||
if (in_array($status, ['pending', 'settled'], true)) {
|
||||
$where[] = 'status = %s';
|
||||
$params[] = $status;
|
||||
}
|
||||
@@ -500,22 +412,48 @@ class Revenue {
|
||||
return ['items' => $items ?: [], 'total' => $total, 'page' => (int) floor($offset / $limit) + 1, 'max_pages' => (int) ceil(max(1, $total) / $limit)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return separate cash and points totals; amounts must never be added across units.
|
||||
*
|
||||
* @return array{cash:array{total:float,pending:float,settled:float,count:int},points:array{total:float,pending:float,settled:float,count:int}}
|
||||
*/
|
||||
public function get_stats(int $user_id = 0): array {
|
||||
global $wpdb;
|
||||
$where = $user_id > 0 ? 'WHERE author_id = %d' : '';
|
||||
$params = $user_id > 0 ? [$user_id] : [];
|
||||
$row = $params ? $wpdb->get_row($wpdb->prepare("SELECT COALESCE(SUM(amount),0) total, COALESCE(SUM(CASE WHEN status='pending' THEN amount ELSE 0 END),0) pending, COALESCE(SUM(CASE WHEN status='settled' THEN amount ELSE 0 END),0) settled, COUNT(*) count FROM {$this->table()} {$where}", $params), ARRAY_A) : $wpdb->get_row("SELECT COALESCE(SUM(amount),0) total, COALESCE(SUM(CASE WHEN status='pending' THEN amount ELSE 0 END),0) pending, COALESCE(SUM(CASE WHEN status='settled' THEN amount ELSE 0 END),0) settled, COUNT(*) count FROM {$this->table()}", ARRAY_A);
|
||||
return ['total' => (float) ($row['total'] ?? 0), 'pending' => (float) ($row['pending'] ?? 0), 'settled' => (float) ($row['settled'] ?? 0), 'count' => (int) ($row['count'] ?? 0)];
|
||||
$empty = [
|
||||
'cash' => ['total' => 0.0, 'pending' => 0.0, 'settled' => 0.0, 'count' => 0],
|
||||
'points' => ['total' => 0.0, 'pending' => 0.0, 'settled' => 0.0, 'count' => 0],
|
||||
];
|
||||
$where = ["status IN ('pending', 'settled')"];
|
||||
$params = [];
|
||||
if ($user_id > 0) {
|
||||
$where[] = 'author_id = %d';
|
||||
$params[] = $user_id;
|
||||
}
|
||||
$stats_sql = "SELECT currency,
|
||||
COALESCE(SUM(amount), 0) total,
|
||||
COALESCE(SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END), 0) pending,
|
||||
COALESCE(SUM(CASE WHEN status = 'settled' THEN amount ELSE 0 END), 0) settled,
|
||||
COUNT(*) count
|
||||
FROM {$this->table()} WHERE " . implode(' AND ', $where) . " GROUP BY currency";
|
||||
$rows = $wpdb->get_results($params ? $wpdb->prepare($stats_sql, $params) : $stats_sql, ARRAY_A);
|
||||
foreach ((array) $rows as $row) {
|
||||
$key = (string) ($row['currency'] ?? '') === 'points' ? 'points' : 'cash';
|
||||
$empty[$key] = [
|
||||
'total' => (float) ($row['total'] ?? 0),
|
||||
'pending' => (float) ($row['pending'] ?? 0),
|
||||
'settled' => (float) ($row['settled'] ?? 0),
|
||||
'count' => (int) ($row['count'] ?? 0),
|
||||
];
|
||||
}
|
||||
return $empty;
|
||||
}
|
||||
|
||||
private function resolve_share_ratio(int $author_level): float {
|
||||
private function resolve_share_ratio(string $currency, int $author_level): float {
|
||||
$settings = \AKSRT\Settings\Settings::get_payment_revenue_settings();
|
||||
$ratio = $author_level >= 2 ? 80.0 : ($author_level === 1 ? 70.0 : 60.0);
|
||||
$key = $author_level >= 2 ? 'ratio_vip2' : ($author_level === 1 ? 'ratio_vip1' : 'share_ratio');
|
||||
if (isset($settings[$key]) && is_numeric($settings[$key])) {
|
||||
$ratio = (float) $settings[$key];
|
||||
}
|
||||
return max(0, min(100, $ratio));
|
||||
$bucket = $currency === 'points' ? 'points' : 'cash';
|
||||
$level_key = $author_level >= 2 ? 'vip2' : ($author_level === 1 ? 'vip1' : 'ordinary');
|
||||
$ratio = $settings['ratios'][$bucket][$level_key] ?? 0;
|
||||
return max(0, min(100, (float) $ratio));
|
||||
}
|
||||
|
||||
private function decode_order_meta(array $order): array {
|
||||
|
||||
Reference in new issue
Block a user