CarboAny / src /lib /__tests__ /settlementTrigger.test.ts
Esketch's picture
deploy: [P4] Strangler Pattern API Adapter Release (Orphan Clean Build v2)
daaf9d7
Raw
History Blame Contribute Delete
7.82 kB
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { supabase } from '../supabase';
vi.mock('../supabase', () => {
return {
supabase: {
from: vi.fn(),
rpc: vi.fn(),
},
};
});
// Import code under test after mock definition
import { triggerCampaignSettlementOnApproval } from '../settlementTrigger';
describe('triggerCampaignSettlementOnApproval', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('successfully triggers campaign credit issuance and distribution based on actual activities', async () => {
// 1. Mock external mapping and campaign query
const mockMappingSingle = vi.fn().mockResolvedValue({
data: {
id: 'map-777',
registry_code: 'VCS',
internal_type: 'campaign',
internal_id: 'camp-777',
verification_level: 'submitted',
campaigns: {
id: 'camp-777',
title: '์ œ์ฃผ ๋‹คํšŒ์šฉ ํ…€๋ธ”๋Ÿฌ ์—์ฝ” ์บ ํŽ˜์ธ',
nature_beneficiary_id: 'nature:ocean:jeju',
nature_beneficiary_name: '์ œ์ฃผ ๋ฐ”๋‹ค',
target_co2_kg: 10000,
},
},
error: null,
});
const mockMappingEq = vi.fn().mockReturnValue({
single: mockMappingSingle,
});
const mockMappingSelect = vi.fn().mockReturnValue({
eq: mockMappingEq,
});
// 2. Mock activities CO2 sum (e.g. 5,500,000 grams = 5.5 tCO2e = 5.5 TON)
const mockActivitySelect = vi.fn().mockReturnValue({
eq: vi.fn().mockReturnValue({
eq: vi.fn().mockResolvedValue({
data: [{ co2_grams: 5500000 }],
error: null,
}),
}),
});
// 3. Mock distribution rules query (e.g. 70% Reserve, 20% SME, 10% Platform)
const mockRulesSingle = vi.fn().mockResolvedValue({
data: {
reserve_rate: 0.70,
donation_rate: 0.20,
platform_rate: 0.10,
},
error: null,
});
const mockRulesEq = vi.fn().mockReturnValue({
maybeSingle: mockRulesSingle,
});
const mockRulesSelect = vi.fn().mockReturnValue({
eq: mockRulesEq,
});
// 4. Set up supabase.from routing mock
vi.mocked(supabase.from).mockImplementation((table: string) => {
if (table === 'external_project_mappings') {
return { select: mockMappingSelect } as any;
}
if (table === 'activities') {
return { select: mockActivitySelect } as any;
}
if (table === 'distribution_rules') {
return { select: mockRulesSelect } as any;
}
return {} as any;
});
// 5. Mock append_ledger_entry RPC
vi.mocked(supabase.rpc).mockResolvedValue({
data: { id: 'led-1' },
error: null,
} as any);
// 6. Act
await expect(triggerCampaignSettlementOnApproval('map-777')).resolves.not.toThrow();
// 7. Assertions - Credit Issuance Check (5,500,000g -> 5.5 TON)
expect(supabase.rpc).toHaveBeenCalledWith('append_ledger_entry', expect.objectContaining({
p_category: 'CREDIT_ISSUANCE',
p_asset: 'TON',
p_amount: 5.5,
p_sender_id: 'VCS',
p_receiver_id: 'nature:ocean:jeju',
}));
// Assertions - Distribution Checks
// Reserve: 5.5 * 0.7 = 3.85 TON
expect(supabase.rpc).toHaveBeenCalledWith('append_ledger_entry', expect.objectContaining({
p_category: 'CREDIT_RETIRE',
p_asset: 'TON',
p_amount: 3.85,
p_sender_id: 'nature:ocean:jeju',
p_receiver_id: 'nature:ocean:jeju',
}));
// SME: 5.5 * 0.2 = 1.1 TON
expect(supabase.rpc).toHaveBeenCalledWith('append_ledger_entry', expect.objectContaining({
p_category: 'CREDIT_DONATION',
p_asset: 'TON',
p_amount: 1.1,
p_sender_id: 'nature:ocean:jeju',
p_receiver_id: 'sme:allocated',
}));
// Platform: 5.5 * 0.1 = 0.55 TON
expect(supabase.rpc).toHaveBeenCalledWith('append_ledger_entry', expect.objectContaining({
p_category: 'FEE_DEDUCTION',
p_asset: 'TON',
p_amount: 0.55,
p_sender_id: 'nature:ocean:jeju',
p_receiver_id: 'platform:fees',
}));
});
it('falls back to campaign target_co2_kg when actual activities sum is 0', async () => {
// 1. Mock external mapping and campaign query (target: 15,000 kg = 15 TON)
const mockMappingSingle = vi.fn().mockResolvedValue({
data: {
id: 'map-888',
registry_code: 'VCS',
internal_type: 'campaign',
internal_id: 'camp-888',
verification_level: 'submitted',
campaigns: {
id: 'camp-888',
title: '์ œ์ฃผ ๋‹คํšŒ์šฉ ํ…€๋ธ”๋Ÿฌ ์—์ฝ” ์บ ํŽ˜์ธ',
nature_beneficiary_id: 'nature:ocean:jeju',
nature_beneficiary_name: '์ œ์ฃผ ๋ฐ”๋‹ค',
target_co2_kg: 15000,
},
},
error: null,
});
const mockMappingEq = vi.fn().mockReturnValue({
single: mockMappingSingle,
});
const mockMappingSelect = vi.fn().mockReturnValue({
eq: mockMappingEq,
});
// Activities sum is 0
const mockActivitySelect = vi.fn().mockReturnValue({
eq: vi.fn().mockReturnValue({
eq: vi.fn().mockResolvedValue({
data: [],
error: null,
}),
}),
});
// Mock distribution rules (no rules, falls back to default 70:20:10)
const mockRulesSingle = vi.fn().mockResolvedValue({
data: null,
error: null,
});
const mockRulesEq = vi.fn().mockReturnValue({
maybeSingle: mockRulesSingle,
});
const mockRulesSelect = vi.fn().mockReturnValue({
eq: mockRulesEq,
});
vi.mocked(supabase.from).mockImplementation((table: string) => {
if (table === 'external_project_mappings') {
return { select: mockMappingSelect } as any;
}
if (table === 'activities') {
return { select: mockActivitySelect } as any;
}
if (table === 'distribution_rules') {
return { select: mockRulesSelect } as any;
}
return {} as any;
});
vi.mocked(supabase.rpc).mockResolvedValue({
data: { id: 'led-2' },
error: null,
} as any);
// 2. Act
await expect(triggerCampaignSettlementOnApproval('map-888')).resolves.not.toThrow();
// 3. Assertions - Fallback Credit Issuance (15,000 kg -> 15 TON)
expect(supabase.rpc).toHaveBeenCalledWith('append_ledger_entry', expect.objectContaining({
p_category: 'CREDIT_ISSUANCE',
p_asset: 'TON',
p_amount: 15,
p_sender_id: 'VCS',
p_receiver_id: 'nature:ocean:jeju',
}));
// Platform default 10%: 15 * 0.1 = 1.5 TON
expect(supabase.rpc).toHaveBeenCalledWith('append_ledger_entry', expect.objectContaining({
p_category: 'FEE_DEDUCTION',
p_asset: 'TON',
p_amount: 1.5,
}));
});
it('prevents double issuance if the mapping is already approved', async () => {
// Mapping already approved
const mockMappingSingle = vi.fn().mockResolvedValue({
data: {
id: 'map-already',
registry_code: 'VCS',
internal_type: 'campaign',
internal_id: 'camp-999',
verification_level: 'approved',
},
error: null,
});
const mockMappingEq = vi.fn().mockReturnValue({
single: mockMappingSingle,
});
const mockMappingSelect = vi.fn().mockReturnValue({
eq: mockMappingEq,
});
vi.mocked(supabase.from).mockImplementation((table: string) => {
if (table === 'external_project_mappings') {
return { select: mockMappingSelect } as any;
}
return {} as any;
});
// Act & Assert
await expect(triggerCampaignSettlementOnApproval('map-already')).rejects.toThrow(
'[triggerCampaignSettlementOnApproval] ์ด๋ฏธ ์Šน์ธ ๋ฐ ์ •์‚ฐ ์™„๋ฃŒ๋œ ๋งคํ•‘์ž…๋‹ˆ๋‹ค. (ID: map-already)'
);
expect(supabase.rpc).not.toHaveBeenCalled();
});
});