online payment
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { productModule } from './modules/product/product.module';
|
||||
import { RolesModule } from './modules/roles/roles.module';
|
||||
import { PaymentModule } from './modules/payment/payments.module';
|
||||
import { OrderModule } from './modules/order/orders.module';
|
||||
import { OrderModule } from './modules/order/order.module';
|
||||
import { NotificationsModule } from './modules/notification/notifications.module';
|
||||
import { EventEmitterModule } from '@nestjs/event-emitter';
|
||||
import { CacheModule } from '@nestjs/cache-manager';
|
||||
|
||||
@@ -34,22 +34,7 @@ export class OrdersController {
|
||||
return orders
|
||||
}
|
||||
|
||||
@Post('admin/orders/:orderId/create-invoice')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Create invoice for new order' })
|
||||
createInvoice(@Param('orderId') orderId: string, @Body() body: CreateInvoiceDto) {
|
||||
return this.ordersService.createInvoice(orderId, body);
|
||||
}
|
||||
|
||||
// @UseGuards(AuthGuard)
|
||||
// @Patch('public/orders/:id/:status')
|
||||
// @ApiParam({
|
||||
// name: 'status',
|
||||
// description: 'Order status',
|
||||
// enum: OrderStatus,
|
||||
// })
|
||||
|
||||
@Post('admin/orders/:orderId/items/:orderItemId')
|
||||
@Post('public/orders/:orderId/items/:orderItemId')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Confirm Invoice Item By User' })
|
||||
confirmOrderItem(
|
||||
@@ -60,7 +45,17 @@ export class OrdersController {
|
||||
return this.ordersService.confirmOrderItem(userId, orderId, +orderItemId);
|
||||
}
|
||||
|
||||
// /******************** Admin Routes **********************/
|
||||
|
||||
|
||||
|
||||
|
||||
/*========================== Admin Routes =====================*/
|
||||
@Post('admin/orders/:orderId/create-invoice')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiOperation({ summary: 'Create invoice for new order' })
|
||||
createInvoice(@Param('orderId') orderId: string, @Body() body: CreateInvoiceDto) {
|
||||
return this.ordersService.createInvoice(orderId, body);
|
||||
}
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Permissions(Permission.MANAGE_ORDERS)
|
||||
// @Get('admin/orders')
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ import { CreateInvoiceDto } from '../dto/create-invoice.dto';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class OrdersService {
|
||||
private readonly logger = new Logger(OrdersService.name);
|
||||
export class OrderService {
|
||||
private readonly logger = new Logger(OrderService.name);
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
ApiHeader,
|
||||
} from '@nestjs/swagger';
|
||||
import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
|
||||
import { CreatePaymentMethodDto } from '../dto/create-payment-method.dto';
|
||||
import { UpdatePaymentMethodDto } from '../dto/update-payment-method.dto';
|
||||
import { VerifyPaymentDto } from '../dto/verify-payment.dto';
|
||||
import { PaymentChartDto } from '../dto/payment-chart.dto';
|
||||
@@ -19,27 +19,23 @@ import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { PayOrderDto } from '../dto/pay-order.dto';
|
||||
|
||||
@ApiTags('payments')
|
||||
@Controller()
|
||||
export class PaymentsController {
|
||||
constructor(
|
||||
private readonly paymentsService: PaymentsService,
|
||||
) { }
|
||||
) { }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// @UseGuards(AuthGuard)
|
||||
// @ApiBearerAuth()
|
||||
// @Get('public/payments/pay-order/:orderId')
|
||||
// @ApiOperation({ summary: 'Pay for an order' })
|
||||
// @ApiParam({ name: 'orderId', type: 'string', description: 'Order ID' })
|
||||
|
||||
// payAnOrder(@Param('orderId') orderId: string) {
|
||||
// return this.paymentsService.payOrder(orderId);
|
||||
// }
|
||||
@Get('public/payments/pay-order/:orderId')
|
||||
@UseGuards(AuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ summary: 'Pay for an order' })
|
||||
payAnOrder(@Param('orderId') orderId: string,@Body() dto :PayOrderDto) {
|
||||
return this.paymentsService.payOrder(orderId,dto);
|
||||
}
|
||||
|
||||
// @Post('public/payments/verify')
|
||||
// @ApiOperation({ summary: 'Verify a payment' })
|
||||
@@ -50,7 +46,7 @@ export class PaymentsController {
|
||||
// return this.paymentsService.verifyOnlinePayment(verifyPaymentDto.authority, verifyPaymentDto.orderId);
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
// @UseGuards(AdminAuthGuard)
|
||||
// @Permissions(Permission.MANAGE_PAYMENTS)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { IsString, IsOptional, IsBoolean, IsNumber, IsEnum, IsArray } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PaymentMethodEnum, PaymentGatewayEnum } from '../interface/payment';
|
||||
|
||||
export class PayOrderDto {
|
||||
|
||||
@ApiProperty({ description: 'amount'})
|
||||
@IsNumber()
|
||||
amount!: number;
|
||||
|
||||
@ApiProperty({ description: 'Payment method', enum: PaymentMethodEnum })
|
||||
@IsEnum(PaymentMethodEnum)
|
||||
method!: PaymentMethodEnum;
|
||||
|
||||
@ApiProperty({ description: 'Payment gateway', enum: PaymentGatewayEnum, required: false })
|
||||
@IsOptional()
|
||||
@IsEnum(PaymentGatewayEnum)
|
||||
gateway?: PaymentGatewayEnum ;
|
||||
|
||||
@ApiProperty({ description: 'Payment method description', required: false })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
|
||||
@ApiProperty({ description: 'Merchant ID', required: false })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
attachments?: string[];
|
||||
}
|
||||
@@ -6,9 +6,9 @@ import { ulid } from 'ulid';
|
||||
|
||||
@Entity({ tableName: 'payments' })
|
||||
export class Payment extends BaseEntity {
|
||||
@PrimaryKey({ type: 'string', columnType:'char(26)' })
|
||||
id: string=ulid()
|
||||
|
||||
@PrimaryKey({ type: 'string', columnType: 'char(26)' })
|
||||
id: string = ulid()
|
||||
|
||||
@ManyToOne(() => Order)
|
||||
@Index()
|
||||
order!: Order;
|
||||
@@ -40,4 +40,10 @@ export class Payment extends BaseEntity {
|
||||
@Property({ nullable: true })
|
||||
failedAt?: Date | null = null;
|
||||
|
||||
@Property({ nullable: true })
|
||||
attachments?: string[]
|
||||
|
||||
@Property({ nullable: true })
|
||||
description?: string
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { PaymentMethodEnum } from "../interface/payment";
|
||||
export class paymentSucceedEvent {
|
||||
constructor(
|
||||
public readonly orderId: string,
|
||||
public readonly: string,
|
||||
public readonly orderNumber: string,
|
||||
public readonly paymentMethod: PaymentMethodEnum,
|
||||
public readonly total: number
|
||||
@@ -16,7 +15,6 @@ export class onlinePaymentSucceedEvent {
|
||||
constructor(
|
||||
public readonly paymentId: string,
|
||||
public readonly orderId: string,
|
||||
public readonly: string,
|
||||
public readonly orderNumber: string,
|
||||
public readonly total: number,
|
||||
) { }
|
||||
|
||||
@@ -20,6 +20,8 @@ export class ZarinpalGateway implements IPaymentGateway {
|
||||
private readonly zarinpalRequestUrl: string;
|
||||
private readonly zarinpalVerifyUrl: string;
|
||||
private readonly zarinpalPaymentBaseUrl: string;
|
||||
private readonly zarinpalMerchantId: string;
|
||||
private readonly websiteUrl: string = 'https://negareh.ir'
|
||||
private readonly axiosConfig = {
|
||||
timeout: 10_000,
|
||||
headers: {
|
||||
@@ -32,6 +34,7 @@ export class ZarinpalGateway implements IPaymentGateway {
|
||||
this.zarinpalPaymentBaseUrl = zarinpalBaseUrl;
|
||||
this.zarinpalRequestUrl = `${zarinpalBaseUrl}/pg/v4/payment/request.json`;
|
||||
this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`;
|
||||
this.zarinpalMerchantId = this.configService.getOrThrow<string>('ZARINPAL_BASE_MERCHANT')
|
||||
}
|
||||
|
||||
private getAxiosErrorData(error: unknown): { status?: number; data?: unknown } | null {
|
||||
@@ -39,12 +42,12 @@ export class ZarinpalGateway implements IPaymentGateway {
|
||||
return { status: error.response.status, data: error.response.data };
|
||||
}
|
||||
|
||||
async requestPayment({ amount, merchantId, orderId, domain }: IRequestPaymentParams): Promise<IRequestPaymentData> {
|
||||
async requestPayment({ amount, orderId }: IRequestPaymentParams): Promise<IRequestPaymentData> {
|
||||
// Transform camelCase to snake_case for Zarinpal API v4
|
||||
const callbackUrl = `${domain}/verify/${orderId}`;
|
||||
const callbackUrl = `${this.websiteUrl}/verify/${orderId}`;
|
||||
const zarinpalRequest: IZarinpalRequestPayment = {
|
||||
amount,
|
||||
merchant_id: merchantId,
|
||||
merchant_id: this.zarinpalMerchantId,
|
||||
description: `Payment for order #${orderId}`,
|
||||
callback_url: callbackUrl,
|
||||
currency: 'IRT',
|
||||
@@ -84,7 +87,9 @@ export class ZarinpalGateway implements IPaymentGateway {
|
||||
throw new BadRequestException(message ?? PaymentMessage.ZARINPAL_PAYMENT_REQUEST_ERROR);
|
||||
}
|
||||
|
||||
return { transactionId: authority };
|
||||
const paymentUrl = `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}`
|
||||
|
||||
return { token: authority, paymentUrl };
|
||||
} catch (error) {
|
||||
if (error instanceof BadRequestException) throw error;
|
||||
|
||||
@@ -109,13 +114,13 @@ export class ZarinpalGateway implements IPaymentGateway {
|
||||
}
|
||||
}
|
||||
|
||||
async verifyPayment({ merchantId, amount, transactionId }: IPaymentVerifyParams): Promise<IPaymentVerifyData> {
|
||||
async verifyPayment({ amount, token }: IPaymentVerifyParams): Promise<IPaymentVerifyData> {
|
||||
try {
|
||||
// Transform camelCase to snake_case for Zarinpal API v4
|
||||
const zarinpalVerifyRequest: IZarinpalVerifyRequest = {
|
||||
merchant_id: merchantId,
|
||||
merchant_id: this.zarinpalMerchantId,
|
||||
amount,
|
||||
authority: transactionId,
|
||||
authority: token,
|
||||
};
|
||||
|
||||
const res = await axios.post<IZarinpalVerifyResponse>(
|
||||
@@ -152,7 +157,5 @@ export class ZarinpalGateway implements IPaymentGateway {
|
||||
}
|
||||
}
|
||||
|
||||
getPaymentUrl(authority: string): string {
|
||||
return `${this.zarinpalPaymentBaseUrl}/pg/StartPay/${authority}`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
export interface IPaymentGateway {
|
||||
requestPayment(requestPaymentParams: IRequestPaymentParams): Promise<IRequestPaymentData>;
|
||||
verifyPayment(verifyPaymentParam: IPaymentVerifyParams): Promise<IPaymentVerifyData>;
|
||||
getPaymentUrl(authority: string): string;
|
||||
}
|
||||
|
||||
export interface IRequestPaymentParams {
|
||||
amount: number;
|
||||
merchantId: string;
|
||||
domain: string;
|
||||
orderId: string;
|
||||
}
|
||||
|
||||
export interface IRequestPaymentData {
|
||||
transactionId: string;
|
||||
token: string;
|
||||
paymentUrl:string
|
||||
}
|
||||
|
||||
export interface IPaymentVerifyParams {
|
||||
amount: number;
|
||||
merchantId: string;
|
||||
transactionId: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export interface IPaymentVerifyData {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Order } from 'src/modules/order/entities/order.entity';
|
||||
import { User } from 'src/modules/user/entities/user.entity';
|
||||
|
||||
export enum PaymentMethodEnum {
|
||||
Online = 'Online',
|
||||
Cash = 'Cash',
|
||||
Wallet = 'Wallet',
|
||||
Credit = 'Credit',
|
||||
}
|
||||
export enum PaymentStatusEnum {
|
||||
Pending = 'pending',
|
||||
@@ -24,10 +25,11 @@ export interface ICreatePayment {
|
||||
}
|
||||
|
||||
export interface OrderPaymentContext {
|
||||
user:User;
|
||||
order: Order;
|
||||
amount: number;
|
||||
method: PaymentMethodEnum;
|
||||
gateway: PaymentGatewayEnum | null;
|
||||
merchantId: string | null;
|
||||
restaurantDomain: string | null;
|
||||
method: PaymentMethodEnum
|
||||
attachments?: string[]
|
||||
description?: string
|
||||
gateway?: PaymentGatewayEnum
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { PaymentRepository } from './repositories/payment.repository';
|
||||
import { PaymentListeners } from './listeners/payment.listeners';
|
||||
import { AdminModule } from '../admin/admin.module';
|
||||
import { NotificationsModule } from '../notification/notifications.module';
|
||||
import { OrderModule } from '../order/orders.module';
|
||||
import { OrderModule } from '../order/order.module';
|
||||
|
||||
@Module({
|
||||
imports: [MikroOrmModule.forFeature([Payment]),
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment';
|
||||
import { Payment } from '../entities/payment.entity';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { Order } from '../../order/entities/order.entity';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import { GatewayManager } from '../gateways/gateway.manager';
|
||||
import { OrderPaymentContext } from '../interface/payment';
|
||||
import { OrderStatusEnum } from 'src/modules/order/interface/order.interface';
|
||||
import { ChartPeriodEnum, PaymentChartDto } from '../dto/payment-chart.dto';
|
||||
import { PaymentMessage, OrderMessage } from 'src/common/enums/message.enum';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { onlinePaymentSucceedEvent, paymentSucceedEvent } from '../events/payment.events';
|
||||
import { OrderService } from 'src/modules/order/providers/order.service';
|
||||
import { PayOrderDto } from '../dto/pay-order.dto';
|
||||
import { PaymentRepository } from '../repositories/payment.repository';
|
||||
import { CreditRepository } from 'src/modules/user/repositories/credit.repository';
|
||||
import { CreditService } from 'src/modules/user/providers/credit.service';
|
||||
import { CreditTransactionType } from 'src/modules/user/interface/credit';
|
||||
import { Payment } from '../entities/payment.entity';
|
||||
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class PaymentsService {
|
||||
@@ -18,218 +24,204 @@ export class PaymentsService {
|
||||
|
||||
constructor(
|
||||
private readonly em: EntityManager,
|
||||
private readonly orderService: OrderService,
|
||||
private readonly paymentRepository: PaymentRepository,
|
||||
private readonly gatewayManager: GatewayManager,
|
||||
private readonly eventEmitter: EventEmitter2,
|
||||
private readonly creditService: CreditService,
|
||||
private readonly creditRepository: CreditRepository,
|
||||
) { }
|
||||
|
||||
// async payOrder(orderId: string): Promise<{ paymentUrl: string | null }> {
|
||||
// const ctx = await this.loadAndValidateOrder(orderId);
|
||||
async payOrder(orderId: string, dto: PayOrderDto): Promise<{ paymentUrl: string | null }> {
|
||||
|
||||
// // Idempotency: avoid creating/charging again for already-paid orders
|
||||
// if (ctx.order.status === OrderStatus.PAID) {
|
||||
// return { paymentUrl: null };
|
||||
// }
|
||||
const { method } = dto
|
||||
|
||||
// switch (ctx.method) {
|
||||
// case PaymentMethodEnum.Cash:
|
||||
// await this.handleCashPayment(ctx);
|
||||
// return { paymentUrl: null };
|
||||
|
||||
// case PaymentMethodEnum.Wallet:
|
||||
// await this.handleWalletPayment(ctx);
|
||||
// return { paymentUrl: null };
|
||||
|
||||
// case PaymentMethodEnum.Online:
|
||||
// return this.handleOnlinePayment(ctx);
|
||||
|
||||
// default:
|
||||
// throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
|
||||
// }
|
||||
// }
|
||||
|
||||
// private async loadAndValidateOrder(orderId: string): Promise<OrderPaymentContext> {
|
||||
// const order = await this.em.findOne(
|
||||
// Order,
|
||||
// { id: orderId },
|
||||
// { populate: ['user', 'restaurant', 'paymentMethod', 'paymentMethod.restaurant'] },
|
||||
// );
|
||||
|
||||
// if (!order) {
|
||||
// throw new NotFoundException(OrderMessage.NOT_FOUND);
|
||||
// }
|
||||
|
||||
// if (order.total <= 0) {
|
||||
// throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO);
|
||||
// }
|
||||
|
||||
// const pm = order.paymentMethod;
|
||||
// if (!pm) {
|
||||
// throw new NotFoundException(PaymentMessage.PAYMENT_METHOD_NOT_FOUND);
|
||||
// }
|
||||
|
||||
// if (pm.method === PaymentMethodEnum.Online) {
|
||||
// if (!pm.gateway) throw new BadRequestException(PaymentMessage.GATEWAY_REQUIRED);
|
||||
// if (!pm.merchantId) throw new BadRequestException(PaymentMessage.MERCHANT_ID_REQUIRED);
|
||||
// if (!pm.restaurant?.domain) throw new BadRequestException(PaymentMessage.RESTAURANT_DOMAIN_REQUIRED);
|
||||
// }
|
||||
|
||||
// return {
|
||||
// order,
|
||||
// amount: order.total,
|
||||
// method: pm.method,
|
||||
// gateway: pm.gateway ?? null,
|
||||
// merchantId: pm.merchantId ?? null,
|
||||
// restaurantDomain: pm.restaurant.domain ?? null,
|
||||
// };
|
||||
// }
|
||||
|
||||
// private async handleCashPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||
// await this.getOrCreateLatestPendingPayment(ctx.order.id, {
|
||||
// amount: ctx.amount,
|
||||
// method: PaymentMethodEnum.Cash,
|
||||
// gateway: null,
|
||||
// });
|
||||
// }
|
||||
const ctx = await this.loadAndValidateOrder(orderId, dto)
|
||||
|
||||
|
||||
// private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||
// await this.em.transactional(async em => {
|
||||
// const order = await em.findOne(Order, { id: ctx.order.id }, { populate: ['user', 'restaurant'] });
|
||||
// if (!order) throw new NotFoundException(OrderMessage.NOT_FOUND);
|
||||
switch (method) {
|
||||
case PaymentMethodEnum.Cash:
|
||||
await this.handleCashPayment(ctx);
|
||||
return { paymentUrl: null };
|
||||
|
||||
// if (order.status === OrderStatus.PAID) {
|
||||
// return;
|
||||
// }
|
||||
case PaymentMethodEnum.Credit:
|
||||
await this.handleCreditPayment(ctx);
|
||||
return { paymentUrl: null };
|
||||
|
||||
// const walletTransaction = await em.findOne(WalletTransaction, {
|
||||
// user: { id: order.user.id },
|
||||
// restaurant: { id: order.restaurant.id },
|
||||
// }, {
|
||||
// orderBy: { createdAt: 'DESC' }
|
||||
// });
|
||||
case PaymentMethodEnum.Online:
|
||||
return this.handleOnlinePayment(ctx);
|
||||
|
||||
// if (!walletTransaction) {
|
||||
// throw new NotFoundException('User wallet not found');
|
||||
// }
|
||||
|
||||
// if (walletTransaction.balance < ctx.amount) {
|
||||
// throw new BadRequestException('Insufficient wallet balance');
|
||||
// }
|
||||
|
||||
// const newBalance = walletTransaction.balance - ctx.amount;
|
||||
default:
|
||||
throw new BadRequestException(PaymentMessage.UNSUPPORTED_PAYMENT_METHOD);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// const payment = await this.getOrCreateLatestPendingPayment(order.id, {
|
||||
// em,
|
||||
// amount: ctx.amount,
|
||||
// method: PaymentMethodEnum.Wallet,
|
||||
// gateway: null,
|
||||
// });
|
||||
private async loadAndValidateOrder(orderId: string, dto: PayOrderDto): Promise<OrderPaymentContext> {
|
||||
const { amount, method, attachments, description, gateway } = dto
|
||||
|
||||
// const newWalletTransaction = em.create(WalletTransaction, {
|
||||
// user: order.user,
|
||||
// restaurant: order.restaurant,
|
||||
// amount: ctx.amount,
|
||||
// type: WalletTransactionType.DEBIT,
|
||||
// reason: WalletTransactionReason.ORDER_PAYMENT,
|
||||
// balance: newBalance,
|
||||
// });
|
||||
const order = await this.orderService.findOneOrFail(orderId)
|
||||
|
||||
// payment.status = PaymentStatusEnum.Paid;
|
||||
// payment.paidAt = new Date();
|
||||
// order.status = OrderStatus.PAID;
|
||||
if (!order) {
|
||||
throw new NotFoundException(OrderMessage.NOT_FOUND);
|
||||
}
|
||||
|
||||
// em.persist([payment, order, newWalletTransaction]);
|
||||
// await em.flush();
|
||||
// });
|
||||
// this.eventEmitter.emit(
|
||||
// paymentSucceedEvent.name,
|
||||
// new paymentSucceedEvent(ctx.order.id, ctx.order.restaurant.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount),
|
||||
// );
|
||||
// }
|
||||
if (order.balance == 0) {
|
||||
throw new BadRequestException("you can not pay beause Balance is zero")
|
||||
}
|
||||
|
||||
// private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> {
|
||||
// const gateway = this.gatewayManager.get(ctx.gateway!);
|
||||
|
||||
// const payment = await this.getOrCreateLatestPendingPayment(ctx.order.id, {
|
||||
// amount: ctx.amount,
|
||||
// method: PaymentMethodEnum.Online,
|
||||
// gateway: ctx.gateway!,
|
||||
// });
|
||||
if (amount <= 0) {
|
||||
throw new BadRequestException(PaymentMessage.AMOUNT_MUST_BE_GREATER_THAN_ZERO);
|
||||
}
|
||||
|
||||
// // If we already requested a gateway transaction, just return the same URL (idempotent)
|
||||
// if (payment.transactionId) {
|
||||
// return { paymentUrl: gateway.getPaymentUrl(payment.transactionId) };
|
||||
// }
|
||||
|
||||
// const { transactionId } = await gateway.requestPayment({
|
||||
// amount: ctx.amount,
|
||||
// orderId: ctx.order.id,
|
||||
// merchantId: ctx.merchantId!,
|
||||
// domain: ctx.restaurantDomain!,
|
||||
// });
|
||||
return {
|
||||
user: order.user,
|
||||
order,
|
||||
amount,
|
||||
attachments,
|
||||
description,
|
||||
gateway,
|
||||
method
|
||||
};
|
||||
}
|
||||
|
||||
// payment.gateway = ctx.gateway;
|
||||
// payment.transactionId = transactionId;
|
||||
// payment.status = PaymentStatusEnum.Pending;
|
||||
private async handleCashPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||
const { order, amount, method, attachments, description } = ctx
|
||||
const payment = this.paymentRepository.create({
|
||||
order,
|
||||
amount,
|
||||
method,
|
||||
status: PaymentStatusEnum.Pending,
|
||||
attachments,
|
||||
description
|
||||
})
|
||||
await this.em.persistAndFlush(payment)
|
||||
return
|
||||
}
|
||||
|
||||
// await this.em.persistAndFlush(payment);
|
||||
|
||||
// return {
|
||||
// paymentUrl: gateway.getPaymentUrl(transactionId),
|
||||
// };
|
||||
// }
|
||||
private async handleCreditPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||
const { order, amount, user } = ctx
|
||||
|
||||
// async verifyOnlinePayment(transactionId: string, orderId: string): Promise<Payment> {
|
||||
// const payment = await this.em.transactional(async em => {
|
||||
// const payment = await em.findOne(
|
||||
// Payment,
|
||||
// { transactionId, order: { id: orderId } },
|
||||
// { populate: ['order', 'order.paymentMethod'] },
|
||||
// );
|
||||
await this.em.transactional(async em => {
|
||||
// 1. get User remained Credit
|
||||
const remainedCredit = await this.creditService.getCurrentCredit(user.id)
|
||||
|
||||
// if (!payment) {
|
||||
// throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
// }
|
||||
if (remainedCredit < amount) {
|
||||
throw new BadRequestException("You Dont have enough Credit")
|
||||
}
|
||||
|
||||
// if (payment.status === PaymentStatusEnum.Paid) {
|
||||
// return payment;
|
||||
// }
|
||||
const newBalance = remainedCredit - amount;
|
||||
// 2. Create payment record
|
||||
const payment = this.paymentRepository.create({
|
||||
amount: ctx.amount,
|
||||
method: PaymentMethodEnum.Credit,
|
||||
gateway: null,
|
||||
order,
|
||||
status: PaymentStatusEnum.Paid,
|
||||
paidAt: new Date()
|
||||
});
|
||||
// 3. Create Credit transaction record
|
||||
const newCreditTransaction = this.creditRepository.create({
|
||||
user: order.user,
|
||||
amount,
|
||||
type: CreditTransactionType.WITHDRAW,
|
||||
balance: newBalance,
|
||||
orderId: order.id
|
||||
});
|
||||
|
||||
// const pm = payment.order.paymentMethod;
|
||||
// if (!pm?.merchantId || !payment.gateway) {
|
||||
// throw new BadRequestException(PaymentMessage.INVALID_PAYMENT_CONFIGURATION);
|
||||
// }
|
||||
em.persist([payment, newCreditTransaction]);
|
||||
await em.flush();
|
||||
});
|
||||
|
||||
// const gateway = this.gatewayManager.get(payment.gateway);
|
||||
this.eventEmitter.emit(
|
||||
paymentSucceedEvent.name,
|
||||
new paymentSucceedEvent(ctx.order.id, String(ctx.order?.orderNumber) || '', ctx.method, ctx.amount),
|
||||
);
|
||||
}
|
||||
|
||||
// const result = await gateway.verifyPayment({
|
||||
// merchantId: pm.merchantId,
|
||||
// amount: payment.amount,
|
||||
// transactionId: payment.transactionId!,
|
||||
// });
|
||||
private async handleOnlinePayment(ctx: OrderPaymentContext): Promise<{ paymentUrl: string }> {
|
||||
const { amount, order, gateway: requestedGateway } = ctx
|
||||
|
||||
// payment.verifyResponse = result.raw;
|
||||
const gateway = this.gatewayManager.get(requestedGateway!);
|
||||
|
||||
// if (!result.success) {
|
||||
// this.failPayment(payment);
|
||||
// return payment;
|
||||
// }
|
||||
const payment = this.paymentRepository.create({
|
||||
order,
|
||||
amount,
|
||||
method: PaymentMethodEnum.Online,
|
||||
status: PaymentStatusEnum.Pending
|
||||
})
|
||||
|
||||
// this.markPaid(payment, result.referenceId);
|
||||
// if (payment.order.status === OrderStatus.PENDING_PAYMENT) {
|
||||
// payment.order.status = OrderStatus.PAID;
|
||||
// }
|
||||
const { token, paymentUrl } = await gateway.requestPayment({
|
||||
amount: ctx.amount,
|
||||
orderId: ctx.order.id,
|
||||
});
|
||||
|
||||
// await em.flush();
|
||||
// return payment;
|
||||
// });
|
||||
// this.eventEmitter.emit(
|
||||
// onlinePaymentSucceedEvent.name,
|
||||
// new onlinePaymentSucceedEvent(payment.id, payment.order.id, payment.order.restaurant.id, String(payment.order?.orderNumber) || '', payment.amount),
|
||||
// );
|
||||
// return payment;
|
||||
// }
|
||||
payment.gateway = requestedGateway;
|
||||
payment.token = token;
|
||||
payment.status = PaymentStatusEnum.Pending;
|
||||
|
||||
await this.em.persistAndFlush(payment);
|
||||
|
||||
return {
|
||||
paymentUrl,
|
||||
};
|
||||
}
|
||||
|
||||
async verifyOnlinePayment(transactionId: string): Promise<Payment> {
|
||||
const payment = await this.em.transactional(async em => {
|
||||
const payment = await em.findOne(
|
||||
Payment,
|
||||
{ transactionId },
|
||||
{ populate: ['order'] },
|
||||
);
|
||||
|
||||
if (!payment) {
|
||||
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (payment.status === PaymentStatusEnum.Paid) {
|
||||
return payment;
|
||||
}
|
||||
|
||||
if (!payment.gateway) {
|
||||
throw new NotFoundException("Payment gateway not found");
|
||||
}
|
||||
|
||||
const gateway = this.gatewayManager.get(payment.gateway);
|
||||
|
||||
const { raw, referenceId, success } = await gateway.verifyPayment({
|
||||
amount: payment.amount,
|
||||
token: payment.token!,
|
||||
});
|
||||
|
||||
payment.verifyResponse = raw;
|
||||
|
||||
if (!success) {
|
||||
payment.status = PaymentStatusEnum.Failed;
|
||||
payment.failedAt = new Date();
|
||||
return payment;
|
||||
}
|
||||
|
||||
payment.status = PaymentStatusEnum.Paid;
|
||||
payment.paidAt = new Date();
|
||||
payment.transactionId = referenceId;
|
||||
// TODO : use decimal js
|
||||
// does need persist or not
|
||||
payment.order.paidAmount += payment.amount
|
||||
payment.order.balance = payment.order.total - payment.order.paidAmount
|
||||
|
||||
await em.flush();
|
||||
return payment;
|
||||
});
|
||||
this.eventEmitter.emit(
|
||||
onlinePaymentSucceedEvent.name,
|
||||
new onlinePaymentSucceedEvent(payment.id, payment.order.id, String(payment.order?.orderNumber) || '', payment.amount),
|
||||
);
|
||||
return payment;
|
||||
}
|
||||
|
||||
// findAllPaymentsBy(: string, userId: string) {
|
||||
// return this.em.find(
|
||||
@@ -239,30 +231,33 @@ export class PaymentsService {
|
||||
// );
|
||||
// }
|
||||
|
||||
// async verifyCashPayment(id: string): Promise<Payment> {
|
||||
// const payment = await this.em.transactional(async em => {
|
||||
// const payment = await em.findOne(Payment, id, { populate: ['order'] });
|
||||
// if (!payment) {
|
||||
// throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
// }
|
||||
// if (payment.method !== PaymentMethodEnum.Cash) {
|
||||
// throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
|
||||
// }
|
||||
// if (payment.status === PaymentStatusEnum.Paid) {
|
||||
// throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
|
||||
// }
|
||||
// if (payment.order.status === OrderStatus.PENDING_PAYMENT) {
|
||||
// payment.order.status = OrderStatus.PAID;
|
||||
// }
|
||||
// payment.status = PaymentStatusEnum.Paid;
|
||||
// payment.paidAt = new Date();
|
||||
// em.persist(payment);
|
||||
// em.persist(payment.order);
|
||||
// await em.flush();
|
||||
// return payment;
|
||||
// });
|
||||
// return payment;
|
||||
// }
|
||||
async verifyCashPayment(id: string): Promise<Payment> {
|
||||
const payment = await this.em.transactional(async em => {
|
||||
const payment = await em.findOne(Payment, id, { populate: ['order'] });
|
||||
if (!payment) {
|
||||
throw new NotFoundException(PaymentMessage.PAYMENT_NOT_FOUND);
|
||||
}
|
||||
if (payment.method !== PaymentMethodEnum.Cash) {
|
||||
throw new BadRequestException(PaymentMessage.PAYMENT_METHOD_NOT_CASH);
|
||||
}
|
||||
if (payment.status === PaymentStatusEnum.Paid) {
|
||||
throw new BadRequestException(PaymentMessage.PAYMENT_ALREADY_PAID);
|
||||
}
|
||||
|
||||
payment.status = PaymentStatusEnum.Paid;
|
||||
payment.paidAt = new Date();
|
||||
|
||||
// TODO : use decimal js
|
||||
payment.order.paidAmount += payment.amount
|
||||
payment.order.balance = payment.order.total - payment.order.paidAmount
|
||||
|
||||
em.persist(payment);
|
||||
em.persist(payment.order);
|
||||
await em.flush();
|
||||
return payment;
|
||||
});
|
||||
return payment;
|
||||
}
|
||||
|
||||
// private markPaid(payment: Payment, referenceId: string) {
|
||||
// payment.status = PaymentStatusEnum.Paid;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get, UseGuards, Patch, Body} from '@nestjs/common';
|
||||
import { Controller, Get, UseGuards, Patch, Body } from '@nestjs/common';
|
||||
import { ApiTags, ApiBearerAuth, ApiOperation, ApiBody, ApiOkResponse } from '@nestjs/swagger';
|
||||
import { AuthGuard } from 'src/modules/auth/guards/auth.guard';
|
||||
import { UserService } from '../providers/user.service';
|
||||
@@ -14,14 +14,14 @@ import { AdminAuthGuard } from 'src/modules/auth/guards/adminAuth.guard';
|
||||
import { UserSuccessMessage } from 'src/common/enums/message.enum';
|
||||
import { Permissions } from 'src/common/decorators/permissions.decorator';
|
||||
import { Permission } from 'src/common/enums/permission.enum';
|
||||
import { WalletService } from '../providers/wallet.service';
|
||||
import { CreditService } from '../providers/credit.service';
|
||||
|
||||
@ApiTags('User')
|
||||
@Controller()
|
||||
export class UsersController {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly walletService: WalletService,
|
||||
private readonly walletService: CreditService,
|
||||
) { }
|
||||
|
||||
@UseGuards(AuthGuard)
|
||||
|
||||
+25
-4
@@ -1,19 +1,40 @@
|
||||
import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { FilterQuery } from '@mikro-orm/core';
|
||||
import { EntityManager } from '@mikro-orm/postgresql';
|
||||
import { CreditTransactionRepository } from '../repositories/credit-transaction.repository';
|
||||
import { CreditRepository } from '../repositories/credit.repository';
|
||||
import { FindWalletTransactionsDto } from '../dto/find-wallet-transactions.dto';
|
||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||
import { CreditTransaction } from '../entities/credit-transaction.entity';
|
||||
import { UserRepository } from '../repositories/user.repository';
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class WalletService {
|
||||
export class CreditService {
|
||||
constructor(
|
||||
// private readonly walletTransactionRepository: CreditTransactionRepository,
|
||||
|
||||
private readonly creditRepository: CreditRepository,
|
||||
private readonly userRepository: UserRepository,
|
||||
|
||||
) { }
|
||||
|
||||
async getCurrentCredit(userId: string,): Promise<number> {
|
||||
const user = await this.userRepository.findOne(userId)
|
||||
if (!user) {
|
||||
throw new BadRequestException("User not found")
|
||||
}
|
||||
const maxCredit = user.maxCredit
|
||||
|
||||
const latestTransaction = await this.creditRepository.findOne(
|
||||
{
|
||||
user: { id: userId },
|
||||
},
|
||||
{ orderBy: { createdAt: 'desc' } },
|
||||
);
|
||||
const balance = latestTransaction?.balance || 0
|
||||
|
||||
const remained = maxCredit - balance
|
||||
|
||||
return remained
|
||||
}
|
||||
// async getUserWalletTransactions(
|
||||
// userId: string,
|
||||
// dto: FindWalletTransactionsDto,
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { CreditTransaction } from '../entities/credit-transaction.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CreditTransactionRepository extends EntityRepository<CreditTransaction> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, CreditTransaction);
|
||||
}
|
||||
async getCurrentCredit(userId: string,): Promise<number> {
|
||||
const lastRow = await this.em.findOne(
|
||||
CreditTransaction,
|
||||
{
|
||||
user: { id: userId },
|
||||
},
|
||||
{ orderBy: { createdAt: 'desc' } },
|
||||
);
|
||||
return lastRow?.balance || 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||
import { CreditTransaction } from '../entities/credit-transaction.entity';
|
||||
|
||||
@Injectable()
|
||||
export class CreditRepository extends EntityRepository<CreditTransaction> {
|
||||
constructor(readonly em: EntityManager) {
|
||||
super(em, CreditTransaction);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -6,17 +6,28 @@ import { User } from './entities/user.entity';
|
||||
import { UserAddress } from './entities/user-address.entity';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { UserRepository } from './repositories/user.repository';
|
||||
import { CreditTransactionRepository } from './repositories/credit-transaction.repository';
|
||||
import { WalletService } from './providers/wallet.service';
|
||||
import { CreditRepository } from './repositories/credit.repository';
|
||||
import { CreditService } from './providers/credit.service';
|
||||
import { CreditTransaction } from './entities/credit-transaction.entity';
|
||||
|
||||
@Module({
|
||||
providers: [UserService, WalletService, UserRepository, CreditTransactionRepository, CreditTransactionRepository],
|
||||
providers: [
|
||||
UserService,
|
||||
CreditService,
|
||||
UserRepository,
|
||||
CreditRepository,
|
||||
],
|
||||
controllers: [UsersController],
|
||||
imports: [
|
||||
MikroOrmModule.forFeature([User, UserAddress, CreditTransaction]),
|
||||
JwtModule,
|
||||
],
|
||||
exports: [UserService, WalletService, UserRepository, CreditTransactionRepository, CreditTransactionRepository],
|
||||
exports: [
|
||||
UserService,
|
||||
CreditService,
|
||||
UserRepository,
|
||||
CreditRepository,
|
||||
CreditRepository
|
||||
],
|
||||
})
|
||||
export class UserModule { }
|
||||
|
||||
Reference in New Issue
Block a user