diff --git a/.env.example b/.env.example index 611e1d0..d1f5127 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/runtime/server.cjs b/runtime/server.cjs index 1737863..74dd34a 100644 --- a/runtime/server.cjs +++ b/runtime/server.cjs @@ -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); diff --git a/src/app.module.ts b/src/app.module.ts index 3ce2e56..29093bc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -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: [ { diff --git a/src/crm/admin-agents.controller.ts b/src/crm/admin-agents.controller.ts index c549d9e..3c10b2c 100644 --- a/src/crm/admin-agents.controller.ts +++ b/src/crm/admin-agents.controller.ts @@ -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') diff --git a/src/crm/crm.controller.ts b/src/crm/crm.controller.ts index 04f0a99..deae7d5 100644 --- a/src/crm/crm.controller.ts +++ b/src/crm/crm.controller.ts @@ -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') diff --git a/src/crm/customer-crm.controller.ts b/src/crm/customer-crm.controller.ts index 7e651a9..f1e734d 100644 --- a/src/crm/customer-crm.controller.ts +++ b/src/crm/customer-crm.controller.ts @@ -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') diff --git a/src/main.ts b/src/main.ts index 5d5ec1f..ae8758e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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; diff --git a/src/montact/montact-auth.guard.ts b/src/montact/montact-auth.guard.ts new file mode 100644 index 0000000..b9162d6 --- /dev/null +++ b/src/montact/montact-auth.guard.ts @@ -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 { + const request = context.switchToHttp().getRequest<{ + headers: Record; + }>(); + const authHeader = request.headers.authorization; + if (!authHeader?.startsWith('Bearer ')) { + throw new UnauthorizedException('توکن نامعتبر یا منقضی شده است'); + } + 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<{ type?: string }>(token, { + secret: resolveJwtSecret(this.config), + }); + if (payload.type === 'admin') { + return true; + } + } catch { + // fall through + } + throw new UnauthorizedException('توکن نامعتبر یا منقضی شده است'); + } +} diff --git a/src/montact/montact.controller.ts b/src/montact/montact.controller.ts new file mode 100644 index 0000000..4b9656a --- /dev/null +++ b/src/montact/montact.controller.ts @@ -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 در فاز ۳ پیاده می‌شود'); + } +} diff --git a/src/montact/montact.module.ts b/src/montact/montact.module.ts new file mode 100644 index 0000000..5b5291e --- /dev/null +++ b/src/montact/montact.module.ts @@ -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 {} diff --git a/src/montact/montact.service.ts b/src/montact/montact.service.ts new file mode 100644 index 0000000..2126ca8 --- /dev/null +++ b/src/montact/montact.service.ts @@ -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; +type MontactOrder = Record; + +@Injectable() +export class MontactService { + constructor( + private readonly prisma: PrismaService, + private readonly config: ConfigService, + ) {} + + private storeId(): number { + const raw = this.config.get('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[], + }; + } +}