chore: add payment module
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "all",
|
"trailingComma": "all",
|
||||||
"endOfLine": "auto",
|
"endOfLine": "auto",
|
||||||
"printWidth": 140,
|
"printWidth": 150,
|
||||||
"arrowParens": "always",
|
"arrowParens": "always",
|
||||||
"semi": true,
|
"semi": true,
|
||||||
"tabWidth": 2
|
"tabWidth": 2
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { ValidationArguments, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, registerDecorator } from "class-validator";
|
||||||
ValidationArguments,
|
|
||||||
ValidationOptions,
|
|
||||||
ValidatorConstraint,
|
|
||||||
ValidatorConstraintInterface,
|
|
||||||
registerDecorator,
|
|
||||||
} from "class-validator";
|
|
||||||
|
|
||||||
export function isValidIranianNationalCode(nationalCode: string): boolean {
|
export function isValidIranianNationalCode(nationalCode: string): boolean {
|
||||||
if (!/^\d{10}$/.test(nationalCode)) return false;
|
if (!/^\d{10}$/.test(nationalCode)) return false;
|
||||||
|
|||||||
@@ -299,30 +299,6 @@ export const enum TicketMessageEnum {
|
|||||||
CATEGORY_DELETED = "دسته بندی با موفقیت حذف شد",
|
CATEGORY_DELETED = "دسته بندی با موفقیت حذف شد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum WalletMessage {
|
|
||||||
WALLET_NOT_FOUND = "کیف پول یافت نشد",
|
|
||||||
AMOUNT_MUST_BE_INTEGER = "مبلغ شارژ باید یک عدد صحیح باشد",
|
|
||||||
AMOUNT_REQUIRED = "مبلغ شارژ مورد نیاز است",
|
|
||||||
DEPOSIT_TYPE_REQUIRED = "نوع شارژ مورد نیاز است",
|
|
||||||
AMOUNT_MINIMUM = "حداقل مبلغ شارژ ۱۰۰,۰۰۰ تومان است",
|
|
||||||
GATEWAY_REQUIRED = "درگاه پرداخت مورد نیاز است",
|
|
||||||
RECEIPT_URL_SHOULD_BE_URL = "آدرس فیش باید یک آدرس یو آر ال باشد",
|
|
||||||
RECEIPT_URL_REQUIRED = "آدرس فیش مورد نیاز است",
|
|
||||||
BANK_ACCOUNT_ID_REQUIRED = "شناسه حساب بانکی مورد نیاز است",
|
|
||||||
BANK_ACCOUNT_ID_SHOULD_BE_UUID = "شناسه حساب بانکی باید یک UUID معتبر باشد",
|
|
||||||
ERROR_IN_CHARGE_WALLET = "خطا در شارژ کیف پول",
|
|
||||||
DEPOSIT_WALLET_IPG = "شارژ کیف پول از طریق درگاه",
|
|
||||||
TRANSFER_METHOD_REQUIRED = "روش انتفال اجباری است",
|
|
||||||
GATEWAY_ID_SHOULD_BE_UUID = "آیدی درگاه پرداخت باید UUID باشد",
|
|
||||||
DEPOSIT_WALLET_TRANSFER = "شارژ کیف پول از طریق انتقال بانکی",
|
|
||||||
INSUFFICIENT_BALANCE = "موجودی کیف پول کافی نیست",
|
|
||||||
SUBSCRIPTION_WALLET_TRANSFER = "پرداخت اشتراک از طریق کیف پول",
|
|
||||||
SUPPORT_PLAN_WALLET_TRANSFER = "پرداخت پلن پشتیبانی از طریق کیف پول",
|
|
||||||
INVOICE_WALLET_TRANSFER = "پرداخت صورت حساب از طریق کیف پول",
|
|
||||||
TRANSACTION_NOT_FOUND = "تراکنش پیدا نشد",
|
|
||||||
REFERRAL_REWARD_WALLET_TRANSFER = "پرداخت پاداش ارجاع از طریق کیف پول",
|
|
||||||
}
|
|
||||||
|
|
||||||
export const enum PaymentMessage {
|
export const enum PaymentMessage {
|
||||||
PAYMENT_GATEWAY_NOT_FOUND = "درگاه پرداخت یافت نشد",
|
PAYMENT_GATEWAY_NOT_FOUND = "درگاه پرداخت یافت نشد",
|
||||||
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
|
ERROR_IN_PROCESS_PAYMENT = "خطا در پردازش پرداخت",
|
||||||
@@ -357,6 +333,12 @@ export const enum PaymentMessage {
|
|||||||
TRANSFER_METHOD_NOT_ALLOWED = "روش انتقال انتخابی مجاز نیست",
|
TRANSFER_METHOD_NOT_ALLOWED = "روش انتقال انتخابی مجاز نیست",
|
||||||
DEPOSIT_ALREADY_APPROVED = "درخواست واریز قبلا تایید شده است",
|
DEPOSIT_ALREADY_APPROVED = "درخواست واریز قبلا تایید شده است",
|
||||||
DEPOSIT_ALREADY_REJECTED = "درخواسن واریز قبلا رد شده است",
|
DEPOSIT_ALREADY_REJECTED = "درخواسن واریز قبلا رد شده است",
|
||||||
|
GATEWAY_REQUIRED = "درگاه پرداخت مورد نیاز است",
|
||||||
|
GATEWAY_ID_SHOULD_BE_UUID = "آیدی درگاه پرداخت باید UUID باشد",
|
||||||
|
INVOICE_ID_REQUIRED = "صورت حساب مورد نیاز است",
|
||||||
|
INVOICE_ID_SHOULD_BE_UUID = "صورت حساب باید UUID باشد",
|
||||||
|
ERROR_IN_PAYMENT = "خطا در پرداخت",
|
||||||
|
CHECKOUT_INVOICE_MESSAGE = "پرداخت صورت حساب شماره [invoiceNumber] ثبت شد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum SettingMessageEnum {
|
export const enum SettingMessageEnum {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export function zarinpalConfig() {
|
|||||||
return {
|
return {
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
useFactory: (configService: ConfigService) => ({
|
useFactory: (configService: ConfigService) => ({
|
||||||
merchantId: configService.getOrThrow<string>("ZARINPAL_MERCHANT_ID"),
|
|
||||||
gatewayApiUrl: configService.getOrThrow<string>("ZARINPAL_API_URL"),
|
gatewayApiUrl: configService.getOrThrow<string>("ZARINPAL_API_URL"),
|
||||||
callBackUrl: configService.getOrThrow<string>("CALLBACK_URL"),
|
callBackUrl: configService.getOrThrow<string>("CALLBACK_URL"),
|
||||||
ipgType: configService.getOrThrow<string>("IPG_TYPE"),
|
ipgType: configService.getOrThrow<string>("IPG_TYPE"),
|
||||||
@@ -13,7 +12,6 @@ export function zarinpalConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IZarinpalConfig {
|
export interface IZarinpalConfig {
|
||||||
merchantId: string;
|
|
||||||
gatewayApiUrl: string;
|
gatewayApiUrl: string;
|
||||||
callBackUrl: string;
|
callBackUrl: string;
|
||||||
ipgType: string;
|
ipgType: string;
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
import {
|
import { BadRequestException, CallHandler, ExecutionContext, ForbiddenException, Injectable, Logger, NestInterceptor } from "@nestjs/common";
|
||||||
BadRequestException,
|
|
||||||
CallHandler,
|
|
||||||
ExecutionContext,
|
|
||||||
ForbiddenException,
|
|
||||||
Injectable,
|
|
||||||
Logger,
|
|
||||||
NestInterceptor,
|
|
||||||
} from "@nestjs/common";
|
|
||||||
import { FastifyRequest } from "fastify";
|
import { FastifyRequest } from "fastify";
|
||||||
import { Observable } from "rxjs";
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,7 @@ export class AnnouncementController {
|
|||||||
|
|
||||||
@ApiProperty({ description: "Get all announcements for user " })
|
@ApiProperty({ description: "Get all announcements for user " })
|
||||||
@Get("user")
|
@Get("user")
|
||||||
getAnnouncementsByUser(
|
getAnnouncementsByUser(@Query() queryDto: SearchAnnouncementQueryDto, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||||
@Query() queryDto: SearchAnnouncementQueryDto,
|
|
||||||
@UserDec("id") userId: string,
|
|
||||||
@BusinessDec("id") businessId: string,
|
|
||||||
) {
|
|
||||||
return this.announcementService.getAnnouncementsByUser(queryDto, userId, businessId);
|
return this.announcementService.getAnnouncementsByUser(queryDto, userId, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ export class Business extends BaseEntity {
|
|||||||
@Property({ type: "varchar", length: 255, nullable: true })
|
@Property({ type: "varchar", length: 255, nullable: true })
|
||||||
logoUrl?: string;
|
logoUrl?: string;
|
||||||
|
|
||||||
|
@Property({ type: "varchar", length: 255, nullable: true })
|
||||||
|
zarinpalMerchantId?: string;
|
||||||
|
|
||||||
//=========================
|
//=========================
|
||||||
|
|
||||||
@OneToMany(() => Company, (company) => company.business, { cascade: [Cascade.ALL] })
|
@OneToMany(() => Company, (company) => company.business, { cascade: [Cascade.ALL] })
|
||||||
|
|||||||
@@ -1,16 +1,4 @@
|
|||||||
import {
|
import { Cascade, Collection, Entity, EntityRepositoryType, Enum, Formula, ManyToOne, OneToMany, OneToOne, Opt, Property } from "@mikro-orm/core";
|
||||||
Cascade,
|
|
||||||
Collection,
|
|
||||||
Entity,
|
|
||||||
EntityRepositoryType,
|
|
||||||
Enum,
|
|
||||||
Formula,
|
|
||||||
ManyToOne,
|
|
||||||
OneToMany,
|
|
||||||
OneToOne,
|
|
||||||
Opt,
|
|
||||||
Property,
|
|
||||||
} from "@mikro-orm/core";
|
|
||||||
|
|
||||||
import { CompanyProduct } from "./company-product.entity";
|
import { CompanyProduct } from "./company-product.entity";
|
||||||
import { CompanyService } from "./company-service.entity";
|
import { CompanyService } from "./company-service.entity";
|
||||||
|
|||||||
@@ -366,8 +366,7 @@ export class CompaniesService {
|
|||||||
services,
|
services,
|
||||||
user,
|
user,
|
||||||
business,
|
business,
|
||||||
chiefExecutiveOfficer:
|
chiefExecutiveOfficer: "chiefExecutiveOfficer" in createDto ? createDto.chiefExecutiveOfficer : `${createDto.firstName} ${createDto.lastName}`,
|
||||||
"chiefExecutiveOfficer" in createDto ? createDto.chiefExecutiveOfficer : `${createDto.firstName} ${createDto.lastName}`,
|
|
||||||
email: createDto.email,
|
email: createDto.email,
|
||||||
phone: createDto.phone,
|
phone: createDto.phone,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,12 +46,7 @@ export class InvoicesController {
|
|||||||
|
|
||||||
@ApiOperation({ summary: "get single invoice by Id " })
|
@ApiOperation({ summary: "get single invoice by Id " })
|
||||||
@Get(":id")
|
@Get(":id")
|
||||||
getInvoiceById(
|
getInvoiceById(@Param() paramDto: ParamDto, @UserDec("role") role: RoleEnum, @UserDec("id") userId: string, @BusinessDec("id") businessId: string) {
|
||||||
@Param() paramDto: ParamDto,
|
|
||||||
@UserDec("role") role: RoleEnum,
|
|
||||||
@UserDec("id") userId: string,
|
|
||||||
@BusinessDec("id") businessId: string,
|
|
||||||
) {
|
|
||||||
return this.invoiceService.getInvoiceById(paramDto.id, role, userId, businessId);
|
return this.invoiceService.getInvoiceById(paramDto.id, role, userId, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,13 +199,7 @@ export class InvoicesService {
|
|||||||
const otpCode = await this.otpService.generateAndSetInCache(user.phone, "INVOICE_VERIFY");
|
const otpCode = await this.otpService.generateAndSetInCache(user.phone, "INVOICE_VERIFY");
|
||||||
const items = invoice.items.map((item) => item.name).join(", ");
|
const items = invoice.items.map((item) => item.name).join(", ");
|
||||||
|
|
||||||
await this.smsService.sendInvoiceVerifyCode(
|
await this.smsService.sendInvoiceVerifyCode(user.phone, otpCode, invoice.numericId, new Decimal(invoice.totalPrice).toNumber(), items);
|
||||||
user.phone,
|
|
||||||
otpCode,
|
|
||||||
invoice.numericId,
|
|
||||||
new Decimal(invoice.totalPrice).toNumber(),
|
|
||||||
items,
|
|
||||||
);
|
|
||||||
|
|
||||||
await em.commit();
|
await em.commit();
|
||||||
|
|
||||||
@@ -318,10 +312,7 @@ export class InvoicesService {
|
|||||||
{ populate: ["items", "company"] },
|
{ populate: ["items", "company"] },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
invoice = await this.invoiceRepository.findOne(
|
invoice = await this.invoiceRepository.findOne({ id: invoiceId, business: { id: businessId } }, { populate: ["items", "company", "business"] });
|
||||||
{ id: invoiceId, business: { id: businessId } },
|
|
||||||
{ populate: ["items", "company", "business"] },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||||
@@ -360,88 +351,48 @@ export class InvoicesService {
|
|||||||
}
|
}
|
||||||
//*********************************** */
|
//*********************************** */
|
||||||
|
|
||||||
// async payInvoice(invoiceId: string, userId: string, em = this.em.fork()) {
|
async payInvoice(invoiceId: string, userId: string, em = this.em.fork()) {
|
||||||
// let transactionStarted = false;
|
let transactionStarted = false;
|
||||||
|
|
||||||
// try {
|
try {
|
||||||
// if (!em.isInTransaction) {
|
if (!em.isInTransaction) {
|
||||||
// await em.begin();
|
await em.begin();
|
||||||
// transactionStarted = true;
|
transactionStarted = true;
|
||||||
// }
|
}
|
||||||
|
|
||||||
// const user = await this.usersService.findOneByIdWithEntityManager(userId, em);
|
const user = await this.usersService.findOneByIdWithEntityManager(userId, em);
|
||||||
|
|
||||||
// const invoice = await this.getPendingInvoiceByIdWithQueryRunner(invoiceId, user.id, em);
|
const invoice = await this.getPendingInvoiceByIdWithQueryRunner(invoiceId, user.id, em);
|
||||||
|
|
||||||
// if (invoice.status !== InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_PAID);
|
if (invoice.status !== InvoiceStatus.WAIT_PAYMENT) throw new BadRequestException(InvoiceMessage.INVOICE_CAN_NOT_PAID);
|
||||||
|
|
||||||
// const userWallet = await this.walletsService.getWalletByUserId(user.id, em);
|
invoice.status = InvoiceStatus.PAID;
|
||||||
|
invoice.paidAt = dayjs().toDate();
|
||||||
|
|
||||||
// if (userWallet.balance < invoice.totalPrice) throw new BadRequestException(WalletMessage.INSUFFICIENT_BALANCE);
|
await em.persistAndFlush(invoice);
|
||||||
|
|
||||||
// userWallet.balance = new Decimal(userWallet.balance).sub(invoice.totalPrice);
|
await this.notificationQueue.addBillInvoiceNotification(userId, {
|
||||||
|
invoiceId: invoice.numericId.toString(),
|
||||||
|
dueDate: invoice.dueDate,
|
||||||
|
createDate: invoice.createdAt,
|
||||||
|
price: new Decimal(invoice.totalPrice).toNumber(),
|
||||||
|
userPhone: user.phone,
|
||||||
|
userEmail: user.email,
|
||||||
|
items: invoice.items.map((item) => item.name).join(", "),
|
||||||
|
paidAt: invoice.paidAt,
|
||||||
|
});
|
||||||
|
|
||||||
// await em.persistAndFlush(userWallet);
|
if (transactionStarted) await em.commit();
|
||||||
|
|
||||||
// if (invoice.items[0]?.subscriptionPlan) {
|
return {
|
||||||
// const userSubscription = invoice.items[0].subscriptionPlan;
|
message: InvoiceMessage.INVOICE_PAID,
|
||||||
// userSubscription.status = SubscriptionStatus.ACTIVE;
|
invoice,
|
||||||
|
};
|
||||||
// //
|
} catch (error) {
|
||||||
// await queryRunner.manager.save(UserSubscription, userSubscription);
|
if (transactionStarted) await em.rollback();
|
||||||
// await this.walletsService.createSubscriptionTransaction(invoice.totalPrice, userWallet.id, queryRunner);
|
throw error;
|
||||||
// await this.addNotifyForWalletDeduction(invoice, user, userWallet, WalletMessage.SUBSCRIPTION_WALLET_TRANSFER);
|
}
|
||||||
// await this.addNotifyAdminForSubscriptionPaymentToQueue(invoice);
|
}
|
||||||
// //
|
|
||||||
// } else if (invoice.items[0]?.supportPlan) {
|
|
||||||
// const oldUserSupportPlan = await queryRunner.manager.findOne(UserSupportPlan, {
|
|
||||||
// where: { user: { id: user.id }, status: UserSupportPlanStatus.ACTIVE },
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (oldUserSupportPlan) {
|
|
||||||
// await queryRunner.manager.update(UserSupportPlan, oldUserSupportPlan.id, { status: UserSupportPlanStatus.INACTIVE });
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const userSupportPlan = invoice.items[0].supportPlan;
|
|
||||||
// userSupportPlan.status = UserSupportPlanStatus.ACTIVE;
|
|
||||||
|
|
||||||
// await queryRunner.manager.save(UserSupportPlan, userSupportPlan);
|
|
||||||
// await this.walletsService.createSupportPlanTransaction(invoice.totalPrice, userWallet.id, queryRunner);
|
|
||||||
// await this.addNotifyForWalletDeduction(invoice, user, userWallet, WalletMessage.SUPPORT_PLAN_WALLET_TRANSFER);
|
|
||||||
// //
|
|
||||||
// } else {
|
|
||||||
// await this.walletsService.createInvoiceTransaction(invoice.totalPrice, userWallet.id, queryRunner);
|
|
||||||
// await this.addNotifyForWalletDeduction(invoice, user, userWallet, WalletMessage.INVOICE_WALLET_TRANSFER);
|
|
||||||
// }
|
|
||||||
// invoice.status = InvoiceStatus.PAID;
|
|
||||||
// invoice.paidAt = dayjs().toDate();
|
|
||||||
|
|
||||||
// await queryRunner.manager.save(Invoice, invoice);
|
|
||||||
|
|
||||||
// await this.notificationQueue.addBillInvoiceNotification(userId, {
|
|
||||||
// invoiceId: invoice.numericId.toString(),
|
|
||||||
// dueDate: invoice.dueDate,
|
|
||||||
// createDate: invoice.createdAt,
|
|
||||||
// price: new Decimal(invoice.totalPrice).toNumber(),
|
|
||||||
// userPhone: user.phone,
|
|
||||||
// userEmail: user.email,
|
|
||||||
// items: invoice.items.map((item) => item.name).join(", "),
|
|
||||||
// paidAt: invoice.paidAt,
|
|
||||||
// });
|
|
||||||
|
|
||||||
// if (transactionStarted) await queryRunner.commitTransaction();
|
|
||||||
|
|
||||||
// return {
|
|
||||||
// message: InvoiceMessage.INVOICE_PAID,
|
|
||||||
// invoice,
|
|
||||||
// };
|
|
||||||
// } catch (error) {
|
|
||||||
// if (transactionStarted) await queryRunner.rollbackTransaction();
|
|
||||||
// throw error;
|
|
||||||
// } finally {
|
|
||||||
// if (transactionStarted) await queryRunner.release();
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
//*********************************** */
|
//*********************************** */
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Controller, Get, Param, Patch, Query } from "@nestjs/common";
|
import { Controller, Get, Param, Patch, Query } from "@nestjs/common";
|
||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { SearchNotificationQueryDto } from "./DTO/search-notification-query.dto";
|
import { SearchNotificationQueryDto } from "./DTO/search-notification-query.dto";
|
||||||
import { NotificationsService } from "./providers/notifications.service";
|
import { NotificationsService } from "./providers/notifications.service";
|
||||||
@@ -9,7 +9,6 @@ import { UserDec } from "../../common/decorators/user.decorator";
|
|||||||
import { ParamDto } from "../../common/DTO/param.dto";
|
import { ParamDto } from "../../common/DTO/param.dto";
|
||||||
|
|
||||||
@Controller("notifications")
|
@Controller("notifications")
|
||||||
@ApiTags("Notifications")
|
|
||||||
@AuthGuards()
|
@AuthGuards()
|
||||||
export class NotificationController {
|
export class NotificationController {
|
||||||
constructor(private readonly notificationService: NotificationsService) {}
|
constructor(private readonly notificationService: NotificationsService) {}
|
||||||
|
|||||||
@@ -142,10 +142,7 @@ export class NotificationsService {
|
|||||||
);
|
);
|
||||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||||
|
|
||||||
return this.createNotification(
|
return this.createNotification({ title: NotificationMessage.INVOICE_CREATION, type: NotifType.CREATE_INVOICE, message, recipientId }, em);
|
||||||
{ title: NotificationMessage.INVOICE_CREATION, type: NotifType.CREATE_INVOICE, message, recipientId },
|
|
||||||
em,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
//************************ */
|
//************************ */
|
||||||
|
|
||||||
@@ -238,10 +235,7 @@ export class NotificationsService {
|
|||||||
await this.smsService.sendInvoiceOverdueSms(data.userPhone, data.invoiceId, data.price, data.lateFee || 0, dateFormat(data.dueDate));
|
await this.smsService.sendInvoiceOverdueSms(data.userPhone, data.invoiceId, data.price, data.lateFee || 0, dateFormat(data.dueDate));
|
||||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||||
|
|
||||||
return this.createNotification(
|
return this.createNotification({ title: NotificationMessage.INVOICE_OVERDUE, type: NotifType.INVOICE_OVERDUE, message, recipientId }, em);
|
||||||
{ title: NotificationMessage.INVOICE_OVERDUE, type: NotifType.INVOICE_OVERDUE, message, recipientId },
|
|
||||||
em,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//************************ */
|
//************************ */
|
||||||
@@ -252,10 +246,7 @@ export class NotificationsService {
|
|||||||
|
|
||||||
await this.smsService.sendRecurringInvoiceDraftSms(data.userPhone, data.invoiceId, data.price);
|
await this.smsService.sendRecurringInvoiceDraftSms(data.userPhone, data.invoiceId, data.price);
|
||||||
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
if (data.userEmail) await this.emailService.sendInvoiceEmail(data);
|
||||||
return this.createNotification(
|
return this.createNotification({ title: NotificationMessage.RECURRING_INVOICE, type: NotifType.RECURRING_INVOICE, message, recipientId }, em);
|
||||||
{ title: NotificationMessage.RECURRING_INVOICE, type: NotifType.RECURRING_INVOICE, message, recipientId },
|
|
||||||
em,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// //************************ */
|
// //************************ */
|
||||||
|
|||||||
@@ -18,12 +18,7 @@ import { NotificationsService } from "../providers/notifications.service";
|
|||||||
type NotificationJobData = {
|
type NotificationJobData = {
|
||||||
type: NotifType;
|
type: NotifType;
|
||||||
recipientId: string;
|
recipientId: string;
|
||||||
data:
|
data: ITicketNotificationData | IInvoiceNotificationData | IAnnouncementNotificationData | IPaymentNotificationData | IOtpNotificationData;
|
||||||
| ITicketNotificationData
|
|
||||||
| IInvoiceNotificationData
|
|
||||||
| IAnnouncementNotificationData
|
|
||||||
| IPaymentNotificationData
|
|
||||||
| IOtpNotificationData;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
@Processor(NOTIFICATION.QUEUE_NAME, { concurrency: 5 })
|
@Processor(NOTIFICATION.QUEUE_NAME, { concurrency: 5 })
|
||||||
@@ -68,11 +63,7 @@ export class NotificationProcessor extends WorkerProcessor {
|
|||||||
await this.notificationsService.createTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
await this.notificationsService.createTicketNotification(recipientId, data as ITicketNotificationData, entityManager);
|
||||||
break;
|
break;
|
||||||
case NotifType.ASSIGN_TICKET:
|
case NotifType.ASSIGN_TICKET:
|
||||||
await this.notificationsService.createAssignTicketNotificationForAdmin(
|
await this.notificationsService.createAssignTicketNotificationForAdmin(recipientId, data as ITicketNotificationData, entityManager);
|
||||||
recipientId,
|
|
||||||
data as ITicketNotificationData,
|
|
||||||
entityManager,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Invoice Notifications
|
// Invoice Notifications
|
||||||
@@ -80,11 +71,7 @@ export class NotificationProcessor extends WorkerProcessor {
|
|||||||
await this.notificationsService.createInvoiceCreationNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
await this.notificationsService.createInvoiceCreationNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||||
break;
|
break;
|
||||||
case NotifType.BILL_INVOICE_REMINDER:
|
case NotifType.BILL_INVOICE_REMINDER:
|
||||||
await this.notificationsService.createBillInvoiceReminderNotification(
|
await this.notificationsService.createBillInvoiceReminderNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||||
recipientId,
|
|
||||||
data as IInvoiceNotificationData,
|
|
||||||
entityManager,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case NotifType.BILL_INVOICE:
|
case NotifType.BILL_INVOICE:
|
||||||
await this.notificationsService.createBillInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
await this.notificationsService.createBillInvoiceNotification(recipientId, data as IInvoiceNotificationData, entityManager);
|
||||||
@@ -104,11 +91,7 @@ export class NotificationProcessor extends WorkerProcessor {
|
|||||||
await this.notificationsService.createPaymentReminderNotification(recipientId, data as IPaymentNotificationData, entityManager);
|
await this.notificationsService.createPaymentReminderNotification(recipientId, data as IPaymentNotificationData, entityManager);
|
||||||
break;
|
break;
|
||||||
case NotifType.PAYMENT_CANCELLATION:
|
case NotifType.PAYMENT_CANCELLATION:
|
||||||
await this.notificationsService.createPaymentCancellationNotification(
|
await this.notificationsService.createPaymentCancellationNotification(recipientId, data as IPaymentNotificationData, entityManager);
|
||||||
recipientId,
|
|
||||||
data as IPaymentNotificationData,
|
|
||||||
entityManager,
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
import { IsNotEmpty, IsUUID } from "class-validator";
|
||||||
|
|
||||||
|
import { PaymentMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
|
export class CheckoutPaymentDto {
|
||||||
|
@IsNotEmpty({ message: PaymentMessage.GATEWAY_REQUIRED })
|
||||||
|
@IsUUID("4", { message: PaymentMessage.GATEWAY_ID_SHOULD_BE_UUID })
|
||||||
|
@ApiProperty({ description: "Gateway id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
|
||||||
|
gatewayId: string;
|
||||||
|
|
||||||
|
@IsNotEmpty({ message: PaymentMessage.INVOICE_ID_REQUIRED })
|
||||||
|
@IsUUID("4", { message: PaymentMessage.INVOICE_ID_SHOULD_BE_UUID })
|
||||||
|
@ApiProperty({ description: "Invoice id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
|
||||||
|
invoiceId: string;
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
|
||||||
import { IsEnum, IsInt, IsNotEmpty, IsUUID, IsUrl, Min } from "class-validator";
|
|
||||||
|
|
||||||
import { WalletMessage } from "../../../common/enums/message.enum";
|
|
||||||
import { TransferType } from "../enums/payment-type.enum";
|
|
||||||
|
|
||||||
export class DepositDto {
|
|
||||||
@IsNotEmpty({ message: WalletMessage.AMOUNT_REQUIRED })
|
|
||||||
@IsInt({ message: WalletMessage.AMOUNT_MUST_BE_INTEGER })
|
|
||||||
@Min(100_000, { message: WalletMessage.AMOUNT_MINIMUM })
|
|
||||||
@ApiProperty({ description: "Amount to charge", example: 100_000 })
|
|
||||||
amount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class GatewayDepositDto extends DepositDto {
|
|
||||||
// @IsNotEmpty({ message: WalletMessage.GATEWAY_REQUIRED })
|
|
||||||
// @IsEnum(GatewayEnum)
|
|
||||||
// @ApiProperty({ description: "Gateway to use", example: "zarinpal" })
|
|
||||||
// gateway: GatewayEnum;
|
|
||||||
|
|
||||||
@IsNotEmpty({ message: WalletMessage.GATEWAY_REQUIRED })
|
|
||||||
@IsUUID("4", { message: WalletMessage.GATEWAY_ID_SHOULD_BE_UUID })
|
|
||||||
@ApiProperty({ description: "Gateway id to pay", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
|
|
||||||
gatewayId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class TransferDepositDto extends DepositDto {
|
|
||||||
@IsNotEmpty({ message: WalletMessage.RECEIPT_URL_REQUIRED })
|
|
||||||
@IsUrl({ protocols: ["http", "https"], require_protocol: true }, { message: WalletMessage.RECEIPT_URL_SHOULD_BE_URL })
|
|
||||||
@ApiProperty({ description: "the transferReceipt url", example: "https://example.com/image.png" })
|
|
||||||
transferReceiptUrl: string;
|
|
||||||
|
|
||||||
@IsNotEmpty({ message: WalletMessage.BANK_ACCOUNT_ID_REQUIRED })
|
|
||||||
@IsUUID("4", { message: WalletMessage.BANK_ACCOUNT_ID_SHOULD_BE_UUID })
|
|
||||||
@ApiProperty({ description: "bank account id", example: "e6fdce2a-9f91-47c4-8561-48368fc275b5" })
|
|
||||||
bankAccountId: string;
|
|
||||||
|
|
||||||
@IsNotEmpty({ message: WalletMessage.TRANSFER_METHOD_REQUIRED })
|
|
||||||
@IsEnum(TransferType)
|
|
||||||
@ApiProperty({ description: "transfer method", example: TransferType.CARD_TO_CARD, enum: TransferType })
|
|
||||||
method: TransferType;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { ApiProperty } from "@nestjs/swagger";
|
|
||||||
import { IsEnum, IsNotEmpty } from "class-validator";
|
|
||||||
|
|
||||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
|
||||||
import { CommonMessage } from "../../../common/enums/message.enum";
|
|
||||||
import { TransferType } from "../enums/payment-type.enum";
|
|
||||||
|
|
||||||
export class PaymentTransactionQueryDto extends PaginationDto {
|
|
||||||
@IsNotEmpty({ message: CommonMessage.PaymentTypeQueryNotEmpty })
|
|
||||||
@IsEnum(TransferType)
|
|
||||||
@ApiProperty({
|
|
||||||
description: `payment type ${TransferType.CARD_TO_CARD} ${TransferType.SHEBA} `,
|
|
||||||
enum: TransferType,
|
|
||||||
example: TransferType.CARD_TO_CARD,
|
|
||||||
})
|
|
||||||
type: TransferType;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsEnum, IsNotEmpty, IsOptional } from "class-validator";
|
import { IsEnum, IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
import { GatewayEnum } from "../enums/gateway.enum";
|
import { GatewayEnum } from "../enums/gateway.enum";
|
||||||
import { GatewayType } from "../types/gateway.type";
|
import { GatewayType } from "../types/gateway.type";
|
||||||
@@ -8,10 +8,6 @@ export class VerifyParamDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEnum(GatewayEnum)
|
@IsEnum(GatewayEnum)
|
||||||
gateway: GatewayType;
|
gateway: GatewayType;
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNotEmpty()
|
|
||||||
invoiceId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class VerifyQueryDto {
|
export class VerifyQueryDto {
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export enum TransferType {
|
|
||||||
CARD_TO_CARD = "CARD_TO_CARD",
|
|
||||||
SHEBA = "SHEBA",
|
|
||||||
// GATEWAY = "GATEWAY",
|
|
||||||
}
|
|
||||||
@@ -32,9 +32,9 @@ export class ZarinpalGateway implements IPaymentGateway {
|
|||||||
async processPayment(processParams: IProcessPaymentParams) {
|
async processPayment(processParams: IProcessPaymentParams) {
|
||||||
try {
|
try {
|
||||||
const purchaseData: ZarinPalPGNewArgs = {
|
const purchaseData: ZarinPalPGNewArgs = {
|
||||||
merchant_id: this.config.merchantId,
|
merchant_id: processParams.merchantId,
|
||||||
amount: processParams.amount,
|
amount: processParams.amount,
|
||||||
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}${processParams.invoiceId ? "/" + processParams.invoiceId : ""}`,
|
callback_url: `${this.config.callBackUrl}/${GatewayEnum.ZARINPAL}`,
|
||||||
description: processParams.description,
|
description: processParams.description,
|
||||||
currency: "IRT",
|
currency: "IRT",
|
||||||
metadata: { email: processParams.email, mobile: processParams.mobile },
|
metadata: { email: processParams.email, mobile: processParams.mobile },
|
||||||
@@ -68,17 +68,15 @@ export class ZarinpalGateway implements IPaymentGateway {
|
|||||||
const verifyData = {
|
const verifyData = {
|
||||||
authority: verifyParams.reference,
|
authority: verifyParams.reference,
|
||||||
amount: verifyParams.amount,
|
amount: verifyParams.amount,
|
||||||
merchant_id: this.config.merchantId,
|
merchant_id: verifyParams.merchantId,
|
||||||
};
|
};
|
||||||
const { data } = await firstValueFrom(
|
const { data } = await firstValueFrom(
|
||||||
this.httpService
|
this.httpService.post<ZarinPalPGVerifyData>(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader }).pipe(
|
||||||
.post<ZarinPalPGVerifyData>(`${this.gatewayApiUrl}/v4/payment/verify.json`, verifyData, { headers: this.requestHeader })
|
catchError((err: AxiosError) => {
|
||||||
.pipe(
|
this.logger.error(err);
|
||||||
catchError((err: AxiosError) => {
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
||||||
this.logger.error(err);
|
}),
|
||||||
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_VERIFY_PAYMENT);
|
),
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import { GatewayType } from "../types/gateway.type";
|
|||||||
export interface IProcessPaymentParams {
|
export interface IProcessPaymentParams {
|
||||||
amount: number;
|
amount: number;
|
||||||
description: string;
|
description: string;
|
||||||
// callBackPath: string;
|
callBackPath: string;
|
||||||
|
merchantId: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
mobile?: string;
|
mobile?: string;
|
||||||
invoiceId?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessPaymentData {
|
export interface IProcessPaymentData {
|
||||||
@@ -32,6 +32,7 @@ export interface IVerifyPayment {
|
|||||||
export interface IPaymentVerifyParams {
|
export interface IPaymentVerifyParams {
|
||||||
amount: Decimal;
|
amount: Decimal;
|
||||||
reference: string;
|
reference: string;
|
||||||
|
merchantId: string;
|
||||||
// status: GatewayPaymentStatus;
|
// status: GatewayPaymentStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,42 @@
|
|||||||
import { Controller } from "@nestjs/common";
|
import { Body, Controller, Get, Param, Post, Query, Res } from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { FastifyReply } from "fastify";
|
||||||
|
|
||||||
|
import { CheckoutPaymentDto } from "./DTO/checkout-payment.dto";
|
||||||
|
import { VerifyParamDto, VerifyQueryDto } from "./DTO/verify-payment.dto";
|
||||||
|
import { PaymentsService } from "./services/payments.service";
|
||||||
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
|
import { UserDec } from "../../common/decorators/user.decorator";
|
||||||
|
import { PaginationDto } from "../../common/DTO/pagination.dto";
|
||||||
|
|
||||||
@Controller("payments")
|
@Controller("payments")
|
||||||
export class PaymentsController {}
|
@ApiTags("Payments")
|
||||||
|
export class PaymentsController {
|
||||||
|
constructor(private readonly paymentsService: PaymentsService) {}
|
||||||
|
|
||||||
|
@AuthGuards()
|
||||||
|
@ApiOperation({ summary: "get all available gateways" })
|
||||||
|
@Get("gateways")
|
||||||
|
getAvailableGateways() {
|
||||||
|
return this.paymentsService.getAvailableGateways();
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "Charge wallet ==> user route" })
|
||||||
|
@AuthGuards()
|
||||||
|
@Post("checkout")
|
||||||
|
checkoutPayment(@Body() checkoutDto: CheckoutPaymentDto, @UserDec("id") userId: string) {
|
||||||
|
return this.paymentsService.checkoutPayment(checkoutDto, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ApiOperation({ summary: "get deposit gateway payment ==> admin route" })
|
||||||
|
@AuthGuards()
|
||||||
|
@Get("checkout/gateway")
|
||||||
|
getDepositGatewayPayment(@Query() queryDto: PaginationDto) {
|
||||||
|
return this.paymentsService.getDepositGatewayPayment(queryDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get("verify/:gateway")
|
||||||
|
verifyPayments(@Param() paramDto: VerifyParamDto, @Query() queryDto: VerifyQueryDto, @Res({ passthrough: true }) rep: FastifyReply) {
|
||||||
|
return this.paymentsService.verifyPayment(paramDto, queryDto, rep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { PaymentsController } from "./payments.controller";
|
|||||||
import { PaymentProcessor } from "./queue/payment.processor";
|
import { PaymentProcessor } from "./queue/payment.processor";
|
||||||
import { PaymentsService } from "./services/payments.service";
|
import { PaymentsService } from "./services/payments.service";
|
||||||
import { zarinpalConfig } from "../../configs/zarinpal.config";
|
import { zarinpalConfig } from "../../configs/zarinpal.config";
|
||||||
|
import { InvoicesModule } from "../invoices/invoices.module";
|
||||||
import { NotificationModule } from "../notifications/notifications.module";
|
import { NotificationModule } from "../notifications/notifications.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
MikroOrmModule.forFeature([Payment, PaymentGateway]),
|
MikroOrmModule.forFeature([Payment, PaymentGateway]),
|
||||||
@@ -24,6 +24,7 @@ import { NotificationModule } from "../notifications/notifications.module";
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
NotificationModule,
|
NotificationModule,
|
||||||
|
InvoicesModule,
|
||||||
],
|
],
|
||||||
controllers: [PaymentsController],
|
controllers: [PaymentsController],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { EntityManager } from "@mikro-orm/postgresql";
|
import { EntityManager } from "@mikro-orm/postgresql";
|
||||||
import { InjectQueue, Processor } from "@nestjs/bullmq";
|
import { InjectQueue, Processor } from "@nestjs/bullmq";
|
||||||
import { Logger } from "@nestjs/common";
|
import { Logger } from "@nestjs/common";
|
||||||
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Job, Queue } from "bullmq";
|
import { Job, Queue } from "bullmq";
|
||||||
import Decimal from "decimal.js";
|
import Decimal from "decimal.js";
|
||||||
|
|
||||||
@@ -16,15 +17,18 @@ import { PaymentJobData } from "../types/payment-job.type";
|
|||||||
@Processor(PAYMENT.QUEUE_NAME)
|
@Processor(PAYMENT.QUEUE_NAME)
|
||||||
export class PaymentProcessor extends WorkerProcessor {
|
export class PaymentProcessor extends WorkerProcessor {
|
||||||
protected readonly logger = new Logger(PaymentProcessor.name);
|
protected readonly logger = new Logger(PaymentProcessor.name);
|
||||||
|
private readonly zarinpalMerchantId;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@InjectQueue(PAYMENT.QUEUE_NAME) private readonly paymentQueue: Queue,
|
@InjectQueue(PAYMENT.QUEUE_NAME) private readonly paymentQueue: Queue,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
private readonly gatewayFactory: PaymentGatewayFactory,
|
private readonly gatewayFactory: PaymentGatewayFactory,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly notificationQueue: NotificationQueue,
|
private readonly notificationQueue: NotificationQueue,
|
||||||
private readonly paymentsService: PaymentsService,
|
private readonly paymentsService: PaymentsService,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
this.zarinpalMerchantId = this.configService.getOrThrow<string>("ZARINPAL_MERCHANT_ID");
|
||||||
}
|
}
|
||||||
//===============================================
|
//===============================================
|
||||||
async process(job: Job<PaymentJobData>) {
|
async process(job: Job<PaymentJobData>) {
|
||||||
@@ -177,7 +181,7 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
id: paymentId,
|
id: paymentId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
populate: ["user", "paymentGateway"],
|
populate: ["user", "paymentGateway", "business", "invoice"],
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -198,7 +202,11 @@ export class PaymentProcessor extends WorkerProcessor {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const gateway = this.gatewayFactory.getPaymentGateway(payment.paymentGateway.name);
|
const gateway = this.gatewayFactory.getPaymentGateway(payment.paymentGateway.name);
|
||||||
const verifyData = await gateway.verifyPayment({ reference: payment.reference, amount: payment.amount });
|
const verifyData = await gateway.verifyPayment({
|
||||||
|
reference: payment.reference,
|
||||||
|
amount: payment.amount,
|
||||||
|
merchantId: payment.business.zarinpalMerchantId || this.zarinpalMerchantId,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await this.paymentsService.processVerificationResult(verifyData, payment, em);
|
const result = await this.paymentsService.processVerificationResult(verifyData, payment, em);
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import { LockMode } from "@mikro-orm/core";
|
import { EntityManager, LockMode } from "@mikro-orm/postgresql";
|
||||||
import { EntityManager } from "@mikro-orm/postgresql";
|
|
||||||
import { InjectQueue } from "@nestjs/bullmq";
|
import { InjectQueue } from "@nestjs/bullmq";
|
||||||
import { BadRequestException, HttpStatus, Injectable, Logger } from "@nestjs/common";
|
import { BadRequestException, HttpException, HttpStatus, Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||||
import { ConfigService } from "@nestjs/config";
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { Queue } from "bullmq";
|
import { Queue } from "bullmq";
|
||||||
import { Decimal } from "decimal.js";
|
import Decimal from "decimal.js";
|
||||||
import { FastifyReply as FRply } from "fastify";
|
import { FastifyReply as FRply } from "fastify";
|
||||||
|
|
||||||
import { PaymentMessage } from "../../../common/enums/message.enum";
|
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||||
|
import { InvoiceMessage, PaymentMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||||
import { Business } from "../../businesses/entities/business.entity";
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { Invoice } from "../../invoices/entities/invoice.entity";
|
import { Invoice } from "../../invoices/entities/invoice.entity";
|
||||||
|
import { InvoicesService } from "../../invoices/providers/invoices.service";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
import { PaginationUtils } from "../../utils/providers/pagination.utils";
|
||||||
import { PAYMENT } from "../constants";
|
import { PAYMENT } from "../constants";
|
||||||
|
import { CheckoutPaymentDto } from "../DTO/checkout-payment.dto";
|
||||||
import { VerifyParamDto, VerifyQueryDto } from "../DTO/verify-payment.dto";
|
import { VerifyParamDto, VerifyQueryDto } from "../DTO/verify-payment.dto";
|
||||||
import { PaymentGateway } from "../entities/payment-gateway.entity";
|
import { PaymentGateway } from "../entities/payment-gateway.entity";
|
||||||
import { Payment } from "../entities/payment.entity";
|
import { Payment } from "../entities/payment.entity";
|
||||||
@@ -25,15 +28,65 @@ import { GatewayType } from "../types/gateway.type";
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class PaymentsService {
|
export class PaymentsService {
|
||||||
private readonly logger = new Logger(PaymentsService.name);
|
private readonly logger = new Logger(PaymentsService.name);
|
||||||
|
private readonly zarinpalMerchantId;
|
||||||
constructor(
|
constructor(
|
||||||
@InjectQueue(PAYMENT.QUEUE_NAME) private readonly paymentQueue: Queue,
|
@InjectQueue(PAYMENT.QUEUE_NAME) private readonly paymentQueue: Queue,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly gatewayFactory: PaymentGatewayFactory,
|
private readonly gatewayFactory: PaymentGatewayFactory,
|
||||||
private readonly paymentGatewaysRepository: PaymentGatewaysRepository,
|
private readonly paymentGatewaysRepository: PaymentGatewaysRepository,
|
||||||
private readonly paymentsRepository: PaymentRepository,
|
private readonly paymentsRepository: PaymentRepository,
|
||||||
|
private readonly invoiceService: InvoicesService,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
) {}
|
) {
|
||||||
|
this.zarinpalMerchantId = this.configService.getOrThrow<string>("ZARINPAL_MERCHANT_ID");
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
async checkoutPayment(chargeDto: CheckoutPaymentDto, userId: string) {
|
||||||
|
const em = this.em.fork();
|
||||||
|
try {
|
||||||
|
em.begin();
|
||||||
|
|
||||||
|
const { gatewayId, invoiceId } = chargeDto;
|
||||||
|
|
||||||
|
const user = await this.getUserById(userId, em);
|
||||||
|
const paymentGateway = await this.getPaymentGatewayById(gatewayId, em);
|
||||||
|
|
||||||
|
const invoice = await em.findOne(Invoice, { id: invoiceId }, { populate: ["company", "business", "company.user"] });
|
||||||
|
if (!invoice) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID);
|
||||||
|
|
||||||
|
if (invoice.company.user.id !== user.id) throw new BadRequestException(InvoiceMessage.NOT_FOUND_BY_ID_OR_NOT_BELONG_TO_USER);
|
||||||
|
|
||||||
|
const gatewayData = await this.processPayment(paymentGateway.name, {
|
||||||
|
amount: invoice.totalPrice.toNumber(),
|
||||||
|
description: PaymentMessage.CHECKOUT_INVOICE_MESSAGE.replace("[invoiceNumber]", invoice.numericId.toString()),
|
||||||
|
email: user.email,
|
||||||
|
mobile: user.phone,
|
||||||
|
callBackPath: invoice.business.slug,
|
||||||
|
merchantId: invoice.business.zarinpalMerchantId || this.zarinpalMerchantId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payment = await this.createGatewayPaymentForUser(
|
||||||
|
user,
|
||||||
|
invoice.totalPrice.toNumber(),
|
||||||
|
gatewayData.reference,
|
||||||
|
paymentGateway.id,
|
||||||
|
invoice.business.id,
|
||||||
|
invoice.id,
|
||||||
|
em,
|
||||||
|
);
|
||||||
|
|
||||||
|
await em.commit();
|
||||||
|
return {
|
||||||
|
payment,
|
||||||
|
...gatewayData,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
await em.rollback();
|
||||||
|
if (error instanceof HttpException) throw error;
|
||||||
|
throw new InternalServerErrorException(PaymentMessage.ERROR_IN_PAYMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//===============================================
|
//===============================================
|
||||||
|
|
||||||
@@ -44,8 +97,6 @@ export class PaymentsService {
|
|||||||
//===============================================
|
//===============================================
|
||||||
|
|
||||||
async verifyPayment(verifyParamDto: VerifyParamDto, queryDto: VerifyQueryDto, rep: FRply) {
|
async verifyPayment(verifyParamDto: VerifyParamDto, queryDto: VerifyQueryDto, rep: FRply) {
|
||||||
const frontUrl = this.buildFrontendRedirectUrl(verifyParamDto.invoiceId);
|
|
||||||
|
|
||||||
const em = this.em.fork();
|
const em = this.em.fork();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -53,6 +104,7 @@ export class PaymentsService {
|
|||||||
|
|
||||||
const paymentGateway = this.gatewayFactory.getPaymentGateway(verifyParamDto.gateway);
|
const paymentGateway = this.gatewayFactory.getPaymentGateway(verifyParamDto.gateway);
|
||||||
const payment = await this.getPaymentByReference(queryDto.Authority, em);
|
const payment = await this.getPaymentByReference(queryDto.Authority, em);
|
||||||
|
const frontUrl = this.buildFrontendRedirectUrl(payment.business.slug);
|
||||||
|
|
||||||
if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE);
|
if (payment.status === PaymentStatus.COMPLETED) throw new BadRequestException(PaymentMessage.VALIDATED_BEFORE);
|
||||||
|
|
||||||
@@ -61,6 +113,7 @@ export class PaymentsService {
|
|||||||
const verifyData = await paymentGateway.verifyPayment({
|
const verifyData = await paymentGateway.verifyPayment({
|
||||||
reference: queryDto.Authority,
|
reference: queryDto.Authority,
|
||||||
amount: payment.amount,
|
amount: payment.amount,
|
||||||
|
merchantId: payment.business.zarinpalMerchantId || this.zarinpalMerchantId,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.handlePaymentVerificationWithResponse(verifyData, payment, rep, frontUrl, em);
|
return await this.handlePaymentVerificationWithResponse(verifyData, payment, rep, frontUrl, em);
|
||||||
@@ -111,33 +164,26 @@ export class PaymentsService {
|
|||||||
//===============================================
|
//===============================================
|
||||||
private async processSuccessfulVerification(payment: Payment, refId: number, em: EntityManager) {
|
private async processSuccessfulVerification(payment: Payment, refId: number, em: EntityManager) {
|
||||||
// Regular wallet charge
|
// Regular wallet charge
|
||||||
await this.handleSuccessfulPayment(payment, refId, em);
|
const { invoice } = await this.handleSuccessfulPayment(payment, refId, em);
|
||||||
|
|
||||||
const additionalParams: Record<string, string> = {};
|
const additionalParams: Record<string, string> = {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: PaymentStatus.COMPLETED,
|
status: PaymentStatus.COMPLETED,
|
||||||
payment,
|
payment,
|
||||||
|
invoice,
|
||||||
additionalParams,
|
additionalParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
//===============================================
|
//===============================================
|
||||||
|
|
||||||
async createGatewayPaymentForUser(
|
async createGatewayPaymentForUser(usr: User, amt: number, ref_Id: string, gatewayId: string, bunsId: string, invoiceId: string, em: EntityManager) {
|
||||||
usr: User,
|
|
||||||
amt: number,
|
|
||||||
ref_Id: string,
|
|
||||||
gatewayId: string,
|
|
||||||
businessId: string,
|
|
||||||
invoiceId: string,
|
|
||||||
em: EntityManager,
|
|
||||||
) {
|
|
||||||
const payment = em.create(Payment, {
|
const payment = em.create(Payment, {
|
||||||
amount: new Decimal(amt),
|
amount: new Decimal(amt),
|
||||||
reference: ref_Id,
|
reference: ref_Id,
|
||||||
user: usr,
|
user: usr,
|
||||||
paymentGateway: em.getReference(PaymentGateway, gatewayId),
|
paymentGateway: em.getReference(PaymentGateway, gatewayId),
|
||||||
business: em.getReference(Business, businessId),
|
business: em.getReference(Business, bunsId),
|
||||||
invoice: em.getReference(Invoice, invoiceId),
|
invoice: em.getReference(Invoice, invoiceId),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -172,19 +218,38 @@ export class PaymentsService {
|
|||||||
await em.nativeUpdate(Payment, { id: payment.id }, { status: PaymentStatus.COMPLETED, transactionId: transactionId.toString() });
|
await em.nativeUpdate(Payment, { id: payment.id }, { status: PaymentStatus.COMPLETED, transactionId: transactionId.toString() });
|
||||||
|
|
||||||
await this.removePaymentReminderJobs(payment.id);
|
await this.removePaymentReminderJobs(payment.id);
|
||||||
|
return await this.invoiceService.payInvoice(payment.invoice.id, payment.user.id, em);
|
||||||
|
}
|
||||||
|
|
||||||
|
//===============================================
|
||||||
|
|
||||||
|
async getDepositGatewayPayment(queryDto: PaginationDto) {
|
||||||
|
const { limit, skip } = PaginationUtils(queryDto);
|
||||||
|
|
||||||
|
const [payments, count] = await this.paymentsRepository.findAndCount(
|
||||||
|
{ deletedAt: null },
|
||||||
|
{
|
||||||
|
populate: ["user", "paymentGateway"],
|
||||||
|
orderBy: { createdAt: "DESC" },
|
||||||
|
offset: skip,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
payments,
|
||||||
|
count,
|
||||||
|
paginate: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
//-private methods
|
//-private methods
|
||||||
//----------------------------------------
|
//----------------------------------------
|
||||||
|
|
||||||
private buildFrontendRedirectUrl(invoiceId?: string): URL {
|
private buildFrontendRedirectUrl(businessSlug: string): URL {
|
||||||
const frontUrl = new URL(this.configService.getOrThrow<string>("SITE_URL"));
|
const frontUrl = new URL(this.configService.getOrThrow<string>("SITE_URL"));
|
||||||
if (invoiceId) {
|
frontUrl.pathname = `/${businessSlug}/payment`;
|
||||||
frontUrl.pathname = "/receipts/" + invoiceId;
|
|
||||||
} else {
|
|
||||||
frontUrl.pathname = "/payment";
|
|
||||||
}
|
|
||||||
return frontUrl;
|
return frontUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,11 +271,25 @@ export class PaymentsService {
|
|||||||
url.searchParams.append("date", payment.createdAt.toISOString());
|
url.searchParams.append("date", payment.createdAt.toISOString());
|
||||||
url.searchParams.append("amount", payment.amount.toString());
|
url.searchParams.append("amount", payment.amount.toString());
|
||||||
}
|
}
|
||||||
|
//===============================================
|
||||||
|
|
||||||
|
private async getUserById(userId: string, em: EntityManager): Promise<User> {
|
||||||
|
const user = await em.findOne(User, { id: userId }, { populate: ["company"] });
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
//===============================================
|
||||||
|
|
||||||
|
private async getPaymentGatewayById(gatewayId: string, em: EntityManager): Promise<PaymentGateway> {
|
||||||
|
const paymentGateway = await em.findOne(PaymentGateway, { id: gatewayId });
|
||||||
|
if (!paymentGateway) throw new BadRequestException(PaymentMessage.PAYMENT_GATEWAY_NOT_FOUND);
|
||||||
|
return paymentGateway;
|
||||||
|
}
|
||||||
|
|
||||||
//===============================================
|
//===============================================
|
||||||
|
|
||||||
private async getPaymentByReference(reference: string, em: EntityManager): Promise<Payment> {
|
private async getPaymentByReference(reference: string, em: EntityManager): Promise<Payment> {
|
||||||
const payment = await em.findOne(Payment, { reference }, { populate: ["user"], lockMode: LockMode.PESSIMISTIC_WRITE });
|
const payment = await em.findOne(Payment, { reference }, { populate: ["user", "invoice", "business"], lockMode: LockMode.PESSIMISTIC_WRITE });
|
||||||
if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF);
|
if (!payment) throw new BadRequestException(PaymentMessage.PAYMENT_NOT_FOUND_WITH_REF);
|
||||||
return payment;
|
return payment;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,8 +206,7 @@ export class TicketsService {
|
|||||||
const ticket = await this.findTicket(em, ticketId, businessId, userId, role === RoleEnum.ADMIN);
|
const ticket = await this.findTicket(em, ticketId, businessId, userId, role === RoleEnum.ADMIN);
|
||||||
if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED);
|
if (ticket.status === TicketStatus.CLOSED) throw new BadRequestException(TicketMessageEnum.TICKET_CLOSED);
|
||||||
|
|
||||||
if (role === RoleEnum.ADMIN && ticket.assignedTo?.id !== userId)
|
if (role === RoleEnum.ADMIN && ticket.assignedTo?.id !== userId) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_ASSIGNED_TO_USER);
|
||||||
throw new BadRequestException(TicketMessageEnum.TICKET_NOT_ASSIGNED_TO_USER);
|
|
||||||
|
|
||||||
const user = await em.findOne(User, { id: userId });
|
const user = await em.findOne(User, { id: userId });
|
||||||
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
@@ -380,11 +379,7 @@ export class TicketsService {
|
|||||||
private async findTicket(em: EntityManager, ticketId: string, businessId: string, userId: string, isAdmin: boolean): Promise<Ticket> {
|
private async findTicket(em: EntityManager, ticketId: string, businessId: string, userId: string, isAdmin: boolean): Promise<Ticket> {
|
||||||
const ticket = isAdmin
|
const ticket = isAdmin
|
||||||
? await em.findOne(Ticket, { id: ticketId, user: { id: userId }, business: { id: businessId } }, { populate: ["user", "assignedTo"] })
|
? await em.findOne(Ticket, { id: ticketId, user: { id: userId }, business: { id: businessId } }, { populate: ["user", "assignedTo"] })
|
||||||
: await em.findOne(
|
: await em.findOne(Ticket, { id: ticketId, user: { id: userId }, business: { id: businessId } }, { populate: ["user", "assignedTo"] });
|
||||||
Ticket,
|
|
||||||
{ id: ticketId, user: { id: userId }, business: { id: businessId } },
|
|
||||||
{ populate: ["user", "assignedTo"] },
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
if (!ticket) throw new BadRequestException(TicketMessageEnum.TICKET_NOT_FOUND);
|
||||||
return ticket;
|
return ticket;
|
||||||
|
|||||||
@@ -125,12 +125,7 @@ export class TicketsController {
|
|||||||
|
|
||||||
@ApiOperation({ summary: "referer ticket ==> admin route" })
|
@ApiOperation({ summary: "referer ticket ==> admin route" })
|
||||||
@Post(":id/refer")
|
@Post(":id/refer")
|
||||||
referTicket(
|
referTicket(@Param() paramDto: ParamDto, @UserDec("id") userId: string, @Body() referDto: ReferTicketDto, @BusinessDec("id") businessId: string) {
|
||||||
@Param() paramDto: ParamDto,
|
|
||||||
@UserDec("id") userId: string,
|
|
||||||
@Body() referDto: ReferTicketDto,
|
|
||||||
@BusinessDec("id") businessId: string,
|
|
||||||
) {
|
|
||||||
return this.ticketsService.referTicket(paramDto.id, userId, referDto, businessId);
|
return this.ticketsService.referTicket(paramDto.id, userId, referDto, businessId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user