Files
CRM_Montzar/docs/MONTACT_CRM_IMPLEMENTATION_GUIDE.md
T

441 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# راهنمای پیاده‌سازی یکپارچه‌سازی 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<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`
```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ها و سناریوهای خطا (۴۰۱, ۴۰۳, ۴۲۹) نوشته شده است.