'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!';
}
?>
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', '/my_coupons', [
'methods' => 'GET',
'callback' => [self::class, 'get_my_coupons'],
'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'],
]);
// ── Item-Abo Endpoints ────────────────────────────────────────────
register_rest_route('wis/v1', '/item_abo_status', [
'methods' => 'GET',
'callback' => [self::class, 'item_abo_status'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/item_abo_cancel', [
'methods' => 'POST',
'callback' => [self::class, 'item_abo_cancel'],
'permission_callback' => [WIS_Activator::class, 'spigot_permission'],
]);
register_rest_route('wis/v1', '/trigger_abo_delivery', [
'methods' => 'POST',
'callback' => [self::class, 'trigger_abo_delivery'],
'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.
*/
// =========================================================
// ITEM-ABO – REST-Endpunkte
// =========================================================
/**
* GET /wis/v1/item_abo_status?player=
* Gibt alle aktiven Item-Abos eines Spielers zurück.
*/
public static function item_abo_status($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player') ?? '');
if (empty($player)) {
return new WP_REST_Response(['abos' => []], 200);
}
WIS_Activator::create_item_abo_subs_table();
$table = $wpdb->prefix . 'wis_item_abo_subs';
// id muss mit zurückgegeben werden, damit der Spigot-Client
// beim Kündigen gezielt eine einzelne Abo-Zeile ansprechen kann.
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT id, label, item_id, daily_qty, cancelled,
DATE_FORMAT(expires_at, '%%d.%%m.%%Y') AS expires_at
FROM {$table}
WHERE player_name = %s
AND status = 'active'
AND expires_at > NOW()
ORDER BY created_at ASC",
$player
), ARRAY_A);
foreach ($rows as &$row) {
$row['id'] = (int) $row['id'];
$row['daily_qty'] = (int) $row['daily_qty'];
$row['cancelled'] = (bool) $row['cancelled'];
}
unset($row);
return new WP_REST_Response(['abos' => $rows ?: []], 200);
}
/**
* POST /wis/v1/item_abo_cancel
* Body: {"player":"", "abo_id":}
* Kündigt genau das Item-Abo mit der angegebenen ID – nur wenn es dem Spieler gehört.
* Gibt 200 bei Erfolg, 403 bei falschem Besitzer, 404 wenn nicht gefunden.
*/
public static function item_abo_cancel($request) {
global $wpdb;
$body = json_decode($request->get_body(), true);
$player = sanitize_text_field($body['player'] ?? '');
$abo_id = intval($body['abo_id'] ?? 0);
if (empty($player) || $abo_id <= 0) {
return new WP_REST_Response(['success' => false, 'message' => 'Ungültige Parameter'], 400);
}
WIS_Activator::create_item_abo_subs_table();
$table = $wpdb->prefix . 'wis_item_abo_subs';
// Sicherheits-Check: Abo muss dem Spieler gehören und aktiv sein
$existing = $wpdb->get_row($wpdb->prepare(
"SELECT id, label FROM {$table}
WHERE id = %d AND player_name = %s AND status = 'active' AND cancelled = 0",
$abo_id, $player
));
if (!$existing) {
// Unterscheide: existiert gar nicht vs. gehört anderem Spieler
$any = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM {$table} WHERE id = %d", $abo_id
));
if ($any) {
return new WP_REST_Response(['success' => false, 'message' => 'Nicht dein Abo'], 403);
}
return new WP_REST_Response(['success' => false, 'message' => 'Abo nicht gefunden oder bereits gekündigt'], 404);
}
$wpdb->update(
$table,
['cancelled' => 1, 'cancelled_at' => current_time('mysql')],
['id' => $abo_id]
);
error_log("[WIS] Item-Abo #{$abo_id} ({$existing->label}) gekündigt von: {$player}");
return new WP_REST_Response([
'success' => true,
'label' => $existing->label,
'message' => "Item-Abo '{$existing->label}' für {$player} gekündigt",
], 200);
}
/**
* POST /wis/v1/trigger_abo_delivery
* Body: {"player":"", "server":""}
*
* Prüft ob der Spieler aktive Item-Abos hat, die heute noch nicht beliefert wurden,
* und legt für jedes fehlende Abo sofort eine pending-Order an.
* Wird beim Join und beim Login aufgerufen – als Fallback falls der WP-Cron nicht
* gelaufen ist (z.B. kein Traffic auf der WordPress-Seite).
*
* Gibt zurück: {"delivered": } – 0 wenn alles bereits geliefert wurde.
*/
public static function trigger_abo_delivery($request) {
global $wpdb;
$body = json_decode($request->get_body(), true);
$player = sanitize_text_field($body['player'] ?? '');
$server = sanitize_text_field($body['server'] ?? '');
if (empty($player)) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername'], 400);
}
WIS_Activator::create_item_abo_subs_table();
$subs_table = $wpdb->prefix . 'wis_item_abo_subs';
$orders_table = $wpdb->prefix . 'wis_orders';
$today = date('Y-m-d');
// Aktive Abos dieses Spielers die heute noch nicht beliefert wurden
$conditions = "player_name = %s AND status = 'active' AND cancelled = 0"
. " AND expires_at > NOW()"
. " AND (last_delivered IS NULL OR last_delivered < %s)";
// Server-Filter nur wenn angegeben
$args = [$player, $today];
if (!empty($server)) {
$conditions .= " AND server = %s";
$args[] = $server;
}
$pending_subs = $wpdb->get_results(
$wpdb->prepare("SELECT * FROM {$subs_table} WHERE {$conditions}", ...$args)
);
$delivered = 0;
foreach ($pending_subs as $sub) {
$payload = json_encode([
'items' => [['id' => $sub->item_id, 'amount' => intval($sub->daily_qty)]],
'commands' => [],
'abo_delivery' => true,
]);
$wpdb->insert($orders_table, [
'player_name' => $sub->player_name,
'server' => $sub->server,
'item_id' => 'item_abo_delivery',
'item_title' => '📦 Abo-Lieferung: ' . $sub->label . ' ×' . intval($sub->daily_qty),
'price' => 0,
'quantity' => intval($sub->daily_qty),
'status' => 'pending',
'response' => $payload,
]);
$wpdb->update($subs_table, ['last_delivered' => $today], ['id' => $sub->id]);
$delivered++;
}
return new WP_REST_Response(['success' => true, 'delivered' => $delivered], 200);
}
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);
}
// Tageslimit prüfen
$daily_limit = intval($row->daily_sell_limit ?? 0);
if ($daily_limit > 0) {
$sold_today = (int) $wpdb->get_var($wpdb->prepare(
"SELECT COALESCE(SUM(quantity),0) FROM {$wpdb->prefix}wis_sell_log
WHERE player_name = %s AND item_id = %s
AND DATE(sold_at) = CURDATE()",
$player, $row->item_id
));
if ($sold_today + $quantity > $daily_limit) {
$remaining = max(0, $daily_limit - $sold_today);
return new WP_REST_Response([
'success' => false,
'message' => "Tageslimit erreicht. Du kannst heute noch {$remaining}x dieses Item verkaufen.",
], 429);
}
}
$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),
'custom_command' => $item->custom_command ?? null,
];
}
// 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);
// Vor dem Update: Order-Payload lesen, um gift_card_codes zu extrahieren
$order = $wpdb->get_row($wpdb->prepare(
"SELECT response FROM {$wpdb->prefix}wis_orders WHERE id = %d",
$id
));
$wpdb->update(
$wpdb->prefix . 'wis_orders',
['status' => 'completed'],
['id' => $id]
);
// Gift-Card-Codes aus dem gespeicherten Payload sammeln und zurückgeben
$gift_card_codes = [];
if ($order && !empty($order->response)) {
$payload = json_decode($order->response, true);
if (isset($payload['commands']) && is_array($payload['commands'])) {
$currency = get_option('wis_currency_name', 'Coins');
foreach ($payload['commands'] as $cmd) {
if (($cmd['type'] ?? '') === 'gift_card') {
$gift_card_codes[] = [
'code' => $cmd['code'],
'amount' => $cmd['amount'],
'label' => $cmd['label'] ?? '',
];
}
}
}
}
return new WP_REST_Response(['success' => true, 'gift_card_codes' => $gift_card_codes]);
}
/**
* GET /wis/v1/my_coupons?player=Spielername
* Gibt alle ungenutzten Gift-Card-Gutscheine zurück, die ein Spieler gekauft hat.
* Verknüpfung: wis_orders.response enthält die gift_card codes → wis_coupons.used_count = 0
*/
public static function get_my_coupons($request) {
global $wpdb;
$player = sanitize_text_field($request->get_param('player') ?? '');
if (!$player) {
return new WP_REST_Response(['success' => false, 'message' => 'Kein Spielername angegeben'], 400);
}
$currency = get_option('wis_currency_name', 'Coins');
// Alle abgeschlossenen Orders des Spielers mit gift_card commands holen
$orders = $wpdb->get_results($wpdb->prepare(
"SELECT response FROM {$wpdb->prefix}wis_orders
WHERE player_name = %s
AND status = 'completed'
AND response LIKE '%gift_card%'
ORDER BY created_at DESC",
$player
));
$result = [];
$seen = [];
foreach ($orders as $order) {
if (empty($order->response)) continue;
$payload = json_decode($order->response, true);
if (!isset($payload['commands'])) continue;
foreach ($payload['commands'] as $cmd) {
if (($cmd['type'] ?? '') !== 'gift_card') continue;
$code = $cmd['code'] ?? '';
if (!$code || isset($seen[$code])) continue;
$seen[$code] = true;
// Prüfen ob der Coupon noch ungenutzt ist
$coupon = $wpdb->get_row($wpdb->prepare(
"SELECT code, value, used_count, usage_limit, expiry
FROM {$wpdb->prefix}wis_coupons
WHERE code = %s",
$code
));
if (!$coupon) continue;
if ($coupon->used_count >= $coupon->usage_limit) continue; // bereits eingelöst
if ($coupon->expiry && date('Y-m-d') > $coupon->expiry) continue; // abgelaufen
$result[] = [
'code' => $coupon->code,
'value' => intval($coupon->value),
'currency' => $currency,
'label' => ($cmd['label'] ?? ''),
'expiry' => $coupon->expiry ?: null,
];
}
}
return new WP_REST_Response([
'success' => true,
'player' => $player,
'coupons' => $result,
]);
}
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;
// Gutschein-Karte: Preis = Wunschbetrag des Käufers (innerhalb Min/Max)
$custom_amount = 0;
if (preg_match('/^gift_card_(\d+)_(\d+)$/', $item->item_id, $gc_m2)) {
$gc_min2 = intval($gc_m2[1]);
$gc_max2 = intval($gc_m2[2]);
$raw_amount = intval($item_data['custom_amount'] ?? $gc_min2);
$custom_amount = max($gc_min2, min($gc_max2, $raw_amount));
$price = $custom_amount; // Preis = Wunschbetrag
}
$valid_cart[] = [
'id' => $item->item_id,
'title' => $item->name,
'price' => $price,
'qty' => $qty,
'is_offer' => $item->is_offer,
'custom_amount' => $custom_amount ?: $price,
];
$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;
$coupon_error = false; // Gutschein-Fehler der den Kauf blockiert
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_error = true;
$coupon_msg = 'Dein Gutschein ist abgelaufen.';
} elseif ($coupon->used_count >= $coupon->usage_limit) {
$coupon_error = true;
$coupon_msg = 'Dieser Gutschein ist bereits vollständig aufgebraucht.';
} elseif (WIS_DB::coupon_used_by_player($coupon->id, $player)) {
$coupon_error = true;
$coupon_msg = 'Du hast diesen Gutschein bereits eingelöst.';
} else {
if ($exclude_offers === '1' && $total_normal <= 0) {
$coupon_error = true;
$coupon_msg = 'Dieser Gutschein gilt nicht für Angebots-Items.';
} else {
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $total_normal);
if ($restriction_error !== null) {
$coupon_error = true;
$coupon_msg = $restriction_error;
} 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]);
WIS_DB::record_coupon_use($coupon->id, $player);
$coupon_applied = true;
$coupon_msg = "Gutschein eingelöst: -{$coupon_discount} {$currency}";
}
}
} else {
$coupon_error = true;
$coupon_msg = 'Ungültiger Gutschein-Code.';
}
}
// Kauf blockieren wenn Gutschein-Fehler vorliegt und Spieler nicht explizit bestätigt hat
$confirmed_no_coupon = (bool)($data['confirmed_no_coupon'] ?? false);
if ($coupon_error && !$confirmed_no_coupon) {
return new WP_REST_Response([
'success' => false,
'coupon_error' => true,
'message' => $coupon_msg,
], 200);
}
$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'];
} elseif (preg_match('/^item_abo_(.+)_(\d+)_(\d+)$/', $item_id, $ia_m)) {
// Item-Abo: tägliche Lieferung eines Minecraft-Items
$abo_mc_item = $ia_m[1];
$abo_daily_qty = intval($ia_m[2]);
$abo_dur_days = intval($ia_m[3]);
$commands_payload[] = [
'type' => 'item_abo',
'item_id' => $abo_mc_item,
'daily_qty' => $abo_daily_qty,
'duration_days' => $abo_dur_days,
'label' => $item['title'],
];
$title_parts[] = $item['title'];
} elseif (preg_match('/^custom_cmd_(.+)$/', $item_id, $cc_m)) {
// Custom Command Item: Command aus DB holen
$db_item = WIS_DB::get_item_by_item_id($item_id);
$raw_cmd = $db_item ? ($db_item->custom_command ?? '') : '';
for ($q = 0; $q < intval($item['qty']); $q++) {
$commands_payload[] = [
'type' => 'custom_cmd',
'cmd_id' => $cc_m[1],
'command' => $raw_cmd,
'label' => $item['title'],
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
} elseif (preg_match('/^gift_card_(\d+)_(\d+)$/', $item_id, $gc_m)) {
// Gutschein-Karte: für jede Stück einen eigenen Coupon generieren
$gc_amount = intval($item['custom_amount'] ?? $item['price']); // custom_amount vom Frontend
$gc_min = intval($gc_m[1]);
$gc_max = intval($gc_m[2]);
// Betrag auf gültigen Bereich clippen
$gc_amount = max($gc_min, min($gc_max, $gc_amount));
for ($q = 0; $q < intval($item['qty']); $q++) {
// Eindeutigen Code generieren
$gc_code = 'GC-' . strtoupper(bin2hex(random_bytes(4))) . '-' . strtoupper(bin2hex(random_bytes(3)));
// Als Coupon in DB eintragen: Typ 'fixed', usage_limit 1, kein Ablauf
WIS_DB::insert_coupon([
'code' => $gc_code,
'value' => $gc_amount,
'type' => 'fixed',
'usage_limit' => 1,
'used_count' => 0,
'expiry' => null,
'min_order_value' => 0,
'allowed_categories' => null,
'bulk_id' => 'gift_card',
]);
$commands_payload[] = [
'type' => 'gift_card',
'code' => $gc_code,
'amount' => $gc_amount,
'label' => $item['title'] . ' (' . $gc_amount . ' ' . $currency . ')',
'currency'=> $currency,
];
}
$title_parts[] = $item['qty'] . 'x ' . $item['title'];
// Preis = tatsächlicher Betrag x Menge (nicht der Artikel-Mindestpreis)
$gc_amount_total = $gc_amount * intval($item['qty']);
// Korrektur: total_normal wurde mit item->price gerechnet, jetzt auf echten Betrag korrigieren
$total_normal = $total_normal - ($item['price'] * intval($item['qty'])) + $gc_amount_total;
} 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")),
]);
}
}
// Item-Abo Abonnements registrieren / verlängern
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') !== 'item_abo') continue;
$ia_mc_item = sanitize_text_field($cmd['item_id'] ?? '');
$ia_daily_qty = max(1, intval($cmd['daily_qty'] ?? 1));
$ia_dur_days = max(1, intval($cmd['duration_days'] ?? 30));
$ia_label = sanitize_text_field($cmd['label'] ?? 'Item-Abo');
$ia_price = 0;
foreach ($valid_cart as $ci) {
if (preg_match('/^item_abo_/', $ci['id'])) {
$ia_price = intval($ci['price'] ?? 0);
break;
}
}
WIS_Activator::create_item_abo_subs_table();
$ia_table = $wpdb->prefix . 'wis_item_abo_subs';
// Bestehende aktive Abos für dieselbe item_id + player + server verlängern
$ia_existing = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$ia_table} WHERE player_name = %s AND server = %s AND item_id = %s AND status = 'active'",
$player, $server, $ia_mc_item
));
if ($ia_existing) {
$wpdb->query($wpdb->prepare(
"UPDATE {$ia_table}
SET expires_at = DATE_ADD(IF(expires_at > NOW(), expires_at, NOW()), INTERVAL %d DAY),
cancelled = 0,
cancelled_at = NULL,
daily_qty = %d,
label = %s,
price = %d
WHERE id = %d",
$ia_dur_days, $ia_daily_qty, $ia_label, $ia_price, $ia_existing->id
));
$ia_sub_id = $ia_existing->id;
} else {
$wpdb->insert($ia_table, [
'player_name' => $player,
'server' => $server,
'item_id' => $ia_mc_item,
'daily_qty' => $ia_daily_qty,
'label' => $ia_label,
'price' => $ia_price,
'status' => 'active',
'cancelled' => 0,
'expires_at' => date('Y-m-d H:i:s', strtotime("+{$ia_dur_days} days")),
'last_delivered' => null,
]);
$ia_sub_id = $wpdb->insert_id;
}
// Sofort-Lieferung für heute anlegen (damit der Spieler nicht bis Mitternacht warten muss)
$ia_sub = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM {$ia_table} WHERE id = %d", $ia_sub_id
));
if ($ia_sub && (empty($ia_sub->last_delivered) || $ia_sub->last_delivered < date('Y-m-d'))) {
$ia_payload = json_encode([
'items' => [['id' => $ia_mc_item, 'amount' => $ia_daily_qty]],
'commands' => [],
'abo_delivery' => true,
]);
$wpdb->insert($wpdb->prefix . 'wis_orders', [
'player_name' => $player,
'server' => $server,
'item_id' => 'item_abo_delivery',
'item_title' => '📦 Abo-Lieferung: ' . $ia_label . ' ×' . $ia_daily_qty,
'price' => 0,
'quantity' => $ia_daily_qty,
'status' => 'pending',
'response' => $ia_payload,
]);
$wpdb->update($ia_table, ['last_delivered' => date('Y-m-d')], ['id' => $ia_sub_id]);
}
}
$msg = $gift_recipient !== ''
? "🎁 Geschenk-Bestellung für {$gift_recipient} erfolgreich!"
: '✅ Bestellung erfolgreich!';
if ($coupon_msg) $msg .= ' (' . $coupon_msg . ')';
// Gift-Card-Codes aus dem Payload sammeln und in der Antwort zurückgeben
$gift_card_codes = [];
foreach ($commands_payload as $cmd) {
if (($cmd['type'] ?? '') === 'gift_card') {
$gift_card_codes[] = [
'code' => $cmd['code'],
'amount' => $cmd['amount'],
'label' => $cmd['label'] ?? '',
];
}
}
return new WP_REST_Response([
'success' => true,
'message' => $msg,
'gift_card_codes' => [], // Codes werden erst nach complete_order zurückgegeben
]);
}
/**
* Prueft Mindestbestellwert, erlaubte Kategorien und erlaubte Raenge.
* Gibt null zurueck wenn alles OK, sonst Fehlermeldung als String.
*/
private static function check_coupon_restrictions($coupon, $cart, $subtotal_normal) {
// 1. Mindestbestellwert
$min = intval($coupon->min_order_value ?? 0);
if ($min > 0 && $subtotal_normal < $min) {
$currency = get_option('wis_currency_name', 'Coins');
return "Mindestbestellwert von {$min} {$currency} nicht erreicht.";
}
// 2. Erlaubte Kategorien
if (!empty($coupon->allowed_categories)) {
$allowed_cat_ids = array_map('intval', explode(',', $coupon->allowed_categories));
$has_valid_cat = false;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && in_array(intval($item->category_id), $allowed_cat_ids)) {
$has_valid_cat = true;
break;
}
}
if (!$has_valid_cat) {
return 'Dieser Gutschein gilt nicht fuer die Produkte in deinem Warenkorb.';
}
}
return null;
}
public static function validate_coupon($request) {
$data = $request->get_json_params();
$code = sanitize_text_field(strtoupper($data['code'] ?? ''));
$cart = $data['cart'] ?? [];
$player = sanitize_text_field($data['player'] ?? '');
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']);
}
// Spieler-spezifische Prüfung: hat dieser Spieler den Code schon eingelöst?
if ($player && WIS_DB::coupon_used_by_player($coupon->id, $player)) {
return new WP_REST_Response(['success' => false, 'message' => 'Du hast diesen Gutschein bereits eingelöst']);
}
$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']);
}
}
// Neue Einschränkungen prüfen (Mindestbestellwert, Kategorien, Rang)
$subtotal_normal = 0;
foreach ($cart as $item_data) {
$item = WIS_DB::get_item(intval($item_data['id'] ?? 0));
if ($item && !($exclude_offers === '1' && $item->is_offer)) {
$subtotal_normal += $item->price * intval($item_data['quantity'] ?? 1);
}
}
$restriction_error = self::check_coupon_restrictions($coupon, $cart, $subtotal_normal);
if ($restriction_error !== null) {
return new WP_REST_Response(['success' => false, 'message' => $restriction_error]);
}
$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();
?>