# راهنمای کامل ثبت متنی — قالب ۲، برنامه ۴.۳

راهنمای پیش‌نویس، مرجع خارجی، اتصال رکوردها و مانده قبل/بعد در TEXT_WORKFLOW_FA.md مکمل این قرارداد است.

کار روزمره همچنان می‌تواند فقط ارسال یک text باشد. امکانات پیشرفته اختیاری‌اند: پیش‌نمایش، تسویه واقعی، چند رکورد و تاریخچه. مسیر پیشنهادی فرانت: انتخاب قالب ← نوشتن متن ← پیش‌نمایش ← تأیید ثبت. ثبت مستقیم بدون previewId نیز مجاز است.

این قابلیت متن ساختاریافته را می‌خواند؛ تفسیر آزاد با AI، OCR، ساخت خودکار مشتری و حدس ارز انجام نمی‌دهد. اشتباه مبهم رد می‌شود تا حساب مشتری دیگری تغییر نکند.

## ۱. ساده‌ترین معامله

```text
مشتری: C001
دریافتی: 1000 USD
پرداختی: 910 EUR
```

مشتری کد موجود در دیتابیس است. نوع پیش‌فرض معامله و تسویه پیش‌فرض بدون است. نرخ یورویی از تنظیم ارز و درصد کارمزد از تنظیم عمومی خوانده می‌شود؛ نبود نرخ لازم خطاست. مبالغ دریافتی و پرداختی در این قالب تعهد معامله‌اند، نه تأیید جابه‌جایی پول.

برای محاسبه مشخص مستقل از کارمزد/نرخ عمومی، خطوط کارمزد و نرخ‌های یورویی را صریح وارد کنید. نمونه‌ها نرخ آموزشی‌اند و نرخ زنده بازار نیستند.

## ۲. معامله همراه تسویه کامل واقعی

```text
نوع: معامله
مشتری: C001
دریافتی: ۱٬۰۰۰ USD
پرداختی: ۹۱۰ EUR
کارمزد: ۰٫۵
نرخ یورویی دریافتی: ۰٫۹۲
هزینه یورویی: ۹۱۰
تسویه: کامل
توضیحات: دریافت دلار و پرداخت یورو انجام شد
```

این متن **یک معامله و دو Payment واقعی** ایجاد می‌کند: دریافت ۱۰۰۵ دلار (اصل به‌علاوه ۵ دلار کارمزد) و پرداخت ۹۱۰ یورو. هر دو به معامله جدید تخصیص می‌یابند. تغییر حساب مشتری در این عملیات صفر است ولی گردش نقدی USD مثبت ۱۰۰۵ و EUR منفی ۹۱۰ است. مانده قبلی مشتری ممکن است همچنان غیرصفر باشد.

فقط وقتی هر دو جابه‌جایی واقعاً انجام شده‌اند «کامل» بنویسید. برای همان معامله دوباره Payment دستی نسازید. کنترل موجودی منفی صندوق همچنان در این پروژه وجود ندارد.

## ۳. معامله با دریافت/پرداخت جزئی

```text
نوع: معامله
مشتری: C001
دریافتی: 1000 USD
پرداختی: 910 EUR
کارمزد: 0.5
نرخ یورویی دریافتی: 0.92
تسویه: جزئی
دریافت واقعی: 500 USD
پرداخت واقعی: 200 EUR
```

این مثال یک معامله، یک دریافت ۵۰۰ دلار و یک پرداخت ۲۰۰ یورو می‌سازد. مانده همین عملیات مثبت ۵۰۵ دلار و منفی ۷۱۰ یورو است. می‌توانید فقط یکی از دو خط واقعی را بنویسید. اگر خط واقعی دارید و تسویه ننویسید، جزئی فرض می‌شود. جزئی بدون مبلغ، یا کامل/بدون همراه مبلغ واقعی، خطاست.

ارز دریافت واقعی باید ارز دریافتی معامله و ارز پرداخت واقعی باید ارز پرداختنی آن باشد. مبلغ جزئی از تعهد همان سمت بیشتر نمی‌شود؛ مازاد واقعی را با پرداخت مستقل ثبت کنید. تخصیص، Ledger اضافه نمی‌سازد؛ خود Payment گردش پول است.

## ۴. معامله با نرخ توافقی

```text
نوع: معامله
مشتری: C001
دریافتی: 1000 USD
نرخ توافقی: 0.91
ارز پرداختی: EUR
کارمزد: 0
نرخ یورویی دریافتی: 0.92
```

