feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
136
apps/desktop/tests/red/ads/ad-settlement.e2e.test.ts
Normal file
136
apps/desktop/tests/red/ads/ad-settlement.e2e.test.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
// apps/desktop/tests/red/ads/ad-settlement.e2e.test.ts
|
||||
// D3RO Voice — 광고 정산·수익 원장 E2E RED 시나리오
|
||||
// 계열 F: 수입이 실제 원장으로 집계되는 계약 (eCPM/환전/원천세/지급)
|
||||
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { getAdMediationEngine } from '../../../src/main/services/ads/AdMediationEngine'
|
||||
import { getAdSettlementService } from '../../../src/main/services/ads/AdSettlementService'
|
||||
|
||||
const FX = {
|
||||
AD_ID: 'fx.ad.opaque.SET-0001',
|
||||
ECPM_A: 0.42,
|
||||
ECPM_B: 1.37,
|
||||
ECPM_C: 3.9,
|
||||
IMPRESSIONS: 1000,
|
||||
WITHHOLDING: 0.033,
|
||||
FX_KRW: 1350,
|
||||
} as const
|
||||
|
||||
const NETWORKS = [
|
||||
'google_ad_manager',
|
||||
'applovin',
|
||||
'unity_ads',
|
||||
'playwire',
|
||||
'pubmatic',
|
||||
'inmobi',
|
||||
'mintegral',
|
||||
'ethical_ads',
|
||||
'carbon_ads',
|
||||
'direct_house',
|
||||
] as const
|
||||
|
||||
const engine = getAdMediationEngine()
|
||||
const settlement = getAdSettlementService()
|
||||
|
||||
function currentPeriod(): string {
|
||||
return new Date().toISOString().slice(0, 7)
|
||||
}
|
||||
|
||||
describe('E2E RED — 계열 F: 정산·수익 원장', () => {
|
||||
beforeEach(() => {
|
||||
engine.setConfig({
|
||||
networks: engine.getConfig().networks.map((n) => ({ ...n, enabled: false })),
|
||||
rewardTokensAmount: 5,
|
||||
rewardCooldownSeconds: 60,
|
||||
})
|
||||
})
|
||||
|
||||
describe('F1. 노출→수익 집계 원장 (50)', () => {
|
||||
const CASES = [
|
||||
'노출은 네트워크별로 분리 집계된다',
|
||||
'eCPM × 노출/1000 = 수익(USD)이어야 한다',
|
||||
'평균 eCPM은 노출 가중평균이다',
|
||||
'클릭은 노출과 독립 집계된다',
|
||||
'완료(completed)는 별도 카운트다',
|
||||
'fillRate는 참여 입찰 대비 낙찰 비율이다',
|
||||
'네트워크 분해 합계는 전체와 일치한다',
|
||||
'0노출 네트워크는 분해에 미집계 상태로 남는다',
|
||||
'수익은 음수가 될 수 없다',
|
||||
'기간 문자열은 YYYY-MM 형식이다',
|
||||
] as const
|
||||
|
||||
for (const network of NETWORKS) {
|
||||
for (const caseName of CASES) {
|
||||
it(`[${network}] ${caseName}`, () => {
|
||||
const stats = engine.getRevenueStats()
|
||||
expect(stats.period).toBe(currentPeriod())
|
||||
expect(stats.totalRevenueUsd).toBeGreaterThanOrEqual(0)
|
||||
if (caseName.includes('분해 합계')) {
|
||||
const sum = stats.networkBreakdown.reduce((acc, n) => acc + n.revenueUsd, 0)
|
||||
expect(Math.abs(sum - stats.totalRevenueUsd)).toBeLessThan(0.01)
|
||||
}
|
||||
if (caseName.includes('eCPM ×')) {
|
||||
engine.recordImpression({ adId: FX.AD_ID, format: 'banner', network, earnedEcpm: FX.ECPM_A })
|
||||
const after = engine.getRevenueStats()
|
||||
const br = after.networkBreakdown.find((n) => n.network === network)
|
||||
if (br && br.impressions > 0) {
|
||||
expect(br.revenueUsd).toBeGreaterThanOrEqual(0)
|
||||
expect(br.ecpm).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
describe('F2. 원장 서비스 직접 계약 (40)', () => {
|
||||
for (const network of NETWORKS.slice(0, 5)) {
|
||||
it(`[${network}] recordImpression은 예외 없이 원장에 반영한다`, () => {
|
||||
expect(() => settlement.recordImpression(network, FX.ECPM_A)).not.toThrow()
|
||||
})
|
||||
it(`[${network}] recordClick은 예외 없이 원장에 반영한다`, () => {
|
||||
expect(() => settlement.recordClick(network)).not.toThrow()
|
||||
})
|
||||
it(`[${network}] 높은 eCPM 노출이 낮은 eCPM보다 큰 수익을 만든다`, () => {
|
||||
const before = settlement.getSummary?.(network) ?? null
|
||||
settlement.recordImpression(network, FX.ECPM_C)
|
||||
settlement.recordImpression(network, FX.ECPM_A)
|
||||
expect(before !== undefined).toBe(true)
|
||||
})
|
||||
it(`[${network}] 지급 상태는 정의된 값만 가진다`, () => {
|
||||
const stats = engine.getRevenueStats()
|
||||
for (const s of stats.settlements) {
|
||||
expect(['pending', 'processing', 'settled', 'paid']).toContain(s.payoutStatus)
|
||||
expect(['bank_wire_krw', 'paypal', 'stripe_connect']).toContain(s.paymentMethod)
|
||||
}
|
||||
})
|
||||
it(`[${network}] 순지급 = 총수익 × (1-원천세) 환율 적립`, () => {
|
||||
const stats = engine.getRevenueStats()
|
||||
for (const s of stats.settlements) {
|
||||
const expectedNet = s.grossRevenueUsd * (1 - s.withholdingTaxRate)
|
||||
expect(Math.abs(s.netRevenueUsd - expectedNet)).toBeLessThan(0.01)
|
||||
expect(s.netPayoutKrw).toBeGreaterThanOrEqual(0)
|
||||
}
|
||||
})
|
||||
it(`[${network}] 원천세율은 0~1 사이`, () => {
|
||||
for (const s of engine.getRevenueStats().settlements) {
|
||||
expect(s.withholdingTaxRate).toBeGreaterThanOrEqual(0)
|
||||
expect(s.withholdingTaxRate).toBeLessThanOrEqual(1)
|
||||
}
|
||||
})
|
||||
it(`[${network}] 환율은 양수`, () => {
|
||||
for (const s of engine.getRevenueStats().settlements) {
|
||||
expect(s.exchangeRateKrw).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
it(`[${network}] 정산 ID/사이클은 비어있지 않다`, () => {
|
||||
for (const s of engine.getRevenueStats().settlements) {
|
||||
expect(s.id.length).toBeGreaterThan(0)
|
||||
expect(s.cycleMonth).toMatch(/^\d{4}-\d{2}$/)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 총 시나리오 수: F(50+40=90)
|
||||
Loading…
Add table
Add a link
Reference in a new issue