online payment

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