KopiBot AI / Dokumentasi HTML
Semua Dokumen GitHub Repo Kembali ke Landing Page
☕ AI Agent Coffee Shop Commerce Platform

Platform AI untuk otomatisasi order, customer service, loyalty customer, Customer CRM, Customer Portal, dan manajemen multi cabang coffee shop.

🚀 Features

  • AI Chatbot Order Menu
  • WhatsApp / Telegram / Discord Integration
  • Multi Branch Management
  • AI Upselling & Promo Recommendation
  • Order via Website & Chat Apps
  • Variant Product & Topping Support
  • Loyalty Point, Customer CRM, dan Customer Portal
  • Multi Currency, Tax & Timezone
  • AI Customer Interaction Automation

💻 Tech Stack

PHP Native • MySQL • OpenAI • Anthropic
WhatsApp Gateway • REST API • LLM AI

☕ Suitable For

Coffee Shop • Cafe • Restaurant • Bakery • Beverage Store

Dibuat & Dikembangkan oleh

Kukuh TW

📧 Email: kukuhtw@gmail.com

📱 WhatsApp: wa.me/628129893706

🌄 Instagram: @kukuhtw

X/Twitter: @kukuhtw

Facebook: facebook.com/kukuhtw

LinkedIn: linkedin.com/in/kukuhtw

GitHub: github.com/kukuhtw/toko_kopi

🌐 Demo: botlelang.com/toko_kopi

© 2026 Kukuh TW. All rights reserved.

Source Viewer

plugins/loyalty-point/LoyaltyPointPlugin.php

<?php

declare(strict_types=1);

use App\Config\Database;
use App\Helpers\Csrf;
use App\Helpers\Currency;
use App\Plugin\HookManager;
use App\Plugin\PluginInterface;
use App\Services\IntentPatternRegistry;
use App\Skills\SkillRegistry;

class LoyaltyPointPlugin implements PluginInterface
{
    private const SLUG = 'loyalty-point';

    public function getName(): string
    {
        return 'Loyalty Point';
    }

    public function getVersion(): string
    {
        return '1.0.0';
    }

    public function getAuthor(): string
    {
        return 'KopiBot';
    }

    public function register(): void
    {
        (new LoyaltyPointRepository())->ensureSchema();

        HookManager::addAction('order.created', [$this, 'consumeRedeemedPoints'], 10);
        HookManager::addAction('order.completed', [$this, 'handleOrderCompleted'], 10);
        HookManager::addAction('order.payment_updated', [$this, 'handlePaymentUpdated'], 10);
        HookManager::addAction('order.status_changed', [$this, 'refundRedeemedPointsOnCancel'], 10);
        HookManager::addFilter('cart.before_checkout', [$this, 'validateCheckoutRedemption'], 10);
        HookManager::addFilter('order.before_create', [$this, 'attachOrderRedemptionData'], 10);
        HookManager::addFilter('chat.after_ai', [$this, 'appendPointsHint'], 18);
        HookManager::addFilter('settings.sections', [$this, 'addSettingsSection'], 18);
        HookManager::addFilter('super.settings.sections', [$this, 'addSuperSettingsSection'], 18);
        HookManager::addFilter('skills.registered', [$this, 'registerSkill'], 18);
        HookManager::addFilter('intent.patterns', [$this, 'registerIntentPatterns'], 18);
        HookManager::addFilter('dashboard.branch_widgets', [$this, 'addBranchWidget'], 18);
        HookManager::addFilter('dashboard.nav_items', [$this, 'addNavItems'], 18);
    }

    public function handleOrderCompleted(array $order): void
    {
        $this->tryAwardPoints($order);
    }

    public function handlePaymentUpdated(array $order, string $paymentStatus): void
    {
        if ($paymentStatus !== 'paid') {
            return;
        }

        $this->tryAwardPoints($order);
    }

