# راهنمای عملیاتی ثبت متنی — نسخه ۴.۳

این راهنما ادامه کامل قالب ۲ است؛ اصول معامله، کارمزد، تسویه کامل/جزئی، دریافت/پرداخت مستقل و حدود متن در TEXT_IMPORT_V2_FA.md آمده‌اند. مسیر ساده قبلی حفظ شده و هیچ فیلد اجباری جدیدی به POST /api/text-imports اضافه نشده است. قابلیت‌های این راهنما اختیاری‌اند.

## انتخاب مسیر مناسب

| نیاز | مسیر |
|---|---|
| متن آماده دارم و ثبت مستقیم می‌خواهم | POST /api/text-imports با text و Idempotency-Key |
| قبل از ثبت می‌خواهم مبلغ و مانده را ببینم | preview سپس ثبت همان متن با previewId |
| متن را طی چند مرحله آماده می‌کنم | ذخیره پیش‌نویس، ویرایش، preview و commit |
| متن از بانک، پیام یا سیستم دیگر می‌آید | اضافه کردن مرجع خارجی پایدار به هر رکورد |
| معامله و چند پرداخت آن در یک متن‌اند | شناسه رکورد روی معامله و معامله: @شناسه روی پرداخت‌ها |

برای یک عملیات واقعی یکی از مسیر مستقیم یا پیش‌نویس را انتخاب کنید؛ اجرای هر دو با کلیدهای جدید و بدون مرجع خارجی می‌تواند دو ثبت مجزا ایجاد کند.

## ۱. اتصال رکوردهای یک متن

```text
نوع: معامله
شناسه رکورد: T1
مرجع خارجی: EXCHANGE-2026-001
مشتری: C001
دریافتی: 1000 USD
پرداختی: 910 EUR
کارمزد: 0.5
نرخ یورویی دریافتی: 0.92
---
نوع: دریافت
مرجع خارجی: BANK-USD-2026-001
مشتری: C001
مبلغ: 500 USD
نرخ یورویی: 0.92
معامله: @T1
---
نوع: پرداخت
مرجع خارجی: BANK-EUR-2026-002
مشتری: C001
مبلغ: 200 EUR
معامله: @T1
```

پس از ثبت، هر دو Payment به UUID واقعی معامله T1 اشاره می‌کنند. عملیات به ترتیب متن انجام می‌شود و کل گروه اتمیک است. شماره T1 فقط در همین متن معنا دارد و شماره سند دائمی نیست؛ در متن بعدی از UUID یا شماره TR پاسخ استفاده کنید.

شناسه باید با حرف لاتین شروع شود، حداکثر ۳۲ کاراکتر باشد و فقط حرف، عدد، _ یا - داشته باشد. حروف کوچک به بزرگ تبدیل می‌شوند. ارجاع باید به **معامله قبلی** باشد؛ ارجاع رو به جلو، به خود، به یک پرداخت یا به شناسه تکراری رد می‌شود. مشتری، سمت و ارز پرداخت هم باید با معامله هدف تطابق داشته باشند.

اگر معامله را با «تسویه: کامل» نوشته‌اید، پرداخت مربوط به آن را بی‌دلیل دوباره ننویسید؛ تسویه کامل خودش دو Payment می‌سازد. پرداخت مستقل اضافی واقعاً پول اضافی محسوب می‌شود و ممکن است تخصیص‌نیافته بماند. NONE نیز همچنان اجازه ثبت پول بدون تخصیص را می‌دهد، حتی اگر مرجع معامله مشخص باشد.

## ۲. مرجع خارجی و جلوگیری از ثبت دوباره

```text
نوع: دریافت
مرجع خارجی: BANK:ACCOUNT-01:TX-98765
مشتری: C001
مبلغ: 500 USD
نرخ یورویی: 0.92
تخصیص: NONE
```

مرجع خارجی باید شناسه **همان رویداد واقعی** در منبع باشد. با هر بار ارسال آن را عوض نکنید. می‌توانید نام منبع و حساب را به شماره پیگیری اضافه کنید تا شماره‌های مشابه منابع مختلف برخورد نکنند.

مرجع حداکثر ۱۰۰ کاراکتر است؛ حرف لاتین، عدد و . _ : / - می‌پذیرد و به حروف بزرگ تبدیل می‌شود. در کل این دیتابیس یکتاست، نه فقط برای یک کاربر یا مشتری. یک مرجع به یک رکورد از یک Import موفق تعلق دارد. اگر آن رکورد معامله با تسویه کامل باشد، مرجع به کل همان رکورد و اسناد ساخته‌شده آن مربوط است.

