Skip to content

Commit

Permalink
Merge pull request #193 from Roger13579/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
Roger13579 authored Jun 10, 2024
2 parents 5713726 + 71264c9 commit 60c5b2c
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 14 deletions.
12 changes: 12 additions & 0 deletions src/controller/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { EditFavoriteDTO } from '../dto/user/editFavoriteDto';
import { IGetUserFavoriteReq, IUpdateUserDetailReq } from '../types/user.type';
import { GetUserFavoriteDTO } from '../dto/user/getUserFavoriteDto';
import { GetFavoriteVO } from '../vo/user/getFavoriteVo';
import { SellTicketDto } from '../dto/ticket/sellTicketDto';
import { ISellTicketReq } from '../types/ticket.type';

class UserController extends BaseController {
private readonly userService = new UserService();
Expand Down Expand Up @@ -65,6 +67,16 @@ class UserController extends BaseController {
{ favorites: info?.favorites || [] },
);
};

public sellTicket: TMethod = async (req: ISellTicketReq) => {
const editFavoriteDto = new SellTicketDto(req);
await this.userService.sellTicket(editFavoriteDto);
return this.formatResponse(
CustomResponseType.OK_MESSAGE,
CustomResponseType.OK,
{},
);
};
public getTransferableTicket = async (req: IUserReq) => {
const tickets = await this.userService.getTransferableTicket(
(req.user as IUser)._id,
Expand Down
34 changes: 34 additions & 0 deletions src/dto/ticket/sellTicketDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Types } from 'mongoose';
import { ISellTicketReq } from '../../types/ticket.type';
import { IUser } from '../../models/user';

export class SellTicketDto {
private readonly _userId: Types.ObjectId;
private readonly _orderId: Types.ObjectId;
private readonly _productId: Types.ObjectId;
private readonly _sellAmount: number;

get userId(): Types.ObjectId {
return this._userId;
}

get orderId(): Types.ObjectId {
return this._orderId;
}

get productId(): Types.ObjectId {
return this._productId;
}

get sellAmount(): number {
return this._sellAmount;
}

constructor(req: ISellTicketReq) {
const { orderId, productId, amount } = req.body;
this._userId = (req.user as IUser)._id as Types.ObjectId;
this._orderId = new Types.ObjectId(orderId);
this._productId = new Types.ObjectId(productId);
this._sellAmount = amount;
}
}
64 changes: 56 additions & 8 deletions src/repository/ticketRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import moment from 'moment';
import { createGetTicketPipeline } from '../utils/aggregate/ticket/getTickets.pipeline';
import { GetTicketDetailDto } from '../dto/ticket/getTicketDetailDto';
import { createGetTicketDetailPipeline } from '../utils/aggregate/ticket/getTicketDetail.pipeline';
import { SellTicketDto } from '../dto/ticket/sellTicketDto';

export class TicketRepository {
public async createTicket(createTicketDto: CreateTicketDto) {
Expand All @@ -33,6 +34,19 @@ export class TicketRepository {
return results[0];
};

public findTransferableTicketByOrderIdAndProductId = async (
sellTicketDto: SellTicketDto,
) => {
return TicketModel.find({
userId: sellTicketDto.userId,
orderId: sellTicketDto.orderId,
productId: sellTicketDto.productId,
isPublished: false,
status: TicketStatus.unverified,
expiredAt: { $gte: new Date() },
});
};

public findTransferableTicket = async (userId: string) => {
return TicketModel.find({
userId: userId,
Expand All @@ -50,7 +64,7 @@ export class TicketRepository {
public deleteTickets = async (tickets: ITicketId[]) => {
const session = await startSession();
try {
const result = await session.withTransaction(async () => {
return await session.withTransaction(async () => {
const promises = tickets.map(
async (id) =>
await TicketModel.findOneAndDelete(
Expand All @@ -66,8 +80,6 @@ export class TicketRepository {

return deletedTickets;
});

return result;
} catch (error) {
throwError(
(error as Error).message,
Expand All @@ -82,7 +94,7 @@ export class TicketRepository {
const session = await startSession();

try {
const result = await session.withTransaction(async () => {
return await session.withTransaction(async () => {
const promises = tickets.map(
async ({ filter, update }) =>
await TicketModel.findOneAndUpdate(filter, update, {
Expand All @@ -99,7 +111,6 @@ export class TicketRepository {

return updatedTickets;
});
return result;
} catch (error) {
throwError(
(error as Error).message,
Expand All @@ -114,7 +125,7 @@ export class TicketRepository {
const session = await startSession();

try {
const result = await session.withTransaction(async () => {
return await session.withTransaction(async () => {
const promises = tickets.map(
async ({ filter, update }) =>
await TicketModel.findOneAndUpdate(filter, update, {
Expand All @@ -131,7 +142,6 @@ export class TicketRepository {

return updatedTickets;
});
return result;
} catch (error) {
throwError(
(error as Error).message,
Expand All @@ -142,6 +152,44 @@ export class TicketRepository {
}
};

public updateSellTickets = async (tickets: ITicket[]) => {
const session = await startSession();
try {
return await session.withTransaction(async () => {
const promises = tickets.map(
async (ticket) =>
await TicketModel.findOneAndUpdate(
{ _id: ticket._id },
{ isPublished: true },
{
session,
...updateOptions,
},
),
);

const updatedTickets = await Promise.all(promises).then(
(values) => values,
);

const ticketIds: ITicketId[] = tickets.map(({ _id }) => ({
ticketId: _id,
}));

this.checkInvalidTicket(ticketIds, updatedTickets, TicketProcess.edit);

return updatedTickets;
});
} catch (error) {
throwError(
(error as Error).message,
CustomResponseType.INVALID_EDIT_TICKET,
);
} finally {
await session.endSession();
}
};

public updateShareCode = async ({
shareCode,
ticketId,
Expand All @@ -157,7 +205,7 @@ export class TicketRepository {
shareCode,
status: TicketStatus.transfer,
};
return await TicketModel.findOneAndUpdate(filter, update, updateOptions);
return TicketModel.findOneAndUpdate(filter, update, updateOptions);
};

public transferTicket = async (
Expand Down
43 changes: 43 additions & 0 deletions src/routes/userRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,49 @@ export class UserRoute extends BaseRoute {
UserVerify,
this.responseHandler(this.controller.deleteFavorite),
);

this.router.post(
'/v1/user/sell-ticket',
/**
* #swagger.tags = ['User']
* #swagger.summary = '上架分票'
* #swagger.security=[{"Bearer": []}]
*/
/*
#swagger.parameters['orderId'] ={
in:'path',
description:'訂單 ID',
required: true,
type: 'string'
}
*/
/*
#swagger.parameters['productId'] ={
in:'path',
description:'商品 ID',
required: true,
type: 'string'
}
*/
/*
#swagger.parameters['amount'] ={
in:'path',
description:'上架數量',
required: true,
type: 'number'
}
*/
/**
#swagger.responses[200]={
description:'OK',
schema:{
$ref:'#/definitions/Success'
}
}
*/
UserVerify,
this.responseHandler(this.controller.sellTicket),
);
this.router.get(
'/v1/user/share-tickets',
/**
Expand Down
43 changes: 38 additions & 5 deletions src/service/ticketService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,33 @@ export class TicketService {
return await this.ticketRepository.getTicketDetail(getTicketDetailDto);
};

public verifyTickets = async (verifyTicketsDto: VerifyTicketsDTO) =>
await this.ticketRepository.verifyTickets(verifyTicketsDto);
public verifyTickets = async (verifyTicketsDto: VerifyTicketsDTO) => {
const tickets = await this.ticketRepository.verifyTickets(verifyTicketsDto);

public editTickets = async (editTicketsDto: EditTicketsDTO) =>
await this.ticketRepository.editTickets(editTicketsDto);
if (!tickets) {
throwError(
CustomResponseType.INVALID_VERIFIED_TICKET_MESSAGE,
CustomResponseType.INVALID_VERIFIED_TICKET,
);
return [];
}

return tickets;
};

public editTickets = async (editTicketsDto: EditTicketsDTO) => {
const tickets = await this.ticketRepository.editTickets(editTicketsDto);

if (!tickets) {
throwError(
CustomResponseType.INVALID_EDIT_TICKET_MESSAGE,
CustomResponseType.INVALID_EDIT_TICKET,
);
return [];
}

return tickets;
};

private encryptTicketId = (ticketId: Types.ObjectId) => {
// 確保密鑰是 32 字節長度 (AES-256)
Expand Down Expand Up @@ -142,6 +164,17 @@ export class TicketService {
const tickets = ids.map((id) => ({
ticketId: new Types.ObjectId(id),
}));
return await this.ticketRepository.deleteTickets(tickets);

const deletedTickets = await this.ticketRepository.deleteTickets(tickets);

if (!deletedTickets) {
throwError(
CustomResponseType.INVALID_TICKET_DELETE_MESSAGE,
CustomResponseType.INVALID_TICKET_DELETE,
);
return [];
}

return deletedTickets;
};
}
25 changes: 24 additions & 1 deletion src/service/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ import { ProductRepository } from '../repository/productRepository';
import { IUserReq, TMethod } from '../types/common.type';
import { SignUpDTO } from '../dto/user/signUpDto';
import { GoogleSignUpDTO } from '../dto/user/googleSignUpdDto';
import { SellTicketDto } from '../dto/ticket/sellTicketDto';
import { TicketRepository } from '../repository/ticketRepository';
import { ITicket } from '../models/ticket';
import { Types } from 'mongoose';
import { IProduct } from '../models/product';
import { GetTransferableTicketVo } from '../vo/ticket/getTransferableTicketVo';
import { TicketRepository } from '../repository/ticketRepository';

const logger = log4js.getLogger(`UserService`);

Expand Down Expand Up @@ -307,6 +308,28 @@ export class UserService {
}
return favorite;
};

public sellTicket = async (sellTicketDto: SellTicketDto) => {
const tickets =
await this.ticketRepository.findTransferableTicketByOrderIdAndProductId(
sellTicketDto,
);
if (!tickets) {
throwError(
CustomResponseType.TICKET_NOT_FOUND_MESSAGE,
CustomResponseType.TICKET_NOT_FOUND,
);
}
if (tickets.length < sellTicketDto.sellAmount) {
throwError(
CustomResponseType.TICKET_NOT_ENOUGH_MESSAGE,
CustomResponseType.TICKET_NOT_ENOUGH,
);
}
return await this.ticketRepository.updateSellTickets(
tickets.splice(0, sellTicketDto.sellAmount),
);
};
public getTransferableTicket = async (userId: string) => {
const tickets = await this.ticketRepository.findTransferableTicket(userId);
// 查出的ticket依orderId和productId分組
Expand Down
3 changes: 3 additions & 0 deletions src/types/customResponseType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,4 +209,7 @@ export const enum CustomResponseType {

INVALID_TICKET_DELETE = '6546',
INVALID_TICKET_DELETE_MESSAGE = '無效的票券刪除行為',

TICKET_NOT_ENOUGH = '6547',
TICKET_NOT_ENOUGH_MESSAGE = '欲分票之票券數量不足',
}
7 changes: 7 additions & 0 deletions src/types/ticket.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ export interface ITransferTicketReq extends IUserReq {
ticketId: string;
};
}
export interface ISellTicketReq extends IUserReq {
body: {
orderId: string;
productId: string;
amount: number;
};
}

export interface IClaimShareTicketReq extends IUserReq {
body: {
Expand Down

0 comments on commit 60c5b2c

Please sign in to comment.