    public function appendPointsHint(string $reply, int $branchId, string $intent, array $ctx = []): string
    {
        if (!$this->isEnabled($branchId)) {
            return $reply;
        }

        $eligibleIntents = ['lihat_cart', 'checkout', 'tanya_promo'];
        if (!in_array($intent, $eligibleIntents, true)) {
            return $reply;
        }

        $pointsPerUnit = (int)$this->getSetting($branchId, 'points_per_unit', '1');
        $spendAmount   = (float)$this->getSetting($branchId, 'spend_amount', '10000');
        if ($pointsPerUnit <= 0 || $spendAmount <= 0) {
            return $reply;
        }

        $currency = (string)($ctx['currency'] ?? 'IDR');
        $lang     = (string)($ctx['language'] ?? 'id');
        $balance  = 0;
        if (!empty($ctx['customer']['id'])) {
            $row = (new LoyaltyPointRepository())->getBalance($branchId, (int)$ctx['customer']['id']);
            $balance = (int)($row['balance_points'] ?? 0);
        }

        $redeemedPoints = (int)($ctx['cart']['loyalty_points_redeemed'] ?? 0);
        $redeemedAmount = (float)($ctx['cart']['loyalty_discount_amount'] ?? 0);
        $hint = $lang === 'en'
            ? "Loyalty active: every " . Currency::format($spendAmount, $currency) . " earns {$pointsPerUnit} point(s). Your current balance: {$balance}. Type *my points* anytime."
            : "Loyalty aktif: setiap belanja " . Currency::format($spendAmount, $currency) . " dapat {$pointsPerUnit} poin. Saldo kamu saat ini: {$balance} poin. Ketik *poin saya* kapan saja.";
        if ($redeemedPoints > 0 && $redeemedAmount > 0) {
            $hint .= $lang === 'en'
                ? " Redeemed on this cart: {$redeemedPoints} points (" . Currency::format($redeemedAmount, $currency) . ")."
                : " Poin yang sedang dipakai di keranjang ini: {$redeemedPoints} poin (" . Currency::format($redeemedAmount, $currency) . ").";
        }

        return $reply . "\n\n" . $hint;
    }

