# راهنمای پیاده‌سازی یکپارچه‌سازی MONTACT CRM برای زیرسیستم‌های فروشگاهی (Provider Guide) — نسخه v2 ## ۱. مقدمه و هدف سند این سند برای تیم توسعه زیرسیستم‌های فروشگاهی (مانند Montify) نوشته شده است. هدف، فراهم کردن APIای است که سامانه بیرونی **Montact CRM** بتواند داده‌های مشتری، سفارش و تعاملات را Pull کند (فاز ۱ و ۲) و در فاز ۳ رویدادها را به‌صورت Push (Webhook) دریافت نماید. این سند باید کاملاً هم‌راستا با سند سمت کلاینت (`MONTACT_CRM_INTEGRATION.md`) باشد. ## ۲. معماری کلی ``` [Client: Montact CRM] │ HTTPS + Bearer Token ▼ [Middleware احراز هویت] ── بررسی token_hash، expires_at، revoked_at │ ▼ [Provider: Montify API] ── Controller ها (Customers / Orders / Webhooks) │ ▼ [Database: PostgreSQL] │ ▼ [Webhook Dispatcher] ──► Queue (Redis/BullMQ) ──► POST به CRM ``` ## ۳. مدل داده و Migration ها ### جدول `api_keys` ```sql CREATE TABLE api_keys ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), store_id UUID NOT NULL REFERENCES stores(id), token_hash VARCHAR(255) NOT NULL, name VARCHAR(100), scopes JSONB DEFAULT '[]', last_used_at TIMESTAMP NULL, expires_at TIMESTAMP NULL, created_at TIMESTAMP NOT NULL DEFAULT now(), revoked_at TIMESTAMP NULL ); CREATE INDEX idx_api_keys_store_id ON api_keys(store_id); ``` ### جدول `webhook_endpoints` ```sql CREATE TABLE webhook_endpoints ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), store_id UUID NOT NULL REFERENCES stores(id), url VARCHAR(500) NOT NULL, secret VARCHAR(255) NOT NULL, events JSONB NOT NULL DEFAULT '[]', is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMP NOT NULL DEFAULT now(), updated_at TIMESTAMP NOT NULL DEFAULT now() ); ``` ### جدول `webhook_deliveries` ```sql CREATE TABLE webhook_deliveries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), webhook_endpoint_id UUID NOT NULL REFERENCES webhook_endpoints(id), event VARCHAR(100) NOT NULL, payload JSONB NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending|success|failed attempt_count INTEGER NOT NULL DEFAULT 0, last_attempted_at TIMESTAMP NULL, response_status INTEGER NULL, created_at TIMESTAMP NOT NULL DEFAULT now() ); CREATE INDEX idx_webhook_deliveries_status ON webhook_deliveries(status); ``` ## ۴. Middleware احراز هویت (NestJS Guard) ```typescript import { Injectable, CanActivate, ExecutionContext, UnauthorizedException, ForbiddenException } from '@nestjs/common'; import * as crypto from 'crypto'; @Injectable() export class MontactAuthGuard implements CanActivate { constructor(private readonly apiKeyRepository: any) {} async canActivate(context: ExecutionContext): Promise { const request = context.switchToHttp().getRequest(); const authHeader = request.headers['authorization']; if (!authHeader || !authHeader.startsWith('Bearer ')) { throw new UnauthorizedException('Token missing'); } const token = authHeader.replace('Bearer ', ''); const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); const apiKey = await this.apiKeyRepository.findOne({ where: { token_hash: tokenHash } }); if (!apiKey || apiKey.revoked_at) { throw new UnauthorizedException('Invalid or revoked token'); } if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) { throw new UnauthorizedException('Token expired'); } const storeIdParam = request.params.storeId; if (storeIdParam && storeIdParam !== apiKey.store_id) { throw new ForbiddenException('Store mismatch'); } apiKey.last_used_at = new Date(); await this.apiKeyRepository.save(apiKey); request.store_id = apiKey.store_id; return true; } } ``` ## ۵. تعریف کامل Endpointها | Method | Path | توضیح | |---|---|---| | GET | `/montact/customers` | لیست مشتریان (`page`, `limit`, `updated_after`) | | GET | `/montact/customers/:id` | جزئیات مشتری | | GET | `/montact/customers/:id/orders` | سفارش‌های مشتری (`updated_after`) | | GET | `/montact/customers/:id/addresses` | آدرس‌های مشتری | | GET | `/montact/customers/:id/transactions` | تراکنش‌های مشتری | | GET | `/montact/customers/:id/engagement` | داده‌های تعاملی | | GET | `/montact/orders/:orderId` | جزئیات یک سفارش | | POST | `/montact/webhooks/register` | ثبت Webhook جدید | | GET | `/montact/webhooks/:id` | مشاهده جزئیات Webhook | | DELETE | `/montact/webhooks/:id` | حذف Webhook | ### نمونه Query/ORM برای `GET /montact/customers` ```typescript async getCustomers(storeId: string, page: number, limit: number, updatedAfter?: string) { const query = this.customerRepository .createQueryBuilder('c') .where('c.store_id = :storeId', { storeId }); if (updatedAfter) { query.andWhere('c.updated_at > :updatedAfter', { updatedAfter }); } const [data, total] = await query .skip((page - 1) * limit) .take(limit) .getManyAndCount(); return { data: data.map(this.toCustomerDto), meta: { current_page: page, per_page: limit, total, last_page: Math.ceil(total / limit) } }; } ``` ### ساختار JSON خروجی مشتری/سفارش ساختار خروجی باید دقیقاً با فیلدهای تعریف‌شده در سند کلاینت (`MONTACT_CRM_INTEGRATION.md`, بخش ۵) یکسان باشد، شامل: - **Customer:** `id, name, first_name, last_name, phone, email, username, avatar, birthday, gender, type, store_id, created_at, updated_at, description, bio, job, cash_balance, gold_balance, orders, summary, addresses, transactions, engagement` - **Order:** `id, user_id, store_id, code, sts, payment_sts, payment_type, total_price, discount_total_price, total_discounts, total_tax, total_qty, profit, percent_profit, sum_gold, sum_silver, address, address_id, note, cancel_reason, cancelled_at, closed_at, transaction_id, code_discount, discount_code_id, created_at, updated_at, deleted_at, order_items, user, store` - **Engagement:** `saved, likes, comments` (دقیقاً مطابق نمونه JSON سند کلاینت بخش ۵.۶) ## ۶. بهینه‌سازی ### Caching (Redis) ```typescript async getCustomersCached(storeId: string, page: number, limit: number, updatedAfter?: string) { const cacheKey = `customers:${storeId}:${page}:${limit}:${updatedAfter || 'all'}`; const cached = await this.redis.get(cacheKey); if (cached) return JSON.parse(cached); const result = await this.getCustomers(storeId, page, limit, updatedAfter); await this.redis.set(cacheKey, JSON.stringify(result), 'EX', 45); // TTL 45s return result; } // Invalidation هنگام ایجاد/ویرایش سفارش async invalidateCustomerCache(storeId: string) { const keys = await this.redis.keys(`customers:${storeId}:*`); if (keys.length) await this.redis.del(keys); } ``` ### Rate Limiting (Token Bucket) ```typescript import { Injectable, NestMiddleware, HttpException } from '@nestjs/common'; @Injectable() export class RateLimitMiddleware implements NestMiddleware { async use(req: any, res: any, next: () => void) { const apiKeyId = req.apiKeyId; const key = `ratelimit:${apiKeyId}`; const current = await this.redis.incr(key); if (current === 1) { await this.redis.expire(key, 60); // پنجره ۶۰ ثانیه‌ای } if (current > 100) { // 100 req/min res.setHeader('Retry-After', '60'); throw new HttpException('Too Many Requests', 429); } next(); } } ``` ## ۷. فاز ۲ — Incremental Sync سمت Provider - فیلتر `updated_after` به شکل `WHERE updated_at > :updated_after` در تمام کوئری‌های لیست (customers, orders) اعمال می‌شود. - برای پرفورمنس، باید ایندکس روی ستون `updated_at` ایجاد شود: ```sql CREATE INDEX idx_customers_updated_at ON customers(updated_at); CREATE INDEX idx_orders_updated_at ON orders(updated_at); ``` ## ۸. فاز ۳ — Webhook (Push) سمت Provider ### ثبت Webhook ```http POST /montact/webhooks/register Authorization: Bearer {token} Content-Type: application/json { "url": "https://crm.montact.com/webhooks/montify", "events": ["order.created", "order.paid"] } ``` ```typescript async registerWebhook(storeId: string, url: string, events: string[]) { if (!url.startsWith('https://')) { throw new BadRequestException('Webhook URL must use HTTPS'); } // محافظت SSRF: منع IP های داخلی/لوکال const hostname = new URL(url).hostname; if (this.isPrivateOrLocalHost(hostname)) { throw new BadRequestException('Private/local URLs are not allowed'); } const secret = crypto.randomBytes(32).toString('hex'); const webhook = await this.webhookRepository.save({ store_id: storeId, url, secret, // باید رمزنگاری‌شده در دیتابیس ذخیره شود (مثلاً با AES) events, is_active: true, }); return { id: webhook.id, secret }; // secret فقط یک‌بار در پاسخ برگردانده می‌شود } ``` ### مدیریت Webhook ```http GET /montact/webhooks/:id DELETE /montact/webhooks/:id ``` ### رویدادهای پشتیبانی‌شده | رویداد | توضیح | |---|---| | customer.created | ایجاد مشتری جدید | | customer.updated | بروزرسانی مشتری | | order.created | ثبت سفارش جدید | | order.updated | تغییر سفارش | | order.paid | پرداخت سفارش | | order.cancelled | لغو سفارش | ### Event Dispatcher ```javascript const { Queue } = require('bullmq'); const webhookQueue = new Queue('webhook-deliveries', { connection: redisConnection }); async function dispatchEvent(storeId, eventName, data) { const endpoints = await WebhookEndpoint.findAll({ where: { store_id: storeId, is_active: true } }); for (const endpoint of endpoints) { if (!endpoint.events.includes(eventName)) continue; const delivery = await WebhookDelivery.create({ webhook_endpoint_id: endpoint.id, event: eventName, payload: { event: eventName, delivery_id: null, store_id: storeId, data }, status: 'pending', }); const payload = { event: eventName, delivery_id: delivery.id, store_id: storeId, data, }; await webhookQueue.add('deliver', { deliveryId: delivery.id, endpointId: endpoint.id, url: endpoint.url, secret: endpoint.secret, payload, }); } } // مثال استفاده بعد از ثبت سفارش: // await dispatchEvent(order.store_id, 'order.created', orderDto); ``` ### امضای HMAC-SHA256 ```javascript const crypto = require('crypto'); function signPayload(payload, secret) { const rawBody = JSON.stringify(payload); return crypto.createHmac('sha256', secret).update(rawBody).digest('hex'); } ``` ### نمونه Payload ارسالی برای `order.created` ```json { "event": "order.created", "delivery_id": "d2f1c9e0-84a3-4b7f-9c1e-3a2b1c0d9e8f", "store_id": "b2f1e4a0-...", "data": { "id": 4567, "user_id": 123, "code": "ORD-2026-4567", "sts": "pending", "payment_sts": "unpaid", "total_price": 12500000, "total_qty": 2, "created_at": "2026-07-10T12:05:00Z" } } ``` ### Retry Policy (Worker) ```javascript const { Worker } = require('bullmq'); const axios = require('axios'); const RETRY_DELAYS = [1000, 5000, 15000, 60000, 300000]; // 1s,5s,15s,60s,300s const worker = new Worker('webhook-deliveries', async (job) => { const { deliveryId, url, secret, payload } = job.data; const signature = signPayload(payload, secret); try { const response = await axios.post(url, payload, { headers: { 'X-Montact-Signature': `sha256=${signature}`, 'X-Montact-Event': payload.event, 'X-Montact-Delivery-Id': payload.delivery_id, 'Content-Type': 'application/json', }, timeout: 10000, // 10s timeout }); if (response.status >= 200 && response.status < 300) { await WebhookDelivery.update( { status: 'success', response_status: response.status, last_attempted_at: new Date() }, { where: { id: deliveryId } } ); return; } throw new Error(`Unexpected status ${response.status}`); } catch (err) { const attempt = job.attemptsMade; await WebhookDelivery.increment('attempt_count', { where: { id: deliveryId } }); if (attempt >= RETRY_DELAYS.length) { await WebhookDelivery.update( { status: 'failed', last_attempted_at: new Date() }, { where: { id: deliveryId } } ); } else { throw err; // BullMQ طبق backoff تعریف‌شده در صف، تلاش مجدد می‌کند } } }, { connection: redisConnection, settings: { backoffStrategy: (attempt) => RETRY_DELAYS[attempt - 1] }, }); ``` ### نکات امنیتی ثبت Webhook - URL ثبت‌شده باید حتماً `https://` باشد. - IPهای داخلی/لوکال (`127.0.0.1`, `10.x.x.x`, `192.168.x.x`, `169.254.x.x`) باید مسدود شوند (محافظت SSRF). - `secret` باید در دیتابیس رمزنگاری‌شده ذخیره شود، نه به‌صورت plain-text. ## ۹. مانیتورینگ و لاگ - تمام رکوردهای `webhook_deliveries` باید همراه با `attempt_count` و `response_status` لاگ شوند. - در صورتی‌که نرخ `failed` برای یک Webhook Endpoint خاص از ۲۰٪ در بازه ۱ ساعته بیشتر شود، باید Alert ارسال شود (مثلاً از طریق Slack/Email) و در صورت ادامه، Endpoint به‌صورت موقت `is_active=false` شود. ## ۱۰. چک‌لیست پیاده‌سازی برای تیم Provider - [ ] اجرای Migration جداول `api_keys`, `webhook_endpoints`, `webhook_deliveries` - [ ] پیاده‌سازی Middleware احراز هویت (Guard) - [ ] پیاده‌سازی تمام Endpointهای GET مطابق جدول بخش ۵ - [ ] اعمال فیلتر `updated_after` و ایندکس‌گذاری `updated_at` - [ ] پیاده‌سازی Caching با Redis و Invalidation مناسب - [ ] پیاده‌سازی Rate Limiting (۱۰۰ req/min) - [ ] پیاده‌سازی ثبت/حذف/مشاهده Webhook - [ ] پیاده‌سازی Event Dispatcher و اتصال به Queue - [ ] پیاده‌سازی Worker با Retry Policy و Backoff - [ ] تست کامل امضای HMAC-SHA256 در سمت Worker و نمونه CRM ## ۱۱. تست و چک‌لیست امنیتی - [ ] Token هرگز به‌صورت plain-text ذخیره نمی‌شود؛ تنها `token_hash` ذخیره می‌شود. - [ ] `secret` وبهوک رمزنگاری‌شده در دیتابیس ذخیره می‌شود. - [ ] HTTPS برای تمام Webhook URLها اجباری است. - [ ] محافظت SSRF در برابر IPهای داخلی/لوکال فعال است. - [ ] محدودسازی اختیاری بر اساس IP Whitelist برای توکن‌های حساس در نظر گرفته شده است. - [ ] تست‌های Integration برای تمام Endpointها و سناریوهای خطا (۴۰۱, ۴۰۳, ۴۲۹) نوشته شده است.