Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 132 additions & 0 deletions src/escrow/escrow.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { Test, TestingModule } from '@nestjs/testing';
import { EscrowService } from './escrow.service';
import { PrismaService } from '../prisma/prisma.service';
import { EventsService } from '../events/events.service';
import { NotFoundException } from '@nestjs/common';
import { EscrowStatus, AdoptionStatus, EventType, EventEntityType } from '@prisma/client';

describe('EscrowService', () => {
let service: EscrowService;
let prismaService: PrismaService;
let eventsService: EventsService;

const mockPrismaService = {
escrow: {
create: jest.fn(),
findUnique: jest.fn(),
update: jest.fn(),
},
adoption: {
update: jest.fn(),
},
pet: {
update: jest.fn(),
},
$transaction: jest.fn((callback) => callback(mockPrismaService)),
};

const mockEventsService = {
logEvent: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
EscrowService,
{ provide: PrismaService, useValue: mockPrismaService },
{ provide: EventsService, useValue: mockEventsService },
],
}).compile();

service = module.get<EscrowService>(EscrowService);
prismaService = module.get<PrismaService>(PrismaService);
eventsService = module.get<EventsService>(EventsService);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(service).toBeDefined();
});

describe('releaseEscrow', () => {
const escrowId = 'escrow-123';
const txHash = 'tx-hash-abc';

it('should throw NotFoundException if escrow does not exist', async () => {
mockPrismaService.escrow.findUnique.mockResolvedValueOnce(null);

await expect(service.releaseEscrow(escrowId, txHash)).rejects.toThrow(NotFoundException);
});

it('should release escrow and not update adoption if no adoption is attached', async () => {
const mockEscrow = {
id: escrowId,
amount: 100,
status: EscrowStatus.CREATED,
};

mockPrismaService.escrow.findUnique.mockResolvedValueOnce(mockEscrow);
mockPrismaService.escrow.update.mockResolvedValueOnce({ ...mockEscrow, status: EscrowStatus.RELEASED });

const result = await service.releaseEscrow(escrowId, txHash);

expect(mockPrismaService.escrow.update).toHaveBeenCalledWith({
where: { id: escrowId },
data: { status: EscrowStatus.RELEASED, releaseTxHash: txHash },
});

expect(mockEventsService.logEvent).toHaveBeenCalledWith({
entityType: EventEntityType.ESCROW,
entityId: escrowId,
eventType: EventType.ESCROW_RELEASED,
txHash,
payload: { amount: 100 },
});

expect(mockPrismaService.adoption.update).not.toHaveBeenCalled();
expect(result.status).toBe(EscrowStatus.RELEASED);
});

it('should release escrow and update adoption and pet if adoption is attached', async () => {
const mockAdoption = {
id: 'adoption-123',
petId: 'pet-123',
adopterId: 'adopter-123',
};

const mockEscrow = {
id: escrowId,
amount: 100,
status: EscrowStatus.CREATED,
adoption: mockAdoption,
};

mockPrismaService.escrow.findUnique.mockResolvedValueOnce(mockEscrow);
mockPrismaService.escrow.update.mockResolvedValueOnce({ ...mockEscrow, status: EscrowStatus.RELEASED });

const result = await service.releaseEscrow(escrowId, txHash);

expect(mockPrismaService.adoption.update).toHaveBeenCalledWith({
where: { id: mockAdoption.id },
data: { status: AdoptionStatus.COMPLETED },
});

expect(mockPrismaService.pet.update).toHaveBeenCalledWith({
where: { id: mockAdoption.petId },
data: { currentOwnerId: mockAdoption.adopterId },
});

expect(mockEventsService.logEvent).toHaveBeenCalledWith({
entityType: EventEntityType.ADOPTION,
entityId: mockAdoption.id,
eventType: EventType.ADOPTION_COMPLETED,
payload: { escrowId, petId: mockAdoption.petId },
});

expect(result.status).toBe(EscrowStatus.RELEASED);
});
});
});
65 changes: 63 additions & 2 deletions src/escrow/escrow.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { Injectable } from '@nestjs/common';
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { EventsService } from '../events/events.service';
import { EscrowStatus, AdoptionStatus, EventEntityType, EventType } from '@prisma/client';

@Injectable()
export class EscrowService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly events: EventsService,
) { }

async createEscrow(amount: number, tx?: any) {
const prismaClient = tx || this.prisma;
Expand All @@ -22,4 +27,60 @@ export class EscrowService {
},
});
}

async releaseEscrow(escrowId: string, txHash?: string) {
return this.prisma.$transaction(async (tx) => {
const escrow = await tx.escrow.findUnique({
where: { id: escrowId },
include: { adoption: true },
});

if (!escrow) {
throw new NotFoundException('Escrow not found');
}

// 1. Update Escrow Status
const updatedEscrow = await tx.escrow.update({
where: { id: escrowId },
data: {
status: EscrowStatus.RELEASED,
releaseTxHash: txHash,
},
});

await this.events.logEvent({
entityType: EventEntityType.ESCROW,
entityId: escrowId,
eventType: EventType.ESCROW_RELEASED,
txHash,
payload: { amount: Number(escrow.amount) },
});

// 2. If escrow is tied to an Adoption, update Adoption and Pet
if (escrow.adoption) {
const adoption = escrow.adoption;

await tx.adoption.update({
where: { id: adoption.id },
data: { status: AdoptionStatus.COMPLETED },
});

await tx.pet.update({
where: { id: adoption.petId },
data: {
currentOwnerId: adoption.adopterId,
},
});

await this.events.logEvent({
entityType: EventEntityType.ADOPTION,
entityId: adoption.id,
eventType: EventType.ADOPTION_COMPLETED,
payload: { escrowId, petId: adoption.petId },
});
}

return updatedEscrow;
});
}
}