به‌جای خط پرداختی، نرخ توافقی و ارز پرداختی بدهید. نرخ یعنی تعداد واحد ارز پرداختی به ازای یک واحد ارز دریافتی. مبلغ پرداختی با دقت ارز و HALF_UP گرد می‌شود. اگر مبلغ پرداختی را مستقیم داده باشید، سیستم نرخ را تا ۱۲ رقم اعشار محاسبه و بررسی می‌کند که همان مبلغ دقیقاً بازتولید شود؛ در غیر این صورت خطا می‌دهد. این دو روش را هم‌زمان ننویسید.

## ۵. دریافت و پرداخت مستقل

```text
نوع: دریافت
مشتری: C001
مبلغ: 100 USD
نرخ یورویی: 0.92
معامله: TR-00000001
تخصیص: AUTO
توضیحات: بخشی از بدهی معامله قبلی
```

نوع دریافت یعنی پول از مشتری به صرافی. نوع پرداخت یعنی پول از صرافی به مشتری. برای پیش‌دریافت/پیش‌پرداخت بدون تخصیص، معامله را حذف و تخصیص را NONE بنویسید:

```text
نوع: پرداخت
مشتری: C001
مبلغ: 90 EUR
تخصیص: NONE
```

معامله اختیاری است و UUID یا شماره TR واقعی می‌گیرد. در نسخه ۴.۳، @T1 نیز برای اشاره به معامله قبلی همین متن پذیرفته می‌شود؛ روی معامله «شناسه رکورد: T1» بنویسید. مشتری، ارز، سمت و وضعیت سند بررسی می‌شوند. برای معامله و پرداخت‌های مربوط در یک رکورد، تسویه کامل/جزئی همچنان در دسترس است.

AUTO پیش‌فرض است: مطابق سرویس تخصیص قبلی، تا ۱۰۰ تعهد باز مناسب انتخاب می‌شوند. اگر مرجع معامله دارید به همان محدود می‌شود؛ بدون مرجع فقط معاملات شماره‌دار نسخه ۴ هدف‌اند. مازاد تخصیص‌نیافته در حساب باقی می‌ماند. NONE گردش پول را ثبت می‌کند ولی تخصیص نمی‌دهد. پیش‌نمایش، مبلغ پرداخت را تثبیت می‌کند؛ تخصیص نهایی به مانده‌های لحظه ثبت وابسته است و در پاسخ Payment می‌آید.

## ۶. چند رکورد در یک متن

رکوردها را با یک خط حاوی فقط --- جدا کنید. هر رکورد مشتری و فیلدهای خودش را دارد و چیزی از رکورد قبلی به ارث نمی‌برد:

```text
نوع: معامله
مشتری: C001
دریافتی: 1000 USD
پرداختی: 910 EUR
نرخ یورویی دریافتی: 0.92
---
نوع: دریافت
مشتری: C002
مبلغ: 500 USD
نرخ یورویی: 0.92
تخصیص: NONE
```

گروه به ترتیب متن و در یک تراکنش Serializable ثبت می‌شود. خطای هر رکورد یا خطای دیتابیس، همه اسناد، گردش‌ها، تخصیص‌ها و مصرف preview را rollback می‌کند. پاسخ موفق یعنی کل گروه ثبت شده است. «ادامه دادن رکوردهای سالم و نادیده‌گرفتن خراب‌ها» وجود ندارد. رکورد کاملاً یکسان در یک متن برای جلوگیری از تکرار تصادفی رد می‌شود.

## ۷. قواعد نوشتن و حدود

| مورد | قرارداد |
|---|---|
| تعداد | حداکثر ۲۰ رکورد، ۳۰۰ خط و ۱۵۰۰۰ کاراکتر |
| توضیحات | حداکثر ۲۰۰۰ کاراکتر در هر رکورد |
| عنوان‌ها | فارسیِ مشخص‌شده؛ فاصله، نیم‌فاصله و ی/ک عربی در عنوان استاندارد می‌شوند |
| نام‌های جایگزین | کد مشتری = مشتری؛ مبلغ دریافتی = دریافتی؛ مبلغ پرداختی = پرداختی؛ شرح = توضیحات |
| دو نقطه | : یا ： |
| مبلغ | ارقام انگلیسی/فارسی/عربی؛ ممیز . یا ٫ |
| هزارگان | 1,000 یا ۱٬۰۰۰ با گروه دقیق سه‌رقمی؛ ترکیب جداکننده‌ها رد می‌شود |
| ارز | کد لاتین سه‌حرفی، حروف کوچک هم پذیرفته می‌شوند؛ ارز باید فعال باشد |
| مقدارها | نماد علمی، منفی، علامت درصد و واحد چسبیده به عدد مجاز نیستند |
| خطوط خالی | نادیده گرفته می‌شوند |
| کامنت | خطی که با # شروع شود نادیده گرفته می‌شود |
| توضیح چندخطی | پس از توضیحات، خط ادامه را با \| شروع کنید |
| نام اختیاری | «نام مشتری» باید دقیقاً برابر نام ذخیره‌شده باشد؛ کد مشتری همچنان الزامی است |
| تاریخ | تاریخ ثبت UTC سرور است؛ ثبت با تاریخ دلخواه/عقب‌گرد پشتیبانی نمی‌شود |

نمونه شرح چندخطی:

```text
توضیحات: حواله مشتری
| پرداخت در دو مرحله انجام شد
| شماره پیگیری: ABC123
```

عنوان ناشناخته یا تکراری، فیلد مربوط به نوع دیگر، مقدار خالی و خط آزاد خطاست. داخل مقدار نام/کد، تبدیل خودکار حروف و ارقام انجام نمی‌شود. متن فقط در JSON پذیرفته می‌شود؛ text/plain و آپلود فایل پشتیبانی نمی‌شوند. محدودیت عمومی HTTP نیز برقرار است؛ JSON با escape بسیار زیاد ممکن است پیش از parser به سقف حجم بدنه برسد.

## ۸. فیلدها و پیش‌فرض‌ها

| نوع | الزامی | اختیاری |
|---|---|---|
| معامله | مشتری، دریافتی، پرداختی یا جفت نرخ توافقی/ارز پرداختی | نام مشتری، کارمزد، نرخ یورویی دریافتی/پرداختی، هزینه یورویی، تسویه، مبالغ واقعی، توضیحات |
| دریافت/پرداخت | نوع، مشتری، مبلغ | نام مشتری، نرخ یورویی، معامله، تخصیص، توضیحات |

نوع‌های TRADE/RECEIVE/PAY و تسویه NONE/FULL/PARTIAL هم پذیرفته‌اند. تخصیص خودکار/بدون معادل AUTO/NONE است. کارمزد صفر معتبر است؛ حذف کارمزد یعنی مقدار تنظیمات. نرخ EUR همیشه ۱ است. نبود هزینه یورویی سود خریدوفروش را null می‌کند و هشدار می‌دهد. هزینه یورویی برای دارایی پرداختنی EUR باید دقیقاً مبلغ پرداختنی باشد. قاعده کارمزد همان سیستم فعلی و همیشه به ارز دریافتی است.

## ۹. روت‌ها

| روش | مسیر | نتیجه |
|---|---|---|
| GET | /api/text-imports/templates | قالب‌ها، فیلدها، نسخه و محدودیت‌ها |
| POST | /api/text-imports/parse | فقط تجزیه ساختار؛ 200 با valid و errors |
| POST | /api/text-imports/validate | تطبیق مشتری/ارز/نرخ/مرجع و محاسبه؛ بدون نوشتن |
| POST | /api/text-imports/preview | اعتبارسنجی و ذخیره توکن پیش‌نمایش پنج‌دقیقه‌ای؛ بدون گردش مالی |
| POST | /api/text-imports | ثبت واقعی کل گروه؛ 201 |
| GET | /api/text-imports?page=1&limit=25 | تاریخچه خود کاربر با صفحه‌بندی |
| GET | /api/text-imports/:id | متن اصلی، پاسخ تاریخی و وضعیت زنده اسناد |

همه روت‌ها Bearer می‌خواهند. قالب برای همه نقش‌های واردشده است؛ سایر روت‌ها ADMIN/CASHIER هستند. حتی مدیر تاریخچهٔ کاربران دیگر را از این روت‌ها نمی‌خواند. parse با اینکه به اطلاعات مشتری مراجعه نمی‌کند، همچنان نشست را از دیتابیس احراز می‌کند.

بدنه parse/validate/preview فقط text است. بدنه ثبت text و previewId اختیاری دارد. هدر Idempotency-Key فقط برای ثبت الزامی است؛ ۸ تا ۱۰۰ کاراکتر لاتین، عدد، خط فاصله یا زیرخط.

## ۱۰. نمونه درخواست و پاسخ

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

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

پاسخ زیر خلاصهٔ ساختار است؛ records شامل مشتری تطبیق‌شده، ورودی، pricing، پرداخت‌های برنامه‌ریزی‌شده و warnings است:

```json
{
  "data": {
    "formatVersion": 2,
    "valid": true,
    "previewId": "UUID",
    "expiresAt": "UTC_DATE",
    "records": [],
    "errors": [],
    "summary": {
      "recordCount": 1,
      "tradeCount": 1,
      "paymentCount": 0,
      "effects": [
        {"customerId":1,"customerCode":"C001","currency":"USD","accountChange":"1005","cashChange":"0"},
        {"customerId":1,"customerCode":"C001","currency":"EUR","accountChange":"-910","cashChange":"0"}
      ]
    }
  }
}
```

effects تغییر همین عملیات را نشان می‌دهد، نه مانده کل حساب یا موجودی صندوق. summary.balances در نسخه ۴.۳ مانده before و after مشتری را برای ارزهای درگیر اضافه می‌کند. مثبت accountChange یعنی افزایش بدهی مشتری، منفی یعنی افزایش طلب او. مثبت cashChange ورود پول و منفی خروج پول است. ارزهای مختلف جمع نمی‌شوند.

برای تأیید همان text و previewId را به POST /api/text-imports با کلید یکتای عملیات بفرستید. نتیجه دارای importId، replayed، summary و documents است؛ هر ردیف documents، trade اختیاری و payments دارد. UUID اسناد برای receipt، settlement و void همان روت‌های قبلی قابل استفاده است. balancesBefore/After سند مربوط به لحظه همان سند در گروه است، نه الزاماً پایان همه رکوردها.

## ۱۱. پیش‌نمایش و جلوگیری از تکرار

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

کلید تکرار برای **کل گروه** است. پس از timeout یا قطع ارتباط با همان متن و همان کلید تکرار کنید؛ نتیجه ذخیره‌شده بدون ثبت دوباره برمی‌گردد و replayed=true است. حتی اگر مشتری بعداً تغییر کرده یا preview منقضی شده باشد، درخواست ثبت‌شده با همان محتوای استانداردشده بازپخش می‌شود. وضعیت اسناد داخل نتیجه، تاریخی است؛ currentDocuments در جزئیات وضعیت فعلی را نشان می‌دهد.

کلید یکسان برای متن دیگر یا مالک دیگر خطای IDEMPOTENCY_CONFLICT می‌دهد. تغییر ترتیب رکوردها تغییر درخواست است؛ تغییر ترتیب خطوط، کامنت و فاصله‌های بیرونی بر محتوای استانداردشده اثر ندارد. تغییر نمایش عدد در بعضی فیلدهای اختیاری مانند 0.5 به 0.50 ممکن است محتوا را تغییر دهد؛ هنگام تکرار همان متن را نگه دارید. کلید جدید با متن مشابه، عملیات جدید محسوب می‌شود؛ سیستم قصد کاربر را از شباهت متن حدس نمی‌زند.

## ۱۲. خطا و رفتار فرانت

parse و validate برای متن نامعتبر پاسخ 200 با valid=false می‌دهند؛ فرانت نباید 200 را به‌تنهایی مجوز ثبت بداند. preview و ثبت برای همان خطاها 400 TEXT_IMPORT_INVALID دارند و جزئیات در error.details.errors است. در هر رکورد اولین خطای ساختاری یا معنایی گزارش می‌شود؛ پس از اصلاح، دوباره validate کنید. اگر مرحله ساختاری خطا داشته باشد، مرحله دیتابیس آغاز نمی‌شود. در validate ناموفق ممکن است records فقط شامل رکوردهای محاسبه‌شده باشد؛ هیچ‌کدام ثبت نشده‌اند و summary موفق وجود ندارد.

```json
{"error":{"code":"TEXT_IMPORT_INVALID","message":"متن معتبر نیست؛ خطاهای رکوردها را اصلاح کنید.","field":"text","details":{"errors":[{"record":2,"line":9,"field":"مبلغ","code":"INVALID_MONEY","message":"مبلغ نامعتبر است."}]}}}
```

| کد | اقدام |
|---|---|
| UNKNOWN_FIELD / DUPLICATE_FIELD | عنوان خط را اصلاح کنید |
| REQUIRED_FIELD / EMPTY_FIELD | فیلد لازم را پر کنید |
| INVALID_NUMBER / INVALID_MONEY | عدد یا گروه‌بندی را اصلاح کنید |
| CONFLICTING_FIELDS / FIELD_NOT_ALLOWED | قالب همان نوع عملیات را رعایت کنید |
| DUPLICATE_RECORD / EMPTY_RECORD | رکورد تکراری یا جداکننده اضافی را حذف کنید |
| CUSTOMER_UNAVAILABLE / CUSTOMER_NAME_MISMATCH | مشتری موجود و فعال و کد درست را انتخاب کنید |
| CURRENCY_RATE_MISSING | نرخ را وارد یا تنظیم کنید |
| INVALID_TRADE_REFERENCE / ALLOCATION_MISMATCH | مرجع، مشتری، ارز و سمت را بررسی کنید |
| CASH_CURRENCY_MISMATCH / CASH_EXCEEDS_OBLIGATION | مبلغ واقعی و سمت معامله را اصلاح کنید |
| TEXT_AMOUNT_MISMATCH | مبلغ با دقت نرخ ۱۲رقمی قابل ثبت نیست؛ روش نرخ توافقی را بررسی کنید |
| PREVIEW_CHANGED / PREVIEW_EXPIRED | پیش‌نمایش تازه بگیرید؛ عملیات ثبت‌شده قبلی را ابتدا بررسی کنید |
| IDEMPOTENCY_CONFLICT | متن/کلید قبلی را تطبیق دهید؛ کورکورانه کلید جدید نسازید |
| WRITE_CONFLICT / DUPLICATE_VALUE | ممکن است رقابت هم‌زمان باشد؛ همان درخواست و کلید را تکرار کنید |
| DATABASE_TIMEOUT / DATABASE_UNAVAILABLE | نتیجه قبلی را پیگیری و با همان کلید تکرار کنید |

خطای عمومی body، مجوز، محدودیت درخواست و دیتابیس همان قرارداد اصلی API است. بعضی خطاهای حسابداری مثل اعشار نامجاز، BAD_REQUEST با پیام توضیحی دارند. requestId برای پیگیری در پاسخ خطا موجود است.

## ۱۳. Postman و فایل‌های آماده

docs/postman_collection.json همه ۸۵ روت را دارد. docs/postman_text_imports.json کالکشن اختصاصی با ۹ مثال اعتبارسنجی، مسیر Preview → Commit → Detail و مسیر پیش‌نویس است. در متغیرها token و textImportText را تنظیم کنید. Preview موفق textPreviewId را ذخیره می‌کند؛ Commit اولین بار textImportKey می‌سازد و برای تکرار آن را نگه می‌دارد.

برای **عملیات جدید** پس از تعیین تکلیف قبلی، textImportKey و textPreviewId را خالی و متن جدید را جایگزین کنید. برای retry عملیات قبلی این متغیرها را تغییر ندهید. نمونه‌های پوشه Examples فقط validate می‌کنند. کالکشن اصلی برای همه روت‌هاست؛ برای روند پیش‌نمایش و تأیید، کالکشن اختصاصی ساده‌تر است.

فایل‌های قابل کپی در examples/text-imports شامل trade.txt، settled.txt، partial.txt، byRate.txt، receive.txt، pay.txt و batch.txt هستند. metadata قالب در docs/text-import-templates.json است. همه این‌ها با npm run docs از قالب‌های واقعی تولید می‌شوند.

## ۱۴. مدیریت، اصلاح و محدوده

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

قالب ۱ و روت‌های /api/trade-text برای سازگاری باقی‌اند؛ این روت‌ها امکانات گروهی و preview قابل تأیید قالب ۲ را ندارند. قابلیت جدید نیاز به migration TextImport دارد؛ UPGRADE_V4_2_FA.md را بخوانید.

محاسبات و HTTP به‌صورت محلی تست شده‌اند. تست واقعی MySQL، migration روی اطلاعات واقعی، کارایی گروه‌های بزرگ و رقابت دیتابیس در محیط ساخت اجرا نشده است. آزمون یکپارچه آماده شامل ثبت گروهی، تسویه، replay و درخواست‌های هم‌زمان است؛ پیش از بهره‌برداری روی MySQL مستقل اجرا کنید.
