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
6 changes: 6 additions & 0 deletions src/common/exceptions/escrow.exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,9 @@ export class BuyerWalletNotFoundException extends NotFoundException {
super(`Buyer with wallet address "${walletAddress}" not found`);
}
}

export class WalletUserNotFoundException extends NotFoundException {
constructor(walletAddress: string) {
super(`No user found with wallet address "${walletAddress}"`);
}
}
7 changes: 6 additions & 1 deletion src/modules/escrow-payments/__tests__/create.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,12 @@ describe("EscrowPaymentsCrudService › create", () => {
});

it("save() 호출", async () => {
const instance = { save: jest.fn().mockResolvedValue({}) };
const payment = { buyerId: BUYER_ID, sellerId: SELLER_ID };
const instance = {
save: jest.fn().mockReturnThis(),
toObject: jest.fn().mockReturnValue(payment),
};
instance.save.mockResolvedValue(instance);
ctx.escrowPaymentModel.mockReturnValue(instance);

await ctx.service.create(makeDto(), BUYER_ID.toString());
Expand Down
2 changes: 2 additions & 0 deletions src/modules/escrow-payments/__tests__/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function makePayment(overrides: object = {}) {
...overrides,
};
doc.save = jest.fn().mockResolvedValue(doc);
doc.toObject = jest.fn().mockReturnValue(doc);
return doc;
}

Expand Down Expand Up @@ -129,6 +130,7 @@ export function makeEscrowPaymentModelMock() {
const ModelMock: any = jest.fn().mockImplementation((data: any) => {
const instance = { ...data };
instance.save = jest.fn().mockResolvedValue(instance);
instance.toObject = jest.fn().mockReturnValue(instance);
return instance;
});
ModelMock.findById = jest.fn().mockReturnValue(makeQueryChain(null));
Expand Down
166 changes: 166 additions & 0 deletions src/modules/escrow-payments/dto/escrow-payment-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Types } from "mongoose";

export class EventApprovalResponse {
@ApiProperty({ description: "이벤트 이름" })
eventType: string;

@ApiProperty({ description: "구매자 승인 여부" })
buyerApproved: boolean;

@ApiPropertyOptional({ description: "구매자 승인 일시" })
buyerApprovedAt?: Date;

@ApiProperty({ description: "판매자 승인 여부" })
sellerApproved: boolean;

@ApiPropertyOptional({ description: "판매자 승인 일시" })
sellerApprovedAt?: Date;

@ApiPropertyOptional({ description: "이벤트 완료 일시" })
completedAt?: Date;
}

export class EscrowItemResponse {
@ApiProperty({ type: String, description: "에스크로 항목 ID" })
_id: Types.ObjectId;

@ApiProperty({ description: "에스크로 항목 라벨" })
label: string;

@ApiProperty({ description: "XRP 금액" })
amountXrp: number;

@ApiProperty({ description: "항목 순서" })
order: number;

@ApiProperty({
description: "항목 상태",
enum: [
"PENDING_ESCROW",
"SUBMITTING",
"ESCROWED",
"RELEASING",
"RELEASED",
"CANCELLING",
"CANCELLED",
],
})
status: string;

@ApiProperty({ type: [String], description: "필요 이벤트 목록" })
requiredEventTypes: string[];

@ApiProperty({
type: [EventApprovalResponse],
description: "이벤트 승인 현황",
})
approvals: EventApprovalResponse[];

@ApiPropertyOptional({ description: "XRPL 시퀀스 번호" })
xrplSequence?: number;

@ApiPropertyOptional({ description: "XRPL 에스크로 조건" })
condition?: string;

@ApiPropertyOptional({ description: "에스크로 생성 트랜잭션 해시" })
txHashCreate?: string;

@ApiPropertyOptional({ description: "에스크로 해제 트랜잭션 해시" })
txHashRelease?: string;

@ApiPropertyOptional({ description: "XRPL 제출 일시" })
submittingAt?: Date;

@ApiPropertyOptional({ description: "에스크로 확정 일시" })
escrowedAt?: Date;

@ApiPropertyOptional({ description: "에스크로 해제 일시" })
releasedAt?: Date;
}

export class EscrowPaymentResponse {
@ApiProperty({ type: String, description: "에스크로 결제 ID" })
_id: Types.ObjectId;

@ApiProperty({ type: String, description: "내 유저 ID" })
myId: Types.ObjectId;

@ApiProperty({ type: String, description: "상대방 유저 ID" })
partnerId: Types.ObjectId;

@ApiProperty({ description: "내 이름" })
myName: string;

@ApiProperty({ description: "상대방 이름" })
partnerName: string;

@ApiProperty({ description: "내 지갑 주소" })
myWalletAddress: string;

@ApiProperty({ description: "상대방 지갑 주소" })
partnerWalletAddress: string;

@ApiProperty({ description: "총 XRP 금액" })
totalAmountXrp: number;

@ApiProperty({ description: "통화 (XRP/RLUSD)", enum: ["XRP", "RLUSD"] })
currency: string;

@ApiProperty({
description: "결제 상태",
enum: [
"PENDING_APPROVAL",
"APPROVED",
"PROCESSING",
"ACTIVE",
"COMPLETED",
"CANCELLED",
],
})
status: string;

@ApiProperty({ description: "구매자 승인 여부" })
buyerApproved: boolean;

@ApiPropertyOptional({ description: "구매자 승인 일시" })
buyerApprovedAt?: Date;

@ApiProperty({ description: "판매자 승인 여부" })
sellerApproved: boolean;

@ApiPropertyOptional({ description: "판매자 승인 일시" })
sellerApprovedAt?: Date;

@ApiProperty({ description: "메모" })
memo: string;

@ApiProperty({
type: [EscrowItemResponse],
description: "세부 에스크로 항목",
})
escrows: EscrowItemResponse[];

@ApiProperty({ description: "생성 일시" })
createdAt: Date;

@ApiProperty({ description: "수정 일시" })
updatedAt: Date;
}

export class EscrowPaymentListResponse {
@ApiProperty({
type: [EscrowPaymentResponse],
description: "결제 목록 데이터",
})
data: EscrowPaymentResponse[];

@ApiProperty({ description: "전체 항목 수" })
total: number;

@ApiProperty({ description: "현재 페이지 번호" })
page: number;

@ApiProperty({ description: "페이지당 항목 수" })
limit: number;
}
118 changes: 38 additions & 80 deletions src/modules/escrow-payments/escrow-payments-crud.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
EscrowPaymentNotFoundException,
SellerWalletNotFoundException,
UnauthorizedPaymentActionException,
WalletUserNotFoundException,
} from "../../common/exceptions";
import {
EscrowPayment,
Expand All @@ -15,25 +16,10 @@ import { User, UserDocument } from "../users/schemas/user.schema";
import { CreateEscrowPaymentDto } from "./dto/create-escrow-payment.dto";
import { QueryEscrowPaymentDto } from "./dto/query-escrow-payment.dto";

export interface EscrowPaymentResponse extends Omit<
EscrowPayment,
| "buyerId"
| "sellerId"
| "buyerName"
| "sellerName"
| "buyerWalletAddress"
| "sellerWalletAddress"
> {
_id: Types.ObjectId;
myId: Types.ObjectId;
partnerId: Types.ObjectId;
myName: string;
myWalletAddress: string;
partnerName: string;
partnerWalletAddress: string;
createdAt: Date;
updatedAt: Date;
}
import {
EscrowPaymentListResponse,
EscrowPaymentResponse,
} from "./dto/escrow-payment-response.dto";

@Injectable()
export class EscrowPaymentsCrudService {
Expand All @@ -50,7 +36,7 @@ export class EscrowPaymentsCrudService {
async create(
dto: CreateEscrowPaymentDto,
userId: string,
): Promise<EscrowPaymentDocument> {
): Promise<EscrowPaymentResponse> {
const creator = await this.userModel
.findById(userId, { type: 1, name: 1, wallet: 1 })
.lean();
Expand Down Expand Up @@ -109,7 +95,7 @@ export class EscrowPaymentsCrudService {
}

if (!buyerWalletAddress || !sellerWalletAddress) {
throw new UnauthorizedPaymentActionException(); // Or a more specific exception if wallet is missing
throw new UnauthorizedPaymentActionException();
}

const totalAmountXrp = dto.escrows.reduce((sum, e) => sum + e.amountXrp, 0);
Expand Down Expand Up @@ -142,7 +128,8 @@ export class EscrowPaymentsCrudService {
})),
});