    public function addSettingsSection(array $sections, int $branchId): array
    {
        $scope = $this->getSettingsScope();
        $enabled       = $this->getSetting($branchId, 'enabled', '1') !== '0';
        $pointsPerUnit = $this->getSetting($branchId, 'points_per_unit', '1');
        $spendAmount   = $this->getSetting($branchId, 'spend_amount', '10000');
        $requirePaid   = $this->getSetting($branchId, 'require_paid', '0') === '1';
        $redeemPointsUnit = $this->getSetting($branchId, 'redeem_points_unit', '10');
        $redeemValueAmount = $this->getSetting($branchId, 'redeem_value_amount', '1000');
        $minRedeemPoints = $this->getSetting($branchId, 'min_redeem_points', '10');
        $currency      = $this->getBranchCurrency($branchId);
        $summary       = (new LoyaltyPointRepository())->getBranchSummary($branchId);

        ob_start();
        ?>
        <div class="card" style="margin-top:16px">
          <div class="card-title">⭐ Loyalty Point</div>
          <p style="font-size:.875rem;color:var(--text-light);margin-bottom:14px">
            Beri poin otomatis ke customer saat order selesai. Customer juga bisa cek saldo poin langsung dari chatbot.
          </p>

          <?php if ($scope === 'global'): ?>
            <div style="margin-bottom:14px;padding:12px 14px;background:var(--bg-light,#faf9f7);border-radius:8px;color:var(--text-light)">
              Pengaturan loyalty saat ini bersifat global untuk semua cabang. Ubah dari halaman Super Admin Settings jika ingin mengganti program loyalty.
            </div>
          <?php else: ?>
          <form method="POST">
            <?= Csrf::field() ?>
            <input type="hidden" name="action" value="save_plugin_settings">
            <input type="hidden" name="plugin_slug" value="<?= self::SLUG ?>">

            <div class="form-group">
              <input type="hidden" name="enabled" value="0">
              <label style="display:flex;align-items:center;gap:8px;cursor:pointer">
                <input type="checkbox" name="enabled" value="1" <?= $enabled ? 'checked' : '' ?>>
                <span>Aktifkan loyalty point untuk cabang ini</span>
              </label>
            </div>

            <div class="form-row">
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_spend_amount">Belanja per Kelipatan (<?= htmlspecialchars($currency) ?>)</label>
                <input type="number" id="lp_spend_amount" name="spend_amount" class="form-control"
                       min="1" step="1000" value="<?= htmlspecialchars($spendAmount) ?>">
                <small style="color:var(--text-light)">Contoh 10000 = setiap belanja 10.000 dapat poin.</small>
              </div>
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_points_per_unit">Poin Diberikan</label>
                <input type="number" id="lp_points_per_unit" name="points_per_unit" class="form-control"
                       min="1" step="1" value="<?= htmlspecialchars($pointsPerUnit) ?>">
                <small style="color:var(--text-light)">Contoh 1 = dapat 1 poin per kelipatan.</small>
              </div>
            </div>

            <div class="form-row">
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_redeem_points_unit">Poin per Redeem</label>
                <input type="number" id="lp_redeem_points_unit" name="redeem_points_unit" class="form-control"
                       min="1" step="1" value="<?= htmlspecialchars($redeemPointsUnit) ?>">
              </div>
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_redeem_value_amount">Nilai Diskon (<?= htmlspecialchars($currency) ?>)</label>
                <input type="number" id="lp_redeem_value_amount" name="redeem_value_amount" class="form-control"
                       min="1" step="500" value="<?= htmlspecialchars($redeemValueAmount) ?>">
              </div>
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_min_redeem_points">Minimal Redeem Poin</label>
                <input type="number" id="lp_min_redeem_points" name="min_redeem_points" class="form-control"
                       min="1" step="1" value="<?= htmlspecialchars($minRedeemPoints) ?>">
              </div>
            </div>

            <div class="form-group">
              <input type="hidden" name="require_paid" value="0">
              <label style="display:flex;align-items:center;gap:8px;cursor:pointer">
                <input type="checkbox" name="require_paid" value="1" <?= $requirePaid ? 'checked' : '' ?>>
                <span>Hanya berikan poin jika order sudah berstatus paid</span>
              </label>
            </div>

            <button type="submit" class="btn btn-primary">💾 Simpan Pengaturan Loyalty</button>
          </form>
          <?php endif; ?>

          <div style="margin-top:16px;background:var(--bg-light,#faf9f7);border-radius:8px;padding:14px">
            <div style="font-weight:600;margin-bottom:10px">Ringkasan Program Saat Ini</div>
            <div style="font-size:.8rem;color:var(--text-light);margin-bottom:12px">
              Scope aktif: <strong><?= $scope === 'global' ? 'Global untuk semua cabang' : 'Per cabang' ?></strong>
            </div>
            <div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-bottom:12px">
              <div>
                <div style="font-size:.78rem;color:var(--text-light)">Member Aktif</div>
                <div style="font-size:1.2rem;font-weight:700"><?= number_format((int)($summary['member_count'] ?? 0)) ?></div>
              </div>
              <div>
                <div style="font-size:.78rem;color:var(--text-light)">Saldo Poin Beredar</div>
                <div style="font-size:1.2rem;font-weight:700"><?= number_format((int)($summary['total_balance_points'] ?? 0)) ?></div>
              </div>
              <div>
                <div style="font-size:.78rem;color:var(--text-light)">Total Poin Diberikan</div>
                <div style="font-size:1.2rem;font-weight:700"><?= number_format((int)($summary['total_lifetime_points'] ?? 0)) ?></div>
              </div>
            </div>

            <div style="font-weight:600;font-size:.85rem;margin-bottom:8px">Top Member</div>
            <?php foreach (($summary['top_members'] ?? []) as $member): ?>
              <div style="display:flex;justify-content:space-between;gap:12px;padding:6px 0;border-bottom:1px solid rgba(0,0,0,.06)">
                <div>
                  <div style="font-weight:600"><?= htmlspecialchars((string)($member['name'] ?: $member['identifier'])) ?></div>
                  <div style="font-size:.75rem;color:var(--text-light)">Lifetime <?= number_format((int)($member['lifetime_points'] ?? 0)) ?> poin</div>
                </div>
                <div style="font-weight:700"><?= number_format((int)($member['balance_points'] ?? 0)) ?> poin</div>
              </div>
            <?php endforeach; ?>
            <?php if (empty($summary['top_members'])): ?>
              <div style="font-size:.85rem;color:var(--text-light)">Belum ada customer yang mengumpulkan poin.</div>
            <?php endif; ?>
          </div>
        </div>
        <?php

        $sections[self::SLUG] = ob_get_clean();
        return $sections;
    }

