Enable Montact CRM integration and admin CRM APIs.

This commit is contained in:
2026-10-10 17:44:30 +03:30
parent 7481ac5243
commit 68752f0403
11 changed files with 1158 additions and 2 deletions
+4
View File
@@ -18,6 +18,10 @@ RUN_SEED=false
JWT_SECRET=change-me-to-a-long-random-string
JWT_EXPIRES_IN=7d
# Montact CRM (اختیاری): توکن ثابت mk_live_...؛ اگر خالی باشد فقط JWT ادمین قبول می‌شود
# MONTACT_API_TOKEN=
# MONTACT_STORE_ID=1
MOCK_OTP_CODE=1234
OTP_TTL_MINUTES=5
+592 -2
View File
@@ -134757,6 +134757,7 @@ var require_crm_controller = __commonJS({
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.CrmController = void 0;
var common_12 = require_common();
var swagger_12 = require_dist6();
var client_1 = require_default2();
var auth_decorators_1 = require_auth_decorators();
var crm_service_1 = require_crm_service();
@@ -134803,6 +134804,8 @@ var require_crm_controller = __commonJS({
__metadata2("design:returntype", void 0)
], CrmController.prototype, "updateOpportunity", null);
exports2.CrmController = CrmController = __decorate2([
(0, swagger_12.ApiTags)("Crm"),
(0, swagger_12.ApiBearerAuth)(),
(0, common_12.Controller)("admin/crm"),
(0, auth_decorators_1.AuthType)("admin"),
(0, auth_decorators_1.Roles)("admin", "sales"),
@@ -134832,6 +134835,7 @@ var require_customer_crm_controller = __commonJS({
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.AdminNextActionsController = exports2.CustomerCrmController = void 0;
var common_12 = require_common();
var swagger_12 = require_dist6();
var auth_decorators_1 = require_auth_decorators();
var current_user_decorator_1 = require_current_user_decorator();
var crm_service_1 = require_crm_service();
@@ -134912,6 +134916,8 @@ var require_customer_crm_controller = __commonJS({
__metadata2("design:returntype", void 0)
], CustomerCrmController.prototype, "setAssignment", null);
exports2.CustomerCrmController = CustomerCrmController = __decorate2([
(0, swagger_12.ApiTags)("CustomerCrm"),
(0, swagger_12.ApiBearerAuth)(),
(0, common_12.Controller)("admin/customers"),
(0, auth_decorators_1.AuthType)("admin"),
(0, auth_decorators_1.Roles)("admin", "sales"),
@@ -134939,6 +134945,8 @@ var require_customer_crm_controller = __commonJS({
__metadata2("design:returntype", void 0)
], AdminNextActionsController.prototype, "update", null);
exports2.AdminNextActionsController = AdminNextActionsController = __decorate2([
(0, swagger_12.ApiTags)("CustomerCrm"),
(0, swagger_12.ApiBearerAuth)(),
(0, common_12.Controller)("admin/next-actions"),
(0, auth_decorators_1.AuthType)("admin"),
(0, auth_decorators_1.Roles)("admin", "sales"),
@@ -134968,6 +134976,7 @@ var require_admin_agents_controller = __commonJS({
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.AdminAssignmentRequestsController = exports2.AdminAgentsController = void 0;
var common_12 = require_common();
var swagger_12 = require_dist6();
var client_1 = require_default2();
var auth_decorators_1 = require_auth_decorators();
var current_user_decorator_1 = require_current_user_decorator();
@@ -135025,6 +135034,8 @@ var require_admin_agents_controller = __commonJS({
__metadata2("design:returntype", void 0)
], AdminAgentsController.prototype, "customers", null);
exports2.AdminAgentsController = AdminAgentsController = __decorate2([
(0, swagger_12.ApiTags)("AdminAgents"),
(0, swagger_12.ApiBearerAuth)(),
(0, common_12.Controller)("admin/agents"),
(0, auth_decorators_1.AuthType)("admin"),
(0, auth_decorators_1.Roles)("admin", "sales"),
@@ -135063,6 +135074,8 @@ var require_admin_agents_controller = __commonJS({
__metadata2("design:returntype", void 0)
], AdminAssignmentRequestsController.prototype, "review", null);
exports2.AdminAssignmentRequestsController = AdminAssignmentRequestsController = __decorate2([
(0, swagger_12.ApiTags)("AdminAgents"),
(0, swagger_12.ApiBearerAuth)(),
(0, common_12.Controller)("admin/assignment-requests"),
(0, auth_decorators_1.AuthType)("admin"),
(0, auth_decorators_1.Roles)("admin", "sales"),
@@ -142743,6 +142756,579 @@ var require_internal_messages_module = __commonJS({
}
});
// dist/montact/montact-auth.guard.js
var require_montact_auth_guard = __commonJS({
"dist/montact/montact-auth.guard.js"(exports2) {
"use strict";
var __decorate2 = exports2 && exports2.__decorate || function(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata2 = exports2 && exports2.__metadata || function(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.MontactAuthGuard = void 0;
var common_12 = require_common();
var config_1 = require_config2();
var jwt_1 = require_jwt();
var jwt_secret_1 = require_jwt_secret();
var MontactAuthGuard = class MontactAuthGuard {
static {
__name(this, "MontactAuthGuard");
}
jwt;
config;
constructor(jwt, config) {
this.jwt = jwt;
this.config = config;
}
async canActivate(context) {
const request = context.switchToHttp().getRequest();
const authHeader = request.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
throw new common_12.UnauthorizedException("\u062A\u0648\u06A9\u0646 \u0646\u0627\u0645\u0639\u062A\u0628\u0631 \u06CC\u0627 \u0645\u0646\u0642\u0636\u06CC \u0634\u062F\u0647 \u0627\u0633\u062A");
}
const token = authHeader.slice(7).trim();
const montactToken = this.config.get("MONTACT_API_TOKEN")?.trim();
if (montactToken && token === montactToken) {
return true;
}
try {
const payload = await this.jwt.verifyAsync(token, {
secret: (0, jwt_secret_1.resolveJwtSecret)(this.config)
});
if (payload.type === "admin") {
return true;
}
} catch {
}
throw new common_12.UnauthorizedException("\u062A\u0648\u06A9\u0646 \u0646\u0627\u0645\u0639\u062A\u0628\u0631 \u06CC\u0627 \u0645\u0646\u0642\u0636\u06CC \u0634\u062F\u0647 \u0627\u0633\u062A");
}
};
exports2.MontactAuthGuard = MontactAuthGuard;
exports2.MontactAuthGuard = MontactAuthGuard = __decorate2([
(0, common_12.Injectable)(),
__metadata2("design:paramtypes", [
jwt_1.JwtService,
config_1.ConfigService
])
], MontactAuthGuard);
}
});
// dist/montact/montact.service.js
var require_montact_service = __commonJS({
"dist/montact/montact.service.js"(exports2) {
"use strict";
var __decorate2 = exports2 && exports2.__decorate || function(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata2 = exports2 && exports2.__metadata || function(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.MontactService = void 0;
var common_12 = require_common();
var client_1 = require_default2();
var config_1 = require_config2();
var prisma_service_1 = require_prisma_service();
var name_1 = require_name();
var MontactService = class MontactService {
static {
__name(this, "MontactService");
}
prisma;
config;
constructor(prisma, config) {
this.prisma = prisma;
this.config = config;
}
storeId() {
const raw2 = this.config.get("MONTACT_STORE_ID");
const n = raw2 ? Number.parseInt(raw2, 10) : 1;
return Number.isFinite(n) ? n : 1;
}
async listCustomers(page = 1, limit = 50, updatedAfter) {
const safePage = Math.max(1, page);
const safeLimit = Math.min(100, Math.max(1, limit));
const skip2 = (safePage - 1) * safeLimit;
const updatedFilter = updatedAfter ? { updatedAt: { gte: new Date(updatedAfter) } } : {};
const where = {
appRole: client_1.AppUserRole.customer,
...updatedFilter
};
const [total, rows] = await Promise.all([
this.prisma.customer.count({ where }),
this.prisma.customer.findMany({
where,
orderBy: { updatedAt: "desc" },
skip: skip2,
take: safeLimit,
include: {
orders: { select: { finalAmount: true } }
}
})
]);
const data = rows.map((c) => this.mapCustomerListItem(c, c.orders));
const lastPage = Math.max(1, Math.ceil(total / safeLimit));
return {
data,
meta: {
current_page: safePage,
per_page: safeLimit,
total,
last_page: lastPage
}
};
}
async getCustomer(id) {
const customer = await this.prisma.customer.findFirst({
where: { id, appRole: client_1.AppUserRole.customer },
include: {
orders: {
orderBy: { createdAt: "desc" },
include: { items: true }
},
addresses: true,
favorites: { include: { product: { select: { id: true, name: true } } } }
}
});
if (!customer)
throw new common_12.NotFoundException("\u0645\u0646\u0628\u0639 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
const orders = customer.orders.map((o) => this.mapOrder(o, false));
const addresses = customer.addresses.map((a) => this.mapAddress(a, customer.id));
const summary = this.buildSummary(customer.orders);
return {
...this.mapCustomerBase(customer),
orders,
summary,
addresses,
transactions: [],
engagement: this.mapEngagement(customer.favorites)
};
}
async listCustomerOrders(customerId, updatedAfter) {
await this.assertCustomer(customerId);
const orders = await this.prisma.order.findMany({
where: {
userId: customerId,
...updatedAfter ? { updatedAt: { gte: new Date(updatedAfter) } } : {}
},
orderBy: { createdAt: "desc" },
include: { items: true }
});
return { data: orders.map((o) => this.mapOrder(o, true)) };
}
async listCustomerAddresses(customerId) {
await this.assertCustomer(customerId);
const rows = await this.prisma.customerAddress.findMany({
where: { customerId },
orderBy: { createdAt: "desc" }
});
return { data: rows.map((a) => this.mapAddress(a, customerId)) };
}
async listCustomerTransactions(_customerId) {
await this.assertCustomer(_customerId);
return { data: [] };
}
async getCustomerEngagement(customerId) {
await this.assertCustomer(customerId);
const favorites = await this.prisma.productFavorite.findMany({
where: { customerId },
include: { product: { select: { id: true, name: true } } },
orderBy: { createdAt: "desc" }
});
return this.mapEngagement(favorites);
}
async getOrder(orderId) {
const order = await this.prisma.order.findUnique({
where: { id: orderId },
include: {
items: true,
user: {
select: {
id: true,
firstName: true,
lastName: true,
phone: true
}
}
}
});
if (!order)
throw new common_12.NotFoundException("\u0645\u0646\u0628\u0639 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
return this.mapOrder(order, true);
}
async assertCustomer(id) {
const c = await this.prisma.customer.findFirst({
where: { id, appRole: client_1.AppUserRole.customer },
select: { id: true }
});
if (!c)
throw new common_12.NotFoundException("\u0645\u0646\u0628\u0639 \u06CC\u0627\u0641\u062A \u0646\u0634\u062F");
}
mapCustomerListItem(c, orders) {
const summary = this.buildSummary(orders);
return {
...this.mapCustomerBase(c),
orders: [],
summary,
addresses: [],
transactions: [],
engagement: { saved: [], likes: [], comments: [] }
};
}
mapCustomerBase(c) {
const name = (0, name_1.joinFullName)(c.firstName, c.lastName);
return {
id: c.id,
name,
first_name: c.firstName,
last_name: c.lastName,
phone: c.phone,
email: null,
username: c.phone,
avatar: c.avatarUrl,
birthday: null,
gender: null,
type: c.customerType === "retail" ? "individual" : "business",
store_id: this.storeId(),
created_at: c.createdAt.toISOString(),
updated_at: c.updatedAt.toISOString(),
description: c.businessName,
bio: null,
job: null,
cash_balance: 0,
gold_balance: 0
};
}
buildSummary(orders) {
let totalSpent = 0;
for (const o of orders) {
totalSpent += o.finalAmount ?? 0;
}
return {
total_orders: orders.length,
total_spent: Math.round(totalSpent)
};
}
mapOrder(o, withNested) {
const totalPrice = o.finalAmount ?? o.totalAmount ?? 0;
const qty = o.items.length;
const base = {
id: o.id,
user_id: o.userId,
store_id: this.storeId(),
code: o.orderNumber,
sts: this.mapOrderStatus(o.status),
payment_sts: this.mapPaymentStatus(o.status),
payment_type: "unknown",
total_price: Math.round(totalPrice),
discount_total_price: Math.round(totalPrice),
total_discounts: Math.round(o.totalDiscount ?? 0),
total_tax: 0,
total_qty: qty,
profit: 0,
percent_profit: 0,
sum_gold: 0,
sum_silver: 0,
address: null,
address_id: null,
note: o.notes,
cancel_reason: null,
cancelled_at: null,
closed_at: null,
transaction_id: null,
code_discount: null,
discount_code_id: null,
created_at: o.createdAt.toISOString(),
updated_at: o.updatedAt.toISOString(),
deleted_at: null,
order_items: o.items.map((item) => ({
id: item.id,
order_id: item.orderId,
product_id: item.productId ?? "",
product_name: item.productName,
qty: 1,
price: Math.round(item.unitPrice ?? 0),
weight: item.confirmedMeterage ?? item.requestedMeterage,
total_price: Math.round(item.lineTotal ?? 0)
}))
};
if (withNested && o.user) {
base.user = {
id: o.user.id,
name: (0, name_1.joinFullName)(o.user.firstName, o.user.lastName),
phone: o.user.phone
};
base.store = { id: this.storeId() };
}
return base;
}
mapOrderStatus(status) {
if (status === client_1.OrderStatus.cancelled || status === client_1.OrderStatus.rejected) {
return "cancelled";
}
if (status === client_1.OrderStatus.delivered)
return "delivered";
if (status === client_1.OrderStatus.shipped || status === client_1.OrderStatus.ready_to_ship) {
return "shipped";
}
if (status === client_1.OrderStatus.final_approved || status === client_1.OrderStatus.preparing) {
return "processing";
}
return "pending";
}
mapPaymentStatus(status) {
if (status === client_1.OrderStatus.cash_confirmed || status === client_1.OrderStatus.checks_approved || status === client_1.OrderStatus.delivered) {
return "paid";
}
if (status === client_1.OrderStatus.cancelled || status === client_1.OrderStatus.rejected) {
return "cancelled";
}
return "unpaid";
}
mapAddress(a, userId) {
return {
id: a.id,
user_id: userId,
province: "",
city: "",
address: a.text,
postal_code: "",
receiver_name: a.label ?? "",
receiver_phone: "",
is_default: a.isDefault
};
}
mapEngagement(favorites) {
return {
saved: favorites.map((f) => ({
product_id: f.product.id,
product_name: f.product.name,
saved_at: f.createdAt.toISOString()
})),
likes: [],
comments: []
};
}
};
exports2.MontactService = MontactService;
exports2.MontactService = MontactService = __decorate2([
(0, common_12.Injectable)(),
__metadata2("design:paramtypes", [
prisma_service_1.PrismaService,
config_1.ConfigService
])
], MontactService);
}
});
// dist/montact/montact.controller.js
var require_montact_controller = __commonJS({
"dist/montact/montact.controller.js"(exports2) {
"use strict";
var __decorate2 = exports2 && exports2.__decorate || function(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata2 = exports2 && exports2.__metadata || function(k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param2 = exports2 && exports2.__param || function(paramIndex, decorator) {
return function(target, key) {
decorator(target, key, paramIndex);
};
};
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.MontactController = void 0;
var common_12 = require_common();
var swagger_12 = require_dist6();
var auth_decorators_1 = require_auth_decorators();
var montact_auth_guard_1 = require_montact_auth_guard();
var montact_service_1 = require_montact_service();
var MontactController = class MontactController {
static {
__name(this, "MontactController");
}
montact;
constructor(montact) {
this.montact = montact;
}
listCustomers(page, limit, updatedAfter) {
const p = page ? Number.parseInt(page, 10) : 1;
const l = limit ? Number.parseInt(limit, 10) : 50;
return this.montact.listCustomers(p, l, updatedAfter);
}
createCustomer() {
throw new common_12.NotImplementedException("\u062B\u0628\u062A \u0645\u0634\u062A\u0631\u06CC \u0627\u0632 CRM \u062F\u0631 \u0627\u06CC\u0646 \u0641\u0627\u0632 \u0641\u0639\u0627\u0644 \u0646\u06CC\u0633\u062A");
}
getCustomer(id) {
return this.montact.getCustomer(id);
}
customerOrders(id, updatedAfter) {
return this.montact.listCustomerOrders(id, updatedAfter);
}
createOrder() {
throw new common_12.NotImplementedException("\u062B\u0628\u062A \u0633\u0641\u0627\u0631\u0634 \u0627\u0632 CRM \u062F\u0631 \u0627\u06CC\u0646 \u0641\u0627\u0632 \u0641\u0639\u0627\u0644 \u0646\u06CC\u0633\u062A");
}
customerAddresses(id) {
return this.montact.listCustomerAddresses(id);
}
customerTransactions(id) {
return this.montact.listCustomerTransactions(id);
}
customerEngagement(id) {
return this.montact.getCustomerEngagement(id);
}
getOrder(orderId) {
return this.montact.getOrder(orderId);
}
registerWebhook() {
throw new common_12.NotImplementedException("Webhook Montact \u062F\u0631 \u0641\u0627\u0632 \u06F3 \u067E\u06CC\u0627\u062F\u0647 \u0645\u06CC\u200C\u0634\u0648\u062F");
}
getWebhook() {
throw new common_12.NotImplementedException("Webhook Montact \u062F\u0631 \u0641\u0627\u0632 \u06F3 \u067E\u06CC\u0627\u062F\u0647 \u0645\u06CC\u200C\u0634\u0648\u062F");
}
deleteWebhook() {
throw new common_12.NotImplementedException("Webhook Montact \u062F\u0631 \u0641\u0627\u0632 \u06F3 \u067E\u06CC\u0627\u062F\u0647 \u0645\u06CC\u200C\u0634\u0648\u062F");
}
};
exports2.MontactController = MontactController;
__decorate2([
(0, common_12.Get)("customers"),
__param2(0, (0, common_12.Query)("page")),
__param2(1, (0, common_12.Query)("limit")),
__param2(2, (0, common_12.Query)("updated_after")),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", [String, String, String]),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "listCustomers", null);
__decorate2([
(0, common_12.Post)("customers"),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", []),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "createCustomer", null);
__decorate2([
(0, common_12.Get)("customers/:id"),
__param2(0, (0, common_12.Param)("id")),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", [String]),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "getCustomer", null);
__decorate2([
(0, common_12.Get)("customers/:id/orders"),
__param2(0, (0, common_12.Param)("id")),
__param2(1, (0, common_12.Query)("updated_after")),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", [String, String]),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "customerOrders", null);
__decorate2([
(0, common_12.Post)("customers/:id/orders"),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", []),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "createOrder", null);
__decorate2([
(0, common_12.Get)("customers/:id/addresses"),
__param2(0, (0, common_12.Param)("id")),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", [String]),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "customerAddresses", null);
__decorate2([
(0, common_12.Get)("customers/:id/transactions"),
__param2(0, (0, common_12.Param)("id")),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", [String]),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "customerTransactions", null);
__decorate2([
(0, common_12.Get)("customers/:id/engagement"),
__param2(0, (0, common_12.Param)("id")),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", [String]),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "customerEngagement", null);
__decorate2([
(0, common_12.Get)("orders/:orderId"),
__param2(0, (0, common_12.Param)("orderId")),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", [String]),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "getOrder", null);
__decorate2([
(0, common_12.Post)("webhooks/register"),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", []),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "registerWebhook", null);
__decorate2([
(0, common_12.Get)("webhooks/:id"),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", []),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "getWebhook", null);
__decorate2([
(0, common_12.Delete)("webhooks/:id"),
__metadata2("design:type", Function),
__metadata2("design:paramtypes", []),
__metadata2("design:returntype", void 0)
], MontactController.prototype, "deleteWebhook", null);
exports2.MontactController = MontactController = __decorate2([
(0, auth_decorators_1.Public)(),
(0, swagger_12.ApiTags)("Montact"),
(0, swagger_12.ApiBearerAuth)(),
(0, common_12.UseGuards)(montact_auth_guard_1.MontactAuthGuard),
(0, common_12.Controller)("montact"),
__metadata2("design:paramtypes", [montact_service_1.MontactService])
], MontactController);
}
});
// dist/montact/montact.module.js
var require_montact_module = __commonJS({
"dist/montact/montact.module.js"(exports2) {
"use strict";
var __decorate2 = exports2 && exports2.__decorate || function(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports2, "__esModule", { value: true });
exports2.MontactModule = void 0;
var common_12 = require_common();
var auth_module_1 = require_auth_module();
var montact_controller_1 = require_montact_controller();
var montact_service_1 = require_montact_service();
var montact_auth_guard_1 = require_montact_auth_guard();
var MontactModule = class MontactModule {
static {
__name(this, "MontactModule");
}
};
exports2.MontactModule = MontactModule;
exports2.MontactModule = MontactModule = __decorate2([
(0, common_12.Module)({
imports: [auth_module_1.AuthModule],
controllers: [montact_controller_1.MontactController],
providers: [montact_service_1.MontactService, montact_auth_guard_1.MontactAuthGuard]
})
], MontactModule);
}
});
// dist/app.module.js
var require_app_module = __commonJS({
"dist/app.module.js"(exports2) {
@@ -142782,6 +143368,7 @@ var require_app_module = __commonJS({
var addresses_module_1 = require_addresses_module();
var internal_messages_module_1 = require_internal_messages_module();
var crm_module_1 = require_crm_module();
var montact_module_1 = require_montact_module();
var AppModule = class AppModule {
static {
__name(this, "AppModule");
@@ -142814,7 +143401,8 @@ var require_app_module = __commonJS({
banners_module_1.BannersModule,
addresses_module_1.AddressesModule,
internal_messages_module_1.InternalMessagesModule,
crm_module_1.CrmModule
crm_module_1.CrmModule,
montact_module_1.MontactModule
],
providers: [
{
@@ -143067,8 +143655,10 @@ async function bootstrap() {
}));
const swaggerConfig = new swagger_1.DocumentBuilder().setTitle("Fabric Trade API").setDescription("API \u0633\u0631\u0648\u0631 \u062A\u062C\u0627\u0631\u062A \u067E\u0627\u0631\u0686\u0647").setVersion("1.0").addBearerAuth().build();
const document2 = swagger_1.SwaggerModule.createDocument(app, swaggerConfig);
document2.security = [{ bearer: [] }];
swagger_1.SwaggerModule.setup(`${globalPrefix}/docs`, app, document2, {
customSwaggerUiPath: swaggerUiAssetsPath()
customSwaggerUiPath: swaggerUiAssetsPath(),
swaggerOptions: { persistAuthorization: true }
});
const port = Number(process.env.PORT) || 3e3;
await app.listen(port);
+2
View File
@@ -25,6 +25,7 @@ import { BannersModule } from './banners/banners.module';
import { AddressesModule } from './addresses/addresses.module';
import { InternalMessagesModule } from './internal-messages/internal-messages.module';
import { CrmModule } from './crm/crm.module';
import { MontactModule } from './montact/montact.module';
@Module({
imports: [
@@ -52,6 +53,7 @@ import { CrmModule } from './crm/crm.module';
AddressesModule,
InternalMessagesModule,
CrmModule,
MontactModule,
],
providers: [
{
+5
View File
@@ -7,6 +7,7 @@ import {
Post,
Query,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AssignmentRequestStatus } from '@prisma/client';
import { AuthType, Roles } from '../common/decorators/auth.decorators';
import { CurrentUser } from '../common/decorators/current-user.decorator';
@@ -18,6 +19,8 @@ import {
UpdateAdminAgentDto,
} from './dto/crm.dto';
@ApiTags('AdminAgents')
@ApiBearerAuth()
@Controller('admin/agents')
@AuthType('admin')
@Roles('admin', 'sales')
@@ -45,6 +48,8 @@ export class AdminAgentsController {
}
}
@ApiTags('AdminAgents')
@ApiBearerAuth()
@Controller('admin/assignment-requests')
@AuthType('admin')
@Roles('admin', 'sales')
+3
View File
@@ -1,9 +1,12 @@
import { Controller, Get, Param, Patch, Body, Query } from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { SalesPipelineStage } from '@prisma/client';
import { AuthType, Roles } from '../common/decorators/auth.decorators';
import { CrmService } from './crm.service';
import { UpdateOpportunityDto } from './dto/crm.dto';
@ApiTags('Crm')
@ApiBearerAuth()
@Controller('admin/crm')
@AuthType('admin')
@Roles('admin', 'sales')
+5
View File
@@ -6,6 +6,7 @@ import {
Patch,
Post,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { AuthType, Roles } from '../common/decorators/auth.decorators';
import { CurrentUser } from '../common/decorators/current-user.decorator';
import type { AuthPrincipal } from '../common/decorators/current-user.decorator';
@@ -18,6 +19,8 @@ import {
UpdateNextActionDto,
} from './dto/crm.dto';
@ApiTags('CustomerCrm')
@ApiBearerAuth()
@Controller('admin/customers')
@AuthType('admin')
@Roles('admin', 'sales')
@@ -68,6 +71,8 @@ export class CustomerCrmController {
}
}
@ApiTags('CustomerCrm')
@ApiBearerAuth()
@Controller('admin/next-actions')
@AuthType('admin')
@Roles('admin', 'sales')
+3
View File
@@ -56,8 +56,11 @@ async function bootstrap() {
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
// تا دکمه Authorize توکن را به همه endpointها (مثل admin/crm) بفرستد
document.security = [{ bearer: [] }];
SwaggerModule.setup(`${globalPrefix}/docs`, app, document, {
customSwaggerUiPath: swaggerUiAssetsPath(),
swaggerOptions: { persistAuthorization: true },
});
const port = Number(process.env.PORT) || 3000;
+43
View File
@@ -0,0 +1,43 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { JwtService } from '@nestjs/jwt';
import { resolveJwtSecret } from '../common/utils/jwt-secret';
@Injectable()
export class MontactAuthGuard implements CanActivate {
constructor(
private readonly jwt: JwtService,
private readonly config: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest<{
headers: Record<string, string | undefined>;
}>();
const authHeader = request.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
throw new UnauthorizedException('توکن نامعتبر یا منقضی شده است');
}
const token = authHeader.slice(7).trim();
const montactToken = this.config.get<string>('MONTACT_API_TOKEN')?.trim();
if (montactToken && token === montactToken) {
return true;
}
try {
const payload = await this.jwt.verifyAsync<{ type?: string }>(token, {
secret: resolveJwtSecret(this.config),
});
if (payload.type === 'admin') {
return true;
}
} catch {
// fall through
}
throw new UnauthorizedException('توکن نامعتبر یا منقضی شده است');
}
}
+92
View File
@@ -0,0 +1,92 @@
import {
Controller,
Delete,
Get,
NotImplementedException,
Param,
Post,
Query,
UseGuards,
} from '@nestjs/common';
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { Public } from '../common/decorators/auth.decorators';
import { MontactAuthGuard } from './montact-auth.guard';
import { MontactService } from './montact.service';
@Public()
@ApiTags('Montact')
@ApiBearerAuth()
@UseGuards(MontactAuthGuard)
@Controller('montact')
export class MontactController {
constructor(private readonly montact: MontactService) {}
@Get('customers')
listCustomers(
@Query('page') page?: string,
@Query('limit') limit?: string,
@Query('updated_after') updatedAfter?: string,
) {
const p = page ? Number.parseInt(page, 10) : 1;
const l = limit ? Number.parseInt(limit, 10) : 50;
return this.montact.listCustomers(p, l, updatedAfter);
}
@Post('customers')
createCustomer() {
throw new NotImplementedException('ثبت مشتری از CRM در این فاز فعال نیست');
}
@Get('customers/:id')
getCustomer(@Param('id') id: string) {
return this.montact.getCustomer(id);
}
@Get('customers/:id/orders')
customerOrders(
@Param('id') id: string,
@Query('updated_after') updatedAfter?: string,
) {
return this.montact.listCustomerOrders(id, updatedAfter);
}
@Post('customers/:id/orders')
createOrder() {
throw new NotImplementedException('ثبت سفارش از CRM در این فاز فعال نیست');
}
@Get('customers/:id/addresses')
customerAddresses(@Param('id') id: string) {
return this.montact.listCustomerAddresses(id);
}
@Get('customers/:id/transactions')
customerTransactions(@Param('id') id: string) {
return this.montact.listCustomerTransactions(id);
}
@Get('customers/:id/engagement')
customerEngagement(@Param('id') id: string) {
return this.montact.getCustomerEngagement(id);
}
@Get('orders/:orderId')
getOrder(@Param('orderId') orderId: string) {
return this.montact.getOrder(orderId);
}
@Post('webhooks/register')
registerWebhook() {
throw new NotImplementedException('Webhook Montact در فاز ۳ پیاده می‌شود');
}
@Get('webhooks/:id')
getWebhook() {
throw new NotImplementedException('Webhook Montact در فاز ۳ پیاده می‌شود');
}
@Delete('webhooks/:id')
deleteWebhook() {
throw new NotImplementedException('Webhook Montact در فاز ۳ پیاده می‌شود');
}
}
+12
View File
@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { MontactController } from './montact.controller';
import { MontactService } from './montact.service';
import { MontactAuthGuard } from './montact-auth.guard';
@Module({
imports: [AuthModule],
controllers: [MontactController],
providers: [MontactService, MontactAuthGuard],
})
export class MontactModule {}
+397
View File
@@ -0,0 +1,397 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { AppUserRole, OrderStatus, Prisma } from '@prisma/client';
import { ConfigService } from '@nestjs/config';
import { PrismaService } from '../prisma/prisma.service';
import { joinFullName } from '../common/utils/name';
type MontactCustomer = Record<string, unknown>;
type MontactOrder = Record<string, unknown>;
@Injectable()
export class MontactService {
constructor(
private readonly prisma: PrismaService,
private readonly config: ConfigService,
) {}
private storeId(): number {
const raw = this.config.get<string>('MONTACT_STORE_ID');
const n = raw ? Number.parseInt(raw, 10) : 1;
return Number.isFinite(n) ? n : 1;
}
async listCustomers(
page = 1,
limit = 50,
updatedAfter?: string,
) {
const safePage = Math.max(1, page);
const safeLimit = Math.min(100, Math.max(1, limit));
const skip = (safePage - 1) * safeLimit;
const updatedFilter = updatedAfter
? { updatedAt: { gte: new Date(updatedAfter) } }
: {};
const where: Prisma.CustomerWhereInput = {
appRole: AppUserRole.customer,
...updatedFilter,
};
const [total, rows] = await Promise.all([
this.prisma.customer.count({ where }),
this.prisma.customer.findMany({
where,
orderBy: { updatedAt: 'desc' },
skip,
take: safeLimit,
include: {
orders: { select: { finalAmount: true } },
},
}),
]);
const data = rows.map((c) =>
this.mapCustomerListItem(c, c.orders),
);
const lastPage = Math.max(1, Math.ceil(total / safeLimit));
return {
data,
meta: {
current_page: safePage,
per_page: safeLimit,
total,
last_page: lastPage,
},
};
}
async getCustomer(id: string) {
const customer = await this.prisma.customer.findFirst({
where: { id, appRole: AppUserRole.customer },
include: {
orders: {
orderBy: { createdAt: 'desc' },
include: { items: true },
},
addresses: true,
favorites: { include: { product: { select: { id: true, name: true } } } },
},
});
if (!customer) throw new NotFoundException('منبع یافت نشد');
const orders = customer.orders.map((o) => this.mapOrder(o, false));
const addresses = customer.addresses.map((a) => this.mapAddress(a, customer.id));
const summary = this.buildSummary(customer.orders);
return {
...this.mapCustomerBase(customer),
orders,
summary,
addresses,
transactions: [],
engagement: this.mapEngagement(customer.favorites),
};
}
async listCustomerOrders(
customerId: string,
updatedAfter?: string,
) {
await this.assertCustomer(customerId);
const orders = await this.prisma.order.findMany({
where: {
userId: customerId,
...(updatedAfter
? { updatedAt: { gte: new Date(updatedAfter) } }
: {}),
},
orderBy: { createdAt: 'desc' },
include: { items: true },
});
return { data: orders.map((o) => this.mapOrder(o, true)) };
}
async listCustomerAddresses(customerId: string) {
await this.assertCustomer(customerId);
const rows = await this.prisma.customerAddress.findMany({
where: { customerId },
orderBy: { createdAt: 'desc' },
});
return { data: rows.map((a) => this.mapAddress(a, customerId)) };
}
async listCustomerTransactions(_customerId: string) {
await this.assertCustomer(_customerId);
return { data: [] as unknown[] };
}
async getCustomerEngagement(customerId: string) {
await this.assertCustomer(customerId);
const favorites = await this.prisma.productFavorite.findMany({
where: { customerId },
include: { product: { select: { id: true, name: true } } },
orderBy: { createdAt: 'desc' },
});
return this.mapEngagement(favorites);
}
async getOrder(orderId: string) {
const order = await this.prisma.order.findUnique({
where: { id: orderId },
include: {
items: true,
user: {
select: {
id: true,
firstName: true,
lastName: true,
phone: true,
},
},
},
});
if (!order) throw new NotFoundException('منبع یافت نشد');
return this.mapOrder(order, true);
}
private async assertCustomer(id: string) {
const c = await this.prisma.customer.findFirst({
where: { id, appRole: AppUserRole.customer },
select: { id: true },
});
if (!c) throw new NotFoundException('منبع یافت نشد');
}
private mapCustomerListItem(
c: {
id: string;
firstName: string;
lastName: string;
phone: string;
customerType: string;
avatarUrl: string | null;
createdAt: Date;
updatedAt: Date;
businessName: string | null;
},
orders: Array<{ finalAmount: number | null }>,
): MontactCustomer {
const summary = this.buildSummary(orders);
return {
...this.mapCustomerBase(c),
orders: [],
summary,
addresses: [],
transactions: [],
engagement: { saved: [], likes: [], comments: [] },
};
}
private mapCustomerBase(c: {
id: string;
firstName: string;
lastName: string;
phone: string;
customerType: string;
avatarUrl: string | null;
createdAt: Date;
updatedAt: Date;
businessName: string | null;
}): MontactCustomer {
const name = joinFullName(c.firstName, c.lastName);
return {
id: c.id,
name,
first_name: c.firstName,
last_name: c.lastName,
phone: c.phone,
email: null,
username: c.phone,
avatar: c.avatarUrl,
birthday: null,
gender: null,
type: c.customerType === 'retail' ? 'individual' : 'business',
store_id: this.storeId(),
created_at: c.createdAt.toISOString(),
updated_at: c.updatedAt.toISOString(),
description: c.businessName,
bio: null,
job: null,
cash_balance: 0,
gold_balance: 0,
};
}
private buildSummary(
orders: Array<{ finalAmount: number | null }>,
) {
let totalSpent = 0;
for (const o of orders) {
totalSpent += o.finalAmount ?? 0;
}
return {
total_orders: orders.length,
total_spent: Math.round(totalSpent),
};
}
private mapOrder(
o: {
id: string;
userId: string;
orderNumber: string;
status: OrderStatus;
finalAmount: number | null;
totalAmount: number | null;
totalDiscount: number | null;
totalMeterage: number;
notes: string | null;
createdAt: Date;
updatedAt: Date;
items: Array<{
id: string;
orderId: string;
productId: string | null;
productName: string;
unitPrice: number | null;
lineTotal: number | null;
confirmedMeterage: number | null;
requestedMeterage: number;
}>;
user?: {
id: string;
firstName: string;
lastName: string;
phone: string;
};
},
withNested: boolean,
): MontactOrder {
const totalPrice = o.finalAmount ?? o.totalAmount ?? 0;
const qty = o.items.length;
const base: MontactOrder = {
id: o.id,
user_id: o.userId,
store_id: this.storeId(),
code: o.orderNumber,
sts: this.mapOrderStatus(o.status),
payment_sts: this.mapPaymentStatus(o.status),
payment_type: 'unknown',
total_price: Math.round(totalPrice),
discount_total_price: Math.round(totalPrice),
total_discounts: Math.round(o.totalDiscount ?? 0),
total_tax: 0,
total_qty: qty,
profit: 0,
percent_profit: 0,
sum_gold: 0,
sum_silver: 0,
address: null,
address_id: null,
note: o.notes,
cancel_reason: null,
cancelled_at: null,
closed_at: null,
transaction_id: null,
code_discount: null,
discount_code_id: null,
created_at: o.createdAt.toISOString(),
updated_at: o.updatedAt.toISOString(),
deleted_at: null,
order_items: o.items.map((item) => ({
id: item.id,
order_id: item.orderId,
product_id: item.productId ?? '',
product_name: item.productName,
qty: 1,
price: Math.round(item.unitPrice ?? 0),
weight: item.confirmedMeterage ?? item.requestedMeterage,
total_price: Math.round(item.lineTotal ?? 0),
})),
};
if (withNested && o.user) {
base.user = {
id: o.user.id,
name: joinFullName(o.user.firstName, o.user.lastName),
phone: o.user.phone,
};
base.store = { id: this.storeId() };
}
return base;
}
private mapOrderStatus(status: OrderStatus): string {
if (status === OrderStatus.cancelled || status === OrderStatus.rejected) {
return 'cancelled';
}
if (status === OrderStatus.delivered) return 'delivered';
if (
status === OrderStatus.shipped ||
status === OrderStatus.ready_to_ship
) {
return 'shipped';
}
if (
status === OrderStatus.final_approved ||
status === OrderStatus.preparing
) {
return 'processing';
}
return 'pending';
}
private mapPaymentStatus(status: OrderStatus): string {
if (
status === OrderStatus.cash_confirmed ||
status === OrderStatus.checks_approved ||
status === OrderStatus.delivered
) {
return 'paid';
}
if (status === OrderStatus.cancelled || status === OrderStatus.rejected) {
return 'cancelled';
}
return 'unpaid';
}
private mapAddress(
a: {
id: string;
label: string | null;
text: string;
isDefault: boolean;
},
userId: string,
) {
return {
id: a.id,
user_id: userId,
province: '',
city: '',
address: a.text,
postal_code: '',
receiver_name: a.label ?? '',
receiver_phone: '',
is_default: a.isDefault,
};
}
private mapEngagement(
favorites: Array<{
createdAt: Date;
product: { id: string; name: string };
}>,
) {
return {
saved: favorites.map((f) => ({
product_id: f.product.id,
product_name: f.product.name,
saved_at: f.createdAt.toISOString(),
})),
likes: [] as unknown[],
comments: [] as unknown[],
};
}
}