Files
AriWeb-Suite/portal/commerce/public/billing.js
T

29 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* portal/commerce/public/billing.js — 결제 화면(§6.3~6.5): 활성 상품·다음 청구일·예상 금액·결제 대기·주문 목록·권리·환불/크레딧·축소 보류·해지·AI Wallet·초과 사용·도메인 등록 안내. */
(() => {
const el = document.getElementById('billing'); const esc = aw.esc; const money = (n) => Number(n).toLocaleString('ko-KR') + '원'; const GB = 1024 ** 3;
const fmt = (n, unit) => (unit.startsWith('bytes') ? (n / GB).toFixed(1) + ' GB' : Number(n).toLocaleString('ko-KR'));
async function load() {
try { const d = await aw.api('GET', '/api/commerce/billing');
let h = `<div class="cards"><div class="card"><h2>이번 달 예상</h2><p class="stat">${money(d.monthEstimate)}</p><p class="sub">다음 청구일 ${d.nextBillingAt ? aw.fmt(d.nextBillingAt) : '없음'}</p></div><div class="card"><h2>결제 대기</h2><p class="stat">${money(d.awaitingTotal)}</p><p class="sub">${d.awaitingOrders.length}건</p></div><div class="card"><h2>크레딧 잔액</h2><p class="stat">${money(d.creditBalance)}</p></div></div>`;
h += `<div class="card"><h2>활성 상품과 옵션</h2>${d.subscriptions.length ? d.subscriptions.map((s) => `<dl class="kv"><dt>상품</dt><dd>${esc(s.product.name)} v${s.product.version} <span class="badge ${s.status === 'active' ? 'ok' : 'warning'}">${s.status === 'active' ? '●' : '!'} ${esc(s.label)}</span></dd><dt>항목</dt><dd>${s.items.map((i) => esc(i.name) + '×' + i.qty).join(', ')}</dd><dt>청구</dt><dd>${money(s.total)} / ${s.billing_cycle === 'monthly' ? '월' : '년'} · 다음 ${s.next_billing_at ? aw.fmt(s.next_billing_at) : '-'}</dd>${s.payment_due_at ? `<dt>갱신 입금 기한</dt><dd>${aw.fmt(s.payment_due_at)} · 미납 제한 예정 ${aw.fmt(s.restrict_at)}</dd>` : ''}${s.cancel_at ? `<dt>해지 적용일</dt><dd>${aw.fmt(s.cancel_at)}</dd>` : ''}</dl><div class="actions">${s.status === 'cancel_scheduled' ? '<button class="btn" data-act="undo-cancel">해지 철회</button>' : '<button class="btn danger" data-act="cancel">상품 해지</button>'}<a class="btn" href="/products">상품 변경·축소</a></div>`).join('') : '<p class="muted">활성 상품이 없습니다.</p><a class="btn primary" href="/products">상품 보기</a>'}</div>`;
if (d.hold) h += `<div class="card" id="downsize"><h2>상품 축소 보류</h2><p>새 한도 안에 들어오지 않는 항목이 있어 축소가 보류되었습니다. 아래 항목을 정리한 뒤 "다시 검증" 을 누르세요.</p><ul>${d.hold.shortages.map((s) => `<li><b>${esc(s.label)}</b>: ${fmt(s.reduceBy, s.unit)} 줄이기 (새 한도 ${fmt(s.newLimit, s.unit)}, 배정+예약 ${fmt(s.inUse, s.unit)}, 사용 ${fmt(s.used, s.unit)})<ul>${s.items.map((i) => `<li>${esc(i.subjectType)} ${esc(i.subjectId)}: 배정 ${fmt(i.amount, s.unit)}</li>`).join('')}</ul></li>`).join('')}</ul><p class="sub">마지막 검증 ${aw.fmt(d.hold.last_checked_at)}</p><div class="actions"><button class="btn primary" data-act="revalidate">다시 검증</button><button class="btn" data-act="cancel-hold">축소 요청 취소</button></div></div>`;
h += `<div class="card"><h2>활성화된 권리</h2><table class="r"><thead><tr><th>자원</th><th>구매</th><th>배정</th><th>사용</th></tr></thead><tbody>${d.entitlements.filter((e) => e.purchased).map((e) => `<tr><td data-l="자원">${esc(e.label)}</td><td data-l="구매">${fmt(e.purchased, e.unit)}</td><td data-l="배정">${fmt(e.allocated, e.unit)}</td><td data-l="사용">${fmt(e.used, e.unit)}</td></tr>`).join('') || '<tr><td colspan="4" class="muted">없음</td></tr>'}</tbody></table></div>`;
h += `<div class="card"><h2>주문</h2>${d.orders.length ? `<table class="r"><thead><tr><th>번호</th><th>종류</th><th>금액</th><th>상태</th><th>일시</th></tr></thead><tbody>${d.orders.map((o) => `<tr><td data-l="번호"><a href="/orders/${esc(o.id)}">${esc(o.order_no)}</a></td><td data-l="종류">${esc({ new: '신규', renewal: '갱신', downsize: '축소' }[o.kind] || o.kind)}</td><td data-l="금액">${money(o.total)}</td><td data-l="상태">${esc(o.statusLabel)}</td><td data-l="일시">${aw.fmt(o.created_at)}</td></tr>`).join('')}</tbody></table>` : '<p class="muted">주문이 없습니다.</p>'}</div>`;
h += `<div class="card"><h2>환불·크레딧</h2>${d.credits.length ? `<table class="r"><thead><tr><th>종류</th><th>금액</th><th>사유</th><th>적용일</th></tr></thead><tbody>${d.credits.map((c) => `<tr><td data-l="종류">${esc({ refund: '환불', credit: '크레딧', adjust: '조정' }[c.kind])}</td><td data-l="금액">${money(c.amount)}</td><td data-l="사유">${esc(c.reason)}</td><td data-l="적용일">${aw.fmt(c.applies_at)}</td></tr>`).join('')}</tbody></table>` : '<p class="muted">없음</p>'}</div>`;
h += `<div class="card"><h2>AI Wallet</h2><p class="sub">제작(AI) 기능 전용이며 호스팅 디스크·트래픽·메일 사용량과 분리되어 있습니다.</p>${d.wallet && d.wallet.balance !== undefined ? `<p class="stat">${esc(d.wallet.unit ? d.wallet.balanceText || d.wallet.balance : money(d.wallet.balance))}</p>${(d.wallet.ledger || []).slice(0, 10).map((l) => `<p class="sub">${aw.fmt(l.at)} ${esc(l.type)} ${esc(String(l.amount))} ${esc(l.memo || '')}${l.refunded ? ' (실패 환급)' : ''}</p>`).join('')}<a class="btn" href="${esc(d.wallet.chargeUrl || '/sites')}">충전·이력</a>` : `<p class="muted">${esc(d.wallet?.message || '')}</p>`}</div>`;
h += `<div class="card"><h2>초과 사용 허용</h2><p class="sub">자동 초과 사용과 자동 추가 과금은 기본으로 꺼져 있습니다. 허용하려면 항목별 최대 금액 또는 최대 사용량과 기간을 정해야 하며, 철회하면 즉시 100% 제한이 다시 적용됩니다.</p>${d.overage.filter((o) => !o.revoked_at).map((o) => `<p>${esc(o.resource)}: 상한 ${o.cap_amount ? money(o.cap_amount) : o.cap_units + ' 단위'} · ${o.period_start.slice(0, 10)}~${o.period_end.slice(0, 10)} <button class="btn" data-act="revoke-overage" data-res="${esc(o.resource)}">철회</button></p>`).join('') || '<p class="muted">허용한 항목 없음</p>'}<form id="ov" class="f"><label>자원</label><select name="resource"><option value="web_out">월간 웹 아웃바운드</option><option value="mail_msgs">월간 메일 통수</option></select><label>최대 금액(원)</label><input name="capAmount" type="number" min="0"><label>기간</label><input name="periodStart" type="date" required> ~ <input name="periodEnd" type="date" required><div class="actions"><button class="btn">초과 사용 허용</button></div></form></div>`;
h += `<div class="card"><h2>도메인 등록</h2>${d.domainRegistration.enabled ? '<p>등록기관을 통해 도메인을 검색·등록할 수 있습니다.</p>' : `<p class="muted">${esc(d.domainRegistration.note)}</p>`}</div>`;
el.innerHTML = h;
el.querySelectorAll('[data-act]').forEach((b) => { b.onclick = async () => { const a = b.dataset.act; try {
if (a === 'cancel') { const p = await aw.api('GET', '/api/commerce/cancel/preview'); if (!(await aw.confirmDanger({ title: '상품 해지', target: '현재 그룹의 활성 상품', impacts: [`적용일: ${p.effectiveAt ? aw.fmt(p.effectiveAt) : '즉시'}`, ...p.effects, `보존: 사이트 ${p.retention?.site}일 · 메일 ${p.retention?.mail}일 · 소스 ${p.retention?.source}일`], typeToConfirm: '해지', confirmLabel: '해지 예약' }))) return; await aw.api('POST', '/api/commerce/cancel'); }
if (a === 'undo-cancel') await aw.api('POST', '/api/commerce/cancel/undo');
if (a === 'revalidate') { const r = await aw.api('POST', '/api/commerce/downsize/revalidate'); aw.toast(r.fits ? '정리가 확인되어 축소 주문이 만들어졌습니다' : '아직 정리할 항목이 남아 있습니다'); }
if (a === 'cancel-hold') await aw.api('POST', '/api/commerce/downsize/cancel');
if (a === 'revoke-overage') { if (!(await aw.confirmDanger({ title: '초과 사용 철회', target: b.dataset.res, impacts: ['철회 즉시 100% 제한이 다시 적용됩니다'] }))) return; await aw.api('POST', '/api/commerce/overage', { revoke: true, resource: b.dataset.res }); }
load(); } catch (e) { aw.toast(e.message); } }; });
el.querySelector('#ov').onsubmit = async (ev) => { ev.preventDefault(); const f = new FormData(ev.target); try { await aw.api('POST', '/api/commerce/overage', { resource: f.get('resource'), capAmount: Number(f.get('capAmount')) || null, periodStart: new Date(f.get('periodStart')).toISOString(), periodEnd: new Date(f.get('periodEnd') + 'T23:59:59').toISOString() }); load(); } catch (e) { aw.toast(e.message); } };
} catch (e) { aw.state(el, e.status === 403 ? 'forbidden' : 'failed', e.message, { retry: load }); }
}
load();
})();