    public function addSuperSettingsSection(array $sections, int $branchId): array
    {
        $scope = $this->getSettingsScope();
        $enabled = $this->getGlobalSetting('enabled', '1') !== '0';
        $pointsPerUnit = $this->getGlobalSetting('points_per_unit', '1');
        $spendAmount = $this->getGlobalSetting('spend_amount', '10000');
        $requirePaid = $this->getGlobalSetting('require_paid', '0') === '1';
        $redeemPointsUnit = $this->getGlobalSetting('redeem_points_unit', '10');
        $redeemValueAmount = $this->getGlobalSetting('redeem_value_amount', '1000');
        $minRedeemPoints = $this->getGlobalSetting('min_redeem_points', '10');

        ob_start();
        ?>
        <div class="card" style="margin-top:16px">
          <div class="card-title">Loyalty Point</div>
          <p style="font-size:.875rem;color:var(--text-light);margin-bottom:14px">
            Atur apakah program loyalty dipakai global untuk semua cabang atau dikelola masing-masing cabang.
          </p>

          <form method="POST">
            <?= Csrf::field() ?>
            <input type="hidden" name="action" value="save_global_plugin_settings">
            <input type="hidden" name="plugin_slug" value="<?= self::SLUG ?>">

            <div class="form-group" style="max-width:280px">
              <label class="form-label" for="lp_settings_scope">Mode Pengaturan</label>
              <select id="lp_settings_scope" name="settings_scope" class="form-control">
                <option value="global" <?= $scope === 'global' ? 'selected' : '' ?>>Global untuk semua cabang</option>
                <option value="branch" <?= $scope === 'branch' ? 'selected' : '' ?>>Diatur per cabang</option>
              </select>
              <small style="color:var(--text-light)">Jika pilih global, halaman cabang hanya menampilkan ringkasan dan memakai nilai dari sini.</small>
            </div>

            <div class="form-group">
              <input type="hidden" name="enabled" value="0">
              <label style="display:flex;align-items:center;gap:8px;cursor:pointer">
                <input type="checkbox" name="enabled" value="1" <?= $enabled ? 'checked' : '' ?>>
                <span>Aktifkan loyalty point global</span>
              </label>
            </div>

            <div class="form-row">
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_global_spend_amount">Belanja per Kelipatan</label>
                <input type="number" id="lp_global_spend_amount" name="spend_amount" class="form-control"
                       min="1" step="1000" value="<?= htmlspecialchars($spendAmount) ?>">
              </div>
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_global_points_per_unit">Poin Diberikan</label>
                <input type="number" id="lp_global_points_per_unit" name="points_per_unit" class="form-control"
                       min="1" step="1" value="<?= htmlspecialchars($pointsPerUnit) ?>">
              </div>
            </div>

            <div class="form-row">
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_global_redeem_points_unit">Poin per Redeem</label>
                <input type="number" id="lp_global_redeem_points_unit" name="redeem_points_unit" class="form-control"
                       min="1" step="1" value="<?= htmlspecialchars($redeemPointsUnit) ?>">
              </div>
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_global_redeem_value_amount">Nilai Diskon</label>
                <input type="number" id="lp_global_redeem_value_amount" name="redeem_value_amount" class="form-control"
                       min="1" step="500" value="<?= htmlspecialchars($redeemValueAmount) ?>">
              </div>
              <div class="form-group" style="max-width:220px">
                <label class="form-label" for="lp_global_min_redeem_points">Minimal Redeem Poin</label>
                <input type="number" id="lp_global_min_redeem_points" name="min_redeem_points" class="form-control"
                       min="1" step="1" value="<?= htmlspecialchars($minRedeemPoints) ?>">
              </div>
            </div>

            <div class="form-group">
              <input type="hidden" name="require_paid" value="0">
              <label style="display:flex;align-items:center;gap:8px;cursor:pointer">
                <input type="checkbox" name="require_paid" value="1" <?= $requirePaid ? 'checked' : '' ?>>
                <span>Hanya berikan poin jika order sudah berstatus paid</span>
              </label>
            </div>

            <button type="submit" class="btn btn-primary">Simpan Scope & Pengaturan Global Loyalty</button>
          </form>
        </div>
        <?php

        $sections[self::SLUG] = ob_get_clean();
        return $sections;
    }

