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, '&').replace(/</g, '<').replace(/>/g, '>') 54 .replace(/"/g, '"').replace(/'/g, '''); 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.