Files
CRM_Montzar/docs/MONTACT_CRM_IMPLEMENTATION_GUIDE.md
T

15 KiB
Raw Blame History

راهنمای پیاده‌سازی یکپارچه‌سازی 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

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

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

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)

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<boolean> {
    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

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)

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)

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 ایجاد شود:
CREATE INDEX idx_customers_updated_at ON customers(updated_at);
CREATE INDEX idx_orders_updated_at ON orders(updated_at);

۸. فاز ۳ — Webhook (Push) سمت Provider

ثبت Webhook

POST /montact/webhooks/register
Authorization: Bearer {token}
Content-Type: application/json

{
  "url": "https://crm.montact.com/webhooks/montify",
  "events": ["order.created", "order.paid"]
}
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

GET /montact/webhooks/:id
DELETE /montact/webhooks/:id

رویدادهای پشتیبانی‌شده

رویداد توضیح
customer.created ایجاد مشتری جدید
customer.updated بروزرسانی مشتری
order.created ثبت سفارش جدید
order.updated تغییر سفارش
order.paid پرداخت سفارش
order.cancelled لغو سفارش

Event Dispatcher

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

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

{
  "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)

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ها و سناریوهای خطا (۴۰۱, ۴۰۳, ۴۲۹) نوشته شده است.