return doc.save();
const saved = await doc.save();
return this.mapToResponse(saved.toObject(), userId);
}

/**
Expand All @@ -151,12 +138,7 @@ export class EscrowPaymentsCrudService {
async findAll(
userId: string,
dto: QueryEscrowPaymentDto,
): Promise<{
data: EscrowPaymentResponse[];
total: number;
page: number;
limit: number;
}> {
): Promise<EscrowPaymentListResponse> {
const { status, group, page = 1, limit = 5 } = dto;
const uid = new Types.ObjectId(userId);

Expand Down Expand Up @@ -185,37 +167,9 @@ export class EscrowPaymentsCrudService {
this.escrowPaymentModel.countDocuments(filter),
]);

const mappedData = data.map((item: any) => {
const isBuyer = item.buyerId.toString() === userId;
const myId = isBuyer ? item.buyerId : item.sellerId;
const partnerId = isBuyer ? item.sellerId : item.buyerId;
const myName = isBuyer ? item.buyerName : item.sellerName;
const partnerName = isBuyer ? item.sellerName : item.buyerName;
const myWalletAddress = isBuyer
? item.buyerWalletAddress
: item.sellerWalletAddress;
const partnerWalletAddress = isBuyer
? item.sellerWalletAddress
: item.buyerWalletAddress;

const rest = { ...item };
delete rest.buyerId;
delete rest.sellerId;
delete rest.buyerName;
delete rest.sellerName;
delete rest.buyerWalletAddress;
delete rest.sellerWalletAddress;

return {
...rest,
myId,
partnerId,
myName,
myWalletAddress,
partnerName,
partnerWalletAddress,
};
});
const mappedData = data.map((item: any) =>
this.mapToResponse(item, userId),
);

return {
data: mappedData,
Expand All @@ -235,19 +189,34 @@ export class EscrowPaymentsCrudService {
throw new UnauthorizedPaymentActionException();
}

const isBuyer = doc.buyerId.toString() === userId;
const myId = isBuyer ? doc.buyerId : doc.sellerId;
const partnerId = isBuyer ? doc.sellerId : doc.buyerId;
const myName = isBuyer ? doc.buyerName : doc.sellerName;
const partnerName = isBuyer ? doc.sellerName : doc.buyerName;
return this.mapToResponse(doc, userId);
}

/**
* XRPL 지갑 주소로 사용자 조회
*/
async findUserByWalletAddress(walletAddress: string): Promise<UserDocument> {
const user = await this.userModel
.findOne({ "wallet.address": walletAddress })
.lean();
if (!user) throw new WalletUserNotFoundException(walletAddress);
return user as UserDocument;
}