| کنترل | کاربرد |
|---|---|
| Idempotency-Key | بازپخش همان درخواست/گروه پس از timeout بدون ثبت مجدد |
| مرجع خارجی | جلوگیری از ثبت همان رویداد با کلید جدید یا توسط اپراتور دیگر |
| نسخه پیش‌نویس | جلوگیری از بازنویسی تغییرات هم‌زمان و تأیید نسخه قدیمی |

مرجع موجود در validate باعث valid=false و در preview/ثبت باعث 409 EXTERNAL_REFERENCE_EXISTS می‌شود. خطای رقابت دیتابیس ممکن است ابتدا DUPLICATE_VALUE باشد؛ درخواست را با همان کلید تکرار کنید تا نتیجه روشن شود. قید یکتای دیتابیس علاوه بر بررسی برنامه مانع دو درج هم‌زمان مرجع می‌شود؛ اجرای واقعی رقابت MySQL در محیط ساخت تأیید نشده است.

پس از ابطال سند، مرجع حذف نمی‌شود؛ همان رویداد نباید بی‌صدا دوباره وارد شود. سند اصلاحی باید مرجع مستقل و قابل پیگیری داشته باشد. API آزادسازی یا تغییر مرجع ثبت‌شده وجود ندارد. اسناد قدیمی بدون مرجع به‌صورت خودکار مرجع نمی‌گیرند. حذف خط مرجع یا ساخت مرجع تازه، این حفاظت را برای درخواست تازه دور می‌زند؛ نگهداری مرجع منبع وظیفه کلاینت/اپراتور است.

پیگیری ثبت‌های خودتان با مرجع:

```http
GET /api/text-imports?reference=BANK-2026-001&page=1&limit=25
```

جست‌وجو تطابق دقیق و در تاریخچه همان کاربر است. مرجع متعلق به دیگری صرفاً خطای تکراری می‌دهد؛ متن، مالک و شناسه ثبت او افشا نمی‌شود.

## ۳. مانده قبل و بعد در پیش‌نمایش

validate، preview و نتیجه ثبت جدید در summary.balances اطلاعات زیر را دارند:

```json
{
  "customerId": 1,
  "customerCode": "C001",
  "currency": "USD",
  "before": "25.1",
  "accountChange": "1005",
  "after": "1030.1",
  "cashChange": "0"
}
```

before مانده فعلی دفتر مشتری، accountChange اثر کل این گروه و after جمع آن دو است. مقدار مثبت بدهی مشتری و منفی طلب اوست. cashChange تغییر پول واقعی همین گروه است؛ موجودی نهایی صندوق نیست. فقط زوج مشتری/ارزهای درگیر گروه نمایش داده می‌شوند؛ ارزهای متفاوت جمع نمی‌شوند. نبود گردش قبلی برای یک ارز، before را صفر می‌کند. همه مبالغ رشته Decimal هستند.

وقتی previewId فرستاده می‌شود، علاوه بر ورودی و محاسبات، مانده قبلی ارزهای درگیر هم بررسی می‌شود. اگر مقدار مانده تغییر کرده باشد، PREVIEW_CHANGED برمی‌گردد و پیش‌نمایش تازه لازم است. هیچ موجودی رزرو یا قفل بلندمدتی بین پیش‌نمایش و تأیید نگه داشته نمی‌شود. تخصیص AUTO با وضعیت تعهدها در زمان ثبت انجام می‌شود؛ یکسان بودن مانده کل تضمین نمی‌کند تخصیص به همان سند قبلی باشد.

نتیجه موفق و replay تاریخی‌اند؛ برای مانده زنده از balances مشتری و برای وضعیت زنده اسناد از جزئیات Import استفاده کنید. نتایج ذخیره‌شده نسخه ۴.۲ ممکن است summary.balances نداشته باشند؛ فرانت باید نبود آن را مدیریت کند. ثبت مستقیم بدون previewId همچنان مجاز است و مانده لحظه ثبت را مبنا می‌گیرد.

## ۴. چرخه پیش‌نویس

پیش‌نویس متعلق به کاربر جاری است. ADMIN نیز از این روت‌ها پیش‌نویس دیگران را نمی‌خواند. وضعیت DRAFT یعنی قابل ویرایش و بدون نتیجه ثبت؛ COMMITTED یعنی ثبت نهایی شده و متن دیگر قابل تغییر/حذف نیست.

### ذخیره

```http
POST /api/text-drafts
Content-Type: application/json
Authorization: Bearer TOKEN

{"title":"معاملات صبح","text":"مشتری: C001\nدریافتی: 1000 USD"}
```

