feat(forum): P1 topic coupons, P2 plate follow gateway, cash creator revenue
- Topic_Pay: soft/hard coupon validate, payable amount, free-after-discount, paid burn - Creator_Revenue: settle content/topic/plate_follow cash (type=order), keep shop path - Plate VIP1/VIP2 meta + resolve_plate_follow_pay_price via aksrt_get_user_vip_level - New AKSRT_Plate_Follow_Pay REST multi-gateway; remove Credits×100 money bypass - Frontend: cms-download plate_follow apiKind + forum-governance money cashier bridge
This commit is contained in:
1 parent
1a15ae7cea
commit
78d3189f8f
14 files changed
+1189
-169
No files matched your search
@@ -2,16 +2,15 @@
|
||||
/**
|
||||
* AKSRT Creator Revenue Settlement.
|
||||
*
|
||||
* Wires order-paid + non-shop paid content into the pre-existing revenue ledger
|
||||
* (`aksrt_revenue` table + AKSRT_Revenue service). This closes the deferred
|
||||
* "结算引擎" gap: the ledger, service and share-ratio settings already existed
|
||||
* but nothing credited creator revenue when an order was paid.
|
||||
* Wires order-paid into the pre-existing revenue ledger
|
||||
* (`aksrt_revenue` table + AKSRT_Revenue service).
|
||||
*
|
||||
* Scope (kept intentionally narrow):
|
||||
* - Only shop-product orders and post-level paid content credit revenue.
|
||||
* - Share ratio comes from AKSRT_Settings::get_payment_revenue_settings().
|
||||
* - Buyer == author is skipped (no self-dealing).
|
||||
* - Idempotent per order_number via a guard row lookup.
|
||||
* 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
|
||||
*/
|
||||
@@ -110,10 +109,18 @@ if (!class_exists('AKSRT_Creator_Revenue')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($items)) {
|
||||
return; // Only structured shop-product orders settle creator revenue here.
|
||||
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) {
|
||||
@@ -125,7 +132,7 @@ if (!class_exists('AKSRT_Creator_Revenue')) {
|
||||
|
||||
$author_id = (int) get_post_field('post_author', $product_id);
|
||||
if ($author_id <= 0 || $author_id === $buyer_id) {
|
||||
continue; // No author, or buyer is the author.
|
||||
continue;
|
||||
}
|
||||
|
||||
$ratio = $this->resolve_share_ratio($author_id);
|
||||
@@ -141,5 +148,96 @@ if (!class_exists('AKSRT_Creator_Revenue')) {
|
||||
$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 : [];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user