    public function registerSkill(array $skills): array
    {
        return SkillRegistry::register($skills, new LoyaltyPointSkill(), 55);
    }

    public function registerIntentPatterns(array $patterns): array
    {
        $patterns = IntentPatternRegistry::extend($patterns, 'cek_poin_loyalty', [
            'poin saya',
            'cek poin',
            'saldo poin',
            'loyalty point',
            'point saya',
            'my points',
            'reward saya',
            'cek reward',
        ]);

        $patterns = IntentPatternRegistry::extend($patterns, 'pakai_poin_loyalty', [
            'pakai poin',
            'gunakan poin',
            'redeem point',
            'redeem points',
            'use points',
            'tukar poin',
        ]);

        return IntentPatternRegistry::extend($patterns, 'hapus_poin_loyalty', [
            'hapus poin',
            'batal pakai poin',
            'jangan pakai poin',
            'remove points',
            'clear points',
            'cancel points',
        ]);
    }

    public function addBranchWidget(array $widgets, int $branchId): array
    {
        if (!$this->isEnabled($branchId)) {
            return $widgets;
        }

        $summary = (new LoyaltyPointRepository())->getBranchSummary($branchId);

        ob_start();
        ?>
        <div class="card" style="margin-bottom:20px">
          <div class="card-title">⭐ Loyalty Point</div>
          <div style="display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px">
            <div>
              <div style="font-size:.78rem;color:var(--text-light)">Member</div>
              <div style="font-size:1.3rem;font-weight:700"><?= number_format((int)($summary['member_count'] ?? 0)) ?></div>
            </div>
            <div>
              <div style="font-size:.78rem;color:var(--text-light)">Saldo Beredar</div>
              <div style="font-size:1.3rem;font-weight:700"><?= number_format((int)($summary['total_balance_points'] ?? 0)) ?></div>
            </div>
            <div>
              <div style="font-size:.78rem;color:var(--text-light)">Total Earned</div>
              <div style="font-size:1.3rem;font-weight:700"><?= number_format((int)($summary['total_lifetime_points'] ?? 0)) ?></div>
            </div>
          </div>
        </div>
        <?php

        $widgets[] = ob_get_clean();
        return $widgets;
    }