متن ناقص یا خالی مجاز است؛ این عملیات مالی نیست. title حداکثر ۱۲۰ و text حداکثر ۱۵۰۰۰ کاراکتر است. title پیش‌فرض «پیش‌نویس جدید» و text پیش‌فرض خالی است. پاسخ 200، id و version=1 می‌دهد. ساخت پیش‌نویس Idempotency-Key نمی‌گیرد؛ تکرار create می‌تواند پیش‌نویس دیگری ایجاد کند، اما سند مالی نمی‌سازد.

### ویرایش

```http
PATCH /api/text-drafts/DRAFT_UUID

{"expectedVersion":1,"text":"مشتری: C001\nدریافتی: 1000 USD\nپرداختی: 910 EUR\nنرخ یورویی دریافتی: 0.92"}
```

حداقل title یا text لازم است. متن به‌طور کامل جایگزین می‌شود. نسخه موفق یک واحد زیاد می‌شود. اگر تب یا اپراتور همان حساب نسخه را تغییر داده باشد، VERSION_CONFLICT می‌گیرید؛ آخرین متن را بخوانید و با تغییر خود تطبیق دهید. سیستم متن دیگری را خودکار overwrite یا merge نمی‌کند. حتی ویرایش عنوان نسخه را تغییر می‌دهد.

### پیش‌نمایش نسخه ذخیره‌شده

```http
POST /api/text-drafts/DRAFT_UUID/preview

{"expectedVersion":2}
```

این روت از متن ذخیره‌شده استفاده می‌کند؛ text در بدنه نمی‌گیرد. پاسخ شامل previewId، expiresAt، records، summary و draft.id/version است. اعتبار پنج دقیقه و متعلق به همان کاربر است. پیش‌نمایش سند مالی نمی‌سازد.

### تأیید نهایی

```http
POST /api/text-drafts/DRAFT_UUID/commit
Idempotency-Key: draft-confirm-000001

{"expectedVersion":2,"previewId":"PREVIEW_UUID"}
```

برای پیش‌نویس، previewId الزامی است؛ مسیر مستقیم همچنان پیش‌نمایش اجباری ندارد. ثبت مالی و تغییر وضعیت پیش‌نویس در یک تراکنش انجام می‌شوند. اگر هر مرحله شکست بخورد، پیش‌نویس ثبت‌نشده می‌ماند و سند ناقص ایجاد نمی‌شود.

پاسخ 201:

```json
{
  "data": {
    "draft": {"id":"DRAFT_UUID","title":"معاملات صبح","version":3,"status":"COMMITTED","importId":"IMPORT_UUID"},
    "result": {"importId":"IMPORT_UUID","replayed":false,"formatVersion":2,"documents":[],"summary":{}}
  }
}
```

این پاسخ برای اختصار کوتاه شده؛ result همان نتیجه کامل Import است. در retry **همان کلید و expectedVersion زمان تأیید (در مثال ۲)** را بفرستید، نه نسخه بعد از ثبت (۳). replay، نتیجه قبلی را با replayed=true می‌دهد؛ حتی انقضای preview بعد از ثبت موفق مانع replay نیست. کلید جدید برای پیش‌نویس ثبت‌شده DRAFT_ALREADY_COMMITTED می‌دهد و سند دوم نمی‌سازد.

### فهرست، جزئیات و حذف

| روش | مسیر | توضیح |
|---|---|---|
| GET | /api/text-drafts?page=1&limit=25 | فهرست شخصی؛ متن کامل در فهرست نیست |
| GET | /api/text-drafts?status=DRAFT&search=صبح | فیلتر وضعیت و جست‌وجوی عنوان |
| GET | /api/text-drafts/:id | متن کامل و آخرین نسخه |
| DELETE | /api/text-drafts/:id?expectedVersion=2 | حذف فقط پیش‌نویس ثبت‌نشده با نسخه درست |

تمام روت‌های پیش‌نویس ADMIN/CASHIER و دارای احراز هویت‌اند. جزئیات پیش‌نویس، commitKey داخلی را نمایش نمی‌دهد. حذف پیش‌نویس سند مالی حذف نمی‌کند؛ پیش‌نویس COMMITTED حذف‌پذیر نیست. برای اصلاح سند ثبت‌شده از روند ابطال اسناد قبلی و ثبت صحیح استفاده کنید.

## ۵. خطاهای کاربردی و رفتار فرانت

