Montify stores integration: multi-store connections, customer/order/transaction sync, webhooks
This commit is contained in:
@@ -0,0 +1,440 @@
|
||||
# راهنمای پیادهسازی یکپارچهسازی 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ها و سناریوهای خطا (۴۰۱, ۴۰۳, ۴۲۹) نوشته شده است.
|
||||
@@ -0,0 +1,438 @@
|
||||
# راهنمای اتصال CRM (MONTACT) به Montify — نسخه کامل v2
|
||||
|
||||
## ۱. خلاصه و دیاگرام جریان
|
||||
|
||||
این سند مشخصات اتصال سامانه **Montact CRM** به زیرسیستمهای فروشگاهی (مانند Montify) را تعریف میکند. هر فروشگاه یک `Base URL` و یک `Token` اختصاصی دارد.
|
||||
|
||||
```
|
||||
[فروشنده/Store] → [پنل Montify] → [صدور Token] → [سرور CRM]
|
||||
│
|
||||
▼
|
||||
GET {Base URL}/montact/customers
|
||||
│
|
||||
▼
|
||||
پاسخ JSON (لیست مشتریان)
|
||||
│
|
||||
▼
|
||||
GET {Base URL}/montact/customers/:id/orders
|
||||
│
|
||||
▼
|
||||
پاسخ JSON (سفارشهای مشتری)
|
||||
```
|
||||
|
||||
## ۲. دریافت اطلاعات اتصال از فروشنده
|
||||
|
||||
1. فروشنده وارد پنل Montify میشود.
|
||||
2. از مسیر «تنظیمات → یکپارچهسازیها → CRM» وارد بخش Montact میشود.
|
||||
3. دکمه «صدور Token» را میزند و مقادیر زیر را در اختیار تیم CRM قرار میدهد:
|
||||
|
||||
| فیلد | مقدار نمونه | توضیح |
|
||||
|---|---|---|
|
||||
| Base URL | `https://store1.montify.com/api/v1` | آدرس پایه API فروشگاه |
|
||||
| Token | `mk_live_8f3a1c9d2e...` | توکن احراز هویت Bearer |
|
||||
| Store ID | `b2f1e4a0-...` | شناسه فروشگاه |
|
||||
|
||||
> **نکته:** توکنها میتوانند منقضی شوند (`expires_at`). در صورت دریافت خطای ۴۰۱، باید توکن جدید از فروشنده درخواست شود. توکن هرگز نباید در فرانتاند ذخیره یا افشا شود؛ تنها باید در سرور CRM نگهداری گردد.
|
||||
|
||||
## ۳. احراز هویت
|
||||
|
||||
تمام درخواستها باید هدرهای زیر را داشته باشند:
|
||||
|
||||
```http
|
||||
Authorization: Bearer {Token}
|
||||
Content-Type: application/json
|
||||
Accept: application/json
|
||||
```
|
||||
|
||||
## ۴. فهرست کامل APIها
|
||||
|
||||
| Method | Path | توضیح |
|
||||
|---|---|---|
|
||||
| GET | `/montact/customers` | لیست مشتریان (پشتیبانی از `page`, `limit`, `updated_after`) |
|
||||
| POST | `/montact/customers` | ثبت/همگامسازی مشتری از سمت CRM (اختیاری) |
|
||||
| GET | `/montact/customers/:id` | جزئیات کامل یک مشتری |
|
||||
| GET | `/montact/customers/:id/orders` | لیست سفارشهای مشتری (پشتیبانی از `updated_after`) |
|
||||
| POST | `/montact/customers/:id/orders` | ثبت سفارش از CRM (اختیاری، در صورت فعال بودن) |
|
||||
| GET | `/montact/customers/:id/addresses` | لیست آدرسهای مشتری |
|
||||
| GET | `/montact/customers/:id/transactions` | لیست تراکنشهای مالی مشتری |
|
||||
| GET | `/montact/customers/:id/engagement` | دادههای تعاملی (`saved`, `likes`, `comments`) |
|
||||
| GET | `/montact/orders/:orderId` | جزئیات یک سفارش خاص |
|
||||
| POST | `/montact/webhooks/register` | ثبت Webhook برای رویدادهای بلادرنگ (فاز ۳) |
|
||||
| GET | `/montact/webhooks/:id` | مشاهده جزئیات یک Webhook ثبتشده |
|
||||
| DELETE | `/montact/webhooks/:id` | حذف Webhook |
|
||||
|
||||
> پارامتر `updated_after` یک مقدار ISO 8601 است (مثلاً `2026-07-01T00:00:00Z`) و فقط رکوردهای تغییر یافته بعد از آن زمان را برمیگرداند (برای Incremental Sync).
|
||||
|
||||
### نمونههای curl
|
||||
|
||||
```bash
|
||||
BASE="https://store1.montify.com/api/v1"
|
||||
TOKEN="mk_live_8f3a1c9d2e..."
|
||||
|
||||
# لیست مشتریان
|
||||
curl -X GET "$BASE/montact/customers?page=1&limit=50" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Accept: application/json"
|
||||
|
||||
# لیست مشتریان تغییریافته بعد از یک زمان خاص (Incremental Sync)
|
||||
curl -X GET "$BASE/montact/customers?updated_after=2026-07-01T00:00:00Z" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# جزئیات یک مشتری
|
||||
curl -X GET "$BASE/montact/customers/123" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# سفارشهای یک مشتری
|
||||
curl -X GET "$BASE/montact/customers/123/orders?updated_after=2026-07-01T00:00:00Z" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# آدرسهای مشتری
|
||||
curl -X GET "$BASE/montact/customers/123/addresses" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# تراکنشهای مشتری
|
||||
curl -X GET "$BASE/montact/customers/123/transactions" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# دادههای تعاملی مشتری
|
||||
curl -X GET "$BASE/montact/customers/123/engagement" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# جزئیات یک سفارش خاص
|
||||
curl -X GET "$BASE/montact/orders/4567" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# ثبت Webhook (فاز ۳)
|
||||
curl -X POST "$BASE/montact/webhooks/register" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url":"https://crm.montact.com/webhooks/montify","events":["order.created","order.paid"]}'
|
||||
```
|
||||
|
||||
## ۵. ساختار پاسخ کامل
|
||||
|
||||
### ۵.۱ فیلدهای مشتری (Customer)
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| id | integer | شناسه مشتری |
|
||||
| name | string | نام کامل |
|
||||
| first_name | string | نام |
|
||||
| last_name | string | نام خانوادگی |
|
||||
| phone | string | شماره موبایل |
|
||||
| email | string\|null | ایمیل |
|
||||
| username | string | نام کاربری |
|
||||
| avatar | string\|null | URL آواتار |
|
||||
| birthday | date\|null | تاریخ تولد |
|
||||
| gender | string\|null | جنسیت |
|
||||
| type | string | نوع مشتری (حقیقی/حقوقی) |
|
||||
| store_id | integer | شناسه فروشگاه |
|
||||
| created_at | datetime | تاریخ ایجاد |
|
||||
| updated_at | datetime | آخرین بروزرسانی |
|
||||
| description | string\|null | توضیحات |
|
||||
| bio | string\|null | بیوگرافی |
|
||||
| job | string\|null | شغل |
|
||||
| cash_balance | number | موجودی نقدی |
|
||||
| gold_balance | number | موجودی طلا (گرم) |
|
||||
| orders | array | لیست سفارشها |
|
||||
| summary | object | خلاصه آماری مشتری |
|
||||
| addresses | array | لیست آدرسها |
|
||||
| transactions | array | لیست تراکنشها |
|
||||
| engagement | object | دادههای تعاملی |
|
||||
|
||||
### ۵.۲ فیلدهای سفارش (Order)
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| id | integer | شناسه سفارش |
|
||||
| user_id | integer | شناسه مشتری |
|
||||
| store_id | integer | شناسه فروشگاه |
|
||||
| code | string | کد پیگیری سفارش |
|
||||
| sts | string | وضعیت سفارش |
|
||||
| payment_sts | string | وضعیت پرداخت |
|
||||
| payment_type | string | نوع پرداخت |
|
||||
| total_price | number | قیمت کل |
|
||||
| discount_total_price | number | قیمت پس از تخفیف |
|
||||
| total_discounts | number | مجموع تخفیفها |
|
||||
| total_tax | number | مجموع مالیات |
|
||||
| total_qty | integer | تعداد کل اقلام |
|
||||
| profit | number | سود |
|
||||
| percent_profit | number | درصد سود |
|
||||
| sum_gold | number | مجموع وزن طلا |
|
||||
| sum_silver | number | مجموع وزن نقره |
|
||||
| address | object | آدرس ارسال |
|
||||
| address_id | integer | شناسه آدرس |
|
||||
| note | string\|null | یادداشت |
|
||||
| cancel_reason | string\|null | دلیل لغو |
|
||||
| cancelled_at | datetime\|null | تاریخ لغو |
|
||||
| closed_at | datetime\|null | تاریخ بستن سفارش |
|
||||
| transaction_id | integer\|null | شناسه تراکنش مرتبط |
|
||||
| code_discount | string\|null | کد تخفیف |
|
||||
| discount_code_id | integer\|null | شناسه کد تخفیف |
|
||||
| created_at | datetime | تاریخ ایجاد |
|
||||
| updated_at | datetime | آخرین بروزرسانی |
|
||||
| deleted_at | datetime\|null | تاریخ حذف نرم |
|
||||
| order_items | array | اقلام سفارش |
|
||||
| user | object | اطلاعات خلاصه مشتری |
|
||||
| store | object | اطلاعات خلاصه فروشگاه |
|
||||
|
||||
### ۵.۳ اقلام سفارش (Order Items)
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| id | integer | شناسه قلم سفارش |
|
||||
| order_id | integer | شناسه سفارش |
|
||||
| product_id | integer | شناسه محصول |
|
||||
| product_name | string | نام محصول |
|
||||
| qty | integer | تعداد |
|
||||
| price | number | قیمت واحد |
|
||||
| weight | number | وزن (گرم) |
|
||||
| total_price | number | قیمت کل قلم |
|
||||
|
||||
### ۵.۴ آدرسها (Addresses)
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| id | integer | شناسه آدرس |
|
||||
| user_id | integer | شناسه مشتری |
|
||||
| province | string | استان |
|
||||
| city | string | شهر |
|
||||
| address | string | متن آدرس |
|
||||
| postal_code | string | کد پستی |
|
||||
| receiver_name | string | نام گیرنده |
|
||||
| receiver_phone | string | شماره گیرنده |
|
||||
| is_default | boolean | آدرس پیشفرض |
|
||||
|
||||
### ۵.۵ تراکنشها (Transactions)
|
||||
|
||||
| فیلد | نوع | توضیح |
|
||||
|---|---|---|
|
||||
| id | integer | شناسه تراکنش |
|
||||
| user_id | integer | شناسه مشتری |
|
||||
| type | string | نوع تراکنش (واریز/برداشت) |
|
||||
| amount | number | مقدار |
|
||||
| currency | string | واحد (ریال/گرم طلا) |
|
||||
| status | string | وضعیت تراکنش |
|
||||
| reference_code | string | کد پیگیری بانکی |
|
||||
| created_at | datetime | تاریخ ثبت |
|
||||
|
||||
### ۵.۶ دادههای تعاملی (Engagement)
|
||||
|
||||
مسیر: `GET /montact/customers/:id/engagement`
|
||||
|
||||
```json
|
||||
{
|
||||
"saved": [
|
||||
{ "product_id": 55, "product_name": "سکه بهار آزادی", "saved_at": "2026-06-10T11:22:00Z" }
|
||||
],
|
||||
"likes": [
|
||||
{ "product_id": 78, "liked_at": "2026-06-15T09:00:00Z" }
|
||||
],
|
||||
"comments": [
|
||||
{
|
||||
"id": 301,
|
||||
"product_id": 78,
|
||||
"text": "کیفیت عالی بود",
|
||||
"rating": 5,
|
||||
"created_at": "2026-06-16T08:30:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### ۵.۷ نمونه پاسخ JSON کامل لیست مشتریان
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": 123,
|
||||
"name": "علی رضایی",
|
||||
"first_name": "علی",
|
||||
"last_name": "رضایی",
|
||||
"phone": "09121234567",
|
||||
"email": "ali@example.com",
|
||||
"username": "ali_r",
|
||||
"avatar": "https://cdn.montify.com/avatars/123.png",
|
||||
"birthday": "1990-05-12",
|
||||
"gender": "male",
|
||||
"type": "individual",
|
||||
"store_id": 10,
|
||||
"created_at": "2025-01-10T08:00:00Z",
|
||||
"updated_at": "2026-07-01T10:15:00Z",
|
||||
"description": null,
|
||||
"bio": null,
|
||||
"job": "مهندس",
|
||||
"cash_balance": 1500000,
|
||||
"gold_balance": 2.35,
|
||||
"orders": [],
|
||||
"summary": { "total_orders": 12, "total_spent": 85000000 },
|
||||
"addresses": [],
|
||||
"transactions": [],
|
||||
"engagement": { "saved": [], "likes": [], "comments": [] }
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"current_page": 1,
|
||||
"per_page": 50,
|
||||
"total": 532,
|
||||
"last_page": 11
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ۶. قوانین نگاشت پیشنهادی در CRM
|
||||
|
||||
| فیلد Montify | فیلد معادل در CRM |
|
||||
|---|---|
|
||||
| id | external_customer_id |
|
||||
| name | full_name |
|
||||
| phone | mobile |
|
||||
| cash_balance | wallet_balance |
|
||||
| gold_balance | gold_wallet_balance |
|
||||
| orders[].code | order_reference |
|
||||
| orders[].sts | order_status |
|
||||
|
||||
**استراتژی Sync گام به گام:**
|
||||
1. دریافت لیست مشتریان صفحهبندیشده و Upsert در جدول مشتریان CRM بر اساس `id`.
|
||||
2. برای هر مشتری، دریافت سفارشها، آدرسها و تراکنشها بهصورت جداگانه و Upsert در جداول مرتبط.
|
||||
3. ذخیره `updated_at` هر رکورد برای استفاده در Sync افزایشی بعدی.
|
||||
|
||||
## ۷. استراتژی Sync سهفازی
|
||||
|
||||
### فاز ۱ — Polling ساده
|
||||
|
||||
- فراخوانی دورهای `/montact/customers` با `page` و `limit`.
|
||||
- فاصله پیشنهادی: هر ۵ تا ۱۵ دقیقه (بسته به حجم داده فروشگاه).
|
||||
- در صورت دریافت `429 Too Many Requests`، باید بر اساس هدر `Retry-After` صبر و تلاش مجدد شود.
|
||||
|
||||
### فاز ۲ — Incremental Sync
|
||||
|
||||
- استفاده از `updated_after=<ISO8601>` روی `/montact/customers` و `/montact/customers/:id/orders`.
|
||||
- CRM باید مقدار `last_sync_timestamp` را پس از هر Sync موفق ذخیره کند و در فراخوانی بعدی بهعنوان `updated_after` ارسال کند.
|
||||
|
||||
```bash
|
||||
curl -X GET "$BASE/montact/customers?updated_after=2026-07-10T12:00:00Z" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### فاز ۳ — Webhook (Push-based، بلادرنگ)
|
||||
|
||||
برای دریافت رویدادها بدون تأخیر Polling، CRM باید یک Endpoint عمومی HTTPS در اختیار Montify قرار دهد:
|
||||
|
||||
```bash
|
||||
curl -X POST "$BASE/montact/webhooks/register" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"url": "https://crm.montact.com/webhooks/montify",
|
||||
"events": ["customer.created", "customer.updated", "order.created", "order.updated", "order.paid", "order.cancelled"]
|
||||
}'
|
||||
```
|
||||
|
||||
**رویدادهای پشتیبانیشده:**
|
||||
|
||||
| رویداد | توضیح |
|
||||
|---|---|
|
||||
| customer.created | ایجاد مشتری جدید |
|
||||
| customer.updated | بروزرسانی اطلاعات مشتری |
|
||||
| order.created | ثبت سفارش جدید |
|
||||
| order.updated | تغییر در سفارش |
|
||||
| order.paid | پرداخت سفارش |
|
||||
| order.cancelled | لغو سفارش |
|
||||
|
||||
**نمونه Payload برای `order.created`:**
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "order.created",
|
||||
"delivery_id": "d2f1c9e0-84a3-4b7f-9c1e-3a2b1c0d9e8f",
|
||||
"store_id": 10,
|
||||
"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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**هدرهای ارسالی Webhook:**
|
||||
|
||||
```http
|
||||
X-Montact-Signature: sha256=3b4f9a1c7e8d2f...
|
||||
X-Montact-Event: order.created
|
||||
X-Montact-Delivery-Id: d2f1c9e0-84a3-4b7f-9c1e-3a2b1c0d9e8f
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
**وریفای امضا (Node.js):**
|
||||
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
function verifySignature(rawBody, signatureHeader, secret) {
|
||||
const expected = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(rawBody)
|
||||
.digest('hex');
|
||||
|
||||
const received = signatureHeader.replace('sha256=', '');
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(expected, 'hex'),
|
||||
Buffer.from(received, 'hex')
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Retry Policy:**
|
||||
|
||||
- در صورت عدم دریافت پاسخ `2xx` ظرف ۱۰ ثانیه، تلاش مجدد با backoff نمایی انجام میشود.
|
||||
- فاصله تلاشها: `1s → 5s → 15s → 60s → 300s` (حداکثر ۵ بار).
|
||||
- CRM باید سریعاً `200 OK` برگرداند و پردازش سنگین را بهصورت async انجام دهد.
|
||||
- Idempotency بر اساس `X-Montact-Delivery-Id` باید رعایت شود تا پردازش تکراری رخ ندهد.
|
||||
|
||||
## ۸. کدهای خطا
|
||||
|
||||
| کد HTTP | معنی | اقدام پیشنهادی |
|
||||
|---|---|---|
|
||||
| 200 | موفق | - |
|
||||
| 400 | درخواست نامعتبر | بررسی پارامترها |
|
||||
| 401 | توکن نامعتبر یا منقضی | درخواست توکن جدید از فروشنده |
|
||||
| 403 | عدم دسترسی به منبع | بررسی scope توکن |
|
||||
| 404 | منبع یافت نشد | بررسی شناسه ارسالی |
|
||||
| 429 | تعداد درخواست بیش از حد مجاز | توجه به هدر `Retry-After` و کاهش نرخ درخواست |
|
||||
| 500 | خطای سرور | تلاش مجدد بعد از فاصله زمانی |
|
||||
|
||||
## ۹. محدودیتها و رفتار فعلی
|
||||
|
||||
- API برای دادههای اصلی (مشتری، سفارش، آدرس، تراکنش) صرفاً **خواندنی (Read-only)** است؛ تنها استثنا ثبت/حذف Webhook است.
|
||||
- فیلدهای حساس (مانند رمز عبور، هشهای داخلی) از پاسخها حذف شدهاند.
|
||||
- CORS برای دامنههای مجاز CRM فعال است.
|
||||
- نرخ مجاز درخواست پیشنهادی: **۱۰۰ درخواست در دقیقه به ازای هر Token**.
|
||||
|
||||
## ۱۰. چکلیست اتصال برنامهنویس CRM
|
||||
|
||||
- [ ] دریافت `Base URL`، `Token` و `Store ID` از فروشنده
|
||||
- [ ] تست احراز هویت با `GET /montact/customers`
|
||||
- [ ] پیادهسازی Polling فاز ۱
|
||||
- [ ] پیادهسازی Incremental Sync با `updated_after` (فاز ۲)
|
||||
- [ ] ثبت Webhook Endpoint و تست دریافت رویداد (فاز ۳)
|
||||
- [ ] پیادهسازی وریفای امضای HMAC-SHA256
|
||||
- [ ] تست Idempotency بر اساس `X-Montact-Delivery-Id`
|
||||
- [ ] مدیریت خطاهای ۴۰۱/۴۲۹ و Retry مناسب
|
||||
- [ ] ذخیره امن Token در سمت سرور (نه فرانتاند)
|
||||
|
||||
## ۱۱. پشتیبانی
|
||||
|
||||
برای ارسال تیکت پشتیبانی، اطلاعات زیر لازم است:
|
||||
|
||||
- `Store ID`
|
||||
- زمان دقیق درخواست (UTC)
|
||||
- کد خطای دریافتی
|
||||
- `X-Montact-Delivery-Id` (در صورت مرتبط بودن به Webhook)
|
||||
- نمونه Request/Response (بدون افشای Token کامل)
|
||||
Reference in New Issue
Block a user