/* 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 = `

이번 달 예상

${money(d.monthEstimate)}

다음 청구일 ${d.nextBillingAt ? aw.fmt(d.nextBillingAt) : '없음'}

결제 대기

${money(d.awaitingTotal)}

${d.awaitingOrders.length}건

크레딧 잔액

${money(d.creditBalance)}

`; h += `

활성 상품과 옵션

${d.subscriptions.length ? d.subscriptions.map((s) => `
상품
${esc(s.product.name)} v${s.product.version} ${s.status === 'active' ? '●' : '!'} ${esc(s.label)}
항목
${s.items.map((i) => esc(i.name) + '×' + i.qty).join(', ')}
청구
${money(s.total)} / ${s.billing_cycle === 'monthly' ? '월' : '년'} · 다음 ${s.next_billing_at ? aw.fmt(s.next_billing_at) : '-'}
${s.payment_due_at ? `
갱신 입금 기한
${aw.fmt(s.payment_due_at)} · 미납 제한 예정 ${aw.fmt(s.restrict_at)}
` : ''}${s.cancel_at ? `
해지 적용일
${aw.fmt(s.cancel_at)}
` : ''}
${s.status === 'cancel_scheduled' ? '' : ''}상품 변경·축소
`).join('') : '

활성 상품이 없습니다.

상품 보기'}
`; if (d.hold) h += `

상품 축소 보류

새 한도 안에 들어오지 않는 항목이 있어 축소가 보류되었습니다. 아래 항목을 정리한 뒤 "다시 검증" 을 누르세요.

마지막 검증 ${aw.fmt(d.hold.last_checked_at)}

`; h += `

활성화된 권리

${d.entitlements.filter((e) => e.purchased).map((e) => ``).join('') || ''}
자원구매배정사용
${esc(e.label)}${fmt(e.purchased, e.unit)}${fmt(e.allocated, e.unit)}${fmt(e.used, e.unit)}
없음
`; h += `

주문

${d.orders.length ? `${d.orders.map((o) => ``).join('')}
번호종류금액상태일시
${esc(o.order_no)}${esc({ new: '신규', renewal: '갱신', downsize: '축소' }[o.kind] || o.kind)}${money(o.total)}${esc(o.statusLabel)}${aw.fmt(o.created_at)}
` : '

주문이 없습니다.

'}
`; h += `

환불·크레딧

${d.credits.length ? `${d.credits.map((c) => ``).join('')}
종류금액사유적용일
${esc({ refund: '환불', credit: '크레딧', adjust: '조정' }[c.kind])}${money(c.amount)}${esc(c.reason)}${aw.fmt(c.applies_at)}
` : '

없음

'}
`; h += `

AI Wallet

제작(AI) 기능 전용이며 호스팅 디스크·트래픽·메일 사용량과 분리되어 있습니다.

${d.wallet && d.wallet.balance !== undefined ? `

${esc(d.wallet.unit ? d.wallet.balanceText || d.wallet.balance : money(d.wallet.balance))}

${(d.wallet.ledger || []).slice(0, 10).map((l) => `

${aw.fmt(l.at)} ${esc(l.type)} ${esc(String(l.amount))} ${esc(l.memo || '')}${l.refunded ? ' (실패 환급)' : ''}

`).join('')}충전·이력` : `

${esc(d.wallet?.message || '')}

`}
`; h += `

초과 사용 허용

자동 초과 사용과 자동 추가 과금은 기본으로 꺼져 있습니다. 허용하려면 항목별 최대 금액 또는 최대 사용량과 기간을 정해야 하며, 철회하면 즉시 100% 제한이 다시 적용됩니다.

${d.overage.filter((o) => !o.revoked_at).map((o) => `

${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)}

`).join('') || '

허용한 항목 없음

'}
~
`; h += `

도메인 등록

${d.domainRegistration.enabled ? '

등록기관을 통해 도메인을 검색·등록할 수 있습니다.

' : `

${esc(d.domainRegistration.note)}

`}
`; 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(); })();