PageSourceSearch

https://binderim.com/js/vendor/wallet-topup-modal.yxclx6o8fa.js

js binderim.com collected 2026-09-24 14:41:59 UTC 19,371 bytes, 434 lines download raw bytes

1// ============================================================
2// Vendor Bakiye Yukle Modal kontrol scripti
3// ============================================================
4// Sorumluluklar:
5//   * Modal acildiginda config ve vendor billing profile'i yuklenir
6//   * Sirket/sahis radio'su degisince ilgili alanlar gosterilir/gizlenir
7//   * Net tutar input'u degisince KDV / islem ucreti canli hesaplanir
8//   * stripeEnabled=true -> Stripe modali: ucret ozeti + oran tablosu + "Kartla Ode"
9//   * stripeEnabled=false -> Havale modu: KDV ozeti + banka bilgisi + "Odememi Tamamladim"
10//   * Submit -> POST /api/vendor/wallet/top-up
11//     - Stripe modunda: response.data.mode=="stripe" -> window.location.href = redirectUrl
12//     - Havale modunda: modal kapanir, 'wallet-topup:created' event yayar
13//
14// API kontrati:
15//   GET  /api/vendor/wallet/top-up/config    -> { stripeEnabled, stripe{percentFee,fixedFeeTry,...}, vatRate, minimumNetAmount, bank }
16//   GET  /api/vendor/wallet/billing-profile  -> { exists, ... }
17//   POST /api/vendor/wallet/top-up           -> { data: { mode, redirectUrl, ... } | WalletTopUpResponse }
18// ============================================================
19
20(function () {
21    'use strict';
22
23    const MODAL_ID = 'walletTopUpModal';
24    let cachedConfig = null;
25    let cachedProfile = null;
26    let inflight = false;
27
28    // Seffaflik tablosu icin orneklem tutarlar (TL)
29    var RATE_TABLE_SAMPLES = [100, 250, 500, 1000, 2500, 5000];
30
31    function $(id) { return document.getElementById(id); }
32
33    // ---- i18n (Issue #1625 B, vendor dilimi) -------------------------------------
34    // Kok eleman modal partial'inin kendisi (#walletTopUpModal) -- boylece hem Vendor/Index
35    // hem Vendor/Wallet sayfasinda TEK yerde (Shared/_WalletTopUpModal.cshtml) tanimlanir ve
36    // anahtarlar _WalletTopUpModal.resx / .en.resx'ten gelir. Attribute yoksa bugunku TR metin.
37    const i18nRoot = document.getElementById(MODAL_ID);
38    function t(name, fallback) {
39        if (!i18nRoot) return fallback;
40        const value = i18nRoot.getAttribute('data-i18n-' + name);
41        return (value === null || value === '') ? fallback : value;
42    }
43    function fmt(template) {
44        var args = Array.prototype.slice.call(arguments, 1);
45        return String(template == null ? '' : template).replace(/\{(\d+)\}/g, function (match, index) {
46            var v = args[Number(index)];
47            return v === undefined ? match : v;
48        });
49    }
50    function escapeHtml(s) {
51        if (s == null) return '';
52        return String(s)
53            .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
54            .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
55    }
56
57    // 🔴 #1625 B: Stripe'a yonlendirme durumu ARTIK METINDEN OKUNMUYOR. Onceki kod finally
58    // blogunda `btn.innerHTML.indexOf('yonlendiriliyorsunuz')` ile TR literal ariyordu --
59    // metin /en'de Ingilizce olunca bu kosul HIC tutmaz, buton yeniden etkinlesir ve
60    // kullanici Stripe'a giderken "Kartla Ode"ye tekrar basabilirdi. Durum artik bayrakta.
61    let redirectingToStripe = false;
62
63    // SuperAdmin debug modu: ?debuguser=... varsa API cagrilarina tasi
64    function withDebugUser(url) {
65        var du = new URLSearchParams(window.location.search).get('debuguser');
66        if (!du) return url;
67        return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'debuguser=' + encodeURIComponent(du);
68    }
69
70    function formatTry(amount) {
71        if (amount == null || isNaN(amount)) return '- ₺';
72        var n = Number(amount);
73        return window.BinderimFormat.formatNumber(n, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ₺';
74    }
75
76    function showAlert(level, message) {
77        var el = $('walletTopUpAlert');
78        if (!el) return;
79        el.className = 'alert alert-' + level;
80        el.textContent = message;
81        el.classList.remove('d-none');
82    }
83
84    function hideAlert() {
85        var el = $('walletTopUpAlert');
86        if (el) el.classList.add('d-none');
87    }
88
89    function getCsrfToken() {
90        var tok = document.querySelector('input[name="__RequestVerificationToken"]');
91        return tok ? tok.value : null;
92    }
93
94    async function fetchJson(url, init) {
95        var opts = init || {};
96        opts.headers = Object.assign({ 'Accept': 'application/json' }, opts.headers || {});
97        if (opts.body && !(opts.body instanceof FormData)) {
98            opts.headers['Content-Type'] = 'application/json';
99        }
100        var tok = getCsrfToken();
101        if (tok) opts.headers['RequestVerificationToken'] = tok;
102        opts.credentials = 'same-origin';
103        var res = await fetch(withDebugUser(url), opts);
104        var payload = null;
105        try { payload = await res.json(); } catch (e) { /* ignore */ }
106        if (!res.ok || (payload && payload.success === false)) {
107            var msg = (payload && (payload.message || payload.errorMessage)) || ('HTTP ' + res.status);
108            throw new Error(msg);
109        }
110        return payload;
111    }
112
113    function isStripeMode() {
114        return !!(cachedConfig && cachedConfig.stripeEnabled);
115    }
116
117    // ── Ücret formülü (Stripe modu): gross = ceil2((net + fixedFeeTry) / (1 - percentFee/100))
118    function calcStripeFee(net) {
119        if (!cachedConfig || !cachedConfig.stripe) return { gross: net, fee: 0 };
120        var percentFee = Number(cachedConfig.stripe.percentFee || 0);
121        var fixedFeeTry = Number(cachedConfig.stripe.fixedFeeTry || 0);
122        var divisor = 1 - percentFee / 100;
123        if (divisor <= 0) return { gross: net, fee: 0 };
124        var rawGross = (net + fixedFeeTry) / divisor;
125        // Yukarı 2 ondalık yuvarla
126        var gross = Math.ceil(rawGross * 100) / 100;
127        var fee = gross - net;
128        return { gross: gross, fee: fee };
129    }
130
131    // ── UI modu: Stripe vs Havale
132    function applyModeUi() {
133        var stripe = isStripeMode();
134
135        // KDV kartı (havale) <-> İşlem ücreti kartı (stripe)
136        var vatSection = $('walletTopUpVatSection');
137        var feeSection = $('walletTopUpFeeSection');
138        var rateTable  = $('walletTopUpRateTable');
139        var bankSection = $('walletTopUpBankSection');
140        var submitBtn  = $('walletTopUpSubmitBtn');
141        var noteText   = $('walletTopUpNoteText');
142
143        if (vatSection) vatSection.style.display = stripe ? 'none' : '';
144        if (feeSection) feeSection.style.display = stripe ? '' : 'none';
145        if (rateTable)  rateTable.style.display  = stripe ? '' : 'none';
146        if (bankSection) bankSection.style.display = stripe ? 'none' : '';
147
148        if (submitBtn) {
149            if (stripe) {
150                submitBtn.innerHTML = '<i class="bi bi-credit-card"></i> ' + escapeHtml(t('submit-card', 'Kartla Öde'));
151            } else {
152                submitBtn.innerHTML = '<i class="bi bi-check-circle"></i> ' + escapeHtml(t('submit-bank', 'Ödememi Tamamladım'));
153            }
154        }
155
156        if (noteText) {
157            if (stripe) {
158                // 🔴 BILINCLI ISTISNA (escapeHtml YOK): bu iki anahtar resx'te <strong> isaretlemesi
159                // TASIR -- _WalletTopUpModal.cshtml'in SSR'da @T["walletTopUpModal.invoiceNote"] ile
160                // ham bastigi AYNI degerler (LocalizedHtmlString). Deger sunucu kontrolunde, kullanici
161                // girdisi DEGIL; escapeHtml uygulasaydik SSR ile JS ciktisi ayrisirdi.
162                noteText.innerHTML = t('note-stripe', 'Ödemeniz <strong>Stripe</strong> üzerinden güvenli şekilde alınacak ve adınıza/şirketinize <strong>fatura kesilecektir</strong>.');
163            } else {
164                noteText.innerHTML = t('note-bank', 'Bu tutar şirket hesabımıza geldiği 
164için adınıza/şirketinize <strong>fatura kesilecektir</strong>.');
165            }
166        }
167
168        if (stripe) {
169            buildRateTable();
170        }
171    }
172
173    function updateBillingTypeVisibility() {
174        var isCompany = (document.querySelector('input[name="isCompany"]:checked') || {}).value === 'true';
175        var cFields = $('walletTopUpCompanyFields');
176        var iFields = $('walletTopUpIndividualFields');
177        if (cFields) cFields.style.display = isCompany ? 'grid' : 'none';
178        if (iFields) iFields.style.display = isCompany ? 'none' : 'grid';
179        toggleRequired(['walletTopUpCompanyName', 'walletTopUpTaxNumber', 'walletTopUpTaxOffice'], isCompany);
180        toggleRequired(['walletTopUpTcKimlikNo'], !isCompany);
181    }
182
183    function toggleRequired(ids, isRequired) {
184        ids.forEach(function (id) {
185            var el = $(id);
186            if (!el) return;
187            if (isRequired) el.setAttribute('required', 'required');
188            else el.removeAttribute('required');
189        });
190    }
191
192    // ── Havale KDV hesaplama
193    function recalcVat() {
194        if (!cachedConfig || isStripeMode()) return;
195        var netInput = $('walletTopUpNetAmount');
196        var net = Number(netInput ? netInput.value || 0 : 0);
197        var rate = Number(cachedConfig.vatRate || 0);
198        var vat = +(net * rate).toFixed(2);
199        var gross = +(net + vat).toFixed(2);
200
201        var dispNet = $('walletTopUpDisplayNet');
202        var dispVat = $('walletTopUpDisplayVat');
203        var dispGross = $('walletTopUpDisplayGross');
204        if (dispNet) dispNet.textContent = net > 0 ? formatTry(net) : '- ₺';
205        if (dispVat) dispVat.textContent = net > 0 ? formatTry(vat) : '- ₺';
206        if (dispGross) dispGross.textContent = net > 0 ? formatTry(gross) : '- ₺';
207
208        var lbl = $('walletTopUpVatLabel');
209        if (lbl) lbl.textContent = 'KDV (%' + Math.round(rate * 100) + ')';
210    }
211
212    // ── Stripe işlem ücreti canlı hesaplama
213    function recalcFee() {
214        if (!cachedConfig || !isStripeMode()) return;
215        var netInput = $('walletTopUpNetAmount');
216        var net = Number(netInput ? netInput.value || 0 : 0);
217        var result = calcStripeFee(net);
218
219        var dispNet   = $('walletTopUpFeeDisplayNet');
220        var dispFee   = $('walletTopUpFeeDisplayFee');
221        var dispGross = $('walletTopUpFeeDisplayGross');
222        if (dispNet)   dispNet.textContent   = net > 0 ? formatTry(net) : '- ₺';
223        if (dispFee)   dispFee.textContent   = net > 0 ? formatTry(result.fee) : '- ₺';
224        if (dispGross) dispGross.textContent = net > 0 ? formatTry(result.gross) : '- ₺';
225
226        var lbl = $('walletTopUpFeeLabel');
227        if (lbl && cachedConfig.stripe) {
228            lbl.textContent = fmt(t('fee-label-format', 'İşlem ücreti (kart komisyonu, %{0} + {1} ₺ sabit)'),
229                Number(cachedConfig.stripe.percentFee || 0).toFixed(2),
230                Number(cachedConfig.stripe.fixedFeeTry || 0).toFixed(0));
231        }
232    }
233
234    function recalcAmounts() {
235        if (isStripeMode()) {
236            recalcFee();
237        } else {
238            recalcVat();
239        }
240    }
241
242    // ── Şeffaflık oran tablosunu doldur
243    function buildRateTable() {
244        var tbody = $('walletTopUpRateRows');
245        if (!tbody || !cachedConfig || !cachedConfig.stripe) return;
246        var rows = RATE_TABLE_SAMPLES.map(function (net) {
247            var result = calcStripeFee(net);
248            var effectivePct = net > 0 ? (result.fee / result.gross) * 100 : 0;
249            return '<tr>'
250                + '<td>' + formatTry(net) + '</td>'
251                + '<td>' + formatTry(result.gross) + '</td>'
252                + '<td>%' + effectivePct.toFixed(2) + '</td>'
253                + '</tr>';
254        });
255        tbody.innerHTML = rows.join('');
256    }
257
258    function fillBank(bank) {
259        if (!bank) return;
260        var bn = $('walletTopUpBankName');
261        var bh = $('walletTopUpBankHolder');
262        var bi = $('walletTopUpBankIban');
263        if (bn) bn.textContent = bank.bankName || '-';
264        if (bh) bh.textContent = bank.accountHolder || '-';
265        if (bi) bi.textContent = bank.iban || '-';
266    }
267
268    function fillProfile(profile) {
269        if (!profile || !profile.exists) return;
270        var radio = document.querySelector('input[name="isCompany"][value="' + String(!!profile.isCompany) + '"]');
271        if (radio) radio.checked = true;
272        var setVal = function (id, val) {
273            var el = $(id);
274            if (el && val != null) el.value = val;
275        };
276        setVal('walletTopUpCompanyName', profile.companyName);
277        setVal('walletTopUpTaxNumber', profile.taxNumber);
278        setVal('walletTopUpTaxOffice', profile.taxOffice);
279        setVal('walletTopUpTcKimlikNo', profile.tcKimlikNo);
280        setVal('walletTopUpFirstName', profile.firstName);
281        setVal('walletTopUpLastName', profile.lastName);
282        setVal('walletTopUpPhone', profile.phoneNumber);
283        setVal('walletTopUpCity', profile.city);
284        setVal('walletTopUpDistrict', profile.district);
285        setVal('walletTopUpAddressLine', profile.addressLine);
286        updateBillingTypeVisibility();
287    }
288
289    function collectFormData() {
290        var isCompany = (document.querySelector('input[name="isCompany"]:checked') || {}).value === 'true';
291        return {
292            netAmount: Number($('walletTopUpNetAmount').value || 0
292),
293            isCompany: isCompany,
294            companyName: isCompany ? $('walletTopUpCompanyName').value.trim() : null,
295            taxNumber: isCompany ? $('walletTopUpTaxNumber').value.trim() : null,
296            taxOffice: isCompany ? $('walletTopUpTaxOffice').value.trim() : null,
297            tcKimlikNo: !isCompany ? $('walletTopUpTcKimlikNo').value.trim() : null,
298            firstName: $('walletTopUpFirstName').value.trim(),
299            lastName: $('walletTopUpLastName').value.trim(),
300            phoneNumber: $('walletTopUpPhone').value.trim(),
301            city: $('walletTopUpCity').value.trim(),
302            district: $('walletTopUpDistrict').value.trim(),
303            addressLine: $('walletTopUpAddressLine').value.trim(),
304            saveBillingProfile: true
305        };
306    }
307
308    async function loadInitialData() {
309        try {
310            var results = await Promise.all([
311                fetchJson('/api/vendor/wallet/top-up/config'),
312                fetchJson('/api/vendor/wallet/billing-profile')
313            ]);
314            cachedConfig = results[0].data || null;
315            cachedProfile = results[1].data || null;
316
317            // Minimum tutar input min'ini guncelle
318            if (cachedConfig && cachedConfig.minimumNetAmount) {
319                var el = $('walletTopUpNetAmount');
320                if (el) el.min = cachedConfig.minimumNetAmount;
321                if (el && !el.value) el.value = cachedConfig.minimumNetAmount;
322            }
323
324            // Banka bilgisi (sadece havale modunda anlam tasiyor ama her zaman doldur)
325            fillBank(cachedConfig ? cachedConfig.bank : null);
326
327            // Profile on-dolum
328            fillProfile(cachedProfile);
329
330            // Moda gore UI goster/gizle
331            applyModeUi();
332
333            // Tutarlari hesapla
334            recalcAmounts();
335        } catch (e) {
336            showAlert('warning', fmt(t('load-failed-format', 'Modal verileri yüklenemedi: {0}'), e.message));
337        }
338    }
339
340    async function onSubmit() {
341        if (inflight) return;
342        hideAlert();
343        var data = collectFormData();
344        var minAmount = cachedConfig ? Number(cachedConfig.minimumNetAmount || 0) : 100;
345        if (!data.netAmount || data.netAmount < minAmount) {
346            showAlert('danger', fmt(t('min-amount-format', 'En düşük yükleme tutarı {0} TL.'), minAmount));
347            return;
348        }
349
350        inflight = true;
351        var btn = $('walletTopUpSubmitBtn');
352        var originalHtml = btn ? btn.innerHTML : '';
353        if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> ' + escapeHtml(t('sending', 'Gönderiliyor...')); }
354
355        try {
356            var res = await fetchJson('/api/vendor/wallet/top-up', { method: 'POST', body: JSON.stringify(data) });
357
358            // ── Stripe akisi: yanit mode=="stripe" ise Stripe Checkout'a yon
359            if (res && res.data && res.data.mode === 'stripe') {
360                // 🔴 Review m4: redirectUrl BOS gelirse eskiden bayrak yine de true olur,
361                // window.location.href='' sayfayi yeniden yukler ya da hicbir sey yapmaz ve
362                // finally butonu GERI ACMAZDI -> buton KALICI disabled kalir, kullanici
363                // modali kapatip acmadan tekrar deneyemez. Artik bayrak yalniz GERCEKTEN
364                // yonlendirme baslatildiginda set edilir; aksi halde finally butonu acar.
365                var redirectUrl = res.data.redirectUrl;
366                if (!redirectUrl) {
367                    showAlert('danger', t('redirect-url-missing', 'Ödeme sayfasına yönlendirilemedi. Lütfen tekrar deneyin.'));
368                    return;
369                }
370
371                // Stripe'a yonlendirme — sayfa yukleniyor spinner
372                redirectingToStripe = true;
373                if (btn) { btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> ' + escapeHtml(t('redirecting', 'Ödeme sayfasına yönlendiriliyorsunuz...')); }
374                window.location.href = redirectUrl;
375                // Not: `return` finally blogunu ATLAMAZ (JS'te finally HER ZAMAN kosar).
376                // Buton BILEREK disabled birakilir -- redirectingToStripe true oldugu icin
377                // finally onu geri acmaz; sayfa zaten Stripe'a gidiyor.
378                return;
379            }
380
381            // ── Havale akisi: modal kapat, event yay
382            showAlert('success', (res && res.message) || t('request-received', 'Bakiye yükleme talebiniz alındı.'));
383            document.dispatchEvent(new CustomEvent('wallet-topup:created', { detail: res ? res.data : null }));
384            setTimeout(function () {
385                var modalEl = $(MODAL_ID);
386                if (window.bootstrap && modalEl) {
387                    window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
388                }
389            }, 1500);
390        } catch (e) {
391            showAlert('danger', e.message || t('request-failed', 'Talep gönderilemedi.'));
392        } finally {
393            inflight = false;
394            if (btn && !redirectingToStripe) {
395                // Sadece Stripe redirect dongusu disinda sifirla
396                btn.disabled = false;
397                btn.innerHTML = originalHtml;
398            }
399        }
400    }
401
402    function bind() {
403        var modalEl = $(MODAL_ID);
404        if (!modalEl) return;
405
406        modalEl.addEventListener('shown.bs.modal', function () {
407            hideAlert();
408            if (!cachedConfig) {
409                loadInitialData();
410            } else {
411                applyModeUi();
412                recalcAmounts();
413            }
414        });
415
416        document.querySelectorAll('input[name="isCompany"]').forEach(function (r) {
417            r.addEventListener('change', updateBillingTypeVisibility);
418        });
419
420        var netInput = $('walletTopUpNetAmount');
421        if (netInput) netInput.addEventListener('input', recalcAmounts);
422
423        var submitBtn = $('walletTopUpSubmitBtn');
424        if (submitBtn) submitBtn.addEventListener('click', onSubmit);
425
426        updateBillingTypeVisibility();
427    }
428
429    if (document.readyState === 'loading') {
430        document.addEventListener('DOMContentLoaded', bind);
431    } else {
432        bind();
433    }
434})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.