private mapToResponse(item: any, userId: string): EscrowPaymentResponse {
const isBuyer = item.buyerId.toString() === userId;
const myId = isBuyer ? item.buyerId : item.sellerId;
const partnerId = isBuyer ? item.sellerId : item.buyerId;
const myName = isBuyer ? item.buyerName : item.sellerName;
const partnerName = isBuyer ? item.sellerName : item.buyerName;
const myWalletAddress = isBuyer
? doc.buyerWalletAddress
: doc.sellerWalletAddress;
? item.buyerWalletAddress
: item.sellerWalletAddress;
const partnerWalletAddress = isBuyer
? doc.sellerWalletAddress
: doc.buyerWalletAddress;
? item.sellerWalletAddress
: item.buyerWalletAddress;

const rest = { ...(doc as any) };
const rest = { ...item };
delete rest.buyerId;
delete rest.sellerId;
delete rest.buyerName;
Expand All @@ -265,15 +234,4 @@ export class EscrowPaymentsCrudService {
partnerWalletAddress,
};
}

/**
* XRPL 지갑 주소로 사용자 조회
*/
async findUserByWalletAddress(walletAddress: string): Promise<UserDocument> {
const user = await this.userModel
.findOne({ "wallet.address": walletAddress })
.lean();
if (!user) throw new SellerWalletNotFoundException(walletAddress);
return user;
}
}
Loading
Loading