    public function addNavItems(array $navItems, string $role): array
    {
        if ($role !== 'branch_admin') {
            return $navItems;
        }

        if (!isset($navItems['Order']) || !is_array($navItems['Order'])) {
            $navItems['Order'] = [];
        }

        $navItems['Order'][] = [
            'url' => '/dashboard/branch/loyalty.php',
            'icon' => 'LP',
            'label' => 'Loyalty Member',
        ];

        return $navItems;
    }

    public function validateCheckoutRedemption(array $customerData, array $cart, array $items, int $branchId): array
    {
        $redeemedPoints = (int)($cart['loyalty_points_redeemed'] ?? 0);
        $redeemedAmount = (float)($cart['loyalty_discount_amount'] ?? 0);
        if ($redeemedPoints <= 0 || $redeemedAmount <= 0) {
            return $customerData;
        }

        $balance = (new LoyaltyPointRepository())->getBalance($branchId, (int)($cart['customer_id'] ?? 0));
        if ((int)($balance['balance_points'] ?? 0) < $redeemedPoints) {
            throw new \RuntimeException('Saldo poin tidak mencukupi untuk checkout.');
        }

        return $customerData;
    }

    public function attachOrderRedemptionData(
        array $orderData,
        array $cart,
        array $cartItems = [],
        array $customerData = [],
        int $customerId = 0,
        float $ppnRate = 0.0
    ): array
    {
        $orderData['loyalty_points_redeemed'] = (int)($cart['loyalty_points_redeemed'] ?? 0);
        $orderData['loyalty_discount_amount'] = (float)($cart['loyalty_discount_amount'] ?? 0);
        return $orderData;
    }

    public function consumeRedeemedPoints(array $order): void
    {
        $points = (int)($order['loyalty_points_redeemed'] ?? 0);
        if ($points <= 0) {
            return;
        }

        $repo = new LoyaltyPointRepository();
        $orderId = (int)($order['id'] ?? 0);
        if ($repo->hasTransactionForOrder($orderId, 'redeem')) {
            return;
        }

        $repo->redeemPoints(
            (int)($order['branch_id'] ?? 0),
            (int)($order['customer_id'] ?? 0),
            $orderId,
            $points,
            'Redeem poin untuk order ' . (string)($order['order_number'] ?? ('#' . $orderId))
        );
    }

    public function refundRedeemedPointsOnCancel(array $order, string $oldStatus, string $newStatus): void
    {
        if ($newStatus !== 'cancelled') {
            return;
        }

        $points = (int)($order['loyalty_points_redeemed'] ?? 0);
        if ($points <= 0) {
            return;
        }

        $repo = new LoyaltyPointRepository();
        $orderId = (int)($order['id'] ?? 0);
        if (!$repo->hasTransactionForOrder($orderId, 'redeem') || $repo->hasTransactionForOrder($orderId, 'refund')) {
            return;
        }

        $repo->refundRedeemedPoints(
            (int)($order['branch_id'] ?? 0),
            (int)($order['customer_id'] ?? 0),
            $orderId,
            $points,
            'Refund poin untuk order batal ' . (string)($order['order_number'] ?? ('#' . $orderId))
        );
    }

    private function tryAwardPoints(array $order): void
    {
        $branchId   = (int)($order['branch_id'] ?? 0);
        $customerId = (int)($order['customer_id'] ?? 0);
        $orderId    = (int)($order['id'] ?? 0);

        if (!$this->isEnabled($branchId) || $branchId <= 0 || $customerId <= 0 || $orderId <= 0) {
            return;
        }

        if ($this->requiresPaid($branchId) && ($order['payment_status'] ?? 'unpaid') !== 'paid') {
            return;
        }

        $repo = new LoyaltyPointRepository();
        if ($repo->hasEarnTransactionForOrder($orderId)) {
            return;
        }

        $points = $this->calculateEarnedPoints($branchId, (float)($order['total_amount'] ?? 0));
        if ($points <= 0) {
            return;
        }

        $description = 'Poin dari order ' . (string)($order['order_number'] ?? ('#' . $orderId));
        $repo->awardPoints($branchId, $customerId, $orderId, $points, $description);
    }

