diff --git a/src/common/services/payment-service/midtrans.payment-service.ts b/src/common/services/payment-service/midtrans.payment-service.ts index 2631668..e2399fb 100644 --- a/src/common/services/payment-service/midtrans.payment-service.ts +++ b/src/common/services/payment-service/midtrans.payment-service.ts @@ -2,6 +2,8 @@ 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 { Order } from "@/database/entities/order.entity"; +import { Kit } from "@/database/enums/kit.enum"; +import { OrderType } from "@/database/enums/order-type.enum"; type CreateTransactionResponseSuccess = { token: string; @@ -11,6 +13,12 @@ type CreateTransactionResponseFailed = { error_messages: string[]; }; +const BOOKING_SEAT_PRICE = 1_000_000; +const MINIMAL_KIT_PRICE = 350_000; +const WITHOUT_SUITCASE_KIT_PRICE = 350_000; +const FULL_KIT_PRICE = 350_000; +const VACCINE_PRICE = 500_000; + export class MidtransPaymentService extends AbstractPaymentService { private readonly _basicAuth: string; @@ -20,17 +28,48 @@ export class MidtransPaymentService extends AbstractPaymentService { this._basicAuth = `Basic ${Buffer.from(`${midtransConfig.serverKey}:`).toBase64()}`; } - 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 price = this.calculateOrderPrice(order); + const quantity = order.quad + order.triple + order.double + order.infant; + + let price = 0; + if (order.type === OrderType.bookingSeat) { + price += BOOKING_SEAT_PRICE * quantity; + } else if (order.type === OrderType.downPayment) { + price += + Math.ceil( + ((order.quadDownPaymentPercentage / 100) * + (order.package.quadPrice - order.package.quadDiscount)) / + 100_000, + ) * + 100_000 + + Math.ceil( + ((order.tripleDownPaymentPercentage / 100) * + (order.package.triplePrice - order.package.tripleDiscount)) / + 100_000, + ) * + 100_000 + + Math.ceil( + ((order.doubleDownPaymentPercentage / 100) * + (order.package.doublePrice - order.package.doubleDiscount)) / + 100_000, + ) * + 100_000 + + Math.ceil( + ((order.infantDownPaymentPercentage / 100) * + ((order.package.infantPrice ?? 0) - + (order.package.infantDiscount ?? 0))) / + 100_000, + ) * + 100_000; + } + if (order.kit === Kit.minimal) { + price += MINIMAL_KIT_PRICE; + } else if (order.kit === Kit.withoutSuitcase) { + price += WITHOUT_SUITCASE_KIT_PRICE; + } else if (order.kit === Kit.full) { + price += FULL_KIT_PRICE; + } + price += VACCINE_PRICE * quantity; const response = await fetch(`${midtransConfig.baseUrl}/transactions`, { method: "POST", @@ -42,7 +81,7 @@ export class MidtransPaymentService extends AbstractPaymentService { body: JSON.stringify({ transaction_details: { order_id: order.id, - gross_amount: this.calculateOrderPrice(order), + gross_amount: price, }, item_details: [ { diff --git a/src/database/entities/partner.entity.ts b/src/database/entities/partner.entity.ts index 1657b0d..0b56fb8 100644 --- a/src/database/entities/partner.entity.ts +++ b/src/database/entities/partner.entity.ts @@ -1,10 +1,7 @@ -import { Order } from "@/database/entities/order.entity"; import { Verification } from "@/database/entities/verification.entity"; import { - Collection, Entity, ManyToOne, - OneToMany, PrimaryKey, Property, type Rel, @@ -45,9 +42,4 @@ export class Partner { onUpdate: () => new Date(), }) updatedAt!: Date; - - // Collections - - @OneToMany(() => Order, (order) => order.partner) - orders = new Collection(this); } diff --git a/src/database/migrations/.snapshot-goumrah.json b/src/database/migrations/.snapshot-goumrah.json index 6364234..4b8c7ac 100644 --- a/src/database/migrations/.snapshot-goumrah.json +++ b/src/database/migrations/.snapshot-goumrah.json @@ -3149,16 +3149,6 @@ "length": 30, "mappedType": "string" }, - "code": { - "name": "code", - "type": "varchar(6)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "length": 6, - "mappedType": "string" - }, "package_id": { "name": "package_id", "type": "varchar(30)", @@ -3169,89 +3159,113 @@ "length": 30, "mappedType": "string" }, - "created_at": { - "name": "created_at", - "type": "timestamptz", - "unsigned": false, + "quad": { + "name": "quad", + "type": "int", + "unsigned": true, "autoincrement": false, "primary": false, "nullable": false, - "length": 6, - "mappedType": "datetime" + "mappedType": "integer" }, - "updated_at": { - "name": "updated_at", - "type": "timestamptz", - "unsigned": false, + "triple": { + "name": "triple", + "type": "int", + "unsigned": true, "autoincrement": false, "primary": false, "nullable": false, - "length": 6, - "mappedType": "datetime" - } - }, - "name": "package_consult_session", - "schema": "public", - "indexes": [ - { - "columnNames": [ - "code" - ], - "composite": false, - "keyName": "package_consult_session_code_unique", - "constraint": true, - "primary": false, - "unique": true + "mappedType": "integer" }, - { - "keyName": "package_consult_session_pkey", - "columnNames": [ - "id" - ], - "composite": false, - "constraint": true, - "primary": true, - "unique": true - } - ], - "checks": [], - "foreignKeys": { - "package_consult_session_package_id_foreign": { - "constraintName": "package_consult_session_package_id_foreign", - "columnNames": [ - "package_id" - ], - "localTableName": "public.package_consult_session", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.package", - "updateRule": "cascade" - } - }, - "nativeEnums": {} - }, - { - "columns": { - "id": { - "name": "id", - "type": "varchar(30)", - "unsigned": false, + "double": { + "name": "double", + "type": "int", + "unsigned": true, "autoincrement": false, "primary": false, "nullable": false, - "length": 30, - "mappedType": "string" + "mappedType": "integer" }, - "package_id": { - "name": "package_id", - "type": "varchar(30)", + "infant": { + "name": "infant", + "type": "int", + "unsigned": true, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "type": { + "name": "type", + "type": "text", "unsigned": false, "autoincrement": false, "primary": false, "nullable": false, - "length": 30, - "mappedType": "string" + "enumItems": [ + "booking_seat", + "down_payment" + ], + "mappedType": "enum" + }, + "quad_down_payment_percentage": { + "name": "quad_down_payment_percentage", + "type": "int", + "unsigned": true, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "triple_down_payment_percentage": { + "name": "triple_down_payment_percentage", + "type": "int", + "unsigned": true, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "double_down_payment_percentage": { + "name": "double_down_payment_percentage", + "type": "int", + "unsigned": true, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "infant_down_payment_percentage": { + "name": "infant_down_payment_percentage", + "type": "int", + "unsigned": true, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" + }, + "kit": { + "name": "kit", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "enumItems": [ + "minimal", + "without_suitcase", + "full" + ], + "mappedType": "enum" + }, + "vaccine": { + "name": "vaccine", + "type": "int", + "unsigned": true, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "integer" }, "name": { "name": "name", @@ -3273,26 +3287,6 @@ "length": 20, "mappedType": "string" }, - "verification_id": { - "name": "verification_id", - "type": "varchar(30)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 30, - "mappedType": "string" - }, - "partner_id": { - "name": "partner_id", - "type": "varchar(30)", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": true, - "length": 30, - "mappedType": "string" - }, "expired_at": { "name": "expired_at", "type": "timestamptz", @@ -3371,32 +3365,6 @@ ], "referencedTableName": "public.package_detail", "updateRule": "cascade" - }, - "order_verification_id_foreign": { - "constraintName": "order_verification_id_foreign", - "columnNames": [ - "verification_id" - ], - "localTableName": "public.order", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.verification", - "deleteRule": "set null", - "updateRule": "cascade" - }, - "order_partner_id_foreign": { - "constraintName": "order_partner_id_foreign", - "columnNames": [ - "partner_id" - ], - "localTableName": "public.order", - "referencedColumnNames": [ - "id" - ], - "referencedTableName": "public.partner", - "deleteRule": "set null", - "updateRule": "cascade" } }, "nativeEnums": {} @@ -3413,8 +3381,18 @@ "length": 30, "mappedType": "string" }, - "order_id": { - "name": "order_id", + "code": { + "name": "code", + "type": "varchar(6)", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "string" + }, + "package_id": { + "name": "package_id", "type": "varchar(30)", "unsigned": false, "autoincrement": false, @@ -3423,21 +3401,6 @@ "length": 30, "mappedType": "string" }, - "room_type": { - "name": "room_type", - "type": "text", - "unsigned": false, - "autoincrement": false, - "primary": false, - "nullable": false, - "enumItems": [ - "quad", - "triple", - "double", - "infant" - ], - "mappedType": "enum" - }, "created_at": { "name": "created_at", "type": "timestamptz", @@ -3459,11 +3422,21 @@ "mappedType": "datetime" } }, - "name": "order_detail", + "name": "package_consult_session", "schema": "public", "indexes": [ { - "keyName": "order_detail_pkey", + "columnNames": [ + "code" + ], + "composite": false, + "keyName": "package_consult_session_code_unique", + "constraint": true, + "primary": false, + "unique": true + }, + { + "keyName": "package_consult_session_pkey", "columnNames": [ "id" ], @@ -3475,16 +3448,16 @@ ], "checks": [], "foreignKeys": { - "order_detail_order_id_foreign": { - "constraintName": "order_detail_order_id_foreign", + "package_consult_session_package_id_foreign": { + "constraintName": "package_consult_session_package_id_foreign", "columnNames": [ - "order_id" + "package_id" ], - "localTableName": "public.order_detail", + "localTableName": "public.package_consult_session", "referencedColumnNames": [ "id" ], - "referencedTableName": "public.order", + "referencedTableName": "public.package", "updateRule": "cascade" } }, diff --git a/src/database/migrations/Migration20260108134605.ts b/src/database/migrations/Migration20260108134605.ts new file mode 100644 index 0000000..91bddee --- /dev/null +++ b/src/database/migrations/Migration20260108134605.ts @@ -0,0 +1,113 @@ +import { Migration } from "@mikro-orm/migrations"; + +export class Migration20260108134605 extends Migration { + override async up(): Promise { + const knex = this.getKnex(); + + knex.schema.dropTable("order_detail"); + + await knex.schema.alterTable("order", (table) => { + table.dropColumn("verification_id"); + table.dropColumn("partner_id"); + }); + + await knex.raw(` + create type "order_type" as enum ( + 'booking_seat', + 'down_payment' + ); + `); + + await knex.raw(` + create type "kit" as enum ( + 'minimal', + 'without_suitcase', + 'full' + ); + `); + + await knex.schema.alterTable("order", (table) => { + table.integer("quad").notNullable(); + table.integer("triple").notNullable(); + table.integer("double").notNullable(); + table.integer("infant").notNullable(); + table + .enum("type", null, { + useNative: true, + existingType: true, + enumName: "order_type", + }) + .notNullable(); + table.integer("quad_down_payment_percentage").notNullable(); + table.integer("triple_down_payment_percentage").notNullable(); + table.integer("double_down_payment_percentage").notNullable(); + table.integer("infant_down_payment_percentage").notNullable(); + table + .enum("kit", null, { + useNative: true, + existingType: true, + enumName: "kit", + }) + .notNullable(); + table.integer("vaccine").notNullable(); + }); + } + + override async down(): Promise { + const knex = this.getKnex(); + + await knex.schema.alterTable("order", (table) => { + table.dropColumn("quad"); + table.dropColumn("triple"); + table.dropColumn("double"); + table.dropColumn("infant"); + table.dropColumn("type"); + table.dropColumn("quad_down_payment_percentage"); + table.dropColumn("triple_down_payment_percentage"); + table.dropColumn("double_down_payment_percentage"); + table.dropColumn("infant_down_payment_percentage"); + table.dropColumn("kit"); + table.dropColumn("vaccine"); + }); + + await knex.raw(`drop type "kit";`); + await knex.raw(`drop type "order_type";`); + + await knex.schema.alterTable("order", (table) => { + table.string("verification_id", 30).nullable(); + table.string("partner_id", 30).nullable(); + table + .foreign("verification_id", "order_verification_id_foreign") + .references("verification.id") + .onUpdate("NO ACTION") + .onDelete("SET NULL"); + table + .foreign("partner_id", "order_partner_id_foreign") + .references("partner.id") + .onUpdate("NO ACTION") + .onDelete("SET NULL"); + }); + + await knex.schema.createTable("order_detail", (table) => { + // Columns + table.string("id", 30).notNullable(); + table.string("order_id", 30).notNullable(); + table + .enum("room_type", null, { + useNative: true, + existingType: true, + enumName: "room_type", + }) + .notNullable(); + table.timestamp("created_at", { useTz: true }).notNullable(); + table.timestamp("updated_at", { useTz: true }).notNullable(); + // Constraints + table.primary(["id"], { constraintName: "order_detail_pkey" }); + table + .foreign("order_id", "order_detail_order_id_foreign") + .references("order.id") + .onUpdate("NO ACTION") + .onDelete("CASCADE"); + }); + } +}