add auth and payment api
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
export abstract class AbstractEmailService {
|
||||
public abstract sendVerificationEmail(
|
||||
to: string,
|
||||
code: string,
|
||||
): Promise<void>;
|
||||
}
|
||||
30
src/common/services/email-service/library.email-service.ts
Normal file
30
src/common/services/email-service/library.email-service.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AbstractEmailService } from "@/common/services/email-service/abstract.email-service";
|
||||
import { mailConfig } from "@/configs/mail.config";
|
||||
import * as nodemailer from "nodemailer";
|
||||
|
||||
export class LibraryEmailService extends AbstractEmailService {
|
||||
private readonly _transporter: nodemailer.Transporter;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
this._transporter = nodemailer.createTransport({
|
||||
host: mailConfig.host,
|
||||
port: mailConfig.port,
|
||||
secure: true,
|
||||
auth: {
|
||||
user: mailConfig.username,
|
||||
pass: mailConfig.password,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public async sendVerificationEmail(to: string, code: string): Promise<void> {
|
||||
await this._transporter.sendMail({
|
||||
from: mailConfig.username,
|
||||
to,
|
||||
subject: "Email Verification",
|
||||
text: `Your verification code is: ${code}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
29
src/common/services/jwt-service/abstract.jwt-service.ts
Normal file
29
src/common/services/jwt-service/abstract.jwt-service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Admin } from "@/database/entities/admin.entity";
|
||||
import type { Partner } from "@/database/entities/partner.entity";
|
||||
|
||||
export enum JwtType {
|
||||
access = "access",
|
||||
refresh = "refresh",
|
||||
}
|
||||
|
||||
export abstract class AbstractJwtService {
|
||||
public abstract createAdminToken(
|
||||
admin: Admin,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
|
||||
public abstract verifyAdminToken(token: string): Promise<Admin>;
|
||||
|
||||
public abstract createPartnerToken(
|
||||
partner: Partner,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}>;
|
||||
|
||||
public abstract verifyPartnerToken(token: string): Promise<Partner>;
|
||||
}
|
||||
149
src/common/services/jwt-service/library.jwt-service.ts
Normal file
149
src/common/services/jwt-service/library.jwt-service.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { InvalidJwtError } from "@/common/errors/invalid-jwt.error";
|
||||
import {
|
||||
AbstractJwtService,
|
||||
JwtType,
|
||||
} from "@/common/services/jwt-service/abstract.jwt-service";
|
||||
import { jwtConfig } from "@/configs/jwt.config";
|
||||
import { Admin } from "@/database/entities/admin.entity";
|
||||
import { Partner } from "@/database/entities/partner.entity";
|
||||
import { orm } from "@/database/orm";
|
||||
import * as dateFns from "date-fns";
|
||||
import * as jwt from "jsonwebtoken";
|
||||
import { ulid } from "ulid";
|
||||
import z from "zod";
|
||||
|
||||
const adminPayloadSchema = z.object({
|
||||
sub: z.ulid(),
|
||||
role: z.literal("admin"),
|
||||
});
|
||||
|
||||
const partnerPayloadSchema = z.object({
|
||||
sub: z.ulid(),
|
||||
role: z.literal("partner"),
|
||||
});
|
||||
|
||||
export class LibraryJwtService extends AbstractJwtService {
|
||||
public async createAdminToken(
|
||||
admin: Admin,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}> {
|
||||
const now = new Date();
|
||||
|
||||
let expiresAt: Date;
|
||||
switch (type) {
|
||||
case JwtType.access:
|
||||
expiresAt = dateFns.addDays(now, 1);
|
||||
break;
|
||||
case JwtType.refresh:
|
||||
expiresAt = dateFns.addDays(now, 30);
|
||||
break;
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
role: "admin",
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{
|
||||
algorithm: jwtConfig.algorithm,
|
||||
expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
|
||||
notBefore: "0 seconds",
|
||||
subject: admin.id,
|
||||
issuer: jwtConfig.issuer,
|
||||
jwtid: ulid(),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
public async verifyAdminToken(token: string): Promise<Admin> {
|
||||
const payload = jwt.verify(token, jwtConfig.secret, {
|
||||
algorithms: [jwtConfig.algorithm],
|
||||
issuer: jwtConfig.issuer,
|
||||
});
|
||||
|
||||
const parsePayloadResult = adminPayloadSchema.safeParse(payload);
|
||||
if (!parsePayloadResult.success) {
|
||||
throw new InvalidJwtError("Invalid payload.");
|
||||
}
|
||||
const adminPayload = parsePayloadResult.data;
|
||||
|
||||
const admin = await orm.em.findOne(Admin, {
|
||||
id: adminPayload.sub,
|
||||
});
|
||||
if (!admin) {
|
||||
throw new InvalidJwtError("Admin not found.");
|
||||
}
|
||||
|
||||
return admin;
|
||||
}
|
||||
|
||||
public async createPartnerToken(
|
||||
partner: Partner,
|
||||
type: JwtType,
|
||||
): Promise<{
|
||||
token: string;
|
||||
expiresAt: Date;
|
||||
}> {
|
||||
const now = new Date();
|
||||
|
||||
let expiresAt: Date;
|
||||
switch (type) {
|
||||
case JwtType.access:
|
||||
expiresAt = dateFns.addDays(now, 1);
|
||||
break;
|
||||
case JwtType.refresh:
|
||||
expiresAt = dateFns.addDays(now, 30);
|
||||
break;
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
role: "partner",
|
||||
},
|
||||
jwtConfig.secret,
|
||||
{
|
||||
algorithm: jwtConfig.algorithm,
|
||||
expiresIn: Math.floor((expiresAt.getTime() - now.getTime()) / 1000),
|
||||
notBefore: "0 seconds",
|
||||
subject: partner.id,
|
||||
issuer: jwtConfig.issuer,
|
||||
jwtid: ulid(),
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
token,
|
||||
expiresAt,
|
||||
};
|
||||
}
|
||||
|
||||
public async verifyPartnerToken(token: string): Promise<Partner> {
|
||||
const payload = jwt.verify(token, jwtConfig.secret, {
|
||||
algorithms: [jwtConfig.algorithm],
|
||||
issuer: jwtConfig.issuer,
|
||||
});
|
||||
|
||||
const parsePayloadResult = partnerPayloadSchema.safeParse(payload);
|
||||
if (!parsePayloadResult.success) {
|
||||
throw new InvalidJwtError("Invalid payload.");
|
||||
}
|
||||
const partnerPayload = parsePayloadResult.data;
|
||||
|
||||
const partner = await orm.em.findOne(Partner, {
|
||||
id: partnerPayload.sub,
|
||||
});
|
||||
if (!partner) {
|
||||
throw new InvalidJwtError("Partner not found.");
|
||||
}
|
||||
|
||||
return partner;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { Order } from "@/database/entities/order.entity";
|
||||
|
||||
export abstract class AbstractPaymentService {
|
||||
public abstract createPaymentUrl(order: Order): Promise<string>;
|
||||
}
|
||||
141
src/common/services/payment-service/midtrans.payment-service.ts
Normal file
141
src/common/services/payment-service/midtrans.payment-service.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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;
|
||||
redirect_url: string;
|
||||
};
|
||||
type CreateTransactionResponseFailed = {
|
||||
error_messages: string[];
|
||||
};
|
||||
|
||||
export class MidtransPaymentService extends AbstractPaymentService {
|
||||
private readonly _basicAuth: string;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public async createPaymentUrl(order: Order): Promise<string> {
|
||||
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 response = await fetch(`${midtransConfig.baseUrl}/transactions`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: this._basicAuth,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
transaction_details: {
|
||||
order_id: order.id,
|
||||
gross_amount: this.calculateOrderDetailsPrice(
|
||||
order.details.getItems(),
|
||||
),
|
||||
},
|
||||
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,
|
||||
],
|
||||
customer_details: {
|
||||
first_name: order.name,
|
||||
phone: order.whatsapp,
|
||||
},
|
||||
credit_card: {
|
||||
secure: true,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === 201) {
|
||||
const responseBody =
|
||||
(await response.json()) as CreateTransactionResponseSuccess;
|
||||
return responseBody.redirect_url;
|
||||
} else {
|
||||
const responseBody =
|
||||
(await response.json()) as CreateTransactionResponseFailed;
|
||||
throw new PaymentError(responseBody.error_messages[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user