GN Gerege Nexus
GitHub Нэвтрэх

Баримт ба цахим гарын үсэг · Documents & e-signatures

io.gerege.nexus.documents апп нь баримтыг батлах урсгалд оруулж, иргэний бодит eID батламжаар гарын үсэг зуруулдаг. Энэ бичиг нь тэр урсгалын ойлгомжгүй хэсгүүдийг — яагаад тийм болсныг — тайлбарлана.


1. Гарын үсэг гэж юу вэ

eID Mongolia-д баримт зурах endpoint байхгүй. Байгаа зүйл нь: иргэн өөрийн бүртгэлтэй төхөөрөмж дээр, өөрийн гэрчилгээгээр, RP-ийн сонгосон текстийн эсрэг өгдөг батламж. Тэр батламж нь өөрөө гарын үсэг.

Тиймээс ceremony нь хоёр дуудлагатай:

POST /documents/{id}/sign/eid/start   иргэний eID апп руу батлах хүсэлт түлхэнэ
POST /documents/{id}/sign/eid/poll    батлах / татгалзах / хугацаа дуусахыг хүлээнэ

poll нь COMPLETE буцаах мөчид гарын үсэг бүртгэгдэнэ. Live горимд API нь хүсэлтийг 25 секунд хүртэл онгойлгож барьдаг тул клиент AbortSignal дамжуулж, дэлгэц хаагдмагц тасалдаг.

Статус кодууд ялгаатай гэдэг нь чухал: eID-тэй холбогдож чадаагүй нь 503 (дахин оролдоно), иргэн/оператор буруу зүйл хийсэн нь 400, баримт хүлээгдэхээ больсон нь 409. Клиентийн цикл 4xx дээр зогсож, 5xx-ийг тэвчдэг — эс бөгөөс сүлжээний түр асуудал иргэн утсаа гартаа барьж байхад ceremony-г таслах байсан.

Харагдах текст нь хамгаалалт

Иргэнд харагдах текст баримтыг нэрлэдэг:

Гарын үсэг: Хамтран ажиллах гэрээ 2026

«Gerege Nexus-д нэвтрэх» гэдгийг батлах нь гэрээнд гарын үсэг зурахыг зөвшөөрөх биш. Иргэн юуг батлаж байгаагаа зөвхөн энэ текстээр мэддэг.

Хязгаар нь 60 БАЙТ. eID-ийн талбар нь Smart-ID-ийн displayText60, core client нь dt[:60] гэж байтаар хайчилдаг. Кирилл нь үсэг тутамд 2 байт, тэгэхээр 60 байт ≈ 30 үсэг, бас rune дундуур тасалж хүчингүй UTF-8 гаргаж болно. Иймд:

Session нь баримттай холбогдсон, нэг удаагийн

document_eid_sign_sessions (migration 00015) нь session-ыг эхлүүлсэн баримттайгаа хамт хадгална. Ингэснээр:

Session нь eID-ийн өөрийн хугацаатай (expires_at) хадгалагдаж, poll түүнийг шалгадаг (цагийн зөрүүнд 2 минутын нөөц). Өмнө нь хугацаа хаягдаж байсан тул хэдэн хоногийн өмнөх батламжийг өнөөдрийн огноотой гарын үсэг болгож болох байсан.

Гэхдээ eID push session-д хугацаа хэлдэггүй. Тэгэхээр:

Учир нь зохиосон хугацаа нь хугацаа биш, тасалбар: push session нь үүссэнээс хойш 9 минутын дараа ч RUNNING байсныг production дээр хэмжсэн. Хоёр минутыг хугацаа гэж үзвэл утсаа олж, түгжээгээ тайлж, PIN оруулахад түүнээс их зарцуулсан иргэн өөрийн өгөх гэж байсан батламжаа алдана (нэвтрэх урсгал дээр яг ийм байсан, #22-д зассан).

NULL session-ыг signSessionBackstop = 15 минут барина — энэ нь хугацаа биш, харин log-д хэвтэж байгаа approval id-г хязгааргүй хугацаанд ашиглах боломжийг хаах хамгаалалт. Хүлээлтийг дуусгадаг нь eID-ийн өөрийн EXPIRED.

Дэлгэц ч мөн адил: eID хугацаа хэлсэн үед л тоолуур харуулна, эс бөгөөс зөвхөн дотоод backstop-той хүлээнэ. Өөрсдийн зохиосон тоолуур нь иргэнийг шаардаж, дараа нь eID хүлээж байхад «хугацаа дууслаа» гэж хэлэх байсан.

API-ийн write deadline нь eid.PollWindow-оос гаралтай (cmd/api), тэгэхээр 25 секунд онгойлгож барих poll — нэвтрэх ба гарын үсэг хоёулаа — тасрахгүй.

Гарын үсэг бүртгэгдэхэд session нь яг тэр транзакцид зарлагадана. Дараа нь тусад нь хийвэл холболт тасрахад «500 алдаа» гэж хэлээд гарын үсэг нь бүртгэгдсэн байх, session нь дахин хэрэглэгдэхээр үлдэх байдал үүсдэг байсан.

Батлахыг буцаасан регистр нь push илгээсэн регистртэй таарах ёстой. Bodlogo ба батлах хэлхээг эхлэх үед биш, дуусах үед дахин уншина — хооронд минут өнгөрсөн байж болох тул шийдэх нь тэр мөч.

Хэр олон удаа асууж болох вэ

Гарын үсэг эхлүүлэх нь иргэний утас руу мэдэгдэл түлхдэг тул хязгаартай: минутад 10, бас 10 хүртэл дараалан (maxChainSteps). ДАН нь юу ч түлхдэггүй ч тэр л хувингаас иддэг — 6 оронтой кодыг таах оролдлогыг зогсоох нь тэрний зорилго.

Мөн нэг иргэнээс дахин асуувал өмнөх асуулт дуусна: тэр баримт, тэр иргэнд ганц л session хүчинтэй үлдэнэ (өмнө нь 30 удаа асуувал 30 хүчинтэй approval id үлддэг байсан).

ДАН яагаад өөр вэ

ДАН нь энэ платформд батлах push түлхдэггүй. Тиймээс POST /documents/{id}/sign/dan нь регистр + нэг удаагийн кодын хэлбэрээрээ хэвээр, бас DAN_MOCK_MODE унтарсан үед ажиллахгүй — live ДАН OTP клиент кодод байхгүй. Дэлгэц дээр суваг тус бүр өөрийн юу хийдгээ бичдэг.


2. Хэн зурж болох вэ

Гурван зүйл шийднэ:

Хаана Юу шийднэ
documents.sign эрх Ерөөсөө гарын үсэг зурах/татгалзах эрхтэй эсэх
Баримтын өөрийн шат (document_approval_steps) Дараагийн батламжийг хэн өгөх ёстой
Гарын үсгийн бодлого (/module/documents/signatures) Ямар сувгаар (E-ID/ДАН), нээлттэй шат зөвшөөрөх эсэх

Нэрлэсэн шат нь өөрөө хүчинтэй. Шат регистрийн дугаар агуулж байвал тэр батламжийг зөвхөн тэр иргэн өгч болно — bodlogo-д ямар нэг тэмдэглэгээ хийх шаардлагагүй. (Өмнө нь require_named_signer унтарсан үед шатны регистр огт хэрэгсэхгүй байсан, гэтэл дэлгэц нь хүчинтэй юм шиг харуулдаг байсан.)

Шат хоосон бол нээлттэй: гарын үсэг зурах эрхтэй хэн ч авч болно — гэхдээ хожмын шатад нэрлэгдсэн хүн нээлттэй шатыг авч чадахгүй. Нэг хүн баримтад нэг л удаа зурдаг тул тэр гарын үсгээ эрт зарцуулбал өөрийн шат хэзээ ч дүүрэхгүй болно. (Монголд түгээмэл дараалал — «Хянагч» нээлттэй, дараа «Захирал» нэрлэгдсэн — яг ийм тохиолдол.)

Аль хэдийн зурсан хүнээс дахин батламж гуйхгүй: хоёр суваг хоёулаа push илгээхээс өмнө татгалздаг.

require_named_signer гэдэг нь «нээлттэй шат байхгүй» гэсэн үг. Тиймээс хадгалахад шат бүр нэрлэгдсэн, бас өөр өөр хүн нэрлэгдсэн байхыг шаардана — хоосон шат хэзээ ч дүүрэхгүй, нэг хүнийг хоёр шатад нэрлэвэл (нэг хүн баримтад нэг л удаа зурдаг тул) хоёр дахь нь хэзээ ч дүүрэхгүй. Хоёр тохиолдолд ч тэр төрөл хэнд ч батлагдахгүй болно, тиймээс хоёр дэлгэц хоёулаа энэ дүрмийг шалгадаг — бас хоёулаа нэг advisory lock авдаг тул зэрэг хадгалахад хориглосон төлөв commit болохгүй.

Дүүрэх боломжгүй нэрлэсэн шат

Хадгалагдсан хэлхээ хэн ч дүүргэж чадахгүй шат агуулж болох гурван зам бий:

  1. Нэг хүнийг хоёр шатад нэрлэсэн — нэг хүн баримтад нэг л удаа зурдаг тул хоёр дахь нь хэзээ ч дүүрэхгүй.
  2. Регистрийн дугаар биш юм нэрлэсэн (8 тэмдэгтээс багa, ж. нь AA9) — ийм дугаарыг ямар ч provider хүлээж авахгүй тул тэр шатыг хэн ч дүүргэж чадахгүй.
  3. Хэлбэржээгүй дугаар (aa90010111, эсвэл хажуудаа зайтай) — гарын үсгийн зам иргэний өгсөн дугаарыг том болгож, зай хайчилж харьцуулдаг тул ийм шат өөрийн иргэнтэйгээ хэзээ ч тэнцэхгүй.

Гурав дахийг эхлээд зассан нь чухал: AA90010111 ба aa90010111 хоёр нь нэг иргэн хоёр удаа, гэтэл хэлбэржүүлэхээс нааш давхардал гэж танигдахгүй.

Гурвуулааг өмнө нь хэн ч шалгадаггүй байсан: дугаарыг зөвхөн bodlogo шаардсан үед л хардаг байсан тул AA9 гэдэг нь «нэг гарын үсэг нэмж» гэсэн хоргүй typo байсан. Одоо нэрлэсэн шат өөрөө тухайн иргэнд холбогддог болсон тул ийм шат баримтыг татгалзахаас өөр гарцгүй болгоно.

Тиймээс дүрэм нь нэг Go функцэд байна, SQL-д биш:

Функц Юу хийдэг
normaliseRegNumber зай хайчиж, том болгоно — бүх харьцуулалт үүнийг дамжина
plausibleRegNumber rune-аар 8…64 хооронд эсэхийг хэлнэ
fillableChain дүүрэхгүй шатыг нээж, бусдыг хэлбэржүүлж үлдээнэ

ReplaceWorkflow нь эдгээрээр хадгалахаас татгалздаг, snapshotApprovalChain нь fillableChain-аар хуулдаг — тэгэхээр баримт дээр буусан хэлхээ нь хадгалах зам татгалзах хэлхээ хэзээ ч байхгүй.

Гарын үсгийн хоёр зам ч (SignWithDAN, PollEIDSignature) provider-ын буцаасан дугаарыг normaliseRegNumber-ээр дамжуулдаг — гуравхан харьцуулалт («энэ шат түүний үү», «аль хэдийн зурсан уу», хүснэгтийн UNIQUE(document_id, signer_reg_number)) бүгд энэ мөрөн дээр тулгуурладаг тул provider-ын бичиглэлд найдаж болохгүй.

Гурав дахь урхи — хайлт. ILIKE нь database-ийн ctype-аар том/жижиг үсгийг тохируулдаг. LC_CTYPE=C дээр латиныг тохируулж, кириллийг тохируулдаггүй: 'ГЭРЭЭ 2026' ILIKE '%гэрээ%' нь false. Бид postgres:16-alpine дээр deploy хийдэг бөгөөд musl-д locale байхгүй тул initdb яг ийм cluster гаргадаг — өөрөөр хэлбэл монголоор хайх нь production дээр том/жижиг үсэг хамааруулах байсан. Тиймээс titleMatch нь ICU collation байгаа эсэхийг нэг удаа асууж, байвал d.title COLLATE "und-x-icu" ILIKE … гэж хайна (C-ctype дээр хэмжиж баталсан). ICU байхгүй бол хуучин хэлбэрээр ажиллаж, log-д сануулга бичнэ — байхгүй collation-ыг нэрлэвэл хайлт бүхэлдээ алдана.

Хоёр урхи, хоёулаа туссан:

  1. Go-ийн len() нь байт, Postgres-ийн length() нь тэмдэгт. Монгол регистр кирилл (УБ99010111 = 10 тэмдэгт, 20 байт) тул УБ9901 нь 8 байт боловч 6 тэмдэгт. Байтаар хэмжихэд хадгалах зам түүнийг нэрлэсэн шат гэж хүлээж авч, хуулах зам нээлттэй болгодог байсан — дэлгэц баримтад байхгүй батлагчийг нэрлэж, тэр шатыг хэн ч дүүргэх байдалд оруулна. Одоо бүх хязгаар rune-аар (VARCHAR(64) ч тэмдэгтээр тоолдог).
  2. SQL-ийн upper() нь cluster-ийн ctype-аас шалтгаална. LC_CTYPE=C дээр upper('уб99010111') нь хөдлөхгүй, Go бол хөдөлгөнө. Тиймээс хэлбэржүүлэлт Go дээр байна: migration 00017 §0 нь хадгалагдсан хэлхээг сайн дураараа зассан ч, баримт дээр буух хэлхээ нь locale-аас үл хамааран зөв байна.

Хожмын шатыг нээх нь зөв уншлага: tenant тэр тооны батламж хүссэн бөгөөд тэр тоогоо авсаар байна, зүгээр л тэр шатыг зурж чадах хүн дүүргэнэ. Ингэж §0-г хуулахаас өмнө гүйлгэдэг нь хуучин гарын үсгийг өөрийн шатад буулгах боломжийг өгдөг — үгүй бол хэлхээний сүүлд «парк» болно.

00019 нь зөвхөн тэмдэглэгээг авч үздэг: §0 хэлхээг зассаны дараа хангагдах боломжгүй болсон «зөвхөн нэрлэсэн хүн» flag-ийг арилгана.

documents.sign нь зориудаар documents.manage-аас салангид: баримт зохиох нь гарын үсэг зурах эрх биш. Сервер тал дээр appRequestPermission нь /sign/… ба /reject-ыг documents.sign-аар шалгана (/signatures уншилт нь documents.read хэвээр).

Анхаар: tenant үүсэх үеийн trigger нь admin-д тухайн үед байгаа бүх эрхийг, manager/user-т зөвхөн %.read ба %.manage-ыг олгодог. Тиймээс documents.sign нь manager/user-т хэзээ ч автоматаар олгогдохгүй — tenant админ өөрөө олгох ёстой. Эрхийн мөр өмнө нь суулгасан tenant-уудад байдаггүй байсныг migration 00016 нөхсөн.


3. Хэдэн гарын үсэг

document_signatures (migration 00014) нь ledger: гарын үсэг тутамд нэг мөр, хоёр unique хязгаартай:

Хязгаар Юу боломжгүй болгодог Модуль юу гэж хэлдэг
..._once_per_signer (баримт, зурагч) нэг хүн хоёр батламж болж тоологдох ErrAlreadySigned409
..._one_per_approval (баримт, шат) (migration 00020) хоёр өөр хүн нэг батламжид бичигдэх «нэг батламжид хоёр гарын үсэг» → 500, учир нь энэ нь дуудагчийн бус бидний алдаа

Хоёр дахийг зориудаар ялгаж хэлдэг: «та аль хэдийн зурсан» гэдэг нь эхнийх дээр үнэн, хоёр дахь дээр хуурамч — оператор сэтгэл амарч явах ч батламж бүртгэгдээгүй байх байсан.

Мөр тус бүр аль шатыг дүүргэснийг (step_order) хадгална. Ямар ч шат дүүргээгүй гарын үсэг (хэлхээ нэрлээгүй хүн) нь хэлхээний хамгийн том дугаарын дараа бичигдэнэ — тооны дараа биш: 2, 3 гэж дугаарлагдсан хэлхээ дээр тоогоор бодвол бодит нэрлэсэн шат руу оршиж, тэр батламжийг өөр хүнд бичих байсан.

Баримт өөрийн шаардлагаа өөртөө агуулна

Баримт батлах хүлээлтэд орох мөчид (үүсэх эсвэл route) тухайн үеийн хэлхээ нь баримт дээр хуулбарлагдана (document_approval_steps), шаардагдах тоо нь document_records.required_signatures-д бичигдэнэ (migration 00017).

Өмнө нь шаардлагыг уншилт бүрд төрлийн хэлхээнээс тоолдог, дуусгавар болох шийдвэрийг цоожны гадна уншсан тоотой харьцуулдаг байсан. Тэндээс гурван алдаа гарсан:

Хэлхээ нэрлээгүй хүний хуучин гарын үсэг нь нэрлэсэн шатыг эзэлдэггүй — хэлхээний гадна дугаарлагдана. Эс бөгөөс нэрлэгдсэн иргэн өөрийн шатаас үүрд хаагдаж, баримт нь түүнгүйгээр батлагдаж, ledger нь тэр батламжийг өөр хүнд хамааруулах байсан.

Одоо баримт эхэлсэн дүрмээ хадгална. Дараагийн шат нь дүүрээгүй шатуудын хамгийн бага дугаар — «гарын үсгийн тоо + 1» биш. Учир нь хэлхээ баримтын шинж болохоос өмнөх гарын үсгүүд ямар ч дараалалтай байж болно (тэр үед дараалал хамаагүй байсан), тиймээс migration тэднийг нэрлэсэн шатад тааруулж тавьдаг. Ингэснээр шат 1 хоосон байхад шат 2 дүүрсэн байх нь бүрэн хэвийн, бас баримтыг дуусгах боломжтой хэвээр байна.

Дуусгавар болох нөхцөл нь: хэлхээний шат бүр дүүрсэн ба гарын үсгийн тоо шаардлагаас доогуур биш. Хоёр тоог харьцуулах нь хүрэлцэхгүй — тиймээс API нь outstanding_steps (дүүрээгүй шатны тоо) буцаадаг, дэлгэц «дүүрэн» гэж зөвхөн тэр тэг байхад хэлдэг. Эс бөгөөс хэлхээ нэрлээгүй хүний гарын үсэг тоонд нэмэгдэж, амбер «Хүлээгдэж буй»-ийн хажууд эмералд «дүүрэн» гарч ирнэ.

Хэлхээний ямар ч шатыг дүүргээгүй хуучин гарын үсэг (хэн ч нэрлээгүй, бас нээлттэй шат ч байхгүй) хэлхээний гадна дугаарлагдана, бас шаардлагад нэмэгдэнэ — тэгэхээр баримт «2-аас 2» гэж хэлж байхад нэрлэсэн батламж өртэй хэвээр байх боломжгүй.

Дуусгавар болохыг шийддэг бүхэн — шаардлага, тоолол, дараагийн шат — бүгд цоожилсон транзакцид уншигдана. Иргэний танилтын дуудлага цоожийн гадна байрлана: eID-ийн сүлжээний хүсэлтийг мөр цоожилсон хэвээр хийж болохгүй.

document_records нь хамгийн сүүлийн гарын үсгийг өөр дээрээ тольдож хадгална, ингэснээр жагсаалт нэг query хэвээр байна. Бүтэн түүх нь ledger-т.


4. Төлөвийн эргэлт

DRAFT ──POST /{id}/route──▶ PENDING_APPROVAL ──хэлхээ дуусав──▶ APPROVED
                                    │
                                    └──POST /{id}/reject──▶ REJECTED

CreateDocument нь шууд PENDING_APPROVAL бичдэг тул аппын үүсгэсэн зүйл ноорог болдоггүй. Энэ нь зориудаар: үүсгэх нь нэг алхам болж, оператор «дараа илгээх»-ийг санах шаардлагагүй, бас мартагдсан ноорог хэн ч мэдэлгүй хэвтэхгүй. DRAFT нь баганын үндсэн утга бөгөөд route нь өөр замаар (шууд SQL, хожмын модуль) орж ирсэн мөрийг хөдөлгөнө.

Нэг алхмын үнэ нь гарчгийн алдаа шууд батлагчид хүрэх — тиймээс:

PUT /documents/{id}/title      гарын үсэг зурагдтал гарчгийг засна

Анхны гарын үсгийн дараа гарчиг хөшинө (409 ErrTitleFrozen). Учир нь тэр гарчиг бол иргэн өөрийн төхөөрөмж дээр уншиж зөвшөөрсөн текст (signatureDisplayText-ийг хар) — дараа нь өөрчлөгдвөл тэр зөвшөөрөл өөр зүйлийн зөвшөөрөл болно. Шийдэгдсэн (APPROVED/REJECTED) баримт ч түүх тул хөшинө.

Хамгаалалт нь тэр UPDATE дотор байна (NOT EXISTS (гарын үсэг)), уншаад дараа бичдэггүй — эс бөгөөс уншилт ба бичилтийн хооронд гарын үсэг буух зай гарна, тэр нь яг засагдах ёсгүй тохиолдол. Аудит нь хуучин ба шинэ гарчиг хоёуланг бичнэ. Дэлгэц дээр таних төлөв бүр өөрийн шошготой, танихгүй төлөв raw хэвээр харагдана — «Хүлээгдэж буй» гэж худлаа хэлэхгүйн тулд.


4.5 Жагсаалт: хуудас, шүүлт, хайлт

GET /documents нь бүхэл жагсаалтыг биш, нэг хуудсыг буцаана:

{ "documents": [ … ], "total": 20007, "limit": 200, "offset": 0 }

Учир нь мөр тутам өөрийн гарын үсгийн тоо ба дүүрээгүй шатыг тоолдог (documentColumns) — 20 мянган баримт нь хуудас ачаалах тутам 5.9 MB байсан. Одоо 200 мөр ≈ 60 KB.

Параметр Юу хийдэг
status DRAFT/PENDING_APPROVAL/APPROVED/REJECTED. Танихгүй бол 400, хоосон хуудас биш
doc_type DocTypes-ийн нэг. Танихгүй бол 400
q Гарчгийн дотор хайх (том/жижиг үсэг хамаарахгүй)
order oldest — эс бөгөөс шинэээс
limit Үндсэн 200, дээд тал 500 (хэтрүүлбэл багасгаад буцаж хэлнэ)
offset Хуудсыг ур гүй (тест ба тогтвортой хэсэгт)
after_at + after_id Курсор — өмнө харсан мөрийн дараагаас. Хоёулаа хамт, эс бөгөөс 400

total нь чимээгүй хязгаарыг таслах зорилготой. Дэлгэц «701-ээс хамгийн шинэ 200-г харуулж байна» гэж хэлж, «Дараагийнхыг ачаалах» нь дараагийн хуудсыг нэмж авна.

Дэлгэц нь offset-оор биш курсорoор явдаг. offset нь бусад хүн өөрчилж байгаа олонлогийн эхнээс тоолдог: хоёр хүсэлтийн хооронд нэг баримт батлагдвал бусад нь нэгээр дээшилж, дараагийн хүсэлт нэгийг алгасана — алгасагдсан баримт нь ямар ч дэлгэц дээр байхгүй болно, батлах дараалал дээр бол «хэн ч зурж чадахгүй гарын үсэг». Курсор нь зай биш, газар нэрлэдэг тул хооронд нь юу ч хөдөлж чадахгүй. 700 баримтыг хуудасны хооронд нэгийг батлуулж явж туршсан: 700 мөр, 700 өөр, алгасалт үгүй.

Курсорын предикат нь (tenant_id, created_at, id) index-ыг ашигладаг (migration 00021): 20 мянган баримттай tenant дээр хуудас нь 2.5мс → 0.06мс (index-only, 200 бичлэг уншина). Index нэг — статусаар тэргүүлсэн хувилбар нь дарааллыг 0.08мс хурдан үйлчлэх ч шүүлтгүй жагсаалтыг огт үйлчлэхгүй.

Дуусахыг тооноос үл хамааран мэднэ: хуудас дүүрэн биш ирвэл өгөгдөл дууссан гэсэн үг. total нь хооронд хөдөлж болох тул түүнтэй харьцуулах нь бүрэн бус жагсаалтыг «бүрэн» гэж хэлж болно.

(Түрүүлж limit-ыг өсгөх хувилбар бичсэн — сервер 500 дээр таслахад товч ажиллахаа больж, 500-аас хойшхи баримтыг хаанаас ч гарын үсэг зурах боломжгүй болсон.)

Батлах дараалал нь серверээс шүүнэ, клиент дээр биш: батлагдсан баримтаар дүүрсэн хуудасны сүүлээс хүлээж байгаа баримт унаж болохгүй. Бас хуучнаас эрэмбэлдэг — дараалал хуучин талаасаа ажиллагддаг, бас «хамгийн урт хүлээлт» tile нь тэгэхэд л зөв байна (шинээс эрэмбэлбэл хамгийн урт хүлээсэн нь сүүлийн хуудсанд байх болно).

Нэг ачаалалт нь хэд хэдэн хуудсыг гүйдэг тул:

Хайлт нь бичсэн чигээрээ: %, _, !-ийг wildcard биш гэж үзнэ (ESCAPE '!'). Кирилл том/жижиг үсгийн тухай §2-ын урхиг хар.


5. Аудит

Гарын үсэг зурах нь энэ модулийн хамгийн аудит шаардсан үйлдэл. audit.Record дараах зүйлсийг бичнэ:

Үйлдэл Хэзээ Юу агуулдаг
documents.created Баримт үүсэхэд төрөл, гарчиг, төлөв, шаардлагатай гарын үсгийн тоо
documents.routed Батлахад илгээхэд төрөл, баримтад буусан хэлхээ
documents.signature_requested eID батлах хүсэлт илгээхэд хэн асуусан, харагдах текст, session
documents.signed Гарын үсэг бүртгэгдэхэд хэн зурсан, суваг, гэрчилгээ, шат, хэлхээ дуусав уу
documents.rejected Татгалзахад төрөл, гарчиг, тэр үед хэдэн гарын үсэг байсан
documents.signature_policy_changed / .approval_chain_changed / .retention_rule_changed Тохиргоо шинэ утга
documents.template_created / .template_changed / .template_deleted Загвар нэр, төрөл (устгасны дараа мөр байхгүй тул нэрийг бүртгэлд үлдээнэ)

Асуусан хүн ба зурсан хүн хоёр өөр зүйл: ledger нь зөвхөн хоёр дахийг мэддэг тул хүсэлтийг тусад нь бичдэг.

Бүртгэл бүр actorFor(ctx) — өөрөөр хэлбэл аль операторын session тэр үйлдлийг хийснийг — иргэний регистрийн хажууд агуулна. Платформ хоёрыг хооронд холбож чаддаггүй (users-д регистр байхгүй) ч «аль операторын session тэр иргэний гарын үсгийг гаргасан» гэдэг нь бүртгэлээс уншигдана.

documents.routed нь баримтад буусан хэлхээг агуулдаг нь санамсаргүй биш: төрлийн хэлхээ хожим засагдаж болох ч тэр баримтад хүрэхгүй тул «энэ баримтыг ямар дүрмээр батлав» гэдгийг зөвхөн энэ бүртгэлээс мэдэж болно.


6. Тест

# Нэгж тестүүд — DB шаардахгүй
go test ./internal/apps/documents/

# Бүрэн тестүүд — migration хийсэн хаяж болох DB шаардана
DOCUMENTS_TEST_DATABASE_URL=postgres://... go test ./internal/apps/documents/

Integration тестүүд нь SQL дотор амьдардаг зүйлсийг барина: төлөвийн хамгаалалт, нэг зурагч нэг л удаа гэсэн constraint, хэлхээнээс гарах тоолол, tenant-ийн хуваалт, бас гол хоёр нь — батламжийг өөр баримт руу зөөж болохгүй ба session нь өөр tenant-д хамаарахгүй.

CI нь DOCUMENTS_TEST_DATABASE_URL-ыг тавьж, тестүүд чимээгүй skip болвол job-ыг унагадаг — gov_services-д байдгийн адил, эс бөгөөс skip нь pass шиг харагдана.


7. Гараар турших

Mock дээрх нэг зүйл: eID mock нь session-ыг санах ойд барьдаг тул API-г дундуур нь дахин ачаалбал явж байсан ceremony EXPIRED болно. Энэ нь mock-ийн шинж, production-ы алдаа биш — live үед session нь eID тал дээр байдаг тул deploy түүнийг таслахгүй. (Тэгэхэд ч баримт эрүүл үлдэж, шинэ ceremony эхлүүлж болдгийг туршиж баталсан.)

Mock горимд бүтэн ceremony-г HTTP-ээр гүйцэтгэж болно. EID_MOCK_MODE=true үед иргэн 1.5 секундын дараа өөрөө "батална".

API=http://127.0.0.1:8080/api/v1
TOKEN=$(curl -s -X POST "$API/auth/login" -H 'Content-Type: application/json' \
  -d '{"email":"admin@example.com","password":"Password123!"}' | jq -r .token)
AUTH="Authorization: Bearer $TOKEN"

# Хоёр шаттай хэлхээ: нэг дэх нь нэрлэгдсэн, хоёр дах нь нээлттэй
curl -s -X PUT "$API/documents/workflows/CONTRACT" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"steps":[{"name":"Хэлтсийн дарга","signer_reg_number":"AA90010111"},
                {"name":"Захирал","signer_reg_number":""}]}'

