From 4124d9bf19e87b35b70598adaa5dca4f271ac713 Mon Sep 17 00:00:00 2001 From: ItsMalma Date: Thu, 8 Jan 2026 20:28:01 +0700 Subject: [PATCH] change order flow --- src/common/schemas.ts | 4 +- .../midtrans.payment-service.ts | 99 ++-------- src/configs/_env.ts | 6 +- src/database/entities/order-detail.entity.ts | 35 ---- src/database/entities/order.entity.ts | 52 ++++-- src/database/enums/kit.enum.ts | 5 + src/database/enums/order-type.enum.ts | 4 + src/modules/airline/airline.schemas.ts | 4 +- src/modules/hotel/hotel.schemas.ts | 4 +- src/modules/order/order.controller.ts | 176 +++++------------- src/modules/order/order.schemas.ts | 39 +++- src/modules/testimony/testimony.schemas.ts | 2 +- 12 files changed, 145 insertions(+), 285 deletions(-) delete mode 100644 src/database/entities/order-detail.entity.ts create mode 100644 src/database/enums/kit.enum.ts create mode 100644 src/database/enums/order-type.enum.ts diff --git a/src/common/schemas.ts b/src/common/schemas.ts index 389cd65..c4e5f55 100644 --- a/src/common/schemas.ts +++ b/src/common/schemas.ts @@ -5,12 +5,12 @@ export const paginationQuerySchema = z.object({ page: z.coerce .number("Must be number.") .int("Must be integer.") - .min(1, "Minimum 1.") + .min(1, "Min 1.") .default(1), per_page: z.coerce .number("Must be number.") .int("Must be integer.") - .min(1, "Minimum 1.") + .min(1, "Min 1.") .default(100), }); diff --git a/src/common/services/payment-service/midtrans.payment-service.ts b/src/common/services/payment-service/midtrans.payment-service.ts index a40e77f..2631668 100644 --- a/src/common/services/payment-service/midtrans.payment-service.ts +++ b/src/common/services/payment-service/midtrans.payment-service.ts @@ -1,9 +1,7 @@ import { PaymentError } from "@/common/errors/payment.error"; import { AbstractPaymentService } from "@/common/services/payment-service/abstract.payment-service"; import { midtransConfig } from "@/configs/midtrans.config"; -import type { OrderDetail } from "@/database/entities/order-detail.entity"; import type { Order } from "@/database/entities/order.entity"; -import { RoomType } from "@/database/enums/room-type.enum"; type CreateTransactionResponseSuccess = { token: string; @@ -22,41 +20,17 @@ export class MidtransPaymentService extends AbstractPaymentService { this._basicAuth = `Basic ${Buffer.from(`${midtransConfig.serverKey}:`).toBase64()}`; } - private calculateOrderDetailsPrice(orderDetails: OrderDetail[]): number { - let price = 0; - for (const orderDetail of orderDetails) { - switch (orderDetail.roomType) { - case RoomType.double: - price += orderDetail.order.package.doublePrice; - break; - case RoomType.triple: - price += orderDetail.order.package.triplePrice; - break; - case RoomType.quad: - price += orderDetail.order.package.quadPrice; - break; - case RoomType.infant: - price += orderDetail.order.package.infantPrice ?? 0; - break; - } - } - - return price; + private calculateOrderPrice(order: Order): number { + return ( + order.package.quadPrice * order.quad + + order.package.triplePrice * order.triple + + order.package.doublePrice * order.double + + (order.package.infantPrice ?? 0) * order.infant + ); } public async createPaymentUrl(order: Order): Promise { - const doubleOrderDetails = order.details.filter( - (orderDetail) => orderDetail.roomType === RoomType.double, - ); - const tripleOrderDetails = order.details.filter( - (orderDetail) => orderDetail.roomType === RoomType.double, - ); - const quadOrderDetails = order.details.filter( - (orderDetail) => orderDetail.roomType === RoomType.double, - ); - const infantOrderDetails = order.details.filter( - (orderDetail) => orderDetail.roomType === RoomType.double, - ); + const price = this.calculateOrderPrice(order); const response = await fetch(`${midtransConfig.baseUrl}/transactions`, { method: "POST", @@ -68,55 +42,18 @@ export class MidtransPaymentService extends AbstractPaymentService { body: JSON.stringify({ transaction_details: { order_id: order.id, - gross_amount: this.calculateOrderDetailsPrice( - order.details.getItems(), - ), + gross_amount: this.calculateOrderPrice(order), }, item_details: [ - doubleOrderDetails.length > 0 - ? { - id: doubleOrderDetails[0].id, - price: order.package.doublePrice, - quantity: doubleOrderDetails.length, - name: `${order.package.package.name} / Double`, - brand: "GoUmrah", - category: "Paket", - merchant_name: "GoUmrah", - } - : undefined, - tripleOrderDetails.length > 0 - ? { - id: tripleOrderDetails[0].id, - price: order.package.triplePrice, - quantity: tripleOrderDetails.length, - name: `${order.package.package.name} / Triple`, - brand: "GoUmrah", - category: "Paket", - merchant_name: "GoUmrah", - } - : undefined, - quadOrderDetails.length > 0 - ? { - id: quadOrderDetails[0].id, - price: order.package.quadPrice, - quantity: quadOrderDetails.length, - name: `${order.package.package.name} / Quad`, - brand: "GoUmrah", - category: "Paket", - merchant_name: "GoUmrah", - } - : undefined, - infantOrderDetails.length > 0 - ? { - id: infantOrderDetails[0].id, - price: order.package.infantPrice, - quantity: infantOrderDetails.length, - name: `${order.package.package.name} / Infant`, - brand: "GoUmrah", - category: "Paket", - merchant_name: "GoUmrah", - } - : undefined, + { + id: order.id, + price, + quantity: order.quad + order.triple + order.double + order.infant, + name: `${order.package.package.name}`, + brand: "GoUmrah", + category: "Paket", + merchant_name: "GoUmrah", + }, ], customer_details: { first_name: order.name, diff --git a/src/configs/_env.ts b/src/configs/_env.ts index 2da6193..51ea01d 100644 --- a/src/configs/_env.ts +++ b/src/configs/_env.ts @@ -6,13 +6,13 @@ export const _env = z SERVER_PORT: z.coerce .number("Must be number.") .int("Must be integer.") - .min(0, "Min 0."), + .min(0, "Minimum0."), DATABASE_HOST: z.string("Must be string.").nonempty("Must not empty."), DATABASE_PORT: z.coerce .number("Must be number.") .int("Must be integer.") - .min(0, "Min 0."), + .min(0, "Minimum0."), DATABASE_USERNAME: z.string("Must be string.").nonempty("Must not empty."), DATABASE_PASSWORD: z.string("Must be string.").nonempty("Must not empty."), DATABASE_NAME: z.string("Must be string.").nonempty("Must not empty."), @@ -49,7 +49,7 @@ export const _env = z MAIL_PORT: z.coerce .number("Must be number.") .int("Must be integer.") - .min(0, "Min 0."), + .min(0, "Minimum0."), MAIL_USERNAME: z.string("Must be string.").nonempty("Must not empty."), MAIL_PASSWORD: z.string("Must be string.").nonempty("Must not empty."), diff --git a/src/database/entities/order-detail.entity.ts b/src/database/entities/order-detail.entity.ts deleted file mode 100644 index 350be31..0000000 --- a/src/database/entities/order-detail.entity.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { Order } from "@/database/entities/order.entity"; -import { RoomType } from "@/database/enums/room-type.enum"; -import { - Entity, - Enum, - ManyToOne, - PrimaryKey, - Property, - type Rel, -} from "@mikro-orm/core"; - -@Entity() -export class OrderDetail { - @PrimaryKey({ type: "varchar", length: 30 }) - id!: string; - - @ManyToOne(() => Order) - order!: Rel; - - @Enum({ items: () => RoomType }) - roomType!: RoomType; - - @Property({ - type: "timestamp", - onCreate: () => new Date(), - }) - createdAt!: Date; - - @Property({ - type: "timestamp", - onCreate: () => new Date(), - onUpdate: () => new Date(), - }) - updatedAt!: Date; -} diff --git a/src/database/entities/order.entity.ts b/src/database/entities/order.entity.ts index 76ab940..c7b7ec4 100644 --- a/src/database/entities/order.entity.ts +++ b/src/database/entities/order.entity.ts @@ -1,12 +1,10 @@ -import { OrderDetail } from "@/database/entities/order-detail.entity"; import { PackageDetail } from "@/database/entities/package-detail.entity"; -import { Partner } from "@/database/entities/partner.entity"; -import { Verification } from "@/database/entities/verification.entity"; +import { Kit } from "@/database/enums/kit.enum"; +import { OrderType } from "@/database/enums/order-type.enum"; import { - Collection, Entity, + Enum, ManyToOne, - OneToMany, PrimaryKey, Property, type Rel, @@ -20,18 +18,45 @@ export class Order { @ManyToOne(() => PackageDetail) package!: Rel; + @Property({ type: "int", unsigned: true }) + quad!: number; + + @Property({ type: "int", unsigned: true }) + triple!: number; + + @Property({ type: "int", unsigned: true }) + double!: number; + + @Property({ type: "int", unsigned: true }) + infant!: number; + + @Enum({ items: () => OrderType }) + type!: OrderType; + + @Property({ type: "int", unsigned: true }) + quadDownPaymentPercentage!: number; + + @Property({ type: "int", unsigned: true }) + tripleDownPaymentPercentage!: number; + + @Property({ type: "int", unsigned: true }) + doubleDownPaymentPercentage!: number; + + @Property({ type: "int", unsigned: true }) + infantDownPaymentPercentage!: number; + + @Enum({ items: () => Kit }) + kit!: Kit; + + @Property({ type: "int", unsigned: true }) + vaccine!: number; + @Property({ type: "varchar", length: 100 }) name!: string; @Property({ type: "varchar", length: 20 }) whatsapp!: string; - @ManyToOne(() => Verification, { nullable: true }) - verification!: Rel | null; - - @ManyToOne(() => Partner, { nullable: true }) - partner!: Rel; - @Property({ type: "timestamp", nullable: true }) expiredAt!: Date | null; @@ -53,9 +78,4 @@ export class Order { onUpdate: () => new Date(), }) updatedAt!: Date; - - // Collections - - @OneToMany(() => OrderDetail, (orderDetail) => orderDetail.order) - details = new Collection(this); } diff --git a/src/database/enums/kit.enum.ts b/src/database/enums/kit.enum.ts new file mode 100644 index 0000000..9cc3aa3 --- /dev/null +++ b/src/database/enums/kit.enum.ts @@ -0,0 +1,5 @@ +export enum Kit { + minimal = "minimal", + withoutSuitcase = "without_suitcase", + full = "full", +} diff --git a/src/database/enums/order-type.enum.ts b/src/database/enums/order-type.enum.ts new file mode 100644 index 0000000..20f07ce --- /dev/null +++ b/src/database/enums/order-type.enum.ts @@ -0,0 +1,4 @@ +export enum OrderType { + bookingSeat = "booking_seat", + downPayment = "down_payment", +} diff --git a/src/modules/airline/airline.schemas.ts b/src/modules/airline/airline.schemas.ts index 9c47b48..0248754 100644 --- a/src/modules/airline/airline.schemas.ts +++ b/src/modules/airline/airline.schemas.ts @@ -14,8 +14,8 @@ export const airlineRequestSchema = z.object({ skytrax_rating: z .number("Must be number.") .int("Must be integer.") - .min(1, "Minimum 1.") - .max(5, "Maximum 5."), + .min(1, "Min 1.") + .max(5, "Max 5."), skytrax_type: z.enum( SkytraxType, "Must be either 'full_service' or 'low_cost'.", diff --git a/src/modules/hotel/hotel.schemas.ts b/src/modules/hotel/hotel.schemas.ts index ad41a2f..c553638 100644 --- a/src/modules/hotel/hotel.schemas.ts +++ b/src/modules/hotel/hotel.schemas.ts @@ -12,8 +12,8 @@ export const hotelRequestSchema = z.object({ star: z .number("Must be number.") .int("Must be integer.") - .min(1, "Minimum 1.") - .max(7, "Maximum 7."), + .min(1, "Min 1.") + .max(7, "Max 7."), images: z .array( z.base64("Must be base64 string.").nonempty("Must not empty."), diff --git a/src/modules/order/order.controller.ts b/src/modules/order/order.controller.ts index 4690f33..a58210b 100644 --- a/src/modules/order/order.controller.ts +++ b/src/modules/order/order.controller.ts @@ -12,22 +12,15 @@ import type { ListResponse, SingleResponse, } from "@/common/types"; -import { generateRandomCode } from "@/common/utils"; -import { OrderDetail } from "@/database/entities/order-detail.entity"; import { Order } from "@/database/entities/order.entity"; import { PackageDetail } from "@/database/entities/package-detail.entity"; -import { Partner } from "@/database/entities/partner.entity"; -import { Verification } from "@/database/entities/verification.entity"; -import { VerificationType } from "@/database/enums/verification-type.enum"; import { orm } from "@/database/orm"; import type { OrderMapper } from "@/modules/order/order.mapper"; import { orderParamsSchema, orderRequestSchema, - orderVerifyRequestSchema, } from "@/modules/order/order.schemas"; import type { OrderResponse } from "@/modules/order/order.types"; -import * as dateFns from "date-fns"; import { Router, type Request, type Response } from "express"; import { ulid } from "ulid"; @@ -63,22 +56,22 @@ export class OrderController extends Controller { } satisfies ErrorResponse); } - const verification = orm.em.create(Verification, { - id: ulid(), - code: generateRandomCode(6, "0123456789"), - type: VerificationType.createOrder, - expiredAt: dateFns.addHours(new Date(), 1), - createdAt: new Date(), - updatedAt: new Date(), - }); - const order = orm.em.create(Order, { id: ulid(), package: packageDetail, + quad: body.quad, + triple: body.triple, + double: body.double, + infant: body.infant, + type: body.type, + quadDownPaymentPercentage: body.quad_down_payment_percentage, + tripleDownPaymentPercentage: body.triple_down_payment_percentage, + doubleDownPaymentPercentage: body.double_down_payment_percentage, + infantDownPaymentPercentage: body.infant_down_payment_percentage, + kit: body.kit, + vaccine: body.vaccine, name: body.name, whatsapp: body.whatsapp, - verification, - partner: null, expiredAt: null, purchasedAt: null, finishedAt: null, @@ -86,27 +79,21 @@ export class OrderController extends Controller { updatedAt: new Date(), }); - for (const roomType of body.room_types) { - order.details.add( - orm.em.create(OrderDetail, { - id: ulid(), - order, - roomType, - createdAt: new Date(), - updatedAt: new Date(), - }), - ); - } - await orm.em.flush(); + const paymentUrl = await this.paymentService.createPaymentUrl(order); + return res.status(201).json({ data: { - message: - "Order created successfully. Please check your email for verification.", + ...this.mapper.mapEntityToResponse(order), + payment_url: paymentUrl, }, errors: null, - } satisfies SingleResponse); + } satisfies SingleResponse< + OrderResponse & { + payment_url: string; + } + >); } async list(_req: Request, res: Response) { @@ -123,8 +110,11 @@ export class OrderController extends Controller { const orders = await orm.em.find( Order, { - verification: null, - partner: req.partner, + package: { + package: { + partner: req.partner, + }, + }, }, { limit: query.per_page, @@ -159,8 +149,11 @@ export class OrderController extends Controller { Order, { id: params.id, - verification: null, - partner: req.partner, + package: { + package: { + partner: req.partner, + }, + }, }, { populate: ["*"], @@ -198,8 +191,11 @@ export class OrderController extends Controller { Order, { id: params.id, - verification: null, - partner: req.partner, + package: { + package: { + partner: req.partner, + }, + }, }, { populate: ["*"], @@ -229,94 +225,6 @@ export class OrderController extends Controller { } satisfies SingleResponse); } - async verify(req: Request, res: Response) { - const parseParamsResult = orderParamsSchema.safeParse(req.params); - if (!parseParamsResult.success) { - return this.handleZodError(parseParamsResult.error, res, "params"); - } - const params = parseParamsResult.data; - - const parseBodyResult = orderVerifyRequestSchema.safeParse(req.body); - if (!parseBodyResult.success) { - return this.handleZodError(parseBodyResult.error, res, "body"); - } - const body = parseBodyResult.data; - - const order = await orm.em.findOne( - Order, - { id: params.id }, - { - populate: ["*"], - }, - ); - if (!order) { - return res.status(404).json({ - data: null, - errors: [ - { - path: "id", - location: "params", - message: "Order not found.", - }, - ], - } satisfies ErrorResponse); - } - - if (order.verification === null) { - return res.status(400).json({ - data: null, - errors: [ - { - message: "Order is already verified.", - }, - ], - } satisfies ErrorResponse); - } - - if (order.verification.code !== body.code) { - return res.status(400).json({ - data: null, - errors: [ - { - path: "code", - location: "body", - message: "Incorrect.", - }, - ], - } satisfies ErrorResponse); - } - - orm.em.remove(order.verification); - - const partners = await orm.em.findAll(Partner, { populate: ["*"] }); - const partner = partners.toSorted( - (a, b) => - a.orders.filter((order) => order.finishedAt === null).length - - b.orders.filter((order) => order.finishedAt === null).length, - )[0]; - - order.verification = null; - order.partner = partner; - order.expiredAt = dateFns.addHours(new Date(), 24); - order.updatedAt = new Date(); - - await orm.em.flush(); - - const paymentUrl = await this.paymentService.createPaymentUrl(order); - - return res.status(200).json({ - data: { - ...this.mapper.mapEntityToResponse(order), - payment_url: paymentUrl, - }, - errors: null, - } satisfies SingleResponse< - OrderResponse & { - payment_url: string; - } - >); - } - async delete(_req: Request, res: Response) { const req = _req as Request & PartnerRequestPlugin; @@ -328,7 +236,14 @@ export class OrderController extends Controller { const order = await orm.em.findOne( Order, - { id: params.id, verification: null, partner: req.partner }, + { + id: params.id, + package: { + package: { + partner: req.partner, + }, + }, + }, { populate: ["*"], }, @@ -372,11 +287,6 @@ export class OrderController extends Controller { isPartnerMiddleware(this.jwtService), this.finish.bind(this), ); - router.put( - "/:id/verify", - createOrmContextMiddleware, - this.verify.bind(this), - ); router.delete( "/:id", createOrmContextMiddleware, diff --git a/src/modules/order/order.schemas.ts b/src/modules/order/order.schemas.ts index 256444c..4adb918 100644 --- a/src/modules/order/order.schemas.ts +++ b/src/modules/order/order.schemas.ts @@ -1,5 +1,6 @@ import { phoneNumberSchema } from "@/common/schemas"; -import { RoomType } from "@/database/enums/room-type.enum"; +import { Kit } from "@/database/enums/kit.enum"; +import { OrderType } from "@/database/enums/order-type.enum"; import z from "zod"; export const orderRequestSchema = z.object({ @@ -7,20 +8,38 @@ export const orderRequestSchema = z.object({ .ulid("Must be ulid string.") .nonempty("Must not empty.") .max(30, "Max 30 characters."), + quad: z.number("Must be number.").int("Must be integer.").min(0, "Min 0."), + triple: z.number("Must be number.").int("Must be integer.").min(0, "Min 0."), + double: z.number("Must be number.").int("Must be integer.").min(0, "Min 0."), + infant: z.number("Must be number.").int("Must be integer.").min(0, "Min 0."), + type: z.enum(OrderType, "Must be either 'booking_seat' or 'down_payment'."), + quad_down_payment_percentage: z + .number("Must be number.") + .int("Must be integer") + .min(25, "Min 25.") + .max(100, "Max 100."), + triple_down_payment_percentage: z + .number("Must be number.") + .int("Must be integer") + .min(25, "Min 25.") + .max(100, "Max 100."), + double_down_payment_percentage: z + .number("Must be number.") + .int("Must be integer") + .min(25, "Min 25.") + .max(100, "Max 100."), + infant_down_payment_percentage: z + .number("Must be number.") + .int("Must be integer") + .min(25, "Min 25.") + .max(100, "Max 100."), + kit: z.enum(Kit, "Must be either 'minimal', 'without_suitcase', or 'full'."), + vaccine: z.number("Must be number.").int("Must be integer.").min(0, "Min 0."), name: z .string("Must be string.") .nonempty("Must not empty.") .max(100, "Max 100 characters."), whatsapp: phoneNumberSchema, - room_types: z - .array( - z.enum( - RoomType, - "Must be either 'double', 'triple', 'quad', or 'infant'.", - ), - "Must be array.", - ) - .nonempty("Must not empty."), }); export const orderVerifyRequestSchema = z.object({ diff --git a/src/modules/testimony/testimony.schemas.ts b/src/modules/testimony/testimony.schemas.ts index 94e9f71..238b7cf 100644 --- a/src/modules/testimony/testimony.schemas.ts +++ b/src/modules/testimony/testimony.schemas.ts @@ -12,7 +12,7 @@ export const testimonyRequestSchema = z.object({ rating: z .number("Must be number.") .int("Must be integer.") - .min(1, "Min 1.") + .min(1, "Minimum1.") .max(5, "Max 5."), });