diff --git a/src/controller/userController.ts b/src/controller/userController.ts index 93eb627..d7a4096 100644 --- a/src/controller/userController.ts +++ b/src/controller/userController.ts @@ -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(); @@ -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, diff --git a/src/dto/ticket/sellTicketDto.ts b/src/dto/ticket/sellTicketDto.ts new file mode 100644 index 0000000..0076a8d --- /dev/null +++ b/src/dto/ticket/sellTicketDto.ts @@ -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; + } +} diff --git a/src/repository/ticketRepository.ts b/src/repository/ticketRepository.ts index 77fbeb9..93f3924 100644 --- a/src/repository/ticketRepository.ts +++ b/src/repository/ticketRepository.ts @@ -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) { @@ -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, @@ -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( @@ -66,8 +80,6 @@ export class TicketRepository { return deletedTickets; }); - - return result; } catch (error) { throwError( (error as Error).message, @@ -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, { @@ -99,7 +111,6 @@ export class TicketRepository { return updatedTickets; }); - return result; } catch (error) { throwError( (error as Error).message, @@ -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, { @@ -131,7 +142,6 @@ export class TicketRepository { return updatedTickets; }); - return result; } catch (error) { throwError( (error as Error).message, @@ -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, @@ -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 ( diff --git a/src/routes/userRoute.ts b/src/routes/userRoute.ts index 1acb451..c7b529e 100644 --- a/src/routes/userRoute.ts +++ b/src/routes/userRoute.ts @@ -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', /** diff --git a/src/service/ticketService.ts b/src/service/ticketService.ts index 08468ba..3de8749 100644 --- a/src/service/ticketService.ts +++ b/src/service/ticketService.ts @@ -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) @@ -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; }; } diff --git a/src/service/userService.ts b/src/service/userService.ts index 34926f2..82becf4 100644 --- a/src/service/userService.ts +++ b/src/service/userService.ts @@ -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`); @@ -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分組 diff --git a/src/types/customResponseType.ts b/src/types/customResponseType.ts index 8813eac..6e5479d 100644 --- a/src/types/customResponseType.ts +++ b/src/types/customResponseType.ts @@ -209,4 +209,7 @@ export const enum CustomResponseType { INVALID_TICKET_DELETE = '6546', INVALID_TICKET_DELETE_MESSAGE = '無效的票券刪除行為', + + TICKET_NOT_ENOUGH = '6547', + TICKET_NOT_ENOUGH_MESSAGE = '欲分票之票券數量不足', } diff --git a/src/types/ticket.type.ts b/src/types/ticket.type.ts index 092255c..a1ee55a 100644 --- a/src/types/ticket.type.ts +++ b/src/types/ticket.type.ts @@ -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: {