diff --git a/src/agreements/agreement-lifecycle.spec.ts b/src/agreements/agreement-lifecycle.spec.ts index 62b73c2..f605e9a 100644 --- a/src/agreements/agreement-lifecycle.spec.ts +++ b/src/agreements/agreement-lifecycle.spec.ts @@ -390,15 +390,78 @@ describe('AgreementsService lifecycle enforcement (business rules)', () => { { description: 'Build', amount: '50.00', status: 'released' }, ]; + const backendClient = { + getAgreement: jest.fn(), + createAgreement: jest.fn(), + linkContract: jest.fn(), + updateAgreementStatus: jest.fn(), + updateMilestone: jest.fn(), + listAgreementsByWallet: jest.fn(), + getAgreementByContractId: jest.fn(), + getAgreementActivity: jest.fn(), + logActivity: jest.fn(), + }; + beforeEach(() => { db = new InMemoryDb(); emit = jest.fn(); const activity = new AgreementActivityService(db as unknown as SupabaseService); service = new AgreementsService( db as unknown as SupabaseService, + backendClient as unknown as AgreementsBackendClient, { emit } as unknown as EventEmitter2, activity, ); + + // Reset and setup backend client mocks + backendClient.getAgreement.mockReset(); + backendClient.updateAgreementStatus.mockReset(); + backendClient.logActivity.mockReset(); + + // Make getAgreement return the agreement from the in-memory DB + backendClient.getAgreement.mockImplementation((agreementId) => { + try { + const agreement = db.agreement(agreementId); + const participants = db.tables.agreement_participants.filter( + (p) => p.agreement_id === agreementId, + ); + return Promise.resolve({ + success: true, + data: { agreement, participants }, + }); + } catch { + return Promise.resolve({ + success: false, + error: 'Agreement not found', + }); + } + }); + + // Make updateAgreementStatus actually update the DB + backendClient.updateAgreementStatus.mockImplementation((agreementId, req) => { + const updates: Record = { + status: req.status, + updated_at: new Date().toISOString(), + }; + if (req.status === 'funded') { + updates.funded_at = new Date().toISOString(); + } else if (req.status === 'completed' || req.status === 'resolved') { + updates.completed_at = new Date().toISOString(); + } + db.update('agreements', [{ key: 'id', op: 'eq', value: agreementId }], updates); + return Promise.resolve({ success: true }); + }); + + // Make logActivity actually insert into the activity table + backendClient.logActivity.mockImplementation((req) => { + db.insert('agreement_activity', { + agreement_id: req.agreement_id, + actor_wallet: req.actor_wallet, + action: req.action, + details: req.details || {}, + }); + return Promise.resolve({ success: true }); + }); }); function seedAgreement(status: AgreementStatus, overrides: Row = {}): string { @@ -767,6 +830,18 @@ describe('Dispute flows drive the agreement lifecycle', () => { let disputes: DisputesService; const AGREEMENT_ID = 'agr-dispute-1'; + const backendClient = { + getAgreement: jest.fn(), + createAgreement: jest.fn(), + linkContract: jest.fn(), + updateAgreementStatus: jest.fn(), + updateMilestone: jest.fn(), + listAgreementsByWallet: jest.fn(), + getAgreementByContractId: jest.fn(), + getAgreementActivity: jest.fn(), + logActivity: jest.fn(), + }; + beforeEach(() => { db = new InMemoryDb(); emit = jest.fn(); diff --git a/src/agreements/agreements-backend.client.ts b/src/agreements/agreements-backend.client.ts new file mode 100644 index 0000000..e17eb5b --- /dev/null +++ b/src/agreements/agreements-backend.client.ts @@ -0,0 +1,266 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ApiClient, ApiResponse } from '../common/api/api-client'; + +export interface Agreement { + id: string; + contract_id: string | null; + title: string; + description: string | null; + amount: string; + asset: string; + status: string; + agreement_type: string; + milestones: Array<{ + description: string; + amount: string; + status: string; + evidence_description?: string; + evidence_urls?: string[]; + evidence_submitted_at?: string; + }>; + metadata: Record; + created_by: string; + created_by_profile_id?: string | null; + funded_at: string | null; + completed_at: string | null; + created_at: string; + updated_at: string; +} + +/** + * Agreement participant data structure. + */ +export interface AgreementParticipant { + id: string; + agreement_id: string; + wallet_address: string; + role: string; + profile_id?: string | null; + created_at: string; + updated_at: string; +} + +export interface AgreementActivity { + id: string; + agreement_id: string; + actor_wallet: string; + action: string; + details: Record; + created_at: string; +} + +export interface CreateAgreementRequest { + contract_id?: string | null; + title: string; + description?: string | null; + amount: string; + asset?: string; + agreement_type?: string; + milestones?: Array<{ + description: string; + amount: string; + status: string; + }>; + metadata?: Record; + created_by: string; + created_by_profile_id?: string | null; + participants: Array<{ + wallet_address: string; + role: string; + profile_id?: string | null; + }>; +} + +export interface CreateAgreementResponse { + agreement: Agreement; +} + +export interface LinkContractResponse { + success: boolean; + agreement?: Agreement; +} + +export interface UpdateAgreementStatusRequest { + status: string; + actor_wallet: string; +} + +export interface UpdateAgreementStatusResponse { + success: boolean; + agreement?: Agreement; +} + +export interface UpdateMilestoneRequest { + milestone_index: number; + status: string; + actor_wallet: string; + evidence_description?: string; + evidence_urls?: string[]; +} + +export interface UpdateMilestoneResponse { + success: boolean; + agreement?: Agreement; +} + +export interface LogActivityRequest { + agreement_id: string; + actor_wallet: string; + action: string; + details?: Record; +} + +export interface GetByContractResponse { + agreement: Agreement | null; +} + +export interface GetActivityResponse { + activities: AgreementActivity[]; +} + +export interface GetAgreementResponse { + agreement: Agreement | null; + participants: AgreementParticipant[]; +} + +export interface ListAgreementsResponse { + agreements: Agreement[]; +} + +@Injectable() +export class AgreementsBackendClient { + private readonly logger = new Logger(AgreementsBackendClient.name); + private readonly baseUrl: string; + + constructor(private readonly apiClient: ApiClient) { + // For now, we assume backend endpoints are on the same server + // In the future, this could be configurable via environment variable + this.baseUrl = 'http://localhost:3001'; + } + + async createAgreement( + walletAddress: string, + req: CreateAgreementRequest, + ): Promise> { + return this.apiClient.post(`${this.baseUrl}/agreements`, req, { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }); + } + + async getAgreement( + agreementId: string, + walletAddress: string, + ): Promise> { + return this.apiClient.get(`${this.baseUrl}/agreements/${agreementId}`, { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }); + } + + async getAgreementByContractId( + contractId: string, + walletAddress: string, + ): Promise> { + return this.apiClient.get( + `${this.baseUrl}/agreements/by-contract/${contractId}`, + { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }, + ); + } + + async listAgreementsByWallet( + wallet: string, + walletAddress: string, + ): Promise> { + return this.apiClient.get(`${this.baseUrl}/agreements`, { + query: { wallet }, + headers: { + 'X-Wallet-Address': walletAddress, + }, + }); + } + + async updateAgreementStatus( + agreementId: string, + req: UpdateAgreementStatusRequest, + walletAddress: string, + ): Promise> { + return this.apiClient.patch( + `${this.baseUrl}/agreements/${agreementId}/status`, + req, + { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }, + ); + } + + async updateMilestone( + agreementId: string, + req: UpdateMilestoneRequest, + walletAddress: string, + ): Promise> { + return this.apiClient.patch( + `${this.baseUrl}/agreements/${agreementId}/milestones`, + req, + { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }, + ); + } + + async getAgreementActivity( + agreementId: string, + walletAddress: string, + ): Promise> { + return this.apiClient.get( + `${this.baseUrl}/agreements/${agreementId}/activity`, + { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }, + ); + } + + async logActivity( + req: LogActivityRequest, + walletAddress: string, + ): Promise> { + return this.apiClient.post<{ success: boolean }>( + `${this.baseUrl}/agreements/${req.agreement_id}/activity`, + req, + { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }, + ); + } + + async linkContract( + agreementId: string, + contractId: string, + actorWallet: string, + walletAddress: string, + ): Promise> { + return this.apiClient.patch( + `${this.baseUrl}/agreements/${agreementId}/link-contract`, + { contract_id: contractId, actor_wallet: actorWallet }, + { + headers: { + 'X-Wallet-Address': walletAddress, + }, + }, + ); + } +} diff --git a/src/agreements/agreements.module.ts b/src/agreements/agreements.module.ts index 351fdd2..36c35f2 100644 --- a/src/agreements/agreements.module.ts +++ b/src/agreements/agreements.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; import { AuthModule } from '../auth/auth.module'; +import { ApiClientModule } from '../common/api/api-client.module'; import { AgreementsController } from './agreements.controller'; import { AgreementsService } from './agreements.service'; import { AgreementActivityService } from './agreement-activity.service'; @Module({ - imports: [AuthModule], + imports: [AuthModule, ApiClientModule], controllers: [AgreementsController], providers: [AgreementsService, AgreementActivityService], exports: [AgreementsService, AgreementActivityService], diff --git a/src/agreements/agreements.service.ts b/src/agreements/agreements.service.ts index 05493dc..c1c895a 100644 --- a/src/agreements/agreements.service.ts +++ b/src/agreements/agreements.service.ts @@ -6,6 +6,7 @@ import { } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { SupabaseService } from '../supabase/supabase.service'; +import { AgreementsBackendClient } from './agreements-backend.client'; import { CreateAgreementDto } from './dto/create-agreement.dto'; import { LinkContractDto } from './dto/link-contract.dto'; import { UpdateAgreementStatusDto } from './dto/update-status.dto'; @@ -22,6 +23,7 @@ import { AgreementActivityService } from './agreement-activity.service'; export class AgreementsService { constructor( private readonly supabase: SupabaseService, + private readonly backendClient: AgreementsBackendClient, private readonly eventEmitter: EventEmitter2, private readonly activity: AgreementActivityService, ) {} @@ -67,25 +69,20 @@ export class AgreementsService { const wallet = await this.walletForUserId(userId); if (!wallet) throw new ForbiddenException('No wallet on profile'); - const { data: agreement, error: aErr } = await this.supabase - .getClient() - .from('agreements') - .select('id, created_by') - .eq('id', agreementId) - .maybeSingle(); - if (aErr || !agreement) throw new NotFoundException('Agreement not found'); + // Get agreement via backend client to check access + const result = await this.backendClient.getAgreement(agreementId, wallet); + if (!result.success || !result.data?.agreement) { + throw new NotFoundException('Agreement not found'); + } - const createdBy = (agreement as { created_by: string }).created_by; + const agreement = result.data.agreement; + const createdBy = agreement.created_by; if (createdBy === wallet || createdBy === userId) return; - const { data: parts } = await this.supabase - .getClient() - .from('agreement_participants') - .select('wallet_address') - .eq('agreement_id', agreementId) - .eq('wallet_address', wallet) - .limit(1); - if (!parts?.length) { + // Check if wallet is a participant + const participants = result.data.participants ?? []; + const isParticipant = participants.some((p) => p.wallet_address === wallet); + if (!isParticipant) { throw new ForbiddenException('Not a participant of this agreement'); } } @@ -95,50 +92,23 @@ export class AgreementsService { const createdByProfileId = await this.profileIdByWallet(dto.created_by); - const agreementRow: Record = { + const backendReq = { contract_id: dto.contract_id ?? null, title: dto.title, description: dto.description ?? null, amount: dto.amount, asset: dto.asset ?? 'USDC', - status: 'pending', agreement_type: dto.agreement_type ?? 'single', milestones: dto.milestones ?? [], metadata: dto.metadata ?? {}, created_by: dto.created_by, + created_by_profile_id: createdByProfileId ?? undefined, + participants: dto.participants.map((p) => ({ + wallet_address: p.wallet_address, + role: p.role, + profile_id: p.profile_id, + })), }; - if (createdByProfileId) { - agreementRow.created_by_profile_id = createdByProfileId; - } - - const { data: agreement, error: agreementError } = await this.supabase - .getClient() - .from('agreements') - .insert(agreementRow) - .select() - .single(); - - if (agreementError) { - return { agreement: null, error: agreementError.message }; - } - - const participants = await Promise.all( - dto.participants.map(async (p) => { - const row: Record = { - agreement_id: agreement.id, - wallet_address: p.wallet_address, - role: p.role, - }; - const pid = p.profile_id ?? (await this.profileIdByWallet(p.wallet_address)); - if (pid) row.profile_id = pid; - return row; - }), - ); - - const { error: participantsError } = await this.supabase - .getClient() - .from('agreement_participants') - .insert(participants); if (participantsError) { await this.supabase.getClient().from('agreements').delete().eq('id', agreement.id); @@ -172,11 +142,11 @@ export class AgreementsService { this.eventEmitter.emit(AGREEMENT_EVENTS.CREATED, { agreementId: agreement.id, - title: dto.title, - description: dto.description, - amount: dto.amount, - asset: dto.asset ?? 'USDC', - createdByWallet: dto.created_by, + title: agreement.title, + description: agreement.description, + amount: agreement.amount, + asset: agreement.asset ?? 'USDC', + createdByWallet: agreement.created_by, participantWallets: dto.participants.map((p) => p.wallet_address), }); @@ -187,16 +157,16 @@ export class AgreementsService { await this.assertCanAccessAgreement(userId, agreementId); await this.assertActorWallet(userId, dto.actor_wallet); - const { error } = await this.supabase - .getClient() - .from('agreements') - .update({ - contract_id: dto.contract_id, - updated_at: new Date().toISOString(), - }) - .eq('id', agreementId); + const result = await this.backendClient.linkContract( + agreementId, + dto.contract_id, + dto.actor_wallet, + dto.actor_wallet, + ); - if (error) return { success: false, error: error.message }; + if (!result.success) { + return { success: false, error: result.error || 'Failed to link contract' }; + } await this.activity.logActivity(agreementId, dto.actor_wallet, 'contract_linked', { contract_id: dto.contract_id, @@ -208,18 +178,14 @@ export class AgreementsService { await this.assertCanAccessAgreement(userId, agreementId); await this.assertActorWallet(userId, dto.actor_wallet); - const { data: current, error: fetchError } = await this.supabase - .getClient() - .from('agreements') - .select('status, milestones, title, amount, asset') - .eq('id', agreementId) - .single(); - - if (fetchError || !current) { - return { success: false, error: fetchError?.message || 'Agreement not found' }; + // Get current agreement to validate transition + const getResult = await this.backendClient.getAgreement(agreementId, dto.actor_wallet); + if (!getResult.success || !getResult.data?.agreement) { + return { success: false, error: 'Agreement not found' }; } - const fromStatus = current.status as string; + const current = getResult.data.agreement; + const fromStatus = current.status; if (!canTransition(fromStatus, dto.status)) { throw new BadRequestException(invalidTransitionMessage(fromStatus, dto.status)); @@ -231,23 +197,14 @@ export class AgreementsService { ); } - const updates: Record = { - status: dto.status, - updated_at: new Date().toISOString(), - }; - if (dto.status === 'funded') { - updates.funded_at = new Date().toISOString(); - } else if (dto.status === 'completed' || dto.status === 'resolved') { - updates.completed_at = new Date().toISOString(); - } - - const { error } = await this.supabase - .getClient() - .from('agreements') - .update(updates) - .eq('id', agreementId); - - if (error) return { success: false, error: error.message }; + const updateResult = await this.backendClient.updateAgreementStatus( + agreementId, + { + status: dto.status, + actor_wallet: dto.actor_wallet, + }, + dto.actor_wallet, + ); await this.activity.logActivity( agreementId, @@ -315,14 +272,8 @@ export class AgreementsService { milestone.status = dto.status; - if (dto.evidence_description !== undefined) { - milestone.evidence_description = dto.evidence_description; - } - if (dto.evidence_urls !== undefined) { - milestone.evidence_urls = dto.evidence_urls; - } - if (emitsEvidence) { - milestone.evidence_submitted_at = new Date().toISOString(); + if (!result.success) { + return { success: false, error: result.error || 'Failed to update milestone' }; } const { error: updateError } = await this.supabase @@ -354,104 +305,53 @@ export class AgreementsService { async listByWallet(userId: string, wallet: string) { await this.assertActorWallet(userId, wallet); - const { data: participations, error: partError } = await this.supabase - .getClient() - .from('agreement_participants') - .select('agreement_id') - .eq('wallet_address', wallet); - - if (partError) { - return { agreements: [], error: partError.message }; - } - - const { data: createdRows, error: createdError } = await this.supabase - .getClient() - .from('agreements') - .select('id') - .eq('created_by', wallet); - - if (createdError) { - return { agreements: [], error: createdError.message }; + const result = await this.backendClient.listAgreementsByWallet(wallet, wallet); + if (!result.success) { + return { agreements: [], error: result.error || 'Failed to list agreements' }; } - const idSet = new Set(); - participations?.forEach((p) => { - if (p.agreement_id) idSet.add(p.agreement_id as string); - }); - createdRows?.forEach((r) => { - if (r.id) idSet.add(r.id as string); - }); - - if (idSet.size === 0) { - return { agreements: [], error: null }; - } - - const ids = [...idSet]; - const { data: agreements, error: agError } = await this.supabase - .getClient() - .from('agreements') - .select('*') - .in('id', ids) - .order('created_at', { ascending: false }); - - if (agError) return { agreements: [], error: agError.message }; - return { agreements: agreements ?? [], error: null }; + return { agreements: result.data?.agreements ?? [], error: null }; } async getById(userId: string, agreementId: string) { await this.assertCanAccessAgreement(userId, agreementId); - const { data: agreement, error: agError } = await this.supabase - .getClient() - .from('agreements') - .select('*') - .eq('id', agreementId) - .single(); - - if (agError) { - return { agreement: null, participants: [], error: agError.message }; + const wallet = await this.walletForUserId(userId); + if (!wallet) { + return { agreement: null, participants: [], error: 'No wallet found' }; } - const { data: participants, error: partError } = await this.supabase - .getClient() - .from('agreement_participants') - .select('*') - .eq('agreement_id', agreementId); - - if (partError) { - return { agreement, participants: [], error: partError.message }; + const result = await this.backendClient.getAgreement(agreementId, wallet); + if (!result.success) { + return { agreement: null, participants: [], error: result.error || 'Agreement not found' }; } - return { agreement, participants: participants ?? [], error: null }; + + return { + agreement: result.data?.agreement ?? null, + participants: result.data?.participants ?? [], + error: null, + }; } async getByContractId(userId: string, contractId: string) { - const { data, error } = await this.supabase - .getClient() - .from('agreements') - .select('*') - .eq('contract_id', contractId) - .maybeSingle(); - - if (error) { - if (error.code === 'PGRST116') { - return { - agreement: null, - error: - 'Ningún acuerdo tiene contract_id igual a este valor (revisá Supabase o hacé PATCH link-contract antes).', - }; - } - return { agreement: null, error: error.message }; + const wallet = await this.walletForUserId(userId); + if (!wallet) { + return { agreement: null, error: 'No wallet found' }; } - if (!data) { + + const result = await this.backendClient.getAgreementByContractId(contractId, wallet); + if (!result.success || !result.data?.agreement) { return { agreement: null, error: + result.error || 'Ningún acuerdo tiene contract_id igual a este valor (copiá el texto exacto de la columna contract_id).', }; } - await this.assertCanAccessAgreement(userId, data.id); - return { agreement: data, error: null }; + const agreement = result.data.agreement; + await this.assertCanAccessAgreement(userId, agreement.id); + return { agreement, error: null }; } /** @@ -541,14 +441,16 @@ export class AgreementsService { async getActivity(userId: string, agreementId: string) { await this.assertCanAccessAgreement(userId, agreementId); - const { data, error } = await this.supabase - .getClient() - .from('agreement_activity') - .select('*') - .eq('agreement_id', agreementId) - .order('created_at', { ascending: false }); + const wallet = await this.walletForUserId(userId); + if (!wallet) { + return { activities: [], error: 'No wallet found' }; + } + + const result = await this.backendClient.getAgreementActivity(agreementId, wallet); + if (!result.success) { + return { activities: [], error: result.error || 'Failed to get activity' }; + } - if (error) return { activities: [], error: error.message }; - return { activities: data ?? [], error: null }; + return { activities: result.data?.activities ?? [], error: null }; } } diff --git a/src/disputes/disputes.service.ts b/src/disputes/disputes.service.ts index 237bc1d..a406e5b 100644 --- a/src/disputes/disputes.service.ts +++ b/src/disputes/disputes.service.ts @@ -46,6 +46,7 @@ export class DisputesService { constructor( private readonly supabase: SupabaseService, private readonly agreements: AgreementsService, + private readonly backendClient: AgreementsBackendClient, private readonly eventEmitter: EventEmitter2, private readonly activity: AgreementActivityService, ) {} @@ -72,25 +73,20 @@ export class DisputesService { const wallet = await this.walletForUserId(userId); if (!wallet) throw new ForbiddenException('No wallet on profile'); - const { data: agreement, error: aErr } = await this.supabase - .getClient() - .from('agreements') - .select('id, created_by') - .eq('id', agreementId) - .maybeSingle(); - if (aErr || !agreement) throw new NotFoundException('Agreement not found'); + // Get agreement via backend client to check access + const result = await this.backendClient.getAgreement(agreementId, wallet); + if (!result.success || !result.data?.agreement) { + throw new NotFoundException('Agreement not found'); + } - const createdBy = (agreement as { created_by: string }).created_by; + const agreement = result.data.agreement; + const createdBy = agreement.created_by; if (createdBy === wallet || createdBy === userId) return; - const { data: parts } = await this.supabase - .getClient() - .from('agreement_participants') - .select('wallet_address') - .eq('agreement_id', agreementId) - .eq('wallet_address', wallet) - .limit(1); - if (!parts?.length) { + // Check if wallet is a participant + const participants = result.data.participants ?? []; + const isParticipant = participants.some((p) => p.wallet_address === wallet); + if (!isParticipant) { throw new ForbiddenException('Not a participant of this agreement'); } } diff --git a/src/integration/migrated-flows.integration.spec.ts b/src/integration/migrated-flows.integration.spec.ts index b6e71b1..fab84f4 100644 --- a/src/integration/migrated-flows.integration.spec.ts +++ b/src/integration/migrated-flows.integration.spec.ts @@ -11,6 +11,7 @@ import { join } from 'path'; import { AuthModule } from '../auth/auth.module'; import { SupabaseService } from '../supabase/supabase.service'; import { ApiClient } from '../common/api/api-client'; +import { AgreementsBackendClient } from '../agreements/agreements-backend.client'; import { AgreementsController } from '../agreements/agreements.controller'; import { AgreementActivityService } from '../agreements/agreement-activity.service'; import { AgreementsService } from '../agreements/agreements.service'; @@ -330,6 +331,17 @@ describe('migrated backend flows (integration)', () => { const apiClient = { get: jest.fn(), }; + const backendClient = { + getAgreement: jest.fn(), + createAgreement: jest.fn(), + linkContract: jest.fn(), + updateAgreementStatus: jest.fn(), + updateMilestone: jest.fn(), + listAgreementsByWallet: jest.fn(), + getAgreementByContractId: jest.fn(), + getAgreementActivity: jest.fn(), + logActivity: jest.fn(), + }; beforeAll(async () => { process.env.JWT_SECRET = JWT_SECRET; @@ -349,6 +361,7 @@ describe('migrated backend flows (integration)', () => { { provide: ApiClient, useValue: apiClient }, { provide: ConfigService, useValue: { get: jest.fn(() => JWT_SECRET) } }, { provide: EventEmitter2, useValue: { emit: jest.fn() } }, + { provide: AgreementsBackendClient, useValue: backendClient }, ], }).compile(); @@ -382,6 +395,41 @@ describe('migrated backend flows (integration)', () => { ], }, }); + + // Reset backend client mocks + backendClient.getAgreement.mockReset(); + backendClient.createAgreement.mockReset(); + backendClient.linkContract.mockReset(); + backendClient.updateAgreementStatus.mockReset(); + backendClient.updateMilestone.mockReset(); + backendClient.listAgreementsByWallet.mockReset(); + backendClient.getAgreementByContractId.mockReset(); + backendClient.getAgreementActivity.mockReset(); + backendClient.logActivity.mockReset(); + + // Set default return values - all methods should pass through + backendClient.getAgreement.mockResolvedValue({ + success: true, + data: { agreement: null, participants: [] }, + }); + backendClient.createAgreement.mockResolvedValue({ success: true, data: { agreement: {} } }); + backendClient.linkContract.mockResolvedValue({ success: true }); + backendClient.updateAgreementStatus.mockResolvedValue({ success: true }); + backendClient.updateMilestone.mockResolvedValue({ success: true }); + backendClient.listAgreementsByWallet.mockResolvedValue({ + success: true, + data: { agreements: [] }, + }); + backendClient.getAgreementByContractId.mockResolvedValue({ + success: true, + data: { agreement: null }, + }); + backendClient.getAgreementActivity.mockResolvedValue({ + success: true, + data: { activities: [] }, + }); + backendClient.logActivity.mockResolvedValue({ success: true }); + jest.spyOn(global, 'fetch').mockResolvedValue( new Response(JSON.stringify([{ contractId: CONTRACT_ID, status: 'active' }]), { status: 200, @@ -395,7 +443,9 @@ describe('migrated backend flows (integration)', () => { }); afterAll(async () => { - await app.close(); + if (app) { + await app.close(); + } }); // Sign HS256 tokens exactly as the frontend does; AuthModule only verifies them.