Files

441 lines
15 KiB
Markdown
Raw Permalink Normal View 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`
```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ها و سناریوهای خطا (۴۰۱, ۴۰۳, ۴۲۹) نوشته شده است.