From 26834b3358a5316fcf7bfb75d6974ec2e2423ccf Mon Sep 17 00:00:00 2001 From: Melvin Date: Thu, 26 Mar 2026 18:53:10 +0100 Subject: [PATCH 1/2] Implement custody return and violation lifecycle --- package-lock.json | 52 ++++++------------ prisma/schema.prisma | 2 + src/custody/custody.controller.ts | 53 ++++++++++++++++++ src/custody/custody.module.ts | 3 +- src/custody/custody.service.ts | 90 +++++++++++++++++++++++++++++++ src/escrow/escrow.service.ts | 30 +++++++++++ src/users/users.module.ts | 4 +- src/users/users.service.ts | 41 +++++++++++++- 8 files changed, 236 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 405c801..66958b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -244,7 +244,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -824,8 +823,7 @@ "version": "0.3.15", "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", - "license": "Apache-2.0", - "peer": true + "license": "Apache-2.0" }, "node_modules/@electric-sql/pglite-socket": { "version": "0.0.20", @@ -2832,7 +2830,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3003,7 +3000,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-11.1.14.tgz", "integrity": "sha512-IN/tlqd7Nl9gl6f0jsWEuOrQDaCI9vHzxv0fisHysfBQzfQIkqlv5A7w4Qge02BUQyczXT9HHPgHtWHCxhjRng==", "license": "MIT", - "peer": true, "dependencies": { "file-type": "21.3.0", "iterare": "1.2.1", @@ -3063,7 +3059,6 @@ "integrity": "sha512-7OXPPMoDr6z+5NkoQKu4hOhfjz/YYqM3bNilPqv1WVFWrzSmuNXxvhbX69YMmNmRYascPXiwESqf5jJdjKXEww==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxt/opencollective": "0.4.1", "fast-safe-stringify": "2.1.1", @@ -3147,7 +3142,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.14.tgz", "integrity": "sha512-Fs+/j+mBSBSXErOQJ/YdUn/HqJGSJ4pGfiJyYOyz04l42uNVnqEakvu1kXLbxMabR6vd6/h9d6Bi4tso9p7o4Q==", "license": "MIT", - "peer": true, "dependencies": { "cors": "2.8.6", "express": "5.2.1", @@ -3472,7 +3466,6 @@ "resolved": "https://registry.npmjs.org/@prisma/client/-/client-7.4.1.tgz", "integrity": "sha512-pgIll2W1NVdof37xLeyySW+yfQ4rI+ERGCRwnO3BjVOx42GpYq6jhTyuALK8VKirvJJIvImgfGDA2qwhYVvMuA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/client-runtime-utils": "7.4.1" }, @@ -3829,7 +3822,6 @@ "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -3978,7 +3970,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -4147,7 +4138,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -4855,7 +4845,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4905,7 +4894,6 @@ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5395,7 +5383,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5475,7 +5462,6 @@ "resolved": "https://registry.npmjs.org/bull/-/bull-4.16.5.tgz", "integrity": "sha512-lDsx2BzkKe7gkCYiT5Acj02DpTwDznl/VNN7Psn7M3USPG7Vs/BaClZJJTAG+ufAR9++N1/NiUTdaFBWDIl5TQ==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "^4.9.0", "get-port": "^5.1.1", @@ -5503,7 +5489,6 @@ "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.70.1.tgz", "integrity": "sha512-HjfGHfICkAClrFL0Y07qNbWcmiOCv1l+nusupXUjrvTPuDEyPEJ23MP0lUwUs/QEy1a3pWt/P/sCsSZ1RjRK+w==", "license": "MIT", - "peer": true, "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.9.3", @@ -5757,15 +5742,13 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.5.1.tgz", "integrity": "sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/class-validator": { "version": "0.14.3", "resolved": "https://registry.npmjs.org/class-validator/-/class-validator-0.14.3.tgz", "integrity": "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA==", "license": "MIT", - "peer": true, "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", @@ -6186,7 +6169,8 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.3", @@ -6611,7 +6595,6 @@ "integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -6672,7 +6655,6 @@ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -6905,7 +6887,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -7686,7 +7667,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -8093,7 +8073,6 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -9100,8 +9079,7 @@ "version": "4.17.23", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/lodash.defaults": { "version": "4.2.0", @@ -9936,7 +9914,6 @@ "resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz", "integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==", "license": "MIT", - "peer": true, "dependencies": { "passport-strategy": "1.x.x", "pause": "0.0.1", @@ -10066,7 +10043,6 @@ "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", "license": "MIT", - "peer": true, "dependencies": { "pg-connection-string": "^2.11.0", "pg-pool": "^3.11.0", @@ -10348,7 +10324,6 @@ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -10406,7 +10381,6 @@ "integrity": "sha512-gDKOXwnPiMdB+uYMhMeN8jj4K7Cu3Q2wB/wUsITOoOk446HtVb8T9BZxFJ1Zop6alc89k6PMNdR2FZCpbXp/jw==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "7.4.1", "@prisma/dev": "0.20.0", @@ -10808,7 +10782,8 @@ "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/schema-utils": { "version": "3.3.0", @@ -11430,7 +11405,6 @@ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -11767,7 +11741,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -11934,7 +11907,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12316,6 +12288,7 @@ "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -12334,6 +12307,7 @@ "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3" }, @@ -12347,6 +12321,7 @@ "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -12361,6 +12336,7 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true, "license": "BSD-2-Clause", + "peer": true, "engines": { "node": ">=4.0" } @@ -12370,7 +12346,8 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/webpack/node_modules/mime-db": { "version": "1.52.0", @@ -12378,6 +12355,7 @@ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6" } @@ -12388,6 +12366,7 @@ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mime-db": "1.52.0" }, @@ -12401,6 +12380,7 @@ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 422860f..cbb1625 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -79,9 +79,11 @@ enum EventType { ADOPTION_COMPLETED CUSTODY_STARTED CUSTODY_RETURNED + CUSTODY_VIOLATION ESCROW_CREATED ESCROW_FUNDED ESCROW_RELEASED + ESCROW_REFUNDED TRUST_SCORE_UPDATED } diff --git a/src/custody/custody.controller.ts b/src/custody/custody.controller.ts index b350a6a..22159ab 100644 --- a/src/custody/custody.controller.ts +++ b/src/custody/custody.controller.ts @@ -5,6 +5,7 @@ import { UseGuards, HttpCode, HttpStatus, + Param, } from '@nestjs/common'; import { ApiTags, @@ -79,4 +80,56 @@ export class CustodyController { ): Promise { return this.custodyService.createCustody(user.userId, createCustodyDto); } + + @Post(':id/return') + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Return custody agreement', + description: + 'End custody agreement successfully. Releases escrow and updates trust score positively', + }) + @ApiResponse({ + status: 200, + description: 'Custody successfully returned', + type: CustodyResponseDto, + }) + @ApiResponse({ + status: 400, + description: 'Bad Request - Custody not active', + }) + @ApiResponse({ + status: 404, + description: 'Not Found - Custody does not exist', + }) + async returnCustody(@Param('id') custodyId: string): Promise { + return this.custodyService.returnCustody(custodyId); + } + + @Post(':id/violation') + @UseGuards(JwtAuthGuard) + @HttpCode(HttpStatus.OK) + @ApiBearerAuth() + @ApiOperation({ + summary: 'Mark custody as violation', + description: + 'Report custody violation. Refunds escrow and penalizes trust score', + }) + @ApiResponse({ + status: 200, + description: 'Custody marked as violation', + type: CustodyResponseDto, + }) + @ApiResponse({ + status: 400, + description: 'Bad Request - Custody not active', + }) + @ApiResponse({ + status: 404, + description: 'Not Found - Custody does not exist', + }) + async violationCustody(@Param('id') custodyId: string): Promise { + return this.custodyService.violationCustody(custodyId); + } } diff --git a/src/custody/custody.module.ts b/src/custody/custody.module.ts index 2e40225..bf91634 100644 --- a/src/custody/custody.module.ts +++ b/src/custody/custody.module.ts @@ -4,9 +4,10 @@ import { CustodyController } from './custody.controller'; import { PrismaModule } from '../prisma/prisma.module'; import { EventsModule } from '../events/events.module'; import { EscrowModule } from '../escrow/escrow.module'; +import { UsersModule } from '../users/users.module'; @Module({ - imports: [PrismaModule, EventsModule, EscrowModule], + imports: [PrismaModule, EventsModule, EscrowModule, UsersModule], controllers: [CustodyController], providers: [CustodyService], exports: [CustodyService], diff --git a/src/custody/custody.service.ts b/src/custody/custody.service.ts index a798d26..58dc8e0 100644 --- a/src/custody/custody.service.ts +++ b/src/custody/custody.service.ts @@ -7,6 +7,7 @@ import { import { PrismaService } from '../prisma/prisma.service'; import { EventsService } from '../events/events.service'; import { EscrowService } from '../escrow/escrow.service'; +import { UsersService } from '../users/users.service'; import { CreateCustodyDto } from './dto/create-custody.dto'; import { CustodyResponseDto } from './dto/custody-response.dto'; import { CustodyStatus } from '@prisma/client'; @@ -18,6 +19,7 @@ export class CustodyService { private readonly prisma: PrismaService, private readonly eventsService: EventsService, private readonly escrowService: EscrowService, + private readonly usersService: UsersService, @Optional() private readonly notificationQueueService?: NotificationQueueService, ) {} @@ -180,4 +182,92 @@ export class CustodyService { return custody as CustodyResponseDto; } + + async returnCustody(custodyId: string): Promise { + return this.prisma.$transaction(async (tx) => { + const custody = await tx.custody.findUnique({ + where: { id: custodyId }, + include: { holder: true, pet: true }, + }); + + if (!custody) { + throw new NotFoundException(`Custody with id ${custodyId} not found`); + } + + if (custody.status !== CustodyStatus.ACTIVE) { + throw new BadRequestException( + `Custody must be ACTIVE to return (current status: ${custody.status})`, + ); + } + + const updatedCustody = await tx.custody.update({ + where: { id: custodyId }, + data: { status: CustodyStatus.RETURNED }, + include: { holder: true, pet: true }, + }); + + await this.eventsService.logEvent({ + entityType: 'CUSTODY', + entityId: custodyId, + eventType: 'CUSTODY_RETURNED', + actorId: custody.holderId, + payload: { + petId: custody.petId, + holderId: custody.holderId, + }, + }); + + if (custody.escrowId) { + await this.escrowService.releaseEscrow(custody.escrowId); + } + + await this.usersService.updateTrustScore(custody.holderId, 5); + + return updatedCustody as CustodyResponseDto; + }); + } + + async violationCustody(custodyId: string): Promise { + return this.prisma.$transaction(async (tx) => { + const custody = await tx.custody.findUnique({ + where: { id: custodyId }, + include: { holder: true, pet: true }, + }); + + if (!custody) { + throw new NotFoundException(`Custody with id ${custodyId} not found`); + } + + if (custody.status !== CustodyStatus.ACTIVE) { + throw new BadRequestException( + `Custody must be ACTIVE to mark as violation (current status: ${custody.status})`, + ); + } + + const updatedCustody = await tx.custody.update({ + where: { id: custodyId }, + data: { status: CustodyStatus.VIOLATION }, + include: { holder: true, pet: true }, + }); + + await this.eventsService.logEvent({ + entityType: 'CUSTODY', + entityId: custodyId, + eventType: 'CUSTODY_VIOLATION', + actorId: custody.holderId, + payload: { + petId: custody.petId, + holderId: custody.holderId, + }, + }); + + if (custody.escrowId) { + await this.escrowService.refundEscrow(custody.escrowId); + } + + await this.usersService.updateTrustScore(custody.holderId, -15); + + return updatedCustody as CustodyResponseDto; + }); + } } diff --git a/src/escrow/escrow.service.ts b/src/escrow/escrow.service.ts index 80293e3..522f641 100644 --- a/src/escrow/escrow.service.ts +++ b/src/escrow/escrow.service.ts @@ -83,4 +83,34 @@ export class EscrowService { return updatedEscrow; }); } + + async refundEscrow(escrowId: string, txHash?: string) { + return this.prisma.$transaction(async (tx) => { + const escrow = await tx.escrow.findUnique({ + where: { id: escrowId }, + }); + + if (!escrow) { + throw new NotFoundException('Escrow not found'); + } + + const updatedEscrow = await tx.escrow.update({ + where: { id: escrowId }, + data: { + status: EscrowStatus.REFUNDED, + refundTxHash: txHash, + }, + }); + + await this.events.logEvent({ + entityType: EventEntityType.ESCROW, + entityId: escrowId, + eventType: EventType.ESCROW_REFUNDED, + txHash, + payload: { amount: Number(escrow.amount) }, + }); + + return updatedEscrow; + }); + } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 5bd7a9e..72b335b 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -3,10 +3,12 @@ import { UsersController } from './users.controller'; import { UsersService } from './users.service'; import { PrismaModule } from '../prisma/prisma.module'; import { CloudinaryModule } from '../cloudinary/cloudinary.module'; +import { EventsModule } from '../events/events.module'; @Module({ - imports: [PrismaModule, CloudinaryModule], + imports: [PrismaModule, CloudinaryModule, EventsModule], controllers: [UsersController], providers: [UsersService], + exports: [UsersService], }) export class UsersModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 1854611..f00b9e0 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,10 +1,14 @@ import { Injectable, NotFoundException } from '@nestjs/common'; import { PrismaService } from '../prisma/prisma.service'; +import { EventsService } from '../events/events.service'; import { UpdateUserDto } from './dto/update-user.dto'; @Injectable() export class UsersService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly eventsService: EventsService, + ) {} async getProfile(id: string) { const user = await this.prisma.user.findUnique({ @@ -59,4 +63,39 @@ export class UsersService { }, }); } + + async updateTrustScore(userId: string, delta: number) { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + select: { id: true, trustScore: true }, + }); + + if (!user) { + throw new NotFoundException('User not found'); + } + + const newScore = Math.max(0, Math.min(100, user.trustScore + delta)); + + const updatedUser = await this.prisma.user.update({ + where: { id: userId }, + data: { trustScore: newScore }, + select: { + id: true, + trustScore: true, + }, + }); + + await this.eventsService.logEvent({ + entityType: 'USER', + entityId: userId, + eventType: 'TRUST_SCORE_UPDATED', + payload: { + previousScore: user.trustScore, + newScore, + delta, + }, + }); + + return updatedUser; + } } From 7ce3ffd9d64ac9c59864480bb6d1f901b037a8f2 Mon Sep 17 00:00:00 2001 From: Melvin Date: Sat, 28 Mar 2026 15:31:16 +0100 Subject: [PATCH 2/2] fix: add missing service mocks to CustodyService tests --- src/custody/custody.service.spec.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/custody/custody.service.spec.ts b/src/custody/custody.service.spec.ts index 4567ff0..c095797 100644 --- a/src/custody/custody.service.spec.ts +++ b/src/custody/custody.service.spec.ts @@ -4,6 +4,8 @@ import { CustodyService } from './custody.service'; import { PrismaService } from '../prisma/prisma.service'; import { EventsService } from '../events/events.service'; import { EscrowService } from '../escrow/escrow.service'; +import { UsersService } from '../users/users.service'; +import { NotificationQueueService } from '../jobs/services/notification-queue.service'; import { CreateCustodyDto } from './dto/create-custody.dto'; describe('CustodyService', () => { @@ -34,6 +36,14 @@ describe('CustodyService', () => { createEscrow: jest.fn(), }; + const mockUsersService = { + updateTrustScore: jest.fn(), + }; + + const mockNotificationQueueService = { + addJob: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -50,6 +60,14 @@ describe('CustodyService', () => { provide: EscrowService, useValue: mockEscrowService, }, + { + provide: UsersService, + useValue: mockUsersService, + }, + { + provide: NotificationQueueService, + useValue: mockNotificationQueueService, + }, ], }).compile();