feat(medusa, link-modules): sales channel <> order link (#5810)
This commit is contained in:
8
.changeset/swift-carpets-count.md
Normal file
8
.changeset/swift-carpets-count.md
Normal file
@@ -0,0 +1,8 @@
|
||||
---
|
||||
"@medusajs/link-modules": patch
|
||||
"@medusajs/core-flows": patch
|
||||
"@medusajs/medusa": patch
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
feat: sales channel <> order link
|
||||
@@ -425,7 +425,9 @@ Object {
|
||||
],
|
||||
"locale": null,
|
||||
"order": Object {
|
||||
"afterLoad": [Function],
|
||||
"beforeInsert": [Function],
|
||||
"beforeUpdate": [Function],
|
||||
"billing_address_id": null,
|
||||
"canceled_at": null,
|
||||
"cart_id": null,
|
||||
@@ -753,7 +755,9 @@ Object {
|
||||
|
||||
exports[`medusa-plugin-sendgrid order canceled data 1`] = `
|
||||
Object {
|
||||
"afterLoad": [Function],
|
||||
"beforeInsert": [Function],
|
||||
"beforeUpdate": [Function],
|
||||
"billing_address": null,
|
||||
"billing_address_id": null,
|
||||
"canceled_at": Any<Date>,
|
||||
@@ -982,7 +986,9 @@ Object {
|
||||
|
||||
exports[`medusa-plugin-sendgrid order placed data 1`] = `
|
||||
Object {
|
||||
"afterLoad": [Function],
|
||||
"beforeInsert": [Function],
|
||||
"beforeUpdate": [Function],
|
||||
"billing_address": null,
|
||||
"billing_address_id": null,
|
||||
"canceled_at": null,
|
||||
@@ -1236,7 +1242,9 @@ Object {
|
||||
},
|
||||
"locale": null,
|
||||
"order": Object {
|
||||
"afterLoad": [Function],
|
||||
"beforeInsert": [Function],
|
||||
"beforeUpdate": [Function],
|
||||
"billing_address": null,
|
||||
"billing_address_id": null,
|
||||
"canceled_at": null,
|
||||
@@ -1612,7 +1620,9 @@ Object {
|
||||
],
|
||||
"locale": null,
|
||||
"order": Object {
|
||||
"afterLoad": [Function],
|
||||
"beforeInsert": [Function],
|
||||
"beforeUpdate": [Function],
|
||||
"billing_address_id": null,
|
||||
"canceled_at": null,
|
||||
"cart_id": null,
|
||||
@@ -2078,7 +2088,9 @@ Object {
|
||||
],
|
||||
"locale": null,
|
||||
"order": Object {
|
||||
"afterLoad": [Function],
|
||||
"beforeInsert": [Function],
|
||||
"beforeUpdate": [Function],
|
||||
"billing_address_id": null,
|
||||
"canceled_at": null,
|
||||
"cart_id": null,
|
||||
@@ -2533,4 +2545,4 @@ Object {
|
||||
"tracking_links": Array [],
|
||||
"tracking_number": "",
|
||||
}
|
||||
`;
|
||||
`;
|
||||
|
||||
@@ -4,10 +4,7 @@ const {
|
||||
startBootstrapApp,
|
||||
} = require("../../../environment-helpers/bootstrap-app")
|
||||
const { initDb, useDb } = require("../../../environment-helpers/use-db")
|
||||
const {
|
||||
useApi,
|
||||
useExpressServer,
|
||||
} = require("../../../environment-helpers/use-api")
|
||||
const { useApi } = require("../../../environment-helpers/use-api")
|
||||
|
||||
const adminSeeder = require("../../../helpers/admin-seeder")
|
||||
|
||||
|
||||
@@ -38,14 +38,13 @@ export async function detachSalesChannelFromProducts({
|
||||
|
||||
if (featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key)) {
|
||||
const remoteLink = container.resolve("remoteLink")
|
||||
const promises: Promise<unknown>[] = []
|
||||
|
||||
for (const [
|
||||
salesChannelId,
|
||||
productIds,
|
||||
] of salesChannelIdProductIdsMap.entries()) {
|
||||
productIds.forEach((id) =>
|
||||
promises.push(
|
||||
await promiseAll(
|
||||
productIds.map((id) =>
|
||||
remoteLink.dismiss({
|
||||
[Modules.PRODUCT]: {
|
||||
product_id: id,
|
||||
@@ -57,8 +56,6 @@ export async function detachSalesChannelFromProducts({
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
} else {
|
||||
await promiseAll(
|
||||
Array.from(salesChannelIdProductIdsMap.entries()).map(
|
||||
|
||||
@@ -4,3 +4,4 @@ export * from "./product-variant-price-set"
|
||||
export * from "./product-shipping-profile"
|
||||
export * from "./product-sales-channel"
|
||||
export * from "./cart-sales-channel"
|
||||
export * from "./order-sales-channel"
|
||||
|
||||
66
packages/link-modules/src/definitions/order-sales-channel.ts
Normal file
66
packages/link-modules/src/definitions/order-sales-channel.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
|
||||
import { LINKS } from "../links"
|
||||
|
||||
export const OrderSalesChannel: ModuleJoinerConfig = {
|
||||
serviceName: LINKS.OrderSalesChannel,
|
||||
isLink: true,
|
||||
databaseConfig: {
|
||||
tableName: "order_sales_channel",
|
||||
idPrefix: "ordersc",
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
name: "order_sales_channel",
|
||||
},
|
||||
{
|
||||
name: "order_sales_channels",
|
||||
},
|
||||
],
|
||||
primaryKeys: ["id", "order_id", "sales_channel_id"],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "orderService",
|
||||
isInternalService: true,
|
||||
primaryKey: "id",
|
||||
foreignKey: "order_id",
|
||||
alias: "order",
|
||||
},
|
||||
{
|
||||
serviceName: "salesChannelService",
|
||||
isInternalService: true,
|
||||
primaryKey: "id",
|
||||
foreignKey: "sales_channel_id",
|
||||
alias: "sales_channel",
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
{
|
||||
serviceName: "orderService",
|
||||
fieldAlias: {
|
||||
sales_channel: "sales_channel_link.sales_channel",
|
||||
},
|
||||
relationship: {
|
||||
serviceName: LINKS.OrderSalesChannel,
|
||||
isInternalService: true,
|
||||
primaryKey: "order_id",
|
||||
foreignKey: "id",
|
||||
alias: "sales_channel_link",
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: "salesChannelService",
|
||||
fieldAlias: {
|
||||
orders: "order_link.order",
|
||||
},
|
||||
relationship: {
|
||||
serviceName: LINKS.OrderSalesChannel,
|
||||
isInternalService: true,
|
||||
primaryKey: "sales_channel_id",
|
||||
foreignKey: "id",
|
||||
alias: "order_link",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -34,4 +34,10 @@ export const LINKS = {
|
||||
"salesChannelService",
|
||||
"sales_channel_id"
|
||||
),
|
||||
OrderSalesChannel: composeLinkName(
|
||||
"orderService",
|
||||
"order_id",
|
||||
"salesChannelService",
|
||||
"sales_channel_id"
|
||||
),
|
||||
}
|
||||
|
||||
15
packages/medusa/src/joiner-configs/order-service.ts
Normal file
15
packages/medusa/src/joiner-configs/order-service.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
|
||||
export default {
|
||||
serviceName: "orderService",
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: { order_id: "Order" },
|
||||
alias: [
|
||||
{
|
||||
name: "order",
|
||||
},
|
||||
{
|
||||
name: "orders",
|
||||
},
|
||||
],
|
||||
} as ModuleJoinerConfig
|
||||
@@ -0,0 +1,43 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm"
|
||||
import { MedusaV2Flag } from "@medusajs/utils"
|
||||
|
||||
import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels"
|
||||
|
||||
export const featureFlag = [SalesChannelFeatureFlag.key, MedusaV2Flag.key]
|
||||
|
||||
export class OrderSalesChannelLink1701860329931 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS "order_sales_channel"
|
||||
(
|
||||
"id" character varying NOT NULL,
|
||||
"order_id" character varying NOT NULL,
|
||||
"sales_channel_id" character varying NOT NULL,
|
||||
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||
"deleted_at" TIMESTAMP WITH TIME ZONE,
|
||||
CONSTRAINT "order_sales_channel_pk" PRIMARY KEY ("order_id", "sales_channel_id"),
|
||||
CONSTRAINT "order_sales_channel_order_id_unique" UNIQUE ("order_id")
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS "IDX_id_order_sales_channel" ON "order_sales_channel" ("id");
|
||||
|
||||
insert into "order_sales_channel" (id, order_id, sales_channel_id)
|
||||
(select 'ordersc_' || substr(md5(random()::text), 0, 27), id, sales_channel_id from "order" WHERE sales_channel_id IS NOT NULL);
|
||||
|
||||
ALTER TABLE "order" DROP CONSTRAINT IF EXISTS "FK_6ff7e874f01b478c115fdd462eb";
|
||||
`)
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
UPDATE "order"
|
||||
SET "sales_channel_id" = "order_sales_channel"."sales_channel_id"
|
||||
FROM "order_sales_channel"
|
||||
WHERE "order"."id" = "order_sales_channel"."order_id";
|
||||
|
||||
DROP TABLE IF EXISTS "order_sales_channel";
|
||||
|
||||
ALTER TABLE "order" ADD CONSTRAINT "FK_6ff7e874f01b478c115fdd462eb" FOREIGN KEY ("sales_channel_id") REFERENCES "sales_channel"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
|
||||
`)
|
||||
}
|
||||
}
|
||||
29
packages/medusa/src/models/order-sales-channel.ts
Normal file
29
packages/medusa/src/models/order-sales-channel.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { BeforeInsert, Column, Index, PrimaryColumn } from "typeorm"
|
||||
import { MedusaV2Flag, SalesChannelFeatureFlag } from "@medusajs/utils"
|
||||
|
||||
import { generateEntityId } from "../utils"
|
||||
import { SoftDeletableEntity } from "../interfaces"
|
||||
import { FeatureFlagEntity } from "../utils/feature-flag-decorators"
|
||||
|
||||
@FeatureFlagEntity([MedusaV2Flag.key, SalesChannelFeatureFlag.key])
|
||||
export class OrderSalesChannel extends SoftDeletableEntity {
|
||||
@Column()
|
||||
id: string
|
||||
|
||||
@Index("order_sales_channel_order_id_unique", {
|
||||
unique: true,
|
||||
})
|
||||
@PrimaryColumn()
|
||||
order_id: string
|
||||
|
||||
@PrimaryColumn()
|
||||
sales_channel_id: string
|
||||
|
||||
/**
|
||||
* @apiIgnore
|
||||
*/
|
||||
@BeforeInsert()
|
||||
private beforeInsert(): void {
|
||||
this.id = generateEntityId(this.id, "ordersc")
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
AfterLoad,
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
Column,
|
||||
Entity,
|
||||
Generated,
|
||||
@@ -12,7 +14,10 @@ import {
|
||||
OneToOne,
|
||||
} from "typeorm"
|
||||
import { DbAwareColumn, resolveDbType } from "../utils/db-aware-column"
|
||||
import { FeatureFlagColumn, FeatureFlagDecorators, } from "../utils/feature-flag-decorators"
|
||||
import {
|
||||
FeatureFlagColumn,
|
||||
FeatureFlagDecorators,
|
||||
} from "../utils/feature-flag-decorators"
|
||||
|
||||
import { BaseEntity } from "../interfaces/models/base-entity"
|
||||
import { generateEntityId } from "../utils/generate-entity-id"
|
||||
@@ -36,10 +41,11 @@ import { Return } from "./return"
|
||||
import { SalesChannel } from "./sales-channel"
|
||||
import { ShippingMethod } from "./shipping-method"
|
||||
import { Swap } from "./swap"
|
||||
import { MedusaV2Flag } from "@medusajs/utils"
|
||||
|
||||
/**
|
||||
* @enum
|
||||
*
|
||||
*
|
||||
* The order's status.
|
||||
*/
|
||||
export enum OrderStatus {
|
||||
@@ -48,7 +54,7 @@ export enum OrderStatus {
|
||||
*/
|
||||
PENDING = "pending",
|
||||
/**
|
||||
* The order is completed, meaning that
|
||||
* The order is completed, meaning that
|
||||
* the items have been fulfilled and the payment
|
||||
* has been captured.
|
||||
*/
|
||||
@@ -69,7 +75,7 @@ export enum OrderStatus {
|
||||
|
||||
/**
|
||||
* @enum
|
||||
*
|
||||
*
|
||||
* The order's fulfillment status.
|
||||
*/
|
||||
export enum FulfillmentStatus {
|
||||
@@ -78,7 +84,7 @@ export enum FulfillmentStatus {
|
||||
*/
|
||||
NOT_FULFILLED = "not_fulfilled",
|
||||
/**
|
||||
* Some of the order's items, but not all, are fulfilled.
|
||||
* Some of the order's items, but not all, are fulfilled.
|
||||
*/
|
||||
PARTIALLY_FULFILLED = "partially_fulfilled",
|
||||
/**
|
||||
@@ -113,7 +119,7 @@ export enum FulfillmentStatus {
|
||||
|
||||
/**
|
||||
* @enum
|
||||
*
|
||||
*
|
||||
* The order's payment status.
|
||||
*/
|
||||
export enum PaymentStatus {
|
||||
@@ -321,6 +327,25 @@ export class Order extends BaseEntity {
|
||||
])
|
||||
sales_channel: SalesChannel
|
||||
|
||||
@FeatureFlagDecorators(
|
||||
[MedusaV2Flag.key, "sales_channels"],
|
||||
[
|
||||
ManyToMany(() => SalesChannel, { cascade: ["remove", "soft-remove"] }),
|
||||
JoinTable({
|
||||
name: "order_sales_channel",
|
||||
joinColumn: {
|
||||
name: "cart_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "sales_channel_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
sales_channels?: SalesChannel[]
|
||||
|
||||
// Total fields
|
||||
shipping_total: number
|
||||
shipping_tax_total: number | null
|
||||
@@ -345,6 +370,12 @@ export class Order extends BaseEntity {
|
||||
private async beforeInsert(): Promise<void> {
|
||||
this.id = generateEntityId(this.id, "order")
|
||||
|
||||
if (this.sales_channel_id || this.sales_channel) {
|
||||
this.sales_channels = [
|
||||
{ id: this.sales_channel_id || this.sales_channel?.id },
|
||||
] as SalesChannel[]
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === "development" && !this.display_id) {
|
||||
const disId = await manualAutoIncrement("order")
|
||||
|
||||
@@ -353,6 +384,30 @@ export class Order extends BaseEntity {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiIgnore
|
||||
*/
|
||||
@BeforeUpdate()
|
||||
private beforeUpdate(): void {
|
||||
if (this.sales_channel_id || this.sales_channel) {
|
||||
this.sales_channels = [
|
||||
{ id: this.sales_channel_id || this.sales_channel?.id },
|
||||
] as SalesChannel[]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @apiIgnore
|
||||
*/
|
||||
@AfterLoad()
|
||||
private afterLoad(): void {
|
||||
if (this.sales_channels) {
|
||||
this.sales_channel = this.sales_channels?.[0]
|
||||
this.sales_channel_id = this.sales_channel?.id
|
||||
delete this.sales_channels
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { BeforeInsert, Column, JoinTable, ManyToMany, OneToMany } from "typeorm"
|
||||
|
||||
import { MedusaV2Flag } from "@medusajs/utils"
|
||||
import {
|
||||
FeatureFlagDecorators,
|
||||
FeatureFlagEntity,
|
||||
} from "../utils/feature-flag-decorators"
|
||||
import { MedusaV2Flag } from "@medusajs/utils"
|
||||
import { SoftDeletableEntity } from "../interfaces"
|
||||
import { DbAwareColumn, generateEntityId } from "../utils"
|
||||
import { SalesChannelLocation } from "./sales-channel-location"
|
||||
import { Product } from "./product"
|
||||
import { Cart } from "./cart"
|
||||
import { Order } from "./order"
|
||||
|
||||
@FeatureFlagEntity("sales_channels")
|
||||
export class SalesChannel extends SoftDeletableEntity {
|
||||
@@ -55,6 +56,24 @@ export class SalesChannel extends SoftDeletableEntity {
|
||||
])
|
||||
carts: Cart[]
|
||||
|
||||
@FeatureFlagDecorators(MedusaV2Flag.key,
|
||||
[
|
||||
ManyToMany(() => Order),
|
||||
JoinTable({
|
||||
name: "order_sales_channel",
|
||||
joinColumn: {
|
||||
name: "sales_channel_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
inverseJoinColumn: {
|
||||
name: "order_id",
|
||||
referencedColumnName: "id",
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
orders: Order[]
|
||||
|
||||
@OneToMany(
|
||||
() => SalesChannelLocation,
|
||||
(scLocation) => scLocation.sales_channel,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
|
||||
import { FlagRouter } from "@medusajs/utils"
|
||||
import { LineItemServiceMock } from "../__mocks__/line-item"
|
||||
import { newTotalsServiceMock } from "../__mocks__/new-totals"
|
||||
import { ProductVariantInventoryServiceMock } from "../__mocks__/product-variant-inventory"
|
||||
@@ -151,6 +152,7 @@ describe("OrderService", () => {
|
||||
eventBusService,
|
||||
cartService,
|
||||
productVariantInventoryService,
|
||||
featureFlagRouter: new FlagRouter({}),
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
FlagRouter,
|
||||
isDefined,
|
||||
MedusaError,
|
||||
MedusaV2Flag,
|
||||
promiseAll, selectorConstraintsToString,
|
||||
} from "@medusajs/utils"
|
||||
import {
|
||||
@@ -64,6 +65,7 @@ import { TotalsContext, UpdateOrderInput } from "../types/orders"
|
||||
import { CreateShippingMethodDto } from "../types/shipping-options"
|
||||
import { buildQuery, isString, setMetadata } from "../utils"
|
||||
import EventBusService from "./event-bus"
|
||||
import { RemoteLink } from "@medusajs/modules-sdk"
|
||||
|
||||
export const ORDER_CART_ALREADY_EXISTS_ERROR = "Order from cart already exists"
|
||||
|
||||
@@ -90,6 +92,7 @@ type InjectedDependencies = {
|
||||
eventBusService: EventBusService
|
||||
featureFlagRouter: FlagRouter
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
remoteLink: RemoteLink
|
||||
}
|
||||
|
||||
class OrderService extends TransactionBaseService {
|
||||
@@ -132,6 +135,7 @@ class OrderService extends TransactionBaseService {
|
||||
protected readonly inventoryService_: IInventoryService
|
||||
protected readonly eventBus_: EventBusService
|
||||
protected readonly featureFlagRouter_: FlagRouter
|
||||
protected remoteLink_: RemoteLink
|
||||
// eslint-disable-next-line max-len
|
||||
protected readonly productVariantInventoryService_: ProductVariantInventoryService
|
||||
|
||||
@@ -150,6 +154,7 @@ class OrderService extends TransactionBaseService {
|
||||
taxProviderService,
|
||||
regionService,
|
||||
cartService,
|
||||
remoteLink,
|
||||
addressRepository,
|
||||
giftCardService,
|
||||
draftOrderService,
|
||||
@@ -180,6 +185,7 @@ class OrderService extends TransactionBaseService {
|
||||
this.draftOrderService_ = draftOrderService
|
||||
this.featureFlagRouter_ = featureFlagRouter
|
||||
this.productVariantInventoryService_ = productVariantInventoryService
|
||||
this.remoteLink_ = remoteLink
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -693,7 +699,8 @@ class OrderService extends TransactionBaseService {
|
||||
|
||||
if (
|
||||
cart.sales_channel_id &&
|
||||
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key)
|
||||
this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) &&
|
||||
!this.featureFlagRouter_.isFeatureEnabled(MedusaV2Flag.key)
|
||||
) {
|
||||
toCreate.sales_channel_id = cart.sales_channel_id
|
||||
}
|
||||
@@ -710,6 +717,22 @@ class OrderService extends TransactionBaseService {
|
||||
const rawOrder = orderRepo.create(toCreate)
|
||||
const order = await orderRepo.save(rawOrder)
|
||||
|
||||
if (
|
||||
this.featureFlagRouter_.isFeatureEnabled([
|
||||
SalesChannelFeatureFlag.key,
|
||||
MedusaV2Flag.key,
|
||||
])
|
||||
) {
|
||||
await this.remoteLink_.create({
|
||||
orderService: {
|
||||
order_id: order.id,
|
||||
},
|
||||
salesChannelService: {
|
||||
sales_channel_id: cart.sales_channel_id as string,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (total !== 0 && payment) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(manager)
|
||||
@@ -2082,7 +2105,7 @@ class OrderService extends TransactionBaseService {
|
||||
relationSet.add("shipping_methods.tax_lines")
|
||||
relationSet.add("region")
|
||||
relationSet.add("payments")
|
||||
|
||||
|
||||
return Array.from(relationSet.values())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ export function FeatureFlagDecorators(
|
||||
}
|
||||
|
||||
export function FeatureFlagClassDecorators(
|
||||
featureFlag: string,
|
||||
featureFlag: string | string[],
|
||||
decorators: ClassDecorator[]
|
||||
): ClassDecorator {
|
||||
return function (target) {
|
||||
|
||||
Reference in New Issue
Block a user