refactor order craetion
This commit is contained in:
@@ -16,7 +16,7 @@ import { Delivery } from '../../delivery/entities/delivery.entity';
|
|||||||
import { OrderRepository } from '../repositories/order.repository';
|
import { OrderRepository } from '../repositories/order.repository';
|
||||||
import { FindOrdersDto } from '../dto/find-orders.dto';
|
import { FindOrdersDto } from '../dto/find-orders.dto';
|
||||||
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
import { PaginatedResult } from 'src/common/interfaces/pagination.interface';
|
||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { Payment } from 'src/modules/payments/entities/payment.entity';
|
||||||
|
|
||||||
type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number };
|
type OrderItemData = { food: Food; quantity: number; unitPrice: number; discount: number };
|
||||||
|
|
||||||
@@ -52,34 +52,21 @@ export class OrdersService {
|
|||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly cartService: CartService,
|
private readonly cartService: CartService,
|
||||||
private readonly orderRepository: OrderRepository,
|
private readonly orderRepository: OrderRepository,
|
||||||
// NOTE: kept for future payment integration
|
private readonly paymentsService: PaymentsService,
|
||||||
private readonly _paymentsService: PaymentsService,
|
|
||||||
// NOTE: kept for future event emission
|
|
||||||
private readonly _eventEmitter2: EventEmitter2,
|
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
async checkout(userId: string, restaurantId: string) {
|
async checkout(userId: string, restaurantId: string) {
|
||||||
const cart = await this.cartService.findOneOrFail(userId, restaurantId);
|
const cart = await this.cartService.findOneOrFail(userId, restaurantId);
|
||||||
|
const validated = await this.validateCartForOrder(userId, restaurantId, cart);
|
||||||
|
|
||||||
const { order } = await this.createOrder(userId, restaurantId, cart);
|
const order = await this.em.transactional(async em => {
|
||||||
|
|
||||||
await this.cartService.clearCart(userId, restaurantId);
|
|
||||||
|
|
||||||
this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`);
|
|
||||||
return { order };
|
|
||||||
}
|
|
||||||
|
|
||||||
async createOrder(userId: string, restaurantId: string, cart: Cart) {
|
|
||||||
const validationResult = await this.validateCartForOrder(userId, restaurantId, cart);
|
|
||||||
|
|
||||||
return this.em.transactional(async em => {
|
|
||||||
const order = em.create(Order, {
|
const order = em.create(Order, {
|
||||||
user: validationResult.user,
|
user: validated.user,
|
||||||
restaurant: validationResult.restaurant,
|
restaurant: validated.restaurant,
|
||||||
deliveryMethod: validationResult.delivery,
|
deliveryMethod: validated.delivery,
|
||||||
userAddress: validationResult.userAddress,
|
userAddress: validated.userAddress,
|
||||||
carAddress: validationResult.carAddress,
|
carAddress: validated.carAddress,
|
||||||
paymentMethod: validationResult.paymentMethod,
|
paymentMethod: validated.paymentMethod,
|
||||||
couponDiscount: cart.couponDiscount || 0,
|
couponDiscount: cart.couponDiscount || 0,
|
||||||
itemsDiscount: cart.itemsDiscount || 0,
|
itemsDiscount: cart.itemsDiscount || 0,
|
||||||
totalDiscount: cart.totalDiscount || 0,
|
totalDiscount: cart.totalDiscount || 0,
|
||||||
@@ -96,7 +83,7 @@ export class OrdersService {
|
|||||||
|
|
||||||
em.persist(order);
|
em.persist(order);
|
||||||
|
|
||||||
for (const itemData of validationResult.orderItemsData) {
|
for (const itemData of validated.orderItemsData) {
|
||||||
const { food, quantity, unitPrice, discount } = itemData;
|
const { food, quantity, unitPrice, discount } = itemData;
|
||||||
|
|
||||||
this.assertFoodHasSufficientStock(food, quantity);
|
this.assertFoodHasSufficientStock(food, quantity);
|
||||||
@@ -113,13 +100,27 @@ export class OrdersService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
em.persist(orderItem);
|
em.persist(orderItem);
|
||||||
|
|
||||||
em.persist(food);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const payment = em.create(Payment, {
|
||||||
|
order,
|
||||||
|
amount: order.total,
|
||||||
|
status: PaymentStatusEnum.Pending,
|
||||||
|
method: order.paymentMethod.method,
|
||||||
|
gateway: order.paymentMethod.gateway ?? null,
|
||||||
|
});
|
||||||
|
em.persist(payment);
|
||||||
|
|
||||||
await em.flush();
|
await em.flush();
|
||||||
return { order };
|
this.logger.debug(`Order ${order.id} created for user ${userId} (restaurant ${restaurantId})`);
|
||||||
|
return order;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.cartService.clearCart(userId, restaurantId);
|
||||||
|
|
||||||
|
const { paymentUrl } = await this.paymentsService.payOrder(order.id);
|
||||||
|
|
||||||
|
return { paymentUrl };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -67,8 +67,8 @@ export class PaymentsController {
|
|||||||
name: 'X-Slug',
|
name: 'X-Slug',
|
||||||
required: true,
|
required: true,
|
||||||
})
|
})
|
||||||
payAnOrder(@Param('orderId') orderId: string, @RestId() restId: string) {
|
payAnOrder(@Param('orderId') orderId: string) {
|
||||||
return this.paymentsService.payOrder(orderId, restId);
|
return this.paymentsService.payOrder(orderId);
|
||||||
}
|
}
|
||||||
// @UseGuards(AdminAuthGuard)
|
// @UseGuards(AdminAuthGuard)
|
||||||
// @ApiBearerAuth()
|
// @ApiBearerAuth()
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ export class ZarinpalGateway implements IPaymentGateway {
|
|||||||
this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`;
|
this.zarinpalVerifyUrl = `${zarinpalBaseUrl}/pg/v4/payment/verify.json`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getAxiosErrorData(error: unknown): { status?: number; data?: unknown } | null {
|
||||||
|
if (!axios.isAxiosError(error) || !error.response) return null;
|
||||||
|
return { status: error.response.status, data: error.response.data };
|
||||||
|
}
|
||||||
|
|
||||||
async requestPayment({ amount, merchantId, orderId, domain }: IRequestPaymentParams): Promise<IRequestPaymentData> {
|
async requestPayment({ amount, merchantId, orderId, domain }: 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 = `${domain}/verify/${orderId}`;
|
||||||
@@ -48,23 +53,53 @@ export class ZarinpalGateway implements IPaymentGateway {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.post<IZarinpalPaymentResponse>(this.zarinpalRequestUrl, zarinpalRequest, this.axiosConfig);
|
const res = await axios.post<IZarinpalPaymentResponse>(this.zarinpalRequestUrl, zarinpalRequest, this.axiosConfig);
|
||||||
const { code, message, authority, errors } = res.data;
|
const { data, errors } = res.data ?? {};
|
||||||
// Check if there are errors in the response
|
const code = data?.code;
|
||||||
if ((code !== 100 && code !== 0) || !authority) {
|
const message = data?.message;
|
||||||
|
const authority = data?.authority;
|
||||||
|
|
||||||
|
if (!data) {
|
||||||
|
this.logger.error('Invalid response from Zarinpal API (missing data)', JSON.stringify(res.data));
|
||||||
|
throw new BadRequestException('Invalid response from Zarinpal API');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((Array.isArray(errors) && errors.length > 0) || code !== 100 || !authority) {
|
||||||
|
this.logger.error(
|
||||||
|
'Zarinpal payment request failed',
|
||||||
|
JSON.stringify({
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
errors,
|
||||||
|
callbackUrl,
|
||||||
|
amount,
|
||||||
|
orderId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
throw new BadRequestException(message ?? 'Zarinpal payment request error');
|
throw new BadRequestException(message ?? 'Zarinpal payment request error');
|
||||||
}
|
}
|
||||||
|
|
||||||
return { transactionId: authority };
|
return { transactionId: authority };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log the actual API error response for debugging
|
if (error instanceof BadRequestException) throw error;
|
||||||
if (axios.isAxiosError(error) && error.response) {
|
|
||||||
this.logger.error('Zarinpal API error response', {
|
const axiosErr = this.getAxiosErrorData(error);
|
||||||
status: error.response.status,
|
if (axiosErr) {
|
||||||
data: JSON.stringify(error.response.data),
|
this.logger.error(
|
||||||
request: zarinpalRequest,
|
'Zarinpal payment request axios error',
|
||||||
});
|
JSON.stringify({
|
||||||
|
status: axiosErr.status,
|
||||||
|
data: axiosErr.data,
|
||||||
|
callbackUrl,
|
||||||
|
amount,
|
||||||
|
orderId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
throw new BadRequestException('Zarinpal payment request error');
|
||||||
}
|
}
|
||||||
throw error;
|
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
this.logger.error('Zarinpal payment request error', errorMessage);
|
||||||
|
throw new BadRequestException(`Failed to request payment from gateway: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,12 +41,14 @@ export interface IZarinpalRequestPayment {
|
|||||||
referrer_id?: string;
|
referrer_id?: string;
|
||||||
}
|
}
|
||||||
export interface IZarinpalPaymentResponse {
|
export interface IZarinpalPaymentResponse {
|
||||||
code: number;
|
data: {
|
||||||
message: string;
|
code: number;
|
||||||
authority: string;
|
message: string;
|
||||||
fee_type: string;
|
authority: string;
|
||||||
fee: number;
|
fee_type: string;
|
||||||
errors: [];
|
fee: number;
|
||||||
|
};
|
||||||
|
errors: unknown[];
|
||||||
}
|
}
|
||||||
export interface IZarinpalVerifyRequest {
|
export interface IZarinpalVerifyRequest {
|
||||||
merchant_id: string;
|
merchant_id: string;
|
||||||
@@ -63,5 +65,5 @@ export interface IZarinpalVerifyResponse {
|
|||||||
fee_type: string;
|
fee_type: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
};
|
};
|
||||||
errors: [];
|
errors: unknown[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,13 +11,15 @@ import { JwtModule } from '@nestjs/jwt';
|
|||||||
import { Payment } from './entities/payment.entity';
|
import { Payment } from './entities/payment.entity';
|
||||||
import { ZarinpalGateway } from './gateways/zarinpal.gateway';
|
import { ZarinpalGateway } from './gateways/zarinpal.gateway';
|
||||||
import { GatewayManager } from './services/gateway.manager';
|
import { GatewayManager } from './services/gateway.manager';
|
||||||
|
import { PaymentRepository } from './repositories/payment.repository';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]), AuthModule, JwtModule],
|
imports: [MikroOrmModule.forFeature([PaymentMethod, Payment, Restaurant]), AuthModule, JwtModule],
|
||||||
controllers: [PaymentsController],
|
controllers: [PaymentsController],
|
||||||
providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, ZarinpalGateway, GatewayManager],
|
providers: [PaymentsService, PaymentMethodService, PaymentMethodRepository, PaymentRepository, ZarinpalGateway, GatewayManager],
|
||||||
exports: [
|
exports: [
|
||||||
PaymentMethodRepository,
|
PaymentMethodRepository,
|
||||||
|
PaymentRepository,
|
||||||
PaymentMethodService,
|
PaymentMethodService,
|
||||||
PaymentsService,
|
PaymentsService,
|
||||||
// PaymentGatewayService,
|
// PaymentGatewayService,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
|
import { Payment } from '../entities/payment.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PaymentRepository extends EntityRepository<Payment> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, Payment);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment';
|
import { PaymentGatewayEnum, PaymentMethodEnum, PaymentStatusEnum } from '../interface/payment';
|
||||||
import { Payment } from '../entities/payment.entity';
|
import { Payment } from '../entities/payment.entity';
|
||||||
import { EntityManager } from '@mikro-orm/core';
|
import { EntityManager } from '@mikro-orm/core';
|
||||||
import { Order } from '../../orders/entities/order.entity';
|
import { Order } from '../../orders/entities/order.entity';
|
||||||
@@ -19,9 +19,13 @@ export class PaymentsService {
|
|||||||
|
|
||||||
async payOrder(
|
async payOrder(
|
||||||
orderId: string,
|
orderId: string,
|
||||||
restId: string,
|
|
||||||
): Promise<{ paymentUrl: string | null }> {
|
): Promise<{ paymentUrl: string | null }> {
|
||||||
const ctx = await this.loadAndValidateOrder(orderId, restId);
|
const ctx = await this.loadAndValidateOrder(orderId);
|
||||||
|
|
||||||
|
// Idempotency: avoid creating/charging again for already-paid orders
|
||||||
|
if (ctx.order.paymentStatus === PaymentStatusEnum.Paid) {
|
||||||
|
return { paymentUrl: null };
|
||||||
|
}
|
||||||
|
|
||||||
switch (ctx.method) {
|
switch (ctx.method) {
|
||||||
case PaymentMethodEnum.Cash:
|
case PaymentMethodEnum.Cash:
|
||||||
@@ -42,12 +46,11 @@ export class PaymentsService {
|
|||||||
|
|
||||||
private async loadAndValidateOrder(
|
private async loadAndValidateOrder(
|
||||||
orderId: string,
|
orderId: string,
|
||||||
restId: string,
|
|
||||||
): Promise<OrderPaymentContext> {
|
): Promise<OrderPaymentContext> {
|
||||||
const order = await this.em.findOne(
|
const order = await this.em.findOne(
|
||||||
Order,
|
Order,
|
||||||
{ id: orderId, restaurant: { id: restId } },
|
{ id: orderId },
|
||||||
{ populate: ['user', 'paymentMethod', 'paymentMethod.restaurant'] },
|
{ populate: ['user', 'restaurant', 'paymentMethod', 'paymentMethod.restaurant'] },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!order) {
|
if (!order) {
|
||||||
@@ -63,8 +66,10 @@ export class PaymentsService {
|
|||||||
throw new NotFoundException('Payment method not found');
|
throw new NotFoundException('Payment method not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pm.method === PaymentMethodEnum.Online && !pm.merchantId) {
|
if (pm.method === PaymentMethodEnum.Online) {
|
||||||
throw new BadRequestException('Merchant ID is required for online payment');
|
if (!pm.gateway) throw new BadRequestException('Gateway is required for online payment');
|
||||||
|
if (!pm.merchantId) throw new BadRequestException('Merchant ID is required for online payment');
|
||||||
|
if (!pm.restaurant?.domain) throw new BadRequestException('Restaurant domain is required for online payment');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -78,23 +83,25 @@ export class PaymentsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async handleCashPayment(ctx: OrderPaymentContext): Promise<void> {
|
private async handleCashPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||||
const payment = this.em.create(Payment, {
|
await this.getOrCreateLatestPendingPayment(ctx.order.id, {
|
||||||
amount: ctx.amount,
|
amount: ctx.amount,
|
||||||
order: ctx.order,
|
|
||||||
method: PaymentMethodEnum.Cash,
|
method: PaymentMethodEnum.Cash,
|
||||||
gateway: null,
|
gateway: null,
|
||||||
transactionId: null,
|
|
||||||
status: PaymentStatusEnum.Pending,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.em.persistAndFlush(payment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
|
private async handleWalletPayment(ctx: OrderPaymentContext): Promise<void> {
|
||||||
await this.em.transactional(async em => {
|
await this.em.transactional(async em => {
|
||||||
|
const order = await em.findOne(Order, { id: ctx.order.id }, { populate: ['user', 'restaurant'] });
|
||||||
|
if (!order) throw new NotFoundException('Order not found');
|
||||||
|
|
||||||
|
if (order.paymentStatus === PaymentStatusEnum.Paid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const wallet = await em.findOne(UserWallet, {
|
const wallet = await em.findOne(UserWallet, {
|
||||||
user: { id: ctx.order.user.id },
|
user: { id: order.user.id },
|
||||||
restaurant: { id: ctx.order.restaurant.id },
|
restaurant: { id: order.restaurant.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!wallet) {
|
if (!wallet) {
|
||||||
@@ -107,18 +114,18 @@ export class PaymentsService {
|
|||||||
|
|
||||||
wallet.wallet -= ctx.amount;
|
wallet.wallet -= ctx.amount;
|
||||||
|
|
||||||
const payment = em.create(Payment, {
|
const payment = await this.getOrCreateLatestPendingPayment(order.id, {
|
||||||
|
em,
|
||||||
amount: ctx.amount,
|
amount: ctx.amount,
|
||||||
order: ctx.order,
|
|
||||||
method: PaymentMethodEnum.Wallet,
|
method: PaymentMethodEnum.Wallet,
|
||||||
gateway: null,
|
gateway: null,
|
||||||
status: PaymentStatusEnum.Paid,
|
|
||||||
paidAt: new Date(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ctx.order.paymentStatus = PaymentStatusEnum.Paid;
|
payment.status = PaymentStatusEnum.Paid;
|
||||||
|
payment.paidAt = new Date();
|
||||||
|
order.paymentStatus = PaymentStatusEnum.Paid;
|
||||||
|
|
||||||
em.persist([wallet, payment, ctx.order]);
|
em.persist([wallet, payment, order]);
|
||||||
await em.flush();
|
await em.flush();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -129,6 +136,17 @@ export class PaymentsService {
|
|||||||
): Promise<{ paymentUrl: string }> {
|
): Promise<{ paymentUrl: string }> {
|
||||||
const gateway = this.gatewayManager.get(ctx.gateway!);
|
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 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({
|
const { transactionId } = await gateway.requestPayment({
|
||||||
amount: ctx.amount,
|
amount: ctx.amount,
|
||||||
orderId: ctx.order.id,
|
orderId: ctx.order.id,
|
||||||
@@ -136,14 +154,9 @@ export class PaymentsService {
|
|||||||
domain: ctx.restaurantDomain!,
|
domain: ctx.restaurantDomain!,
|
||||||
});
|
});
|
||||||
|
|
||||||
const payment = this.em.create(Payment, {
|
payment.gateway = ctx.gateway;
|
||||||
amount: ctx.amount,
|
payment.transactionId = transactionId;
|
||||||
order: ctx.order,
|
payment.status = PaymentStatusEnum.Pending;
|
||||||
method: PaymentMethodEnum.Online,
|
|
||||||
gateway: ctx.gateway,
|
|
||||||
transactionId,
|
|
||||||
status: PaymentStatusEnum.Pending,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.em.persistAndFlush(payment);
|
await this.em.persistAndFlush(payment);
|
||||||
|
|
||||||
@@ -212,4 +225,43 @@ export class PaymentsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async getOrCreateLatestPendingPayment(
|
||||||
|
orderId: string,
|
||||||
|
params: {
|
||||||
|
amount: number;
|
||||||
|
method: PaymentMethodEnum;
|
||||||
|
gateway: PaymentGatewayEnum | null;
|
||||||
|
em?: EntityManager;
|
||||||
|
},
|
||||||
|
): Promise<Payment> {
|
||||||
|
const em = params.em ?? this.em;
|
||||||
|
|
||||||
|
const existing = await em.findOne(
|
||||||
|
Payment,
|
||||||
|
{ order: { id: orderId }, status: PaymentStatusEnum.Pending },
|
||||||
|
{ orderBy: { createdAt: 'DESC' } },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
if (existing.method !== params.method) {
|
||||||
|
throw new BadRequestException('Existing pending payment method does not match order payment method');
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderRef = em.getReference(Order, orderId);
|
||||||
|
const payment = em.create(Payment, {
|
||||||
|
amount: params.amount,
|
||||||
|
order: orderRef,
|
||||||
|
method: params.method,
|
||||||
|
gateway: params.gateway,
|
||||||
|
transactionId: null,
|
||||||
|
status: PaymentStatusEnum.Pending,
|
||||||
|
});
|
||||||
|
|
||||||
|
em.persist(payment);
|
||||||
|
await em.flush();
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user