| import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| import { supabase } from '../supabase'; |
|
|
| vi.mock('../supabase', () => { |
| return { |
| supabase: { |
| from: vi.fn(), |
| rpc: vi.fn(), |
| }, |
| }; |
| }); |
|
|
| |
| import { triggerCampaignSettlementOnApproval } from '../settlementTrigger'; |
|
|
| describe('triggerCampaignSettlementOnApproval', () => { |
| beforeEach(() => { |
| vi.clearAllMocks(); |
| }); |
|
|
| it('successfully triggers campaign credit issuance and distribution based on actual activities', async () => { |
| |
| 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, |
| }); |
|
|
| |
| const mockActivitySelect = vi.fn().mockReturnValue({ |
| eq: vi.fn().mockReturnValue({ |
| eq: vi.fn().mockResolvedValue({ |
| data: [{ co2_grams: 5500000 }], |
| error: null, |
| }), |
| }), |
| }); |
|
|
| |
| 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, |
| }); |
|
|
| |
| 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-1' }, |
| error: null, |
| } as any); |
|
|
| |
| await expect(triggerCampaignSettlementOnApproval('map-777')).resolves.not.toThrow(); |
|
|
| |
| 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', |
| })); |
|
|
| |
| |
| 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', |
| })); |
|
|
| |
| 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', |
| })); |
|
|
| |
| 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 () => { |
| |
| 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, |
| }); |
|
|
| |
| const mockActivitySelect = vi.fn().mockReturnValue({ |
| eq: vi.fn().mockReturnValue({ |
| eq: vi.fn().mockResolvedValue({ |
| data: [], |
| error: null, |
| }), |
| }), |
| }); |
|
|
| |
| 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); |
|
|
| |
| await expect(triggerCampaignSettlementOnApproval('map-888')).resolves.not.toThrow(); |
|
|
| |
| 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', |
| })); |
|
|
| |
| 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 () => { |
| |
| 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; |
| }); |
|
|
| |
| await expect(triggerCampaignSettlementOnApproval('map-already')).rejects.toThrow( |
| '[triggerCampaignSettlementOnApproval] ์ด๋ฏธ ์น์ธ ๋ฐ ์ ์ฐ ์๋ฃ๋ ๋งคํ์
๋๋ค. (ID: map-already)' |
| ); |
|
|
| expect(supabase.rpc).not.toHaveBeenCalled(); |
| }); |
| }); |
|
|