// Scope 3 Aggregator // 원장(settlement_ledger)의 CREDIT_RETIRE + nature:* 수신자 엔트리를 // 원청 기업의 Scope 3 증빙으로 자동 집계한다. // 특허 청구항 5 (기존 6): SME 상생 Router → Scope 3 성과 자동 기록 import { getLedgerEntries } from './ledger' import type { LedgerEntry } from '../types' export interface Scope3Contribution { /** Scope 3 인증 제출 대상 (원청 기업) — referenceData.submittedTo */ submittedTo: string /** 인증서 ID — referenceData.scope3CertId */ scope3CertId: string /** 기여한 SME (sender_name) */ smeName: string /** 돌려준 자연 주체 (receiver_name) */ natureBeneficiary: string /** 톤 */ retiredTons: number /** 원장 엔트리 ID */ ledgerEntryId: string /** 기록 시각 */ retiredAt: string } export interface Scope3Summary { /** 원청 기업별 총 Scope 3 크레딧 */ byOriginator: Record /** 자연 주체별 누적 회복량 */ byNatureBeneficiary: Record /** 전체 Scope 3 적격 톤 합계 */ totalScope3Tons: number /** 전체 적격 엔트리 수 */ entryCount: number } /** * 원장에서 Scope 3 적격 엔트리를 추출한다. * 조건: * 1) category === 'CREDIT_RETIRE' * 2) receiverId가 'nature:*' 네임스페이스 * 3) referenceData.scope3CertId 존재 */ export function extractScope3FromLedger(entries: LedgerEntry[]): Scope3Contribution[] { return entries .filter((e) => e.category === 'CREDIT_RETIRE') .filter((e) => typeof e.receiverId === 'string' && e.receiverId.startsWith('nature:')) .filter((e) => { const ref = (e.referenceData ?? {}) as Record return !(ref.is_koc_registered === true || ref.isKocRegistered === true || ref.has_koc_credits === true) }) .map((e) => { const ref = (e.referenceData ?? {}) as Record const certId = ref.scope3CertId ?? ref.scope3_cert_id if (!certId) return null return { submittedTo: (ref.submittedTo as string) ?? (ref.submitted_to as string) ?? '원청 미지정', scope3CertId: String(certId), smeName: e.senderName, natureBeneficiary: e.receiverName, retiredTons: Number(e.amount), ledgerEntryId: e.id ?? e.transactionId, retiredAt: e.createdAt, } satisfies Scope3Contribution }) .filter((x): x is Scope3Contribution => x !== null) } export function summarizeScope3(entries: LedgerEntry[]): Scope3Summary { const contributions = extractScope3FromLedger(entries) const byOriginator: Scope3Summary['byOriginator'] = {} const byNatureBeneficiary: Scope3Summary['byNatureBeneficiary'] = {} let totalScope3Tons = 0 for (const c of contributions) { if (!byOriginator[c.submittedTo]) { byOriginator[c.submittedTo] = { totalTons: 0, contributions: [] } } byOriginator[c.submittedTo].totalTons += c.retiredTons byOriginator[c.submittedTo].contributions.push(c) if (!byNatureBeneficiary[c.natureBeneficiary]) { byNatureBeneficiary[c.natureBeneficiary] = { totalTons: 0, entryCount: 0 } } byNatureBeneficiary[c.natureBeneficiary].totalTons += c.retiredTons byNatureBeneficiary[c.natureBeneficiary].entryCount += 1 totalScope3Tons += c.retiredTons } return { byOriginator, byNatureBeneficiary, totalScope3Tons, entryCount: contributions.length, } } /** * Live 원장에서 Scope 3 요약을 가져온다. 실패 시 빈 요약 반환. */ export async function fetchScope3Summary(campaignId?: string): Promise { try { const entries = await getLedgerEntries({ campaignId, limit: 500 }) return summarizeScope3(entries) } catch { return { byOriginator: {}, byNatureBeneficiary: {}, totalScope3Tons: 0, entryCount: 0, } } }