خطاهای رکورد اکنون hint دارند؛ شماره رکورد، خط، field، code و message هم حفظ شده‌اند. در خطاهای سطح بدنه یا محدودیت کل متن ممکن است شماره خط موجود نباشد. در هر رکورد اولین خطای کشف‌شده گزارش می‌شود؛ خطاها را اصلاح و دوباره validate کنید.

| کد | اقدام اپراتور |
|---|---|
| INVALID_RECORD_ALIAS | شناسه‌ای مانند T1 وارد کنید |
| DUPLICATE_ALIAS | شناسه‌های رکوردها را یکتا کنید |
| INVALID_LOCAL_REFERENCE | معامله هدف را پیش از پرداخت تعریف کنید |
| ALLOCATION_MISMATCH | مشتری، ارز و جهت پرداخت را با معامله تطبیق دهید |
| INVALID_EXTERNAL_REFERENCE | قالب مرجع منبع را اصلاح کنید |
| DUPLICATE_EXTERNAL_REFERENCE | همان مرجع در دو رکورد یک متن تکرار شده است |
| EXTERNAL_REFERENCE_EXISTS | رویداد قبلاً ثبت شده؛ نتیجه قبلی را پیگیری کنید |
| VERSION_CONFLICT | پیش‌نویس تازه را بخوانید؛ ویرایش خود را با آن تطبیق دهید |
| DRAFT_ALREADY_COMMITTED | نتیجه ثبت را باز کنید؛ کلید تازه نسازید |
| PREVIEW_CHANGED | ورودی، نرخ یا مانده تغییر کرده؛ دوباره پیش‌نمایش بگیرید |

فرانت بهتر است دکمه ثبت را هنگام ارسال غیرفعال کند، کلید عملیات را پیش از ارسال بسازد و تا مشخص شدن نتیجه نگه دارد. اعتبارسنجی 200 با valid=false موفقیت مالی نیست. پس از خطای شبکه اول با همان کلید retry کنید. در مسیر پیش‌نویس، id پیش‌نویس و expectedVersion اصلی را هم نگه دارید.

در صفحه پیش‌نمایش، نام و کد مشتری، نوع عملیات، اصل مبلغ، کارمزد، پرداخت‌های واقعی، هشدارها و before/after را نمایش دهید. هشدار REAL_CASH_POSTING یعنی تأیید شما دریافت/پرداخت واقعی ایجاد می‌کند. این موارد توصیه برای فرانت شماست؛ پنل گرافیکی در این بسته اضافه نشده است.

## ۶. Postman، کدها و اجرای پروژه

docs/postman_collection.json همه ۸۵ روت را دارد. docs/postman_text_imports.json شامل ۹ نمونه اعتبارسنجی، مسیر ارسال مستقیم و مسیر Save → Edit → Preview → Commit پیش‌نویس است. قبل از اجرا token و کد مشتری واقعی داخل textImportText را تنظیم کنید. درخواست ویرایش اختیاری است. پس از Preview موفق، نسخه و شناسه پیش‌نمایش خودکار ذخیره می‌شوند؛ Commit همان کلید را در تکرار حفظ می‌کند.

در مثال مرجع خارجی، شناسه نمونه را با شماره واقعی همان رویداد جایگزین کنید. برای عملیات تازه و متفاوت، کلید تازه و در صورت استفاده مرجع مناسب تازه لازم است. برای retry هیچ‌کدام را عوض نکنید. پیش‌نویس تازه از Save ساخته می‌شود؛ درخواست Save، id قبلی را با id جدید جایگزین می‌کند.

| فایل | وظیفه |
|---|---|
| src/text-import-parser.js | قالب، ارقام، عناوین، مرجع‌ها و خطاهای ساختاری |
| src/text-import-service.js | تطبیق داده، مانده‌ها، preview، ثبت اتمیک و حفاظت مرجع |
| src/text-drafts.js | مالکیت، نسخه، ویرایش و تأیید پیش‌نویس |
| src/text-import-contract.js | schema خروجی مستندات |
| examples/text-imports/linked.txt | مثال اتصال رکوردها |
| examples/text-imports/referenced.txt | مثال مرجع خارجی |
| docs/text-import-templates.json | قالب‌ها و اطلاعات ساخت فرم |
| docs/UPGRADE_V4_3_FA.md | نصب و ارتقای دیتابیس |

نسخه ۴.۳ دو جدول افزایشی دارد. نرخ‌ها، مانده‌ها و اسناد قبلی بازنویسی نمی‌شوند. تست‌های واقعی MySQL و مهاجرت روی داده مشتری در محیط ساخت اجرا نشده‌اند؛ آزمون مستقل آماده است و وضعیت دقیق در TEST_STATUS.md نوشته شده است.
