CarboAny / src /lib /scope3Aggregator.ts
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
3.98 kB
// 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<string, {
totalTons: number
contributions: Scope3Contribution[]
}>
/** ์ž์—ฐ ์ฃผ์ฒด๋ณ„ ๋ˆ„์  ํšŒ๋ณต๋Ÿ‰ */
byNatureBeneficiary: Record<string, {
totalTons: number
entryCount: number
}>
/** ์ „์ฒด 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<string, unknown>
return !(ref.is_koc_registered === true || ref.isKocRegistered === true || ref.has_koc_credits === true)
})
.map((e) => {
const ref = (e.referenceData ?? {}) as Record<string, unknown>
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<Scope3Summary> {
try {
const entries = await getLedgerEntries({ campaignId, limit: 500 })
return summarizeScope3(entries)
} catch {
return {
byOriginator: {},
byNatureBeneficiary: {},
totalScope3Tons: 0,
entryCount: 0,
}
}
}