d3ro-voice/apps/desktop/tests/red/ads/ad-settlement.e2e.test.ts
Yun Chan eedd127ea7
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 1m13s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / 모바일 린트·타입·Jest (push) Failing after 1m4s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 37s
ci / .NET API 서버 테스트 (push) Successful in 27s
deploy-site / deploy (push) Failing after 20s
refactor(billing): remove Stripe; payments are Payple (web) and Google Play (mobile)
Stripe is not used. Keeping its checkout, portal and webhook paths meant a
second payment provider, a second return-URL format and dead UI.

- Delete the stripe-checkout, stripe-portal and stripe-webhook functions and
  their config; billing-catalog serves Payple prices only, and the web parser
  rejects a catalog that still mixes in Stripe prices.
- Web: drop the Stripe checkout/portal buttons, provider toggle and return
  notices; billing shows Payple only. Past rows with provider='stripe' are
  still displayed ("Stripe (종료)") with a support contact instead of a portal.
- Desktop: delete the Stripe checkout modal, payment IPC channels, preload
  namespace and their types; "Remove ads with Pro" opens the web billing page
  via license.openBilling. Support/refund copy names Payple.
- billingUrl() loses the Stripe-only success/canceled result option; the
  Deno contract is regenerated.
- Migrations and the DB's accepted provider values are untouched (history).
- Docs and the backlog record the removal (MON-04, EXT-STRIPE-01, GAP-BILL-03).

Verified: typecheck (desktop/web/admin/api-client/mobile), contract:check,
deno check all functions, deno test 80/80, desktop 1478/1480 on the Electron
runtime (2 known environment failures), web and admin builds, release
metadata and mobile boundary self-tests, eslint on changed files.
2026-09-26 20:56:18 +09:00

136 lines
5.1 KiB
TypeScript
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.

// 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']).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)