'Cache erfolgreich geleert!']);
}
// Prüfe auf Updates (läuft im Admin-Hintergrund)
public static function check_for_update() {
$update_data = get_transient(self::$transient_name);
if (false === $update_data) {
$response = wp_remote_get(self::$api_url, ['timeout' => 10]);
if (is_wp_error($response)) {
return; // Fehler beim Abrufen, nichts tun
}
$body = wp_remote_retrieve_body($response);
$data = json_decode($body);
if (isset($data->tag_name) && isset($data->html_url)) {
// Entferne 'v' vom Tag (z.B. v2.1.3 -> 2.1.3)
$remote_version = ltrim($data->tag_name, 'v');
$update_data = [
'new_version' => $remote_version,
'url' => self::$update_url,
'package_url' => isset($data->assets[0]->browser_download_url) ? $data->assets[0]->browser_download_url : '',
'changelog' => isset($data->body) ? substr($data->body, 0, 200) . '...' : ''
];
// Speichere für 12 Stunden
set_transient(self::$transient_name, $update_data, 12 * HOUR_IN_SECONDS);
}
}
}
// Zeige Admin Notice wenn Update verfügbar
public static function show_update_notice() {
$update_data = get_transient(self::$transient_name);
if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')) {
?>
🚀 WP Ingame Shop Pro Update verfügbar!
Neue Version: (Du hast ) Hier im Git Repository ansehen oder unten im Widget Details prüfen.
$wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_items WHERE status = 'publish'"),
'orders' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders WHERE status = 'completed'"),
'pending' => $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}wis_orders WHERE status IN ('pending', 'claimed')"),
'revenue' => $wpdb->get_var("SELECT SUM(price) FROM {$wpdb->prefix}wis_orders WHERE status = 'completed'"),
];
$currency = get_option('wis_currency_name', 'Coins');
// Update Status bestimmen
$update_html = '✅ Aktuell (v' . WIS_VERSION . ')';
if ($update_data && version_compare(WIS_VERSION, $update_data['new_version'], '<')) {
$update_html = '⚠️ Update verfügbar!';
}
?>
Generiere eine JSON-Datei mit allen deinen Items für Gitea.
⚡ Quick-Import von Gitea
Importiert direkt von deinem Gitea Repository!
📥 JSON Import (Manuelle URL)
Importiere Items aus einer beliebigen JSON-URL.
✅ Shop wurde komplett zurückgesetzt!
';
}
?>
🔄 Shop Reset
⚠️ WARNUNG
Diese Aktion löscht ALLE Daten:
❌ Alle Items
❌ Alle Bestellungen
❌ Alle Gutscheine
❌ Alle Server
❌ Alle Kategorien
Diese Aktion kann NICHT rückgängig gemacht werden!
get_results("
SELECT player_name, SUM(price) as total_spent, COUNT(*) as order_count
FROM {$wpdb->prefix}wis_orders
WHERE status = 'completed'
GROUP BY player_name
ORDER BY total_spent DESC
LIMIT 50
");
?>
🏆 Top Spender
Spieler mit den höchsten Gesamtausgaben
Rang
Spieler
Ausgegeben
Bestellungen
Noch keine Statistiken vorhanden.
#
player_name); ?>
total_spent)); ?>
order_count); ?>
= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$date_where_sell = $range === 'all' ? '' : "AND s.sold_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$date_where_ord = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$chart_days = in_array($range, ['7','30','90']) ? intval($range) : ($range === '365' ? 365 : 30);
$oi_table = $wpdb->prefix . 'wis_order_items';
$ord_table = $wpdb->prefix . 'wis_orders';
$sell_table = $wpdb->prefix . 'wis_sell_log';
$oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'") ? true : false;
$sell_exists = $wpdb->get_var("SHOW TABLES LIKE '$sell_table'") ? true : false;
// ---- TOP-KÄUFE aus wis_order_items (nur status=completed Orders) ----
$top_buys = [];
$using_legacy = false;
if ($oi_exists) {
$top_buys = $wpdb->get_results("
SELECT
oi.item_id,
oi.item_name,
oi.item_type,
SUM(oi.quantity) AS qty,
SUM(oi.total) AS revenue,
AVG(oi.price_per_item) AS avg_price,
COUNT(DISTINCT oi.order_id) AS order_count
FROM {$oi_table} oi
INNER JOIN {$ord_table} o ON o.id = oi.order_id AND o.status = 'completed'
WHERE 1=1 {$date_where_oi}
GROUP BY oi.item_id, oi.item_name, oi.item_type
ORDER BY qty DESC
LIMIT 20
");
}
// ---- FALLBACK: wis_order_items leer → response-JSON aus wis_orders parsen ----
if (empty($top_buys)) {
$using_legacy = true;
$date_cond = $range === 'all' ? '' : "AND o.created_at >= DATE_SUB(NOW(), INTERVAL {$range} DAY)";
$old_orders = $wpdb->get_results("
SELECT o.id, o.price, o.response, o.created_at
FROM {$ord_table} o
WHERE o.status = 'completed'
AND o.item_id = 'cart'
AND o.response IS NOT NULL
{$date_cond}
ORDER BY o.created_at DESC
LIMIT 2000
");
// Parsen und aggregieren
$agg = []; // agg_key => data
$item_cache = []; // item_id => db row (cache um DB-Calls zu sparen)
foreach ($old_orders as $ord) {
$payload = json_decode($ord->response, true);
if (!$payload) continue;
$items_list = $payload['items'] ?? [];
$cmds = $payload['commands'] ?? [];
$order_price = floatval($ord->price);
// Gesamtzahl der Positionen für anteilige Preisberechnung
$total_positions = 0;
foreach ($items_list as $pi) { $total_positions += intval($pi['amount'] ?? 1); }
foreach ($cmds as $cmd) { $total_positions += 1; }
if ($total_positions < 1) $total_positions = 1;
// --- Normale Items ---
foreach ($items_list as $pi) {
$pid = $pi['id'] ?? '';
if (!$pid) continue;
$pqty = intval($pi['amount'] ?? 1);
// DB-Lookup mit Cache
if (!array_key_exists($pid, $item_cache)) {
$item_cache[$pid] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $pid
));
}
$db_item = $item_cache[$pid];
$item_name = $db_item ? $db_item->name : $pid;
// Aktueller Katalogpreis als primäre Quelle
$unit_price = $db_item
? ($db_item->offer_price > 0 ? floatval($db_item->offer_price) : floatval($db_item->price))
: round($order_price / $total_positions, 2); // Fallback: anteilig
if (!isset($agg[$pid])) {
$agg[$pid] = ['item_id'=>$pid,'item_name'=>$item_name,'item_type'=>'item',
'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0];
}
$agg[$pid]['qty'] += $pqty;
$agg[$pid]['revenue'] += $unit_price * $pqty;
$agg[$pid]['price_sum']+= $unit_price;
$agg[$pid]['cnt']++;
}
// --- Commands (fly, rank, fly_abo, plot) ---
foreach ($cmds as $cmd) {
$ctype = $cmd['type'] ?? 'item';
$clabel = $cmd['label'] ?? $ctype;
$ckey = $ctype . '||' . $clabel;
// Preis aus wis_items anhand des Labels oder type-basierten item_id
$cmd_price = 0;
if ($ctype === 'fly') {
// Fly-Item anhand der Dauer identifizieren
$dur_sec = intval($cmd['duration_sec'] ?? 0);
$fly_map = [300=>'fly_5min',900=>'fly_15min',1800=>'fly_30min',3600=>'fly_1h',7200=>'fly_2h',10800=>'fly_3h'];
$fly_id = $fly_map[$dur_sec] ?? null;
if ($fly_id) {
if (!array_key_exists($fly_id, $item_cache)) {
$item_cache[$fly_id] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id = %s LIMIT 1", $fly_id
));
}
$fi = $item_cache[$fly_id];
$cmd_price = $fi ? ($fi->offer_price > 0 ? floatval($fi->offer_price) : floatval($fi->price)) : 0;
}
} elseif ($ctype === 'rank') {
// Rank-Item: suche nach rank_{rank_id}* in wis_items
$rid = preg_replace('/[^a-zA-Z0-9_\-]/', '', $cmd['rank_id'] ?? '');
if ($rid && !array_key_exists('rank_'.$rid, $item_cache)) {
$item_cache['rank_'.$rid] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1",
'rank_' . $rid . '%'
));
}
$ri = $rid ? ($item_cache['rank_'.$rid] ?? null) : null;
$cmd_price = $ri ? ($ri->offer_price > 0 ? floatval($ri->offer_price) : floatval($ri->price)) : 0;
} elseif (in_array($ctype, ['fly_abo','plot_abo','plot_slots'])) {
// Abo/Plot: direkt nach type-ähnlicher item_id suchen
if (!array_key_exists($ctype, $item_cache)) {
$item_cache[$ctype] = $wpdb->get_row($wpdb->prepare(
"SELECT name, price, offer_price FROM {$wpdb->prefix}wis_items WHERE item_id LIKE %s LIMIT 1",
$ctype . '%'
));
}
$ai = $item_cache[$ctype] ?? null;
$cmd_price = $ai ? ($ai->offer_price > 0 ? floatval($ai->offer_price) : floatval($ai->price)) : 0;
}
// Letzter Fallback: anteiliger Orderpreis
if ($cmd_price <= 0) {
$cmd_price = round($order_price / $total_positions, 2);
}
if (!isset($agg[$ckey])) {
$agg[$ckey] = ['item_id'=>$ctype,'item_name'=>$clabel,'item_type'=>$ctype,
'qty'=>0,'revenue'=>0,'price_sum'=>0,'cnt'=>0];
}
$agg[$ckey]['qty'] += 1;
$agg[$ckey]['revenue'] += $cmd_price;
$agg[$ckey]['price_sum']+= $cmd_price;
$agg[$ckey]['cnt']++;
}
}
// Sortieren nach qty DESC, in top_buys-kompatibles Format bringen
usort($agg, fn($a,$b) => $b['qty'] - $a['qty']);
$agg = array_slice($agg, 0, 20);
foreach ($agg as $a) {
$obj = new stdClass();
$obj->item_id = $a['item_id'];
$obj->item_name = $a['item_name'];
$obj->item_type = $a['item_type'];
$obj->qty = $a['qty'];
$obj->revenue = $a['revenue'];
$obj->avg_price = $a['cnt'] > 0 ? round($a['price_sum'] / $a['cnt'], 2) : 0;
$obj->order_count = $a['cnt'];
$top_buys[] = $obj;
}
}
// ---- TOP-VERKÄUFE / ANKÄUFE ----
$top_sells = [];
if ($sell_exists) {
$top_sells = $wpdb->get_results("
SELECT
s.item_name,
s.item_id,
SUM(s.quantity) AS qty,
SUM(s.total_paid) AS total_paid,
AVG(s.price_per_item) AS avg_price
FROM {$sell_table} s
WHERE 1=1 {$date_where_sell}
GROUP BY s.item_id, s.item_name
ORDER BY qty DESC
LIMIT 20
");
}
// ---- UMSATZ PRO TAG (nach gewähltem Zeitraum) ----
$chart_interval = $range === 'all' ? 365 : intval($range);
$daily_revenue = $wpdb->get_results($wpdb->prepare("
SELECT DATE(o.created_at) AS day, SUM(o.price) AS revenue, COUNT(*) AS orders
FROM {$ord_table} o
WHERE o.status = 'completed'
AND o.created_at >= DATE_SUB(NOW(), INTERVAL %d DAY)
GROUP BY DATE(o.created_at)
ORDER BY day ASC
", $chart_interval));
// ---- KPI-Gesamtzahlen ----
$buy_qty = $oi_exists ? ($wpdb->get_var("SELECT SUM(oi.quantity) FROM {$oi_table} oi INNER JOIN {$ord_table} o ON o.id=oi.order_id AND o.status='completed' WHERE 1=1 {$date_where_oi}") ?: 0) : 0;
$buy_revenue = $wpdb->get_var("SELECT SUM(o.price) FROM {$ord_table} o WHERE o.status='completed' {$date_where_ord}") ?: 0;
$sell_qty = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.quantity) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0;
$sell_paid = $sell_exists ? ($wpdb->get_var("SELECT SUM(s.total_paid) FROM {$sell_table} s WHERE 1=1 {$date_where_sell}") ?: 0) : 0;
$chart_labels = [];
$chart_revenue = [];
foreach ($daily_revenue as $dr) {
$chart_labels[] = $dr->day;
$chart_revenue[] = floatval($dr->revenue);
}
?>
📊 Analyse – Kauf & Verkauf
'7 Tage','30'=>'30 Tage','90'=>'90 Tage','365'=>'1 Jahr','all'=>'Gesamt'] as $v=>$l): ?>
⚠️ Einzelitem-Tracking noch nicht aktiv
Die Tabelle wis_order_items fehlt noch. Bitte das Plugin einmal deaktivieren und wieder aktivieren. Ab dann werden alle neuen Käufe item-genau erfasst. Bis dahin wird die Auswertung aus den gespeicherten Bestell-JSONs rekonstruiert (Preise sind Näherungswerte aus dem aktuellen Katalog).
🛒 Items gekauft(n/v)' : ''; ?>
💰 Einnahmen ()
📤 Ankäufe (Menge)
📉 Ausgezahlt ()
📈 Tagesumsatz
Keine abgeschlossenen Bestellungen in den letzten 30 Tagen.
Top 20 – meistgekaufte Items (einzeln)
Keine Daten gefunden.
Keine abgeschlossenen Bestellungen für diesen Zeitraum.
#
Item
Ø Preis
Menge
Umsatz
Trend
item_name); ?>
item_id); ?>
avg_price, 0); ?>
qty); ?>
/ order_count); ?> Käufe
revenue, 0); ?>
💡 Viel gekauft + hoher Umsatz → Preis erhöhen. Viel gekauft + niedriger Ø-Preis → evtl. zu günstig.
Top 20 – Ankauf durch Spieler (einzeln)
Ankauf-Tabelle nicht vorhanden. Ankauf-Feature aktivieren.
Keine Ankaufdaten für diesen Zeitraum.
#
Item
Ø Ankaufspreis
Menge
Ausgezahlt
Trend
item_name); ?>
item_id); ?>
avg_price, 2); ?>
qty); ?>
total_paid, 0); ?>
💡 Viel verkauft = Spieler farmen dieses Item massenhaft → Ankaufspreis senken oder Tageslimit einführen.
'POST',
'callback' => [self::class, 'import_json'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/order', [
'methods' => 'POST',
'callback' => [self::class, 'create_order'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/validate_coupon', [
'methods' => 'POST',
'callback' => [self::class, 'validate_coupon'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/shop_items', [
'methods' => 'GET',
'callback' => [self::class, 'get_shop_items'],
'permission_callback' => '__return_true',
]);
register_rest_route('wis/v1', '/pending_orders', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_orders'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/execute_order', [
'methods' => 'POST',
'callback' => [self::class, 'execute_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/complete_order', [
'methods' => 'POST',
'callback' => [self::class, 'complete_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/cancel_order', [
'methods' => 'POST',
'callback' => [self::class, 'cancel_order'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// Gift-System Endpunkte (ab v6.5-gift)
register_rest_route('wis/v1', '/pending_gifts', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_gifts'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/gift_accept', [
'methods' => 'POST',
'callback' => [self::class, 'gift_accept'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/gift_decline', [
'methods' => 'POST',
'callback' => [self::class, 'gift_decline'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/pending_offline', [
'methods' => 'GET',
'callback' => [self::class, 'get_pending_offline'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/orders_history', [
'methods' => 'GET',
'callback' => [self::class, 'get_orders_history'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
// Ankauf-Endpunkte (ab v6.5)
register_rest_route('wis/v1', '/sell_items', [
'methods' => 'GET',
'callback' => [self::class, 'get_sell_items'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/sell_item', [
'methods' => 'POST',
'callback' => [self::class, 'process_sell'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
}
// =========================================================
// ANKAUF – Endpunkte (ab v6.5)
// =========================================================
/**
* GET /wis/v1/sell_items?server=
* Liefert alle Items die auf dem angegebenen Server ankaufbar sind.
*/
public static function get_sell_items($request) {
global $wpdb;
$server = sanitize_text_field($request->get_param('server') ?? '');
$currency = get_option('wis_currency_name', 'Coins');
$table = $wpdb->prefix . 'wis_items';
$items = $wpdb->get_results(
"SELECT id, item_id, name, price, sell_price_mode, sell_price_value
FROM $table
WHERE status = 'publish' AND sell_enabled = 1"
);
$result = [];
foreach ($items as $item) {
// Ankaufspreis berechnen
$sell_price = self::calc_sell_price(
(int) $item->price,
$item->sell_price_mode,
(int) $item->sell_price_value
);
if ($sell_price <= 0) continue;
// Server-Filter
if (!empty($server)) {
$servers = $wpdb->get_var($wpdb->prepare(
"SELECT servers FROM $table WHERE id = %d", $item->id
));
$srv_list = json_decode($servers, true) ?: [];
if (!empty($srv_list) && !in_array(strtolower($server), array_map('strtolower', $srv_list))) {
continue;
}
}
$result[] = [
'item_id' => $item->item_id,
'name' => $item->name,
'buy_price' => (int) $item->price,
'sell_price' => $sell_price,
];
}
return new WP_REST_Response([
'items' => $result,
'currency' => $currency,
]);
}
/**
* POST /wis/v1/sell_item
* Body: { "player": "Steve", "server": "survival", "item_id": "minecraft:diamond", "quantity": 5 }
* Antwortet mit dem Betrag der gutgeschrieben werden soll.
*/
public static function process_sell($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player') ?? '');
$server = sanitize_text_field($request->get_param('server') ?? '');
$item_id = sanitize_text_field($request->get_param('item_id') ?? '');
$quantity = max(1, intval($request->get_param('quantity') ?? 1));
if (!$player || !$item_id) {
return new WP_REST_Response(['success' => false, 'message' => 'Fehlende Parameter'], 400);
}
$table = $wpdb->prefix . 'wis_items';
// Item suchen (case-insensitive, mit/ohne minecraft: prefix)
$clean_id = strtolower(str_replace('minecraft:', '', $item_id));
$row = $wpdb->get_row($wpdb->prepare(
"SELECT id, item_id, name, price, sell_enabled, sell_price_mode, sell_price_value, servers
FROM $table
WHERE sell_enabled = 1
AND (LOWER(item_id) = %s OR LOWER(REPLACE(item_id,'minecraft:','')) = %s)
LIMIT 1",
strtolower($item_id), $clean_id
));
if (!$row) {
return new WP_REST_Response(['success' => false, 'message' => 'Item nicht ankaufbar'], 404);
}
$sell_price = self::calc_sell_price(
(int) $row->price,
$row->sell_price_mode,
(int) $row->sell_price_value
);
if ($sell_price <= 0) {
return new WP_REST_Response(['success' => false, 'message' => 'Ankaufspreis ist 0'], 422);
}
$total = round($sell_price * $quantity, 2);
// Sell-Log schreiben
$wpdb->insert($wpdb->prefix . 'wis_sell_log', [
'player_name' => $player,
'server' => $server,
'item_id' => $row->item_id,
'item_name' => $row->name,
'quantity' => $quantity,
'price_per_item' => $sell_price,
'total_paid' => $total,
]);
return new WP_REST_Response([
'success' => true,
'item_id' => $row->item_id,
'item_name' => $row->name,
'quantity' => $quantity,
'price_per_item' => $sell_price,
'total' => $total,
]);
}
/**
* Berechnet den Ankaufspreis aus Verkaufspreis + Modus.
* mode = "percent" → value ist ein Prozentwert des VK-Preises (z.B. 80 = 80 %)
* mode = "fixed" → value ist ein absoluter Festpreis
* mode = "minus" → value ist ein absoluter Abzug vom VK-Preis
*/
private static function calc_sell_price(int $buy_price, string $mode, int $value): float {
switch ($mode) {
case 'fixed':
return max(0, $value);
case 'minus':
return max(0, $buy_price - $value);
case 'percent':
default:
return max(0, round($buy_price * $value / 100, 2));
}
}
public static function get_shop_items($request) {
$page = max(1, intval($request->get_param('page') ?? 1));
$per_page_param = $request->get_param('per_page');
// -1 = "all", allowed values: 25, 50, 100, -1
$allowed_per_page = [24, 25, 50, 100, -1];
if ($per_page_param !== null) {
$per_page_param = intval($per_page_param);
$per_page = in_array($per_page_param, $allowed_per_page, true) ? $per_page_param : 24;
} else {
$per_page = intval(get_option('wis_default_per_page', 25));
if (!in_array($per_page, $allowed_per_page, true)) $per_page = 24;
}
$category = sanitize_text_field($request->get_param('category') ?? '');
$search = sanitize_text_field($request->get_param('search') ?? '');
global $wpdb;
$table = $wpdb->prefix . 'wis_items';
$where_parts = ["status = 'publish'"];
if (!empty($category)) {
$search_pattern = '%"' . $category . '"%';
$where_parts[] = $wpdb->prepare("categories LIKE %s", $search_pattern);
}
if (!empty($search)) {
$search_like = '%' . $wpdb->esc_like($search) . '%';
$where_parts[] = $wpdb->prepare("(name LIKE %s OR item_id LIKE %s)", $search_like, $search_like);
}
$where_sql = implode(" AND ", $where_parts);
$total = (int) $wpdb->get_var("SELECT COUNT(*) FROM $table WHERE $where_sql");
if ($per_page === -1) {
// Alle Items auf einmal
$items = $wpdb->get_results("SELECT * FROM $table WHERE $where_sql ORDER BY name ASC");
$effective_per_page = $total > 0 ? $total : 1;
} else {
$offset = ($page - 1) * $per_page;
$items = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table WHERE $where_sql ORDER BY name ASC LIMIT %d OFFSET %d",
$per_page, $offset
));
$effective_per_page = $per_page;
}
$img_base = get_option('wis_image_base_url', '');
$currency = get_option('wis_currency_name', 'Coins');
$result = [];
foreach ($items as $item) {
$result[] = [
'id' => $item->id,
'item_id' => $item->item_id,
'name' => $item->name,
'description' => $item->description,
'price' => intval($item->price),
'offer_price' => intval($item->offer_price),
'is_offer' => (bool) $item->is_offer,
'is_daily_deal' => (bool) $item->is_daily_deal,
'servers' => json_decode($item->servers, true) ?: [],
'categories' => json_decode($item->categories, true) ?: [],
'image' => WIS_DB::get_item_image($item),
'has_custom_image' => !empty($item->custom_image_url),
];
}
// Fly-Items nach Dauer sortieren: 5min→15min→30min→1h→2h→3h
$fly_order = ['fly_5min'=>1,'fly_15min'=>2,'fly_30min'=>3,'fly_1h'=>4,'fly_2h'=>5,'fly_3h'=>6];
usort($result, function($a, $b) use ($fly_order) {
$ap = isset($fly_order[$a['item_id']]) ? $fly_order[$a['item_id']] : 999;
$bp = isset($fly_order[$b['item_id']]) ? $fly_order[$b['item_id']] : 999;
if ($ap !== 999 || $bp !== 999) return $ap - $bp;
return strcmp($a['name'], $b['name']);
});
return new WP_REST_Response([
'items' => $result,
'total' => $total,
'page' => $page,
'per_page' => $per_page,
'total_pages' => $per_page === -1 ? 1 : max(1, (int) ceil($total / $per_page)),
'currency' => $currency,
]);
}
public static function get_pending_orders($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player'));
if (!$player) {
return new WP_REST_Response(['orders' => []]);
}
$table = $wpdb->prefix . 'wis_orders';
// Gift-Orders werden genauso wie normale Orders für Spieler A gepollt.
// Spieler A muss zuerst ingame bestätigen (Geld abbuchen), erst dann
// bekommt Spieler B die Gift-Anfrage.
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table
WHERE player_name = %s AND status = 'pending'
ORDER BY created_at ASC LIMIT 5",
$player
));
if (!empty($results)) {
$ids = implode(',', array_map(fn($o) => intval($o->id), $results));
$wpdb->query("UPDATE $table SET status = 'claimed' WHERE id IN ($ids) AND status = 'pending'");
}
return new WP_REST_Response(['orders' => $results]);
}
public static function get_pending_offline($request) {
if (get_option('wis_offline_queue_enabled', '0') !== '1') {
return new WP_REST_Response(['orders' => [], 'message' => 'Offline-Queue deaktiviert']);
}
global $wpdb;
$player = sanitize_text_field($request->get_param('player'));
if (!$player) {
return new WP_REST_Response(['orders' => []]);
}
$table = $wpdb->prefix . 'wis_orders';
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table
WHERE player_name = %s AND status = 'pending'
ORDER BY created_at ASC LIMIT 10",
$player
));
if (!empty($results)) {
$ids = implode(',', array_map(fn($o) => intval($o->id), $results));
$wpdb->query("UPDATE $table SET status = 'claimed' WHERE id IN ($ids) AND status = 'pending'");
}
return new WP_REST_Response(['orders' => $results]);
}
public static function get_orders_history($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player'));
if (!$player) {
return new WP_REST_Response(['orders' => []]);
}
$results = $wpdb->get_results($wpdb->prepare(
"SELECT id, item_title, price, status, created_at
FROM {$wpdb->prefix}wis_orders
WHERE player_name = %s
ORDER BY created_at DESC LIMIT 10",
$player
));
return new WP_REST_Response(['orders' => $results]);
}
public static function execute_order($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->query($wpdb->prepare(
"UPDATE {$wpdb->prefix}wis_orders SET status = 'processing'
WHERE id = %d AND status IN ('pending','claimed')",
$id
));
return new WP_REST_Response(['success' => true]);
}
public static function complete_order($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'completed'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
public static function cancel_order($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'cancelled'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
// ── Gift-System Endpoints ─────────────────────────────────────────────
/**
* GET /wis/v1/pending_gifts?recipient={player}
* Gibt ausstehende Geschenk-Orders zurück, die an diesen Empfänger gerichtet sind.
*/
public static function get_pending_gifts($request) {
global $wpdb;
$recipient = sanitize_text_field($request->get_param('recipient'));
if (!$recipient) {
return new WP_REST_Response(['orders' => []]);
}
$table = $wpdb->prefix . 'wis_orders';
// Status NICHT auf 'claimed' setzen – bleibt 'pending' bis der Empfänger
// ingame annimmt (gift_accept) oder ablehnt (gift_decline).
// Das Spigot-Plugin prüft pendingGiftRequests intern gegen Duplikate.
$results = $wpdb->get_results($wpdb->prepare(
"SELECT * FROM $table
WHERE gift_recipient = %s AND status IN ('pending', 'processing')
ORDER BY created_at ASC LIMIT 1",
$recipient
));
return new WP_REST_Response(['orders' => $results]);
}
/**
* POST /wis/v1/gift_accept { "id": 123 }
* Wird aufgerufen wenn Empfänger das Geschenk annimmt.
* Setzt Status auf 'processing' – der Spigot-Server liefert dann die Ware aus.
*/
public static function gift_accept($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'processing'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
/**
* POST /wis/v1/gift_decline { "id": 123, "sender": "SpielerA", "price": 500 }
* Empfänger lehnt ab → Order stornieren.
* Rückerstattung erfolgt auf Spigot-Seite via Vault direkt.
*/
public static function gift_decline($request) {
global $wpdb;
$data = $request->get_json_params();
$id = intval($data['id'] ?? 0);
if (!$id) return new WP_REST_Response(['success' => false], 400);
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'cancelled'],
['id' => $id]
);
return new WP_REST_Response(['success' => true]);
}
public static function import_json($request) {
$data = $request->get_json_params();
$url = esc_url_raw($data['url'] ?? '');
if (!$url) {
return new WP_REST_Response(['success' => false, 'message' => 'Keine URL angegeben'], 400);
}
$response = wp_remote_get($url, ['timeout' => 30]);
if (is_wp_error($response)) {
return new WP_REST_Response(['success' => false, 'message' => $response->get_error_message()], 400);
}
$body = wp_remote_retrieve_body($response);
$json = json_decode($body, true);
if (!isset($json['items']) || !is_array($json['items'])) {
return new WP_REST_Response(['success' => false, 'message' => 'Ungültiges JSON Format'], 400);
}
$imported = 0;
$skipped = 0;
foreach ($json['items'] as $item) {
$item_id = sanitize_text_field($item['id'] ?? '');
$name = sanitize_text_field($item['name'] ?? 'Unbekannt');
if (empty($item_id)) continue;
$exists = WIS_DB::get_item_by_item_id($item_id);
if ($exists) {
$skipped++;
continue;
}
WIS_DB::insert_item([
'item_id' => $item_id,
'name' => $name,
'description' => sanitize_textarea_field($item['description'] ?? ''),
'price' => intval($item['price'] ?? 0),
'status' => intval($item['price'] ?? 0) > 0 ? 'publish' : 'draft',
'servers' => '[]',
'categories' => '[]'
]);
$imported++;
}
return new WP_REST_Response(['success' => true, 'imported' => $imported, 'skipped' => $skipped]);
}
public static function create_order($request) {
global $wpdb;
$data = $request->get_json_params();
$player = sanitize_text_field($data['player'] ?? '');
$cart = $data['cart'] ?? [];
$server = sanitize_text_field($data['server'] ?? '');
$coupon_code = isset($data['coupon_code']) ? sanitize_text_field(strtoupper($data['coupon_code'])) : '';
// Gift: optionaler Empfänger-Spielername
$gift_recipient = isset($data['gift_recipient'])
? sanitize_text_field(trim($data['gift_recipient'])) : '';
if (!$player || empty($cart) || !$server) {
return new WP_REST_Response(['success' => false, 'message' => 'Fehlende Daten'], 400);
}
// Gift-Validierung: Spieler kann sich nicht selbst beschenken
if ($gift_recipient !== '' && strcasecmp($gift_recipient, $player) === 0) {
return new WP_REST_Response(['success' => false, 'message' => 'Selbst-Geschenk nicht erlaubt'], 400);
}
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
$currency = get_option('wis_currency_name', 'Coins');
$valid_cart = [];
$total_normal = 0;
$total_offer = 0;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if (!$item || $item->status !== 'publish') continue;
$qty = intval($item_data['quantity'] ?? 1);
if ($qty <= 0) continue;
$servers = json_decode($item->servers, true);
if (!in_array($server, $servers ?: [])) continue;
$price = $item->offer_price > 0 ? $item->offer_price : $item->price;
$valid_cart[] = [
'id' => $item->item_id,
'title' => $item->name,
'price' => $price,
'qty' => $qty,
'is_offer' => $item->is_offer
];
$item_total = $price * $qty;
if ($item->is_offer && $exclude_offers === '1') {
$total_offer += $item_total;
} else {
$total_normal += $item_total;
}
}
if (empty($valid_cart)) {
return new WP_REST_Response(['success' => false, 'message' => 'Keine gültigen Items'], 400);
}
$coupon_discount = 0;
$coupon_msg = '';
$coupon_applied = false;
if (!empty($coupon_code)) {
$coupon = WIS_DB::get_coupon_by_code($coupon_code);
if ($coupon) {
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
$coupon_msg = 'Gutschein abgelaufen';
} elseif ($coupon->used_count >= $coupon->usage_limit) {
$coupon_msg = 'Gutschein bereits aufgebraucht';
} else {
if ($exclude_offers === '1' && $total_normal <= 0) {
$coupon_msg = 'Gutschein gilt nicht für Angebote';
} else {
if ($coupon->type === 'percent') {
$coupon_discount = floor($total_normal * ($coupon->value / 100));
} else {
$coupon_discount = $coupon->value;
}
WIS_DB::update_coupon($coupon->id, ['used_count' => $coupon->used_count + 1]);
$coupon_applied = true;
$coupon_msg = "Gutschein eingelöst: -{$coupon_discount} {$currency}";
}
}
} else {
$coupon_msg = 'Ungültiger Code';
}
}
$final_price = max(0, $total_normal - $coupon_discount) + $total_offer;
// ── Steuer ──────────────────────────────────────────────────
$tax_enabled = get_option('wis_tax_enabled', '0');
$tax_rate = floatval(get_option('wis_tax_rate', '0'));
$tax_amount = 0;
if ($tax_enabled === '1' && $tax_rate > 0) {
$tax_amount = floor($final_price * $tax_rate / 100);
$final_price = $final_price + $tax_amount;
}
// ────────────────────────────────────────────────────────────
// Fly-Dauern (item_id → Sekunden)
// Fly-Dauern und lesbares Label (wird dem Spieler auf dem Code angezeigt)
$fly_durations = [
'fly_5min' => 5 * 60,
'fly_15min' => 15 * 60,
'fly_30min' => 30 * 60,
'fly_1h' => 1 * 3600,
'fly_2h' => 2 * 3600,
'fly_3h' => 3 * 3600,
];
$fly_labels = [
'fly_5min' => '5 Minuten Fly',
'fly_15min' => '15 Minuten Fly',
'fly_30min' => '30 Minuten Fly',
'fly_1h' => '1 Stunde Fly',
'fly_2h' => '2 Stunden Fly',
'fly_3h' => '3 Stunden Fly',
];
$items_payload = [];
$commands_payload = [];
$title_parts = [];
foreach ($valid_cart as $item) {
$item_id = $item['id']; // Das ist der item_id-String aus wis_items
if (isset($fly_durations[$item_id])) {
// Fly-Gutschein: pro Stueck einen eigenen Code-Eintrag (qty > 1 = mehrere Codes)
$base_sec = $fly_durations[$item_id];
$base_label = $fly_labels[$item_id];
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'fly',
'duration_sec' => $base_sec,
'label' => $base_label,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^rank_([^_]+)_([a-zA-Z0-9_\-]+)_([a-zA-Z0-9_\-]+)_(\d+)$/', $item_id, $rm)) {
// Neues Format: rank_{rank_id}_{lp_group}_{default_group}_{days}
$rank_id = $rm[1];
$lp_group = $rm[2];
$default_group = $rm[3];
$rank_days = intval($rm[4]);
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'rank',
'rank_id' => $rank_id,
'lp_group' => $lp_group,
'default_group' => $default_group,
'label' => $item['title'],
'days' => $rank_days,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^rank_(.+)_(\d+)$/', $item_id, $rm)) {
// Altes Format (Fallback): rank_{rank_id}_{days}
$rank_id = $rm[1];
$rank_days = intval($rm[2]);
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'rank',
'rank_id' => $rank_id,
'lp_group'=> $rank_id,
'default_group' => 'default',
'label' => $item['title'],
'days' => $rank_days,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^fly_abo$/', $item_id) || preg_match('/^fly_abo_\d*$/', $item_id)) {
// Fly-Abo: monatlich abonniert, Preis = Monatsbeitrag
$commands_payload[] = [
'type' => 'fly_abo',
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^plot_slots_(\d+)$/', $item_id, $ps_m)) {
// Plot-Slots: einmaliger permanenter Kauf
$extra_slots = intval($ps_m[1]) * intval($item['qty']);
$commands_payload[] = [
'type' => 'plot_slots',
'slots' => $extra_slots,
'label' => $item['title'],
];
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^plot_abo_(\d+)$/', $item_id, $pa_m)) {
// Plot-Abo: monatliche Abbuchung, Preis = Monatsbeitrag
$abo_slots = intval($pa_m[1]);
$commands_payload[] = [
'type' => 'plot_abo',
'slots' => $abo_slots,
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} else {
// Normales Item
$items_payload[] = [
'id' => $item_id,
'amount' => $item['qty'],
];
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
}
}
$title = "Warenkorb: " . implode(', ', $title_parts);
if (strlen($title) > 240) $title = substr($title, 0, 237) . '...';
$payload = [
'items' => $items_payload,
'commands' => $commands_payload,
'coupon' => $coupon_applied ? ['code' => $coupon_code, 'discount' => $coupon_discount] : [],
];
WIS_DB::insert_order([
'player_name' => $player,
'gift_recipient' => $gift_recipient !== '' ? $gift_recipient : null,
'server' => $server,
'item_id' => 'cart',
'item_title' => $title,
'price' => $final_price,
'quantity' => count($valid_cart),
'status' => 'pending',
'response' => json_encode($payload)
]);
// ---- Einzelne Items für Analyse-Tabelle speichern ----
$new_order_id = $wpdb->insert_id;
if ($new_order_id) {
$oi_table = $wpdb->prefix . 'wis_order_items';
// Tabelle existiert? (für bestehende Installationen ohne Re-Aktivierung)
$oi_exists = $wpdb->get_var("SHOW TABLES LIKE '$oi_table'");
if ($oi_exists) {
foreach ($valid_cart as $ci) {
$ci_id = $ci['id'];
$ci_title = $ci['title'];
$ci_qty = intval($ci['qty'] ?? 1);
$ci_price = floatval($ci['price'] ?? 0);
// Item-Typ bestimmen
if (isset($fly_durations[$ci_id])) {
$ci_type = 'fly';
} elseif (preg_match('/^rank_/', $ci_id)) {
$ci_type = 'rank';
} elseif ($ci_id === 'fly_abo' || preg_match('/^fly_abo/', $ci_id)) {
$ci_type = 'fly_abo';
} elseif (preg_match('/^plot_/', $ci_id)) {
$ci_type = 'plot';
} else {
$ci_type = 'item';
}
$wpdb->insert($oi_table, [
'order_id' => $new_order_id,
'item_id' => $ci_id,
'item_name' => $ci_title,
'item_type' => $ci_type,
'quantity' => $ci_qty,
'price_per_item'=> $ci_price,
'total' => round($ci_price * $ci_qty, 2),
]);
}
}
}
// Fly-Abo Abonnements registrieren / verlängern
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') !== 'fly_abo') continue;
$abo_days = intval($cmd['days'] ?? 30);
$abo_label = sanitize_text_field($cmd['label'] ?? 'Fly-Abo');
$abo_price = 0;
// Preis aus dem Warenkorb ermitteln (fly_abo mit oder ohne Zahl-Suffix)
foreach ($valid_cart as $ci) {
if ($ci['id'] === 'fly_abo' || preg_match('/^fly_abo/', $ci['id'])) {
$abo_price = intval($ci['price'] ?? 0);
break;
}
}
global $wpdb;
WIS_Activator::create_abo_subs_table();
$subs_table = $wpdb->prefix . 'wis_fly_abo_subs';
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$subs_table} WHERE player_name = %s AND server = %s",
$player, $server
));
if ($existing && $existing->status === 'active') {
// Bestehendes aktives Abo verlängern (kumulativ)
$wpdb->query($wpdb->prepare(
"UPDATE {$subs_table}
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
cancelled = 0,
cancelled_at = NULL,
label = %s,
price = %d
WHERE id = %d",
$abo_days, $abo_label, $abo_price, $existing->id
));
} else {
// Neues Abo anlegen
$wpdb->replace($subs_table, [
'player_name' => $player,
'server' => $server,
'label' => $abo_label,
'price' => $abo_price,
'status' => 'active',
'cancelled' => 0,
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$abo_days} days")),
]);
}
}
$msg = $gift_recipient !== ''
? "🎁 Geschenk-Bestellung für {$gift_recipient} erfolgreich!"
: '✅ Bestellung erfolgreich!';
if ($coupon_msg) $msg .= ' (' . $coupon_msg . ')';
return new WP_REST_Response(['success' => true, 'message' => $msg]);
}
public static function validate_coupon($request) {
$data = $request->get_json_params();
$code = sanitize_text_field(strtoupper($data['code'] ?? ''));
$cart = $data['cart'] ?? [];
if (!$code) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Code']);
}
$coupon = WIS_DB::get_coupon_by_code($code);
if (!$coupon) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein nicht gefunden']);
}
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein abgelaufen']);
}
if ($coupon->used_count >= $coupon->usage_limit) {
return new WP_REST_Response(['success' => false, 'message' => 'Bereits aufgebraucht']);
}
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
if ($exclude_offers === '1' && !empty($cart)) {
$has_normal = false;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && !$item->is_offer) {
$has_normal = true;
break;
}
}
if (!$has_normal) {
return new WP_REST_Response(['success' => false, 'message' => 'Gutschein gilt nicht für Angebote']);
}
}
$currency = get_option('wis_currency_name', 'Coins');
$msg = $coupon->type === 'percent'
? "Gutschein gültig (-{$coupon->value}%)"
: "Gutschein gültig (-{$coupon->value} {$currency})";
return new WP_REST_Response([
'success' => true,
'type' => $coupon->type,
'value' => $coupon->value,
'message' => $msg
]);
}
}
// ===========================================================
// SHORTCODE - FRONTEND SHOP
// ===========================================================
class WIS_Shortcode {
public static function register() {
add_shortcode('ingame_shop_form', [self::class, 'render']);
}
public static function render() {
$servers = WIS_DB::get_servers();
$categories = WIS_DB::get_categories();
$currency = get_option('wis_currency_name', 'Coins');
$header_text = get_option('wis_header_text', '');
$exclude_offers = get_option('wis_coupon_exclude_offers', '0');
$first_category = !empty($categories) ? $categories[0]->slug : '';
ob_start();
?>
🛒 Ingame Shop
Lade Items…
🛒 Dein Warenkorb
Dein Warenkorb ist leer
Zwischensumme:0
⚖️ Steuer (%):0
Gesamt:0
⚠️ Der Empfänger muss das Geschenk ingame annehmen. Bei Ablehnung erhältst du dein Geld zurück.
'Zeigt Daily Deal oder Angebot an']
);
}
public function widget($args, $instance) {
echo $args['before_widget'];
if (!empty($instance['title'])) {
echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title'];
}
global $wpdb;
$currency = get_option('wis_currency_name', 'Coins');
$img_base = get_option('wis_image_base_url', '');
$item = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}wis_items WHERE is_daily_deal = 1 AND status = 'publish' LIMIT 1");
if (!$item) {
$item = $wpdb->get_row("SELECT * FROM {$wpdb->prefix}wis_items WHERE is_offer = 1 AND status = 'publish' ORDER BY updated_at DESC LIMIT 1");
}
if ($item) {
$price = $item->offer_price > 0 ? $item->offer_price : $item->price;
$show_old = $item->offer_price > 0 && $item->offer_price != $item->price;
$img_url = WIS_DB::get_item_image($item);
$target_url = !empty($instance['shop_url']) ? $instance['shop_url'] : home_url();
?>
prefix . 'wis_orders';
// Alle Orders die seit > 7 Tagen pending/claimed sind → cancelled
$affected = $wpdb->query(
"UPDATE {$table}
SET status = 'cancelled'
WHERE status IN ('pending', 'claimed')
AND created_at < DATE_SUB(NOW(), INTERVAL 7 DAY)"
);
if ($affected > 0) {
error_log("[WIS] Auto-Expire: {$affected} Order(s) nach 7 Tagen storniert.");
}
});