    private function calculateEarnedPoints(int $branchId, float $totalAmount): int
    {
        $spendAmount   = (float)$this->getSetting($branchId, 'spend_amount', '10000');
        $pointsPerUnit = (int)$this->getSetting($branchId, 'points_per_unit', '1');

        if ($spendAmount <= 0 || $pointsPerUnit <= 0 || $totalAmount <= 0) {
            return 0;
        }

        return (int) floor($totalAmount / $spendAmount) * $pointsPerUnit;
    }

    public static function getRedeemSettings(int $branchId): array
    {
        $plugin = new self();
        return [
            'points_unit' => max(1, (int)$plugin->getSetting($branchId, 'redeem_points_unit', '10')),
            'value_amount' => max(1.0, (float)$plugin->getSetting($branchId, 'redeem_value_amount', '1000')),
            'min_points' => max(1, (int)$plugin->getSetting($branchId, 'min_redeem_points', '10')),
        ];
    }

    public static function calculateRedeemDiscount(int $points, int $pointsUnit, float $valueAmount): float
    {
        if ($points <= 0 || $pointsUnit <= 0 || $valueAmount <= 0) {
            return 0.0;
        }

        return floor($points / $pointsUnit) * $valueAmount;
    }

    public static function calculateRedeemPointsForDiscount(float $discount, int $pointsUnit, float $valueAmount): int
    {
        if ($discount <= 0 || $pointsUnit <= 0 || $valueAmount <= 0) {
            return 0;
        }

        return (int) floor($discount / $valueAmount) * $pointsUnit;
    }

    private function isEnabled(int $branchId): bool
    {
        return $this->getSetting($branchId, 'enabled', '1') !== '0';
    }

    private function requiresPaid(int $branchId): bool
    {
        return $this->getSetting($branchId, 'require_paid', '0') === '1';
    }

    private function getSetting(int $branchId, string $key, string $default = ''): string
    {
        if ($this->getSettingsScope() === 'global') {
            return $this->getGlobalSetting($key, $default);
        }

        try {
            $stmt = Database::getInstance()->prepare(
                'SELECT setting_val
                 FROM plugin_branch_settings
                 WHERE plugin_slug = ? AND branch_id = ? AND setting_key = ?
                 LIMIT 1'
            );
            $stmt->execute([self::SLUG, $branchId, $key]);
            $value = $stmt->fetchColumn();

            return $value !== false ? (string)$value : $default;
        } catch (\Throwable) {
            return $default;
        }
    }

    private function getSettingsScope(): string
    {
        $scope = $this->getGlobalSetting('settings_scope', 'branch');
        return $scope === 'global' ? 'global' : 'branch';
    }

    private function getGlobalSetting(string $key, string $default = ''): string
    {
        try {
            $stmt = Database::getInstance()->prepare(
                'SELECT setting_val
                 FROM app_settings
                 WHERE setting_key = ?
                 LIMIT 1'
            );
            $stmt->execute([$this->buildGlobalSettingKey($key)]);
            $value = $stmt->fetchColumn();

            return $value !== false && $value !== null ? (string)$value : $default;
        } catch (\Throwable) {
            return $default;
        }
    }

    private function buildGlobalSettingKey(string $key): string
    {
        return 'plugin_' . str_replace('-', '_', self::SLUG) . '_' . $key;
    }

    private function getBranchCurrency(int $branchId): string
    {
        $stmt = Database::getInstance()->prepare(
            'SELECT setting_val
             FROM branch_settings
             WHERE branch_id = ? AND setting_key = "currency"
             LIMIT 1'
        );
        $stmt->execute([$branchId]);
        $value = $stmt->fetchColumn();

        return $value !== false ? (string)$value : 'IDR';
    }
}