PageSourceSearch

https://binderim.com/js/admin/wallet-liability.a7q8n4nvoo.js

js binderim.com collected 2026-09-24 14:40:12 UTC 15,900 bytes, 361 lines download raw bytes

1(function () {
2    'use strict';
3
4    var $ = function (id) { return document.getElementById(id); };
5    var apiBase = '/api/admin/wallet/liability';
6
7    function antiforgeryToken() {
8        var el = $('wlAntiforgeryToken');
9        return el ? el.value : '';
10    }
11
12    function fmtTry(value) {
13        var n = Number(value || 0);
14        return window.BinderimFormat.formatNumber(n, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ₺';
15    }
16
17    // *Utc alanları UTC'dir ama JSON'a offset'siz yazılır ("2026-06-15T20:02:00"). Bu string'i
18    // new Date() YEREL saat sayar → TR'de saatler 3 saat geri görünür. Offset yoksa 'Z' ekle.
19    // NOT: aşağıdaki toDateInputValue/filtre varsayılanları BİLEREK yerel — kullanıcının seçtiği
20    // takvim günü, sunucunun UTC günü değil.
21    function parseUtc(iso) {
22        if (!iso) return null;
23        var normalized = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(iso) ? iso : iso + 'Z';
24        var d = new Date(normalized);
25        return isNaN(d.getTime()) ? null : d;
26    }
27
28    function fmtDate(iso) {
29        var d = parseUtc(iso);
30        if (!d) return '-';
31        return window.BinderimFormat.formatDateTime(d, { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' });
32    }
33
34    function toDateInputValue(d) {
35        var y = d.getFullYear();
36        var m = String(d.getMonth() + 1).padStart(2, '0');
37        var day = String(d.getDate()).padStart(2, '0');
38        return y + '-' + m + '-' + day;
39    }
40
41    async function apiGet(path) {
42        var res = await fetch(apiBase + path, { credentials: 'same-origin' });
43        var json = await res.json().catch(function () { return null; });
44        if (!res.ok || !json || json.success === false) {
45            var msg = (json && (json.errorMessage || json.message)) || ('İstek başarısız (' + res.status + ')');
46            throw new Error(msg);
47        }
48        return json.data;
49    }
50
51    async function apiPost(path) {
52        var res = await fetch(apiBase + path, {
53            method: 'POST',
54            credentials: 'same-origin',
55            headers: { 'RequestVerificationToken': antiforgeryToken() }
56        });
57        var json = await res.json().catch(function () { return null; });
58        if (!res.ok || !json || json.success === false) {
59            var msg = (json && (json.errorMessage || json.message)) || ('İstek başarısız (' + res.status + ')');
60            throw new Error(msg);
61        }
62        return json.data;
63    }
64
65    function currentPeriod() {
66        var start = $('wlPeriodStart').value;
67        var end = $('wlPeriodEnd').value;
68        var qs = '';
69        if (start) qs += '&periodStart=' + encodeURIComponent(start);
70        if (end) qs += '&periodEnd=' + encodeURIComponent(end);
71        return qs;
72    }
73
74    function initDefaultPeriod() {
75        var now = new Date();
76        var firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
77        $('wlPeriodStart').value = toDateInputValue(firstOfMonth);
78        $('wlPeriodEnd').value = toDateInputValue(now);
79    }
80
81    function badgeClassForStatus(isBreach) {
82        return isBreach ? 'bd-badge bd-badge--danger' : 'bd-badge bd-badge--success';
83    }
84
85    function badgeClassForStripe(status) {
86        switch (status) {
87            case 'Ok': return 'bd-badge bd-badge--success';
88            case 'Warning': return 'bd-badge bd-badge--warn';
89            case 'Breach': return 'bd-badge bd-badge--danger';
90            default: return 'bd-badge bd-badge--info';
91        }
92    }
93
94    function renderReport(report) {
95        // Reviewer M1 paketi (PR #1445): K1/K2/K3 "şimdiki durum" (dönem seçiminden bağımsız) —
96        // Kontroller kartındaki alt yazı üretim zamanını gösterir, arşivden bakan biri yanılmasın.
97        $('wlGeneratedUtc').textContent = fmtDate(report.generatedUtc);
98
99        $('wlOpeningValue').textContent = fmtTry(report.openingLiability);
100        $('wlClosingValue').textContent = fmtTry(report.closingLiability);
101        $('wlAccountsValue').textContent = fmtTry(report.accountsBalanceTotal);
102        $('wlHeldValue').textContent = fmtTry(report.heldTotalAtPeriodEnd);
103        $('wlOverrunValue').textContent = fmtTry(report.receiptOverrun);
104        $('wlCoverageValue').textContent = (report.receiptCoveragePercent != null)
105            ? ('Kapsama: %' + Number(report.receiptCoveragePercent).toFixed(1))
106            : '';
107        $('wlOpenWithdrawalsValue').textContent = report.openWithdrawalRequestCount;
108
109        // T-20260806, Epic #1479, Faz-1 (Issue #1480, Karar 4): emanet bilgi kartı + negatif bakiye
110        // "Alacak" bloğu — Faz-1'de her ikisi de normalde 0/gizli (üretim çağıranı yok).
111        var escrowValueEl = $('wlEscrowValue');
112        if (escrowValueEl) escrowValueEl.textContent = fmtTry(report.escrowTotal);
113
114        // T-20260806, Epic #1479, Faz-5 (Issue #1484, Karar 3/9) — donmuş tutar + teminatsız maruziyet.
115        var disputeFrozenEl = $('wlDisputeFrozenValue');
116        if (disputeFrozenEl) disputeFrozenEl.textContent = fmtTry(report.disputeFrozenTotal);
117        var disputeUnsecuredEl = $('wlDisputeUnsecuredValue');
118        if (disputeUnsecuredEl) disputeUnsecuredEl.textContent = fmtTry(report.disputeUnsecuredExposure);
119
120        // T-20260807, Epic #1479, Faz-5 (Issue #1484, Architect Rev-4/6) — platform işlem ücreti kaybı.
121        var platformFeeLossEl = $('wlPlatformFeeLossValue');
122        if (platformFeeLossEl) platformFeeLossEl.textContent = fmtTry(report.platformFeeLoss);
123
124        var negBanner = $('wlNegativeBalanceBanner');
125        if (negBanner) {
126            if (report.negativeBalanceAccountCount > 0) {
127                negBanner.classList.remove('d-none');
128                $('wlNegativeBalanceAccountCount').textContent = report.negativeBalanceAccountCount;
129                $('wlNegativeBalanceTotal').textContent = fmtTry(report.negativeBalanceTotal);
130                $('wlGrossLiabilityValue').textContent = fmtTry(report.grossLiability);
131            } else {
132                negBanner.classList.add('d-none');
133            }
134        }
135
136        var unclassifiedBanner = $('wlUnclassifiedBanner');
137        if (report.unclassifiedTransactionCount > 0) {
138            unclassifiedBanner.classList.remove('d-none');
139            $('wlUnclassifiedText').textContent =
140                report.unclassifiedTransactionCount + ' adet sınıflandırılmamış işlem "Diğer" kalemine düştü — kalem eşleme haritası güncellenmemiş olabilir.';
141        } else {
142            unclassifiedBanner.classList.add('d-none');
143        }
144
145        var k1 = report.controlK1, k2 = report.controlK2, k3 = report.controlK3;
146        $('wlK1Badge').className = badgeClassForStatus(k1.isBreach);
147        $('wlK1Badge').textContent = k1.isBreach ? 'SAPMA' : 'OK';
148        $('wlK1Msg').textContent = k1.message;
149
150        $('wlK2Badge').className = badgeClassForStatus(k2.isBreach);
151        $('wlK2Badge').textContent = k2.isBreach ? 'SAPMA' : 'OK';
152        $('wlK2Msg').textContent = k2.message;
153
154        $('wlK3Badge').className = badgeClassForStripe(k3.status);
155        $('wlK3Badge').textContent = k3.status;
156        var k3msg = k3.reason || '';
157        if (k3.currency) {
158            k3msg = (k3msg ? k3msg + ' — ' : '') +
159                'Payments ' + k3.currency + ': ' + Number((k3.stripeAvailable || 0) + (k3.stripePending || 0)).toFixed(2) +
160                (k3.convertedLiability != null ? (', çevrilmiş yükümlülük: ' + Number(k3.convertedLiability).toFixed(2)) : '');
161        }
162        // Issue #2067 (K3) — Treasury financial account kırılımı (aylık otomatik payout Payments'tan
163        // buraya taşıyor; para Stripe'tan çıkmıyor).
164        if (k3.treasuryAvailable != null) {
165            k3msg += ' | Treasury ' + (k3.treasuryCurrency || '') + ': ' + Number(k3.treasuryAvailable).toFixed(2) +
166                (k3.treasuryIncludedInTotal ? '' : ' (farklı para birimi — toplama dahil değil)');
167        }
168        if (k3.stripeComparisonTotal != null) {
169            k3msg += ' | Toplam: ' + Number(k3.stripeComparisonTotal).toFixed(2);
170        }
171        $('wlK3Msg').textContent = k3msg;
172
173        var lineBody = $('wlLineItemsBody');
174        lineBody.innerHTML = '';
175        (report.lineItems || []).forEach(function (line) {
176            var tr = document.createElement('tr');
177            if (line.amount > 0) tr.className = 'wl-row-positive';
178            else if (line.amount < 0) tr.className = 'wl-row-negative';
179            tr.innerHTML =
180                '<td>' + escapeHtml(line.label) + '</td>' +
181                '<td>' + line.transactionCount + '</td>' +
182                '<td class="wl-amount">' + fmtTry(line.amount) + '</td>';
183            lineBody.appendChild(tr);
184        });
185
186        if (report.userBreakdown && report.userBreakdown.length > 0) {
187            renderUsers(report);
188        }
189    }
190
191    function renderUsers(report) {
192        $('wlUserBreakdownCount').textContent = '(' + report.userBreakdownTotalCount + ' kullanıcı)';
193        var body = $('wlUsersBody');
194        body.innerHTML = '';
195        if (!report.userBreakdown || report.userBreakdown.length === 0) {
196            body.innerHTML = '<tr><td colspan="5" class="wl-empty">Kayıt yok.</td></tr>';
197            return;
198        }
199        report.userBreakdown.forEach(function (u) {
200            var tr = document.createElement('tr');
201            // T-20260806, Epic #1479, Faz-1 (Karar 3): negatif bakiye = borç — kırmızı satır
202            // (.wl-row-negative, --bd-danger-700 tokeni — mevcut kalem tablosu deseni).
203            if (u.availableBalance < 0) tr.className = 'wl-row-negative';
204            tr.innerHTML =
205                '<td>' + escapeHtml(u.displayName) + '</td>' +
206                '<td class="wl-amount">' + fmtTry(u.availableBalance) + '</td>' +
207                '<td>' + fmtTry(u.heldBalance) + '</td>' +
208                // Reviewer Minor fix: Emanet sütunu — Toplam = Available+Held+Escrow, sütunsuz
209                // ekranda "tutmuyormuş" gibi görünürdü (Faz-1'de her zaman 0).
210                '<td>' + fmtTry(u.escrowBalance) + '</td>' +
211                '<td>' + fmtTry(u.totalBalance) + '</td>';
212            body.appendChild(tr);
213        });
214    }
215
216    function statusBadgeHtml(status) {
217        var cls = status === 'Ok' ? 'bd-badge bd-badge--success' : (status === 'Warning' ? 'bd-badge bd-badge--warn' : 'bd-badge bd-badge--danger');
218        return '<span class="' + cls + '">' + escapeHtml(status) + '</span>';
219    }
220
221    function renderRuns(runs) {
222        var body = $('wlRunsBody');
223        body.innerHTML = '';
224        if (!runs || runs.length === 0) {
225            body.innerHTML = '<tr><td colspan="7" class="wl-empty">Henüz koşu yok.</td></tr>';
226            return;
227        }
228        runs.forEach(function (r) {
229            var tr = document.createElement('tr');
230            tr.innerHTML =
231                '<td>' + fmtDate(r.runUtc) + '</td>' +
232                '<td>' + escapeHtml(r.triggeredBy) + '</td>' +
233                '<td>' + fmtTry(r.ledgerDelta) + '</td>' +
234                '<td>' + fmtTry(r.receiptOverrun) + '</td>' +
235                '<td>' + escapeHtml(r.stripeCheckStatus || '-') + '</td>' +
236                '<td>' + statusBadgeHtml(r.overallStatus) + '</td>' +
237                '<td>' + (r.durationMs != null ? (r.durationMs + ' ms') : '-') + '</td>';
238            body.appendChild(tr);
239        });
240    }
241
242    function escapeHtml(s) {
243        if (s == null) return '';
244        return String(s)
245            .replace(/&/g, '&amp;')
246            .replace(/</g, '&lt;')
247            .replace(/>/g, '&gt;')
248            .replace(/"/g, '&quot;');
249    }
250
251    function showError(message) {
252        var banner = $('wlErrorBanner');
253        banner.textContent = message;
254        banner.classList.remove('d-none');
255    }
256
257    function hideError() {
258        $('wlErrorBanner').classList.add('d-none');
259    }
260
261    async function loadReport(includeUsers) {
262        $('wlLoadingBanner').classList.remove('d-none');
263        $('wlContent').classList.add('d-none');
264        hideError();
265        try {
266            var report = await apiGet('?includeUsers=' + (includeUsers ? 'true' : 'false') + currentPeriod());
267            renderReport(report);
268            $('wlLoadingBanner').classList.add('d-none');
269            $('wlContent').classList.remove('d-none');
270        } catch (err) {
271            $('wlLoadingBanner').classList.add('d-none');
272            showError('Rapor yüklenemedi: ' + err.message);
273        }
274    }
275
276    async function loadRuns() {
277        try {
278            var runs = await apiGet('/runs?take=20');
279            renderRuns(runs);
280        } catch (err) {
281            // Son koşu listesi hatası ana raporu bloklamaz — sessizce logla.
282            console.error('Wallet liability runs load failed', err);
283        }
284    }
285
286    // Issue #2068 (Reviewer M3) — Reconcile yanıtındaki k3NotEvaluatedStreak alanı yalnız
287    // DevTools > Network'te görünüyordu; admin körlüğü ekranda görünsün diye eklendi.
288    // textContent kullanılır (innerHTML DEĞİL) — sunucudan gelen sayısal alan olsa da XSS'e
289    // KARŞI güvenli tutuluyor.
290    function showK3StreakWarning(result) {
291        var banner = $('wlK3StreakBanner');
292        if (!banner) return;
293        var streak = Number(result && result.k3NotEvaluatedStreak || 0);
294        if (streak >= 3) {
295            $('wlK3StreakText').textContent =
296                'K3 (Stripe bakiyesi) art arda ' + streak + ' koşudur değerlendirilemiyor — bu kontrol şu an kör.';
297            banner.classList.remove('d-none');
298        } else {
299            banner.classList.add('d-none');
300        }
301    }
302
303    async function reconcileNow() {
304        var btn = $('wlReconcileBtn');
305        btn.disabled = true;
306        var originalHtml = btn.innerHTML;
307        btn.innerHTML = '<i class="bi bi-hourglass-split"></i> Çalışıyor...';
308        try {
309            var result = await apiPost('/reconcile');
310            showK3StreakWarning(result);
311            await loadReport(false);
312            await loadRuns();
313        } catch (err) {
314            showError('Mutabakat koşusu başarısız: ' + err.message);
315        } finally {
316            btn.disabled = false;
317            btn.innerHTML = originalHtml;
318        }
319    }
320
321    function initFilterToggle() {
322        var toggle = document.querySelector('[data-filter-toggle]');
323        if (!toggle) return;
324        toggle.addEventListener('click', function () {
325            var panelId = toggle.getAttribute('data-filter-toggle');
326            var panel = $(panelId);
327            if (panel) panel.classList.toggle('d-none');
328        });
329    }
330
331    function initExportLinks() {
332        $('wlExportCsvSummary').addEventListener('click', function (e) {
333            e.preventDefault();
334            window.location.href = apiBase + '/export.csv?section=summary' + currentPeriod();
335        });
336        $('wlExportCsvUsers').addEventListener('click', function (e) {
337            e.preventDefault();
338            window.location.href = apiBase + '/export.csv?section=users' + currentPeriod();
339        });
340        $('wlExportPdf').addEventListener('click', function (e) {
341            e.preventDefault();
342            window.location.href = apiBase + '/export.pdf' + currentPeriod();
343        });
344    }
345
346    function init() {
347        initDefaultPeriod();
348        initFilterToggle();
349        initExportLinks();
350
351        $('wlApplyFilterBtn').addEventListener('click', function () { loadReport(false); });
352        $('wlRefreshBtn').addEventListener('click', function () { loadReport(false); loadRuns(); });
353        $('wlReconcileBtn').addEventListener('click', reconcileNow);
354        $('wlLoadUsersBtn').addEventListener('click', function () { loadReport(true); });
355
356        loadReport(false);
357        loadRuns();
358    }
359
360    document.addEventListener('DOMContentLoaded', init);
361})();

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.