DOC=$(curl -s -X POST "$API/documents" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"title":"Хамтран ажиллах гэрээ 2026","doc_type":"CONTRACT"}' | jq -r .id)

# Танихгүй хүн нэрлэгдсэн шатыг авч чадахгүй → 400
curl -s -X POST "$API/documents/$DOC/sign/eid/start" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"reg_number":"ZZ99999999"}'

# Нэрлэгдсэн хүн ceremony-г эхлүүлнэ
SID=$(curl -s -X POST "$API/documents/$DOC/sign/eid/start" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"reg_number":"AA90010111"}' | jq -r .session_id)

# COMPLETE болтол poll (mock: 2-3 удаа)
curl -s -X POST "$API/documents/$DOC/sign/eid/poll" -H "$AUTH" -H 'Content-Type: application/json' \
  -d "{\"session_id\":\"$SID\"}"

# Хоёр дах шат нээлттэй тул хэн ч дуусгана → APPROVED 2/2
curl -s -X POST "$API/documents/$DOC/sign/dan" -H "$AUTH" -H 'Content-Type: application/json' \
  -d '{"reg_number":"ZZ99999999","otp_code":"123456"}'

# Ledger: шат тус бүрийг хэн дүүргэсэн
curl -s "$API/documents/$DOC/signatures" -H "$AUTH"

Дараах зүйлсийг ингэж баталсан: /documents/templates нь баримтын id гэж уншигдахгүй; харагдах текст яг 60 байт, зорилго эхэндээ; шатны эрх 400-аар тодорхой татгалздаг; шийдэгдсэн баримт 409 буцаана; урт гарчиг SQLSTATE биш ойлгомжтой 400 өгнө.


8. Одоогийн цоорхойнууд