API лавлах — API Reference

Платформын бүх HTTP endpoint-ийн бүрэн жагсаалт. Backend кодоос (backend/internal/platform/server.go, platform_admin.go ба модуль бүрийн RegisterRoutes) гаргаж авсан.


Ерөнхий зарчим

  • Base URL: /api/v1 (health, metrics, OAuth2, OIDC discovery-гээс бусад).
  • Танилт: session токен session_token HttpOnly cookie эсвэл Authorization: Bearer <token> толгойгоор дамжина. Cookie эхэлж шалгагдана.
  • Хэл: Accept-Language: mn|en толгой (эсвэл ?lang=) — цэс, апп сторын текст серверээс орчуулагдаж ирнэ. Анхдагч mn.
  • Алдааны формат: {"error": "message"}. Gov Services модуль нэмэлт тогтвортой машин код буцаана: {"error": "...", "code": "CONFLICT_VERSION"}.
  • Апп хаалт (app gate): модулийн маршрут бүр тухайн тенантад апп суулгагдаж идэвхжсэн эсэхийг шалгана — үгүй бол 403 Forbidden.
  • Эрхийн автомат зурагдал: ихэнх модульд GET/HEAD → <prefix>.read, бусад арга → <prefix>.manage. Админ эрхтэй хэрэглэгч бүх эрхийн шалгалтыг алгасна.

1. Нээлттэй endpoint-ууд (танилтгүй)

Арга Зам Тайлбар
GET /health Амьд байдал — үргэлж {"status":"ok"}
GET /ready Бэлэн байдал — DB ping; холбогдохгүй бол 503
ANY /metrics Prometheus хэмжүүр. METRICS_TOKEN тохируулсан бол Authorization: Bearer <token> шаардана (эс бөгөөс нээлттэй) — DEPLOYMENT_GUIDE §7
GET /.well-known/openid-configuration OIDC discovery
GET /.well-known/jwks.json id_token-ыг шалгах RS256 нийтийн түлхүүрүүд. Access token нь opaque хэвээр — түүнийг /oauth2/introspect-ээр шалгана
GET /oauth2/auth Authorization code урсгалын эхлэл (RFC 6749 §4.1.1). Session cookie өөрөө уншина: нэвтрээгүй бол /auth/sign-in, workspace-гүй бол /workspaces, зөвшөөрөл дутуу бол /auth/consent руу чиглүүлнэ. redirect_uri яг тааруулна; code_challenge + code_challenge_method=S256 заавал. Client эсвэл redirect шалгагдаагүй үеийн алдааг тэр redirect руу буцаахгүй (open redirect)
GET · POST /oauth2/userinfo OIDC UserInfo. Authorization: Bearer <access_token>; буцаах claim нь олгогдсон scope-оор хязгаарлагдана (openid шаардлагатай). Хоёр арга нь ижил handler — OIDC Core §5.3 хоёуланг зөвшөөрдөг
POST /oauth2/token authorization_code, refresh_token, client_credentials grant; Basic эсвэл form client танилт. Public client (token_endpoint_auth_method=none) нууц үггүйгээр эхний хоёрыг л ашиглана. Код нэг удаагийн — дахин үзүүлбэл түүнээс гарсан бүх refresh хэлхээ устана; refresh нь эргэлддэг ба дахин ашиглалт хэлхээг алдана
POST /oauth2/introspect RFC 7662 — client танилт шаардана
POST /oauth2/revoke Токен цуцлах — client танилт шаардана
GET · POST /api/v1/oauth2/consent Зөвшөөрлийн дэлгэцийн хос: GET нь апп юу хүсэж байгааг (нэр + scope) хэлнэ, POST нь {params, approved} шийдвэрийг бүртгээд {redirect_to} буцаана. Session шаардана; POST нь CSRF хамгаалалтын дор
POST /api/v1/auth/login {identifier, password}identifier нь и-мэйл эсвэл гар утасны дугаар; сервер өөрөө таана. Бичсэн сувгаа баталгаажуулсан байх ёстой. Session нь ямар ч workspace-д хамаарахгүй нээгдэнэ (tenant_id: "") — workspace-ийг /auth/switch-workspace олгоно. Хуучин email талбар мөн ажиллана. Rate limit: 5/мин, burst 5 (IP тус бүр). Баталгаажаагүй бол 403 EMAIL_NOT_VERIFIED / 403 PHONE_NOT_VERIFIED. Google/E-ID-гээр үүссэн бүртгэлд нууц үг байхгүй тул 401
POST /api/v1/auth/eid/start E-ID хүсэлт илгээх (reg_number) → {session_id, verification_code}. Иргэн өөрийн E-ID апп дээр энэ 4 оронтой кодыг шалгаж PIN1 оруулна. Холбоогүй регистрт push илгээхгүй (403) — эс бөгөөс хэн ч танихгүй хүний утсанд мэдэгдэл цохиж чадна
POST /api/v1/auth/eid/qr/start QR / App2App нэвтрэлт эхлүүлэх (регистр шаардахгүй) → {session_id, verification_code, device_link_url}. Хэн нэгнийг оноодоггүй тул холбоосын шалгалтгүй — шалгалт нь complete дээр хэвээр
POST /api/v1/auth/eid/complete Session-ийг poll хийх (session_id) — QR ба регистрийн аль ч урсгалд нийтлэг → {"state":"RUNNING"} эсвэл {state:"COMPLETE", token, user, identity}; иргэн татгалзвал 401. Танилт баталгаажсан ч ERP бүртгэлд холбогдоогүй бол 403 + {"code":"IDENTITY_NOT_LINKED"} — энэ нь танилтын алдаа биш, дахин оролдоод шийдэгдэхгүй тул UI нь холбох алхмуудыг харуулна
POST /api/v1/auth/dan/login ДАН гарц (одоогоор mock горим л ажиллана)
POST /api/v1/auth/logout Session-ийг сервер талд цуцалж cookie арилгана
POST /api/v1/auth/register Өөрөө бүртгүүлэх: {email │ phone, password, password_confirmation}яг нэг танигч. Нэр, workspace асуухгүй: нэр нь танигчаас гарна (дараа засна), workspace нь нэвтэрсний дараах сонгогчийн ажил. Нууц үгийн бодлого сервер талд: 10+ тэмдэгт (rune-ээр), төрлийн оноо, түгээмэл жагсаалт → 400 WEAK_PASSWORD / 400 PASSWORD_MISMATCH. И-мэйл бол баталгаажуулах холбоос, утас бол SMS код явна; session олгохгүй. Rate limit: 5/мин
POST /api/v1/auth/register/phone/confirm Утсаар хийсэн бүртгэлийг дуусгана: {phone, code}. Зөв код нь дугаарыг баталгаажуулж workspace-гүй session нээнэ — дараагийн дэлгэц нь сонгогч. Нэвтрэлтгүй ажилладаг (данс сувгаа хараахан батлаагүй тул session байхгүй); аюулгүйг нэг удаагийн код + 5 оролдлогын хязгаар хангана
GET /api/v1/auth/verify?token= Баталгаажуулах холбоосыг хэрэглэнэ. Нэг удаагийн, 24 цагийн хугацаатай. Хугацаа дууссан / хэрэглэсэн / байхгүй бүгд ижил 400 {"code":"INVALID_VERIFICATION_TOKEN"} — амьд токен хайх боломжгүй
GET /api/v1/auth/providers Энэ deploy ямар нэвтрэлт санал болгож байгааг хэлнэ (password, eid, google). Google нь credential бүртгэгдсэн үед л true — ажиллахгүй товч харуулахаас сэргийлнэ
GET /api/v1/auth/google/start Google-ийн зөвшөөрлийн дэлгэс рүү 302. ?next= нь зөвхөн энэ аппын зам байж болно (open redirect хамгаалалт). State нь санд хадгалагдана — replica хооронд callback таарах ёстой
GET /api/v1/auth/google/callback Google-ийн буцаах цэг. Амжилттай бол session cookie тавиад /auth/callback руу 302; алдаа бүр /auth/sign-in?error=<code> руу очно (redirect-ээр ирсэн browser JSON body уншиж чадахгүй). Session токен хэзээ ч URL-д ордоггүй
POST /api/v1/auth/eid/register Баталгаажсан боловч данс байхгүй E-ID session-ыг данс болгоно: {registration_ticket, email}. Регистрийн дугаарыг ticket-ээс авна — иргэн зөвхөн хаягаа сонгоно, хэн болохоо биш. Хэрэглэгч + identity link нэг гүйлгээнд үүсээд workspace-гүй session олгоно. Хаяг өөр дансанд байвал 409 EMAIL_TAKEN
POST /api/v1/auth/verify/resend Шинэ холбоос илгээнэ. Хаяг байгаа эсэхээс үл хамааран үргэлж 200 — эс бөгөөс бүртгэл тандах хэрэгсэл болно
GET /api/v1/auth/invitations?token= Урилгын холбоос юуг заасныг нэвтрэхээс өмнө уншина → {workspace_name, invited_by, role}. Хугацаа дууссан / ашигласан / цуцлагдсан / байхгүй бүгд ижил 404 INVALID_INVITATION. Rate limit — токены орон зайг шүүрдэж болохгүй

auth_method утгууд: EID_PIN1 (танилтын session — иргэнийг таних) ба EID_PIN2 (гарын үсгийн session). Хуучин PKI_DIGITAL_SIGNATURE / MOBILE_OTP / BANK_SSO / BIOMETRIC_FACE жагсаалт устсан — RP API v3-д сувгийн сонголт байхгүй, иргэн апп дотроо PIN/биометрээ өөрөө сонгоно (backend/internal/platform/eid/eid.go).


2. Танилттай платформ endpoint-ууд

2.1 Хэрэглэгч ба цэс

Арга Зам Нэмэлт шаардлага Тайлбар
GET /api/v1/auth/me Хэрэглэгчийн мэдээлэл + is_admin + permissions[]
POST /api/v1/auth/identity/link/start E-ID холбох хүсэлт эхлүүлэх (provider:"eid", reg_number) → {session_id, verification_code}
POST /api/v1/auth/identity/link Холбоог дуусгах: E-ID бол {provider:"eid", session_id} (poll), ДАН бол нэг алхамт. Өөр хэрэглэгчид холбогдсон регистр → 409
GET /api/v1/auth/identity/links Өөрийн холбосон identity-үүд (регистр маскалсан)
DELETE /api/v1/auth/identity/links/{id} Өөрийн холбоосоо салгах. Өөр хүний холбоос 404 (403 биш — байгаа эсэхийг хэлэхгүй). Салгавал нэвтрэх арга үлдэхгүй бол 409 LAST_SIGN_IN_METHOD — эхлээд нууц үг эсвэл баталгаажсан утас хэрэгтэй
GET /api/v1/menus Тенантад идэвхтэй цэс; админ бус хэрэглэгчид эрхээр шүүгдэнэ
POST /api/v1/auth/phone/start {phone} — дугаарыг бүртгэж 6 оронтой код SMS-ээр илгээнэ. Дугаар баталгаажаагүй хэвээр бүртгэгдэнэ. Өөр бүртгэл түүнийг баталгаажуулсан бол 409 PHONE_TAKEN; зөвхөн баталгаажаагүй нэхэмжлэл нь блоклохгүй (эзэнг нь түгжихээс сэргийлнэ). Нэвтэрсэн байх шаардлагатай — эс бөгөөс дурын дугаар руу SMS цохих хэрэгсэл болно
POST /api/v1/auth/phone/confirm {code} — зөв бол phone_verified_at тавигдана. Код 10 минут, 5 удаа буруу оролдвол шатаана. Хугацаа дууссан / ашигласан / буруу бүгд ижил хариу
POST /api/v1/auth/phone/resend Бүртгэлтэй дугаар руу шинэ код. Өмнөх амьд кодууд цуцлагдана — эс бөгөөс resend бүр таах талбайг үржүүлнэ
GET /api/v1/account/score Бүртгэл хэдэн хувь баталгаажсаныг буцаана: {score, complete, steps[]}. Гурван нотолгоо — email, phone, eidтэнцүү жинтэй, тус бүр 1/3. Оноо 0 / 33 / 67 / 100. complete нь зөвхөн гурвуулаа биелсэн үед true. Нэвтэрсэн байх шаардлагатай: аль суваг нь баталгаажсаныг нэрлэсэн данс тухайд нь задлан хэлэх нь нээлттэй байх ёсгүй мэдээлэл
GET /api/v1/workspaces Харьяалагдах workspace-ууд + owned, owned_limit, can_create. Идэвхтэйг нь active: true заана
POST /api/v1/workspaces Шинэ workspace үүсгэх. Нэг хэрэглэгч дээд тал нь 2 үүсгэнэ (урилгаар нэгдэх нь хязгааргүй) → хэтэрвэл 403 {"code":"WORKSPACE_LIMIT_REACHED"}
POST /api/v1/auth/switch-workspace {workspace_id}сонгогчийн цөм: workspace-гүй session-д tenant олгох цорын ганц зам. Гишүүнчлэл нь эрх — гишүүн бус бол 403. Шинэ session олгож, хуучныг цуцална
POST /api/v1/auth/invitations/accept {token} — урилгыг гишүүнчлэл болгоно. Workspace-гүй session дээр зориуд ажиллана: энэ урилгын улмаас дөнгөж бүртгүүлсэн хүнд workspace байхгүй, workspace шаардвал яг хүлээн авагчийг нь хаах байв. Токеныг шатаах ба гишүүнчлэл үүсгэх нь нэг гүйлгээнд — эс бөгөөс шатсан токен хүнийг гацаана, эсвэл холбоосыг олсон хэн ч дахин ашиглана. Аль хэдийн гишүүн бол 409 ALREADY_MEMBER; хүчингүй токен 404 INVALID_INVITATION
GET /api/v1/workspace/invitations админ Тухайн workspace-ийн урилгууд
POST /api/v1/workspace/invitations админ {email │ phone, role}яг нэг суваг (хоёуланг нь эсвэл аль нь ч биш бол 400). Утас нь монгол гар утасны дугаар болж хэвийшинэ (400 INVALID_PHONE). Урих нь эрх олгох үйлдэл тул requireAdmin-ий цаана
DELETE /api/v1/workspace/invitations/{id} админ Урилгыг цуцлах

2.2 Апп стор

Арга Зам Нэмэлт шаардлага Тайлбар
GET /api/v1/store/apps Каталог + тенантын суулгалтын төлөв (installed/enabled)
GET /api/v1/store/apps/{slug} Нэг аппын дэлгэрэнгүй
GET /api/v1/installed-apps Суулгагдсан аппуудын жагсаалт
POST /api/v1/store/apps/{slug}/install админ Хамаарлын дарааллаар суулгаж, эрхүүдийг role-уудад олгоно. Гадаад апп бол manifest дэх event захиалгыг мөн адил транзакцад үүсгэж, гарын үсгийн нууцыг webhook_secret-ээр нэг л удаа буцаана (дахин уншигдахгүй — аппын зохиогч руу дамжуулах цорын ганц зам нь суулгасан админ)
POST /api/v1/store/apps/{slug}/enable админ Идэвхжүүлэх — гадаад аппын захиалга дахин идэвхжинэ
POST /api/v1/store/apps/{slug}/disable админ Идэвхгүй болгох — гадаад аппын захиалга унтарна (мөр нь үлдэнэ, нууц нь хэвээр)

2.3 Эрхийн удирдлага (админ)

Арга Зам Тайлбар
GET /api/v1/admin/access/overview Role, permission, гишүүдийн нэгдсэн тойм
POST /api/v1/admin/access/roles Role үүсгэх (^[a-z][a-z0-9_.-]{1,62}[a-z0-9]$ код)
PUT /api/v1/admin/access/roles/{id} Role шинэчлэх (system role идэвхгүй болгохгүй — 409)
DELETE /api/v1/admin/access/roles/{id} Role устгах (system role устгахгүй — 409)
PUT /api/v1/admin/access/roles/{id}/permissions Эрхүүд солих (admin role-д хориотой)
PUT /api/v1/admin/access/memberships/{id}/roles Гишүүний role-ууд; сүүлчийн админыг хасахыг хориглоно (409)

Өөрчлөлт бүр access_change_events хүснэгтэд before/after JSON-той бүртгэгдэнэ.

2.4 AI pipeline

Rate limit: 20/мин, burst 10 (IP тус бүр).

Арга Зам Нэмэлт шаардлага Тайлбар
POST /api/v1/ai/chat {prompt, lang, history} — Gemini + тенантын өгөгдөлд суурилсан tool call (max 4 round). Gemini тохируулаагүй бол degraded:true fallback
POST /api/v1/ai/copilot Хуучин copilot endpoint (/ai/chat-аар солигдсон)
POST /api/v1/ai/stt Ярианаас текст (webm/ogg/wav/mp4/mpeg, base64 ≤950k тэмдэгт)
POST /api/v1/ai/tts Текстээс яриа — WAV base64 (анхдагч хоолой Kore)
POST /api/v1/ai/translate Орчуулга (mn/en/ru/zh/ko/ja)
GET /api/v1/ai/stock-forecast Агуулахын нөөцийн энгийн таамаглал
GET /api/v1/admin/ai/prompts админ System prompt-ууд (scope, instructions)
PUT /api/v1/admin/ai/prompts/{key} админ Prompt шинэчлэх (тенант түвшний override)
GET /api/v1/admin/ai/knowledge админ Мэдлэгийн сан жагсаах
POST /api/v1/admin/ai/knowledge админ Мэдлэгийн сан руу бичлэг нэмэх

Алдааны семантик (writeAIError, server.go):

Нөхцөл Статус Бие
Дуудагчийн буруу (ai.ErrInvalidInput — аудио формат, хэмжээ, base64) 400 Тодорхой шалтгаан
Gemini тохируулаагүй (GEMINI_API_KEY хоосон) 502 the AI assistant is not configured on this deployment
Түр зуурын доголдол (gemini.ErrUnavailable — сүлжээ, 5xx, quota дуусах) 503 + Retry-After: 30 the AI assistant is busy; try again shortly
Провайдерын бусад татгалзал (буруу түлхүүр, safety block) 502 the AI assistant is temporarily unavailable

Провайдерын түүхий хариултыг хэзээ ч клиент рүү дамжуулахгүй — дэлгэрэнгүйг серверийн лог (AI request failed upstream) агуулна. Өмнө нь бүх алдаа 400 болж, Google-ийн алдааны биеийг хуулж илгээдэг байсан: буруу түлхүүр нь хэрэглэгчийн асуулт буруу мэт харагддаг байв.

503 ба 502-ын ялгаа нь «хэнийх нь буруу вэ» биш, «дахин оролдох нь тус болох уу»: 503 бол буц гэсэн урилга (gemini клиент backoff-той аль хэдийн оролдсон), 502 бол хүн засах ёстой зүйл. Quota дуусахыг 429 болгодоггүй нь зориуд — 429 нь «та хэт олон удаа асуулаа» гэсэн үг, харин түлхүүр нь байрлуулалтынх бөгөөд бүх tenant хуваалцдаг тул нэг хүнд өөр хүний хэрэглээний талаар худал хэлэх болно.

/ai/chat нь Gemini-д хүрэхгүй үед алдаа биш, degraded:true бүхий локал fallback буцаана — UI үүнийг тэмдэглэж харуулна.

2.5 ХУР (XYP) ба интеграц

Арга Зам Нэмэлт шаардлага Тайлбар
POST /api/v1/xyp/citizen Иргэний бүртгэл (WS100101); live горимд XYP_API_KEY шаардана
POST /api/v1/xyp/company Хуулийн этгээд (WS100201)

Webhook интеграц (/integrations, бүгд зөвхөн админ, тенантад хамааралтай тэсвэртэй webhook — хуучин in-memory connector manager устсан):

Арга Зам Тайлбар
GET /api/v1/integrations Бүртгэлтэй webhook-уудын жагсаалт → {integrations: [{id, name, target_url, event_types[], active, created_at}]} (шинэ нь эхэнд). Нууц буцаагдахгүй
POST /api/v1/integrations Бүртгэх: {name, target_url, secret?, event_types[]}. Амжилттай бол 201 + үүсгэсэн endpoint. target_url http(s) биш эсвэл хувийн/loopback хост руу заавал 400 (SSRF хамгаалалт — бүртгэх үед нэг удаа, дараа нь dial-time дээр дахин шалгагдана). event_types* = бүх төрлийг авна
DELETE /api/v1/integrations/{id} Устгах → {"status":"deleted"}; тенантад тухайн endpoint байхгүй бол 404. Устгахад түүний хүргэлтийн лог cascade-аар устана
GET /api/v1/integrations/{id}/deliveries Хүргэлтийн лог (сүүлийн 50) → {deliveries: [{id, event_type, status, attempts, response_status?, last_error?, created_at, delivered_at?}]}

secret нь write-only — бүртгэх body-д л илгээгдэнэ, GET хариултад хэзээ ч буцаагдахгүй. Хүргэлт бүр X-ERP-Event толгойд төрлөө, X-ERP-Signature толгойд нууцаар тооцсон HMAC-SHA256 гарын үсгээ (hex) авч явна — хүлээн авагч тал үүгээр эх сурвалжийг баталгаажуулна. Амжилтгүй хүргэлт backoff-той дахин оролдож, 6 удаа бүтэлгүйтсэний дараа failed төлөвт last_error-той үлдэнэ.


2.6 Платформын консол (superadmin)

Платформыг ажиллуулдаг хүмүүсийн гадаргуу — tenant-ийн админаас дээш, өөр эрх мэдэл. requirePlatformAdmin хаалганы ард (tenant админ энд 403 авна) ба accountScope бүлэг дотор байрлана: оператор ямар ч workspace-д харьяалагдахгүй байж болох тул хүсэлтүүд tenant-гүй ажиллана.

Арга Зам Тайлбар
GET /api/v1/platform/tenants Бүх байгууллага: хэрэглэгч/апп тоо, төлөв, schema provision хийгдсэн эсэх
GET /api/v1/platform/tenants/{id} Дэлгэрэнгүй + гишүүдийн жагсаалт
POST /api/v1/platform/tenants/{id}/suspend · /activate Түдгэлзүүлэх / сэргээх. Түдгэлзсэн workspace-д доторх session TENANT_SUSPENDED кодоор татгалзана, switch-workspace босгон дээрээ хаагдана. Хоёулаа audit-тай
POST /api/v1/platform/tenants/{id}/provision Tenant schema-г шаардлагаар үүсгэх (idempotent)
POST /api/v1/platform/tenants/{id}/impersonate {reason} заавал. 30 минутын түр session нээж, оператор тухайн workspace дотор админ эрхтэй ажиллана. Session нь tenant_id-гүй, impersonated_tenant_id-тай; оператор эрхээ алдвал тэр дороо хүчингүй болно
GET /api/v1/platform/tenants/{id}/export Байгууллагын бүх бизнес мөрийг нэг JSON-оор. Schema-гүй tenant-д 409 — эхлээд provision (дээрх мөр)
GET /api/v1/platform/users Хэрэглэгчид (?q= хайлт), платформ админ тэмдэглэгээтэй
GET /api/v1/platform/audit Аудитын мөр, ?limit=&offset=
GET /api/v1/platform/health Тоонууд (tenant/хэрэглэгч/session/хүлээгдэж буй эвент) + үндэсний холболтуудын mock/live төлөв + /metrics-ийн зам
GET /api/v1/platform/apps Аппууд, суулгалтын тоотой

Эхний операторыг PLATFORM_ADMIN_EMAIL орчны хувьсагчаар олгоно (данс нь өмнө нь бүртгэгдсэн байх ёстой; зөвхөн нэмнэ, хэзээ ч хасахгүй).


3. Бизнес модулиудын endpoint-ууд (апп хаалттай)

3.1 Contacts (io.example.contacts)

Арга Зам Эрх Тайлбар
GET /api/v1/contacts/ contacts.read Жагсаалт (нэрээр эрэмбэлсэн)
POST /api/v1/contacts/ contacts.manage Үүсгэх — name заавал
PUT /api/v1/contacts/{id} contacts.manage Шинэчлэх — олдохгүй бол 404
GET /api/v1/contacts/{id}/attachments contacts.read Харилцагчид хавсаргасан баримтууд, шинэ нь эхэнд. Файлын сан суулгаагүй бол алдаа биш, [] — сонголттой апп байхгүй нь бичлэгийн доголдол биш. Мөр бүр {id, state, attached_by, attached_at, owner_type, owner_id} ба state нь available үед л file_id, name, mime, size_bytes, version нэмэгдэнэ. state=trashed нь файл хогийн саванд байгааг (нэр нь хэвээр), state=restricted нь дуудагч тэр файлыг унших эрхгүйг хэлнэ — тэр мөрөнд нэр ч, id ч илгээгдэхгүй
POST /api/v1/contacts/{id}/attachments contacts.manage {file_id}201 + хавсралт. Файл нь файлын санд байгаа байх ёстой — энэ зам ачаалалт хийхгүй. 404 нь «тэр файл боломжгүй» гэсэн ганц утга: байхгүй, хогийн саванд, эсвэл дуудагчид нээгдээгүй — гурвыг зориуд ялгахгүй. Файлын сан суулгаагүй бол 501
DELETE /api/v1/contacts/{id}/attachments/{attachmentID} contacts.manage Заагчийг салгана → 204. Файл өөрөө хөндөгдөхгүй — өөр бичлэг дээрх хавсралтууд нь ч хэвээр. Тэр бичлэг дээр байхгүй заагч → 404

3.2 Products (io.example.products)

Арга Зам Эрх Тайлбар
GET /api/v1/products/ products.read Жагсаалт
POST /api/v1/products/ products.manage Үүсгэх — sku+name заавал; tax_class_code сонголттой; давхардсан SKU эсвэл байхгүй татварын анги → 409
PUT /api/v1/products/{id} products.manage Шинэчлэх
GET /api/v1/products/tax/classes products.read Тенантын татварын ангиуд → [{code, name, treatment, rate_bp}]. treatment нь standard/zero_rated/exempt, rate_bp нь basis point (1000 = 10%). Эхний хэрэглээнд гурван анги (VAT10, ZERO, EXEMPT) залхуу байдлаар суулгагдана
GET /api/v1/products/tax/settings products.read Workspace-ийн үнийн суурь → {prices_include_tax, default_class_code}
PUT /api/v1/products/tax/settings products.manage Тэрхүү суурийг хадгална; default_class_code заавал (400), байхгүй анги нэрлэвэл мөн 400

Татварын хоёр зам /products дор байрладаг нь энэ модуль тэдгээрийн дээр internal.TaxCalculator-ыг нийлүүлдэгтэй холбоотой; хоёр статик сегмент нь /{id}-тэй хэзээ ч мөргөлдөхгүй. Бүтээгдэхүүн дээрх tax_class_code нь null байж болно — тэр нь «тенантын анхдагч юу ч бай» гэсэн үг бөгөөс «чөлөөлөгдсөн» гэсэн нэхэмжлэлээс өөр зүйл.

3.3 Inventory (io.example.inventory)

Хамаарал: contacts ^1.0.0, products ^1.0.0.

Арга Зам Эрх Тайлбар
GET /api/v1/inventory/warehouses inventory.read Агуулахууд
POST /api/v1/inventory/warehouses inventory.manage Үүсгэх — давхардсан код → 409
GET /api/v1/inventory/stock-levels inventory.read Үлдэгдэл
GET /api/v1/inventory/movements inventory.read Хөдөлгөөний түүх (append-only)
POST /api/v1/inventory/adjustments inventory.manage Гүйлгээт тохируулга — SELECT FOR UPDATE, сөрөг үлдэгдэл хориотой

3.4 Billing (io.example.billing)

Хамаарал: contacts ^1.0.0. НӨАТ = 10%.

Арга Зам Эрх Тайлбар
GET /api/v1/billing/invoices billing.read Жагсаалт — contact_id (мастер өгөгдлийн холбоос) + e-Barimt талбарууд (ebarimt_status, ebarimt_receipt_id, ebarimt_lottery, ebarimt_qr_data, ebarimt_submitted_at, ebarimt_last_error)
POST /api/v1/billing/invoices billing.manage Үүсгэх — contact_id (зөвлөмжит) эсвэл хуучин contact_name, + эерэг бүхэл amount (integer MNT, бутархай → 400); НӨАТ = amount/10, e-Barimt илгээлт background-оор
GET /api/v1/billing/payments billing.read Төлбөрийн жагсаалт. Шүүлт: ?contact_id=, ?status= (POSTED/CANCELLED), ?unallocated=true (урьдчилгаа — хуваарилагдаагүй үлдэгдэлтэй төлбөрүүд)
POST /api/v1/billing/payments billing.manage Төлбөр бүртгэх — contact_id/contact_name, method (bank_transfer|qpay|card|cash), эерэг бүхэл amount, paid_at заавал (YYYY-MM-DD эсвэл RFC3339), сонголтоор allocations: [{invoice_id, amount}] — нэг гүйлгээнд бичигдэнэ
POST /api/v1/billing/payments/{id}/cancel billing.manage Цуцлах — хуваарилалт бүрийг тайлж, дэвтэрт буцаах бичилт үүсгэнэ. Мөр устахгүй
POST /api/v1/billing/payments/{id}/allocations billing.manage Дараа нь хуваарилах — {invoice_id, amount}
POST /api/v1/billing/allocations/{id}/reverse billing.manage Хуваарилалтыг тайлах — сөрөг шинэ мөр, засвар ч устгал ч биш
GET /api/v1/billing/invoices/{id}/allocations billing.read Тухайн нэхэмжлэхийн хуваарилалтын түүх (хуваарилалт ба тайлалт хоёулаа)
POST /api/v1/billing/invoices/{id}/cancel billing.manage Цуцлах — дэвтэрт буцаах бичилт, e-Barimt баримтыг хүчингүй болгоно. Төлбөр хуваарилагдсан бол 409
POST /api/v1/billing/invoices/{id}/credit-notes billing.manage Залруулах нэхэмжлэхamount (НӨАТ-гүй суурь; орхивол бүтэн нэхэмжлэх), reason. Өөрийн CN- дугаартай шинэ баримт үүсч, эх нэхэмжлэхэд шууд хуваарилагдана
POST /api/v1/billing/credit-notes/{id}/allocations billing.manage Залруулгын үлдэгдлийг өөр нэхэмжлэхэд хуваарилах — {invoice_id, amount}

contact_id өгвөл контактыг contacts модулиас ID-гаар шийдэж, тухайн үеийн нэрийг нэхэмжлэх дээр snapshot болгон буулгана (дараа нь контакт нэрээ солиход гарсан баримт өөрчлөгдөхгүй) — энэ үед илгээсэн contact_name үл тоомсорлогдоно. Өөр тенантын эсвэл байхгүй contact_id → 400. Хоёулаа хоосон бол мөн 400. contact_name дангаараа = холбоосгүй legacy зам.

Нэхэмжлэхийн хоёр төлөв. status нь баримт амьд эсэх (POSTED / CANCELLED), settlement_status нь мөнгө ирсэн эсэх (UNPAID / PARTIAL / PAID). Сүүлийнх нь allocated_amount-аас өгөгдлийн санд гарна — хэн ч гараар бичихгүй. Урьд нь ганц status хоёуланг үүрэх гэж оролдож, PENDING гэж бичигдээд хэзээ ч өөрчлөгддөггүй байв.

Цуцлах уу, залруулах уу. Цуцлах нь «энэ баримт огт байх ёсгүй байсан» — дэвтрийн бичилт буцаагдаж, ДДТД нь ТЕГ дээр хүчингүй болно. Залруулах нь «байсан, буруу байсан» — тусдаа баримт, өөрийн дугаартай, эх баримтын ДДТД-г inactiveId-д зааж бүртгэгдэнэ. Төлбөр хүрсэн нэхэмжлэхийг цуцлах нь 409: мөнгө нь бодит, хаашаа явахыг хүн шийднэ.

status нь POSTEDCANCEL_REQUESTEDCANCELLED. Дунд төлөв нь шийдвэрийн хүлээлт биш — сүлжээний дуудлагын хүлээлт: дэвтэр аль хэдийн буцаагдсан, ТЕГ-т хэлэх нь дэвсгэрээр давтагдаж байна. Хариултын ebarimt_status нь дуусахад VOIDED, бүтэлгүйтвэл ERROR болно.

Гарсан баримт хөлдөнө. PUT/PATCH байхгүй бөгөөд өгөгдлийн сан ч татгалздаг: дүн, татварын snapshot, харилцагчийн нэр, due_date нь гарсны дараа өөрчлөгдөхгүй. Өөрчлөх ганц зам нь цуцлалт эсвэл залруулга.

Алдааны гурван анги. 404 = ийм мөр байхгүй; 409 = мөр нь энэ үйлдлийг татгалзах төлөвтэй (цуцлагдсан төлбөр, аль хэдийн тайлагдсан хуваарилалт, үлдэгдлээс их дүн); 400 = илгээсэн зүйл хүсэлт биш. Дунд ангид 400 буцаах нь аль хэдийн зөв байсан маягтыг засуулахаар хэрэглэгчийг буцаана.

Хариултын мөнгөн дүнгүүд бүгд integer MNT: amount нь НӨАТ-гүй суурь, vat_amount = amount/10 (бүхэл хуваалт), e-Barimt баримтын нийт дүн нь amount + vat_amount (PosAPI-гийн нийлбэр татвар шингэсэн байдаг).

3.5 Documents (io.example.documents)

Арга Зам Эрх Тайлбар
GET /api/v1/documents/ documents.read Жагсаалт
POST /api/v1/documents/ documents.manage Үүсгэх — төрөл: CONTRACT/REQUEST/APPROVAL
GET · POST /api/v1/documents/templates documents.read · .manage Загварын жагсаалт · үүсгэх
PUT · DELETE /api/v1/documents/templates/{id} documents.manage Загвар засах · устгах
POST /api/v1/documents/templates/{id}/use documents.manage Загвараас баримт үүсгэх
GET · PUT /api/v1/documents/policies[/{docType}] documents.read · .manage Гарын үсгийн бодлого (нэрлэсэн гарын үсэг шаардах эсэх, eID/ДАН зөвшөөрөх)
GET · PUT /api/v1/documents/workflows[/{docType}] documents.read · .manage Батламжийн гинж (алхам, дараалал, нэрлэсэн гарын үсэг зурагч)
GET · PUT /api/v1/documents/retention[/{docType}] documents.read · .manage Хадгалалтын хугацааны дүрэм
GET /api/v1/documents/{id}/signatures · /steps documents.read Тухайн баримтын гарын үсгүүд · батламжийн алхмууд
POST /api/v1/documents/{id}/route · /reject documents.manage · documents.sign Дараагийн алхам руу шилжүүлэх · татгалзах
POST /api/v1/documents/{id}/sign/eid/start · /sign/eid/poll documents.sign eID гарын үсгийн ёслол эхлүүлэх · төлөв асуух
POST /api/v1/documents/{id}/sign/dan documents.sign ДАН-аар гарын үсэг зурах

3.6 Developer Portal (io.example.developer_portal)

Арга Зам Эрх Тайлбар
GET /api/v1/developer/apps/ developer.read OAuth2 client-ууд (нууц үг нуугдсан)
POST /api/v1/developer/apps/ developer.manage Client бүртгэх — нууц зөвхөн энэ 201 хариултад ил гарна. redirect_uris заавал (хамгийн багадаа нэг, үнэмлэхүй, fragment-гүй, loopback-аас бусад нь HTTPS). "public": true бол нууц үггүй client (token_endpoint_auth_method=none) — PKCE-ээр нотолно, client_credentials ашиглахгүй
PATCH /api/v1/developer/apps/{client_id} developer.manage client_name, redirect_uris, scopes засах. Redirect URI-г бүртгэхтэй яг ижил дүрмээр шалгана — буруу бол засаж залруулахгүй, татгалзана. grant_types, token_endpoint_auth_method засагдахгүй
POST /api/v1/developer/apps/{client_id}/secret developer.manage Нууцыг сольж шинэчлэх. Шинэ нууц зөвхөн энэ хариултад. Хуучин нь тэр дор нь үхнэ; аль хэдийн олгогдсон токенууд хүчинтэй хэвээр. Public client бол 400
POST /api/v1/developer/apps/{client_id}/disable developer.manage Client-г идэвхгүй болгох: /oauth2/token, /oauth2/auth, introspect, revoke бүгд татгалзана, олгогдсон access ба refresh токен бүгд цуцлагдана. Мөр устахгүй — зөвшөөрөл, түүх нь үлдэнэ
POST /api/v1/developer/apps/{client_id}/enable developer.manage Буцааж идэвхжүүлэх. Цуцлагдсан токенууд сэргэхгүй; зөвшөөрөл нь хэвээр тул хэрэглэгч дахин асуугдахгүй
DELETE /api/v1/developer/apps/{client_id} developer.manage Зөвхөн огт ашиглагдаагүй client устгана. Токен, refresh токен, зөвшөөрөл, эсвэл authorization code-ийн аль нэг нь байсан бол 409 — түүхийг устгахын оронд идэвхгүй болгохыг зөвлөнө

Client болон токенууд миграц 00015-аас хойш өгөгдлийн санд хадгалагдана: client-ууд oauth2_clients (нууц нь client_secret_hash, SHA-256), олгогдсон токенууд oauth2_tokens (token_hash, SHA-256). Тиймээс сервер дахин асахад токен хүчинтэй хэвээр, олон replica хооронд introspection ажиллана.

Дээрх зам бүр дуудагчийн workspace-аар хязгаарлагдана: өөр workspace-ийн client_id нэрлэвэл байхгүй client-тэй ижил 404 ирнэ. catalog/manifests/-ийн kind: "external" аппуудын client нь ачаалах үед manifest-аас үүсдэг бөгөөд ямар ч workspace-д харьяалагддаггүй — тиймээс жагсаалтад ч гарахгүй, дээрх verb-үүдээр ч хөндөгдөхгүй. Тэднийг manifest нь удирдана.

Жагсаалт нь бүртгэсэн workspace-ынхаа client-уудыг л буцаана. Өмнө нь аль ч workspace-ийн гишүүн платформ дээрх бүх client_id ба redirect URI-г уншиж чаддаг байсан — redirect URI бол authorization code хүргэгддэг газар, тэр жагсаалт нь халдагчийн хүсдэг яг тэр газрын зураг юм.

3.7 E-Sign (io.example.esign)

Хоёр төмөр зам нэг баримтын тавиур дээр: HSM (Gerege eSign төхөөрөмж — синхрон) ба eID Mongolia (иргэний төхөөрөмж PIN2-оор зөвшөөрдөг тул асинхрон: init → poll → download). Зөвхөн eID зам нь хуулийн хүчинтэй (qualified) гарын үсэг гаргадаг.

Баримт:

Арга Зам Эрх Тайлбар
GET /api/v1/esign/documents esign.read Баримтын жагсаалт. ?status=, ?q=, ?limit=/?offset=; ?paginated=true бол {items, total, limit, offset} бүрхүүлээр
POST /api/v1/esign/documents esign.manage PDF upload (base64, ≤15MB, %PDF- шалгалттай)
POST /api/v1/esign/documents/upload esign.manage Мөн PDF upload, гэхдээ multipart/form-data — том файлыг base64-оор 33% үрэлгэн болгохгүй. Хэмжээ хэтэрвэл 413
GET /api/v1/esign/documents/{id} esign.read Нэг баримтын мета
GET /api/v1/esign/documents/{id}/download esign.read ?variant=signed → гарын үсэгтэй хувилбар
DELETE /api/v1/esign/documents/{id} esign.manage Soft-delete: жагсаалтаас гарна, мөр устахгүй

HSM зам:

Арга Зам Эрх Тайлбар
POST /api/v1/esign/cert/check esign.manage Сертификат шалгах — phone_no + civil_id
POST /api/v1/esign/documents/{id}/sign esign.sign HSM-ээр гарын үсэг зурах — доорх cert-check заавал

eID Mongolia зам (esign.sign + GET-үүдэд esign.read):

Арга Зам Тайлбар
POST /api/v1/esign/sign/init eID ёслол нээнэ: {document_id} эсвэл multipart PDF. Холбогдоогүй данс signer_id (регистр) нэрлэнэ — NO_SIGNER_IDENTITY. Credential-гүй орчинд 503 EID_NOT_CONFIGURED
GET /api/v1/esign/sign/{id} Ёслолыг ажиглана: pending → completed/failed/expired/rejected. Сервер талдаа eID-г long-poll хийдэг
GET /api/v1/esign/sign/{id}/download Гарын үсэгтэй PDF (зөвхөн completed)
POST /api/v1/esign/sign/{id}/cancel Ёслолыг зогсооно — иргэний утас руу дахин дуудахгүй
GET /api/v1/esign/organizations Гарын үсэг зурагчийн нэрийн өмнөөс нь зурж болох байгууллагууд (eID-гээс)

Багц гарын үсэг:

Арга Зам Эрх Тайлбар
GET /api/v1/esign/batches esign.read Багцын жагсаалт, хуудаслалттай (анхдагч 25)
POST /api/v1/esign/batches esign.manage {name, provider, document_ids[]} — багц бүрдүүлэх
GET /api/v1/esign/batches/{id} esign.read Нэг багц + гишүүдийн төлөв
POST /api/v1/esign/batches/{id}/run esign.sign Багцыг ажиллуулах. Бүрдүүлэх нь manage, зурах нь sign — энэ хоёрын хил бол багц дээр ч хэвээр
POST /api/v1/esign/batches/{id}/cancel esign.manage Багцыг зогсоох

Лог ба тохиргоо:

Арга Зам Эрх Тайлбар
GET /api/v1/esign/logs esign.read Гарын үсгийн лог. Шүүлт: ?action=, ?outcome=, ?provider=, ?document_id=, ?q=, ?from=, ?to=, ?limit=/?offset=
GET /api/v1/esign/logs/export esign.read Тэрхүү шүүлтийн бүтэн гаргалт. Хуудас биш, гэхдээ хязгаартай хэвээр — нэг тенант хязгааргүй үр дүн урсгаж чадахгүй
GET /api/v1/esign/settings esign.read Гарын үсгийн байрлал + бодлого
PUT /api/v1/esign/settings/placement esign.manage Гарын үсэг хуудсан дээр хаана буухыг тохируулна
PUT /api/v1/esign/settings/policy esign.manage Аль замыг зөвшөөрөхийг тохируулна. eID холболт тохируулаагүй байхад require_eid тавихыг татгалзана — тэгвэл тенант юу ч зурж чадахгүй болно
POST /api/v1/esign/settings/hsm/test esign.manage HSM холболтыг шалгах

Энэ модуль эрхээ хоёр удаа шалгуулдаг. Платформын автомат зурагдал (GET/HEAD → esign.read, бусад → esign.manage, /sign төгсгөлтэй эсвэл /sign/ агуулсан зам → esign.sign) нь хүсэлтийг handler хүртэл авчирдаг, ба handler бүр өөрийн шаардлагыг дахин нэрлэдэг (m.require). Үр дүнгийн шаардлага нь хоёрын нэгдэл — жишээ нь GET /sign/{id}/download нь автомат зурагдлаас esign.read, handler-ээс esign.sign шаардана.

Хуулийн хүчинтэй гарын үсэг зурах нь баримт байршуулахаас нарийн эрх — PDF оруулж чадсан хүн бүр тенантын нэрийн өмнөөс гарын үсэг зурж болохгүй. manager role нь суулгах үедээ esign.sign авдаг; өмнө нь esign.manage-тай байсан role-уудад миграц 00016 буцаан олгосон (appRequestPermission, server.go).

Гарын үсэг зурахын өмнө сертификат шалгах нь заавал. /sign нь ижил tenant + хэрэглэгч + phone_no + civil_id дээр сүүлийн 10 минутын дотор амжилттай хийгдсэн /cert/check бичлэгийг (esign_cert_checks) шаардана. Гарын үсэг зурагчийн нэр/регистр нь тэр шалгалтын SubjectDN-ээс авагдана — client-ээс ирсэн нэр/регистр огт хэрэглэгдэхгүй.

Статус Код Хэзээ
428 Precondition Required CERT_CHECK_REQUIRED Хүчинтэй (≤10 мин) cert-check байхгүй
409 Conflict ALREADY_SIGNED_OR_IN_PROGRESS Баримт аль хэдийн гарын үсэгтэй, эсвэл зэрэгцээ өөр хүсэлт түүнийг барьж авсан (atomic claim)
404 Баримт тухайн тенантад олдсонгүй

3.8 Gov Services (io.example.gov_services)

Хамаарал: contacts ^1.0.0. Бүх зам /api/v1/gov доор. Эрх нь handler бүрт тусад нь шалгагдана (автомат зурагдал үйлчлэхгүй). Төлөвийн машин, шилжилтийн дэлгэрэнгүйг GOV_SERVICES_WORKFLOW.md-аас үзнэ үү.

Арга Зам Эрх
GET /api/v1/gov/services gov.read
POST /api/v1/gov/services gov.configure
PUT /api/v1/gov/services/{id}/configuration gov.configure
GET /api/v1/gov/units gov.read
GET /api/v1/gov/units/tree gov.read
POST /api/v1/gov/units gov.configure
POST /api/v1/gov/units/members gov.configure
GET /api/v1/gov/workflow-templates gov.read
GET /api/v1/gov/workflows gov.read
POST /api/v1/gov/workflows gov.configure
GET /api/v1/gov/workflow-versions/{id} gov.read
POST /api/v1/gov/workflow-versions/{id}/publish gov.configure
GET /api/v1/gov/routing-rules gov.read
POST /api/v1/gov/routing-rules gov.configure
POST /api/v1/gov/requests/ingest gov.apply
GET /api/v1/gov/requests/{id} gov.read + нэгжийн хамрах хүрээ
GET /api/v1/gov/tasks gov.read + нэгжийн хамрах хүрээ
POST /api/v1/gov/tasks/{id}/actions Шилжилт бүрийн өөрийн эрх
GET /api/v1/gov/dashboard gov.report
GET /api/v1/gov/outbox gov.configure
GET /api/v1/gov/connectors gov.configure
POST /api/v1/gov/connectors gov.configure
GET /api/v1/gov/applications gov.read
POST /api/v1/gov/applications gov.apply
GET /api/v1/gov/applications/{id}/timeline gov.read
POST /api/v1/gov/applications/{id}/cancel gov.apply (CANCEL шилжилтийн эрх)
GET /api/v1/gov/appointments gov.read
POST /api/v1/gov/appointments gov.apply
GET /api/v1/gov/officer/queue gov.read
POST /api/v1/gov/officer/queue/{id}/decide Шилжилт бүрийн өөрийн эрх

Шилжилтийн эрхүүд (/tasks/{id}/actions, /officer/queue/{id}/decide, /applications/{id}/cancelworkflow.go-гийн baseTransitions):

Үйлдэл Шаардах эрх
ASSIGN, START, COMPLETE, CLOSE, REQUEST_INFO, REJECT gov.process
DELEGATE gov.delegate
VERIFY, RETURN gov.verify
CANCEL gov.apply

REQUEST_INFO, REJECT, RETURN нь comment заавал шаардана.

Gov Services-ийн алдааны машин кодууд (HTTP статустай хамт): ROW_VERSION_REQUIRED (400), CONFLICT_VERSION (409), IDEMPOTENCY_CONFLICT (409), TARGET_UNIT_REQUIRED (400), мөн 401/403/404-т зурагдсан бусад кодууд. Төлөв өөрчлөх бүх хүсэлт body-доо row_version заавал агуулна (optimistic concurrency).

Дээд системийн connector (/gov/connectors). Хүсэлт ingest хийгдэхдээ source_system ба external_request_id авч явдаг; түүнээс хойшхи төлөвийн өөрчлөлт бүр тэр source_system-ийн нэрлэсэн connector руу хаяглагдан gov_delivery_outbox-д дараалалд орж, dispatcher нь гарын үсэг зурж, дахин оролдох шаттай хүргэнэ.

Арга Зам Бие / хариулт
GET /api/v1/gov/connectors Тенантын connector-ууд, source_system-оор эрэмбэлсэн. Идэвхгүйг нь мөн буцаана — унтраасан хаягийг дахин асаахын тулд эхлээд харах ёстой
POST /api/v1/gov/connectors {source_system, target_url, secret_ref?, active?}201 + connector. active нь анхдагчаар true

(tenant_id, source_system) нь unique тул POST нь upsert: дахин бүртгэх нь хаягийг ЗӨӨХ арга бөгөөд операторын бодитоор хийдэг үйлдэл ч тэр. Хоёр идэвхтэй мөр байвал төлөвийн мэдэгдэл аль хаяг руу очих нь планнерын дараалалаас хамаарах байв.

target_url нь үнэмлэхүй http эсвэл https байх ёстой (INVALID_INPUT), эс бөгөөс сервер дуудагчийн сонгосон зүйлийг татаж авах хэрэгсэл болно.

secret_ref нь гарын үсгийн нууцын нэр бөгөөд нууц өөрөө биш — энэ дэлгэцийг уншиж буй хүн өөрийн тохиргоогоо шалгах ёстой тул буцаагдана. Нууц утга нь процессоос гардаггүй.

3.9 Ledger (io.example.ledger)

Давхар бичилтийн ерөнхий дэвтэр. Хоёр эх сурвалжаас бичилт авна: биллингийн гурван факт (event bus-ийн эхний хэрэглэгч), мөн хүн өөрөө бичилт хийж, дансны төлөвлөгөөгөө засаж, эхний үлдэгдлээ оруулж, тайлант үеэ түгжиж чадна.

Event Бичилт Огноо
billing.invoice.created DR авлага / CR борлуулалт + НӨАТ issued_on
billing.payment.posted DR касс эсвэл харилцах / CR авлага paid_onмөнгө хөдөлсөн өдөр
billing.payment.cancelled Дээрх бичилтийн буцаалт Цуцалсан өдөр
billing.invoice.cancelled Нэхэмжлэхийн бичилтийн буцаалт Цуцалсан өдөр
billing.credit_note.created DR борлуулалт + DR НӨАТ / CR авлага Залруулга бичсэн өдөр

Залруулга нь буцаалт биш, өөрийн бичилт: хэсэгчилсэн залруулга юуг ч бүрэн буцаадаггүй, ба бүтэн ч байсан өөрийн дугаар, өөрийн огноотой баримт. Буцаалт гэж тэмдэглэвэл тэр нэхэмжлэхийг дараа нь цуцлахад идемпотентын түлхүүр эзлэгдсэн байж, цуцлалт чимээгүй юу ч бичихгүй байх байсан.

Хуваарилалт нь дэвтэрт юу ч бичихгүй — энэ нь орхигдол биш: авлага нэхэмжлэх дээр дебетлэгдэж, төлбөр дээр кредитлэгдсэн бөгөөд аль төлбөр аль нэхэмжлэхийг хаасныг тохируулах нь дэд дэвтрийн асуулт. Дахин бичвэл тухайн дансны хөдөлгөөн хоёр дахин гарна (Business Central-ийн ижил хил). Бүх мөнгөн дүн integer MNT (бүхэл төгрөг, бутархайгүй).

Огноо хоёр байдаг. entry_date нь гүйлгээ болсон өдөр (YYYY-MM-DD, Улаанбаатарын хуанли) — тайлан бүр үүгээр бүлэглэнэ. posted_at нь мөр хэзээ бичигдсэн аудитын тамга. 8-р сарын 2-нд оруулсан, 7-р сарын 31-ний нэхэмжлэх нь долдугаар сарынх.

Эрх: ledger.read (GET) ба ledger.manage (бусад бүх арга) — server.go-ийн prefix зурагдлаар. Бичилт хийх, дансны төлөвлөгөө засах, түгжээ тавих гурав нь нэг эрхийн дор: тэдгээр нь нэг ажил (данс хөтлөх).

Журнал

Арга Зам Тайлбар
GET /api/v1/ledger/entries Бичилтүүд, шинэ дугаар нь эхэнд → {entries: [...]}. Шүүлт: ?source_type= (ж: billing.invoice, manual, opening, ledger.reversal), ?from=/?to= (бизнесийн огноогоор, YYYY-MM-DD; буруу бол 400), ?limit= (анхдагч 100, дээд тал нь 500 — хязгаараас гадуур утга нь анхдагчид буцаж унана)
POST /api/v1/ledger/entries Гараар бичилт хийх → 201 + бичилт хөлүүдтэйгээ. Бие: {entry_date, description, lines: [{account_code, debit, credit}]}
GET /api/v1/ledger/entries/{id} Нэг бичилт + хөлүүд нь. Олдохгүй бол 404
POST /api/v1/ledger/entries/{id}/reverse Буцаах бичилт хийх → 201 + шинэ бичилт. Бие нь сонголттой: {entry_date} — байхгүй бол өнөөдөр

Бичилтийн биет: id, number (тенант тутмын хүнд харагдах дугаарлал), description, source_type, source_id?, reverses_id?, entry_date, posted_at (RFC 3339), created_by? (хүн бичсэн бол; платформ эвэнтээс бичсэн бол хоосон), total (дебетийн нийлбэр = кредитийн нийлбэр). Дэлгэрэнгүй харах үед lines[] нэмэгдэнэ: account_code, account_name, debit, credit.

POST /entries-ийн source_type нь үргэлж manual ба хүсэлтээс авагддаггүй. Тэр нь idempotency түлхүүрийн нөгөө хагас; дуудагчид сонгуулбал биллингийн бичилттэй мөргөлдөх, эсвэл түүнийг чимээгүй өмчлөх боломж гарна.

Бичилтийн дүрмүүд (бүгд 400): хөл дор хаяж хоёр байх; хөл бүр дебет эсвэл кредит, хоёулаа биш; сөрөг дүн байхгүй; дебетийн нийлбэр кредитийнхтэй тэнцүү; entry_date заавал. Хоёр талдаа тэг хөл нь бичигдэхээсээ өмнө хаягдана (жижиг нэхэмжлэх дээрх тэг НӨАТ-ын мөр).

Буцаалт бол засвар БИШ

Бичигдсэн бичилтийг хэзээ ч дахин бичдэггүй. Залруулга нь шинэ, эсрэг бичилт: дебет бүр кредит, кредит бүр дебет болж, reverses_id-гээр эхнийх рүүгээ заана. Алдаа ба залруулга хоёулаа дэвтэрт үлдэнэ — нягтлан бодогчийн хүлээдэг зүйл ч яг тэр.

Тиймээс зам нь POST .../reverse, DELETE биш: дэвтэр мөр авдаг, алддаггүй. Өгөгдлийн сан ч мөн адил боддог — журнал дээрх DELETE болон нягтлан бодох утга агуулсан баганын UPDATE-ыг триггер татгалзана (DATABASE_SCHEMA.md, tenant/0014), ба тэр татгалзал API дээр 409 болж гарна.

Буцаалтын огноо нь буцааж буй бичилтийнх биш: наймдугаар сард илэрсэн алдааг наймдугаар сард залруулна, наймдугаар сарын дэвтэр өөрөөр хэлээгүй л бол. Хаагдсан үе рүү буцаалт бичих нь бусад бичилттэй ижил татгалзалд орно.

Хоёр дахь буцаалт 409; буцаалтыг өөрийг нь буцаах нь мөн 409 (тэр нь хууль ёсны давхар бичилт боловч энэ товчны хэлэх зүйл биш — хүсвэл гараар бич).

Дансны төлөвлөгөө ба бичилтийн үүрэг

Арга Зам Тайлбар
GET /api/v1/ledger/accounts Тенантын дансны төлөвлөгөө, кодоор эрэмбэлсэн → {accounts: [...]}. ?include_inactive=true нь идэвхгүйжүүлсэн дансыг мөн буцаана
POST /api/v1/ledger/accounts Данс нэмэх → 201 + данс. Бие: {code, name, kind, parent_code?, statement_line?, active?}
PATCH /api/v1/ledger/accounts/{id} Данс засах (нэрлэх, эцгийг нь солих, тайлангийн мөрийг нь солих, идэвхгүй болгох). Илгээсэн талбарууд л өөрчлөгдөнө
GET /api/v1/ledger/account-roles Платформын мэддэг бүх үүрэг, зурагдсан эсэхээс үл хамааран → {roles: [{role, account_code?, account_name?}]}
PUT /api/v1/ledger/account-roles/{role} Үүргийг тенантын нэг данс руу заана. Бие: {account_code}

Дансны биет: id, code, name, kind (asset/liability/equity/ income/expense), parent_code?, statement_line?, active, role?, has_postings. Сүүлийн хоёр нь дэлгэц юуг өөрчилж болохыг мэдэхэд хэрэгтэй хоёр факт.

Аль ч уншилтын зам дээр дансны төлөвлөгөө залхуу байдлаар суулгагдана: эхний өдрөө чат нээсэн тенант хоосон хүснэгт биш, монгол жижиг бизнесийн 19 дансыг хардаг. Дахин суулгах нь тенантын засварыг хадгална (нэр, идэвхгүйжүүлэлт, үүргийн зурагдал бүгд хэвээр).

Бичдэг код нь ҮҮРЭГ нэрлэнэ, кодыг хэзээ ч биш. Авлагаа 1100-аас 1105 болгож дугаарласан тенант нь PUT /account-roles/receivable гэсэн ганц дуудлага хийнэ; нэхэмжлэх бичдэг код юу ч анзаарахгүй. Мэдэгдэх үүргүүд: cash, bank, receivable, vat_input, vat_output, payable, retained_earnings, current_year_earnings, opening_suspense, sales, rounding_gain, rounding_loss. Зурагдаагүй үүрэг нь нуугдахгүй жагсаана — тэр нь ирээдүйн бичилт унах шалтгаан бөгөөд унахаас нь өмнө харагдах ёстой.

Хоёр дүрэм тогтмол: бичилттэй дансны код хөлддөг (нэр нь чөлөөтэй хэвээр) ба үүрэг атгасан данс идэвхгүй болохгүй — эхлээд үүргийг өөр данс руу заа.

Эхний үлдэгдэл

Арга Зам Тайлбар
GET /api/v1/ledger/opening-balances ?year= (анхдагч нь энэ он) → {year, entry, suspense, suspense_account}. Тухайн онд эхний үлдэгдэл байхгүй бол entry: null
POST /api/v1/ledger/opening-balances Импорт: {as_of, balances: [{account_code, debit, credit}]}201 (шинээр бичигдсэн) эсвэл 200 (ижил импорт дахин ирсэн) + {year, suspense, created, entry}

Эхний үлдэгдлийн тусдаа хүснэгт байхгүй — тэр нь ердийн журналын бичилт, зөвхөн source_type нь opening. Ижил тэнцлийн дүрэм, ижил «зөвхөн нэмэгддэг» баталгаа, ижил шалгах баланс.

Тэнцэхгүй тоог татгалзахгүй. Нүүж ирж буй бизнес тоогоо өөр системээс хэсэг хэсгээр нь хуулж бичдэг тул тэнцэхгүй байх нь хэвийн. Зөрүү нь түр данс руу (opening_suspense үүрэг) орно. Түр дансны үлдэгдэл тэг болох нь нүүлт дууссаны шинж — suspense талбар нь яг түүнийг хэлнэ. Импортыг татгалзвал бизнес тоо нь төгс болтол эхэлж чадахгүй байх байсан бөгөөд тоо нь оруулж, тулгаснаараа төгс болдог.

Түр дансанд өөрөө тоо өгөх нь 400: тэр нь тэнцээгүй үлдэгдлийг хүлээж авдаг данс. Ижил тоог дахин илгээх нь 200 (юу ч бичигдээгүй); өөр тоо илгээх нь 409 — бичигдсэн бичилтийг дахин бичихгүй, буцаагаад залруулга бич.

Түгжээний огноо

Арга Зам Тайлбар
GET /api/v1/ledger/settings {lock_date?, tax_lock_date?, hard_lock_date?, fiscal_year_start_month}. Тохируулаагүй дэвтэр юуг ч түгжихгүй
PUT /api/v1/ledger/settings Илгээсэн талбарууд л өөрчлөгдөнө; хоосон мөр нь түгжээг тайлна (хатуу түгжээнээс бусад нь)

Хүчинтэй түгжээ нь хамаарах огноонуудын максимум — түгжээ нэмэх нь хэзээ ч сулруулахгүй. tax_lock_date нь НӨАТ-ын данс хөндсөн бичилтэд л нэмэгдэнэ, тул мэдүүлсэн сар нь дэвтэр нээлттэй байхад ч хамгаалагдана. hard_lock_date нь зөвхөн урагшилна: буцаах гэсэн оролдлого 409. Буцааж чаддаг түгжээ нь юуг ч нотлохгүй.

Түгжигдсэн үе рүү бичих оролдлого нь 409 бөгөөд аль түгжээ зогсоосныг нэрлэнэ. Хоцорсон бичилтийн огноог чимээгүй урагшлуулахгүй — тэр нь хэрэглэгчийн бичээгүй тоог хадгалах явдал.

fiscal_year_start_month нь дотоод удирдлагын тайланд зориулагдсан. Монголын хууль ёсны тайлант жил нь хуанлийн жил тул хууль ёсны тайлангууд түүнийг зориуд үл тоомсорлоно.

Тайлан

Арга Зам Тайлбар
GET /api/v1/ledger/reports/trial-balance Шалгах баланс. ?from=, ?to= (YYYY-MM-DD; анхдагч нь оны эхнээс өнөөдөр хүртэл), ?include_empty=true (хөдөлгөөнгүй дансыг мөн харуулна)

Хариулт: {from, to, rows: [...], totals, balanced}. Мөр бүр нь account_code, account_name, kind, statement_line?, opening (from-оос өмнөх бүхэн, дебет хасах кредит), period_debit, period_credit, closing (to-г оруулаад бүхэн). to нь from-оос өмнө байвал 400.

balanced нь тайлангийн өөрийнх нь нотолгоо: бүх дансны нийлбэр авахад дебет = кредит, opening = 0, closing = 0 байх ёстой. Үгүй бол дэвтэр эвдэрсэн бөгөөд тайлан нь зөвхөн мэдээлэгч.

400 ба 409-ийн ялгаа — зориудынх, ба ачаа үүрдэг

Энэ модулийн алдааны гадаргуу хоёр хагасаас бүрдэнэ, ба хоёр нь өөр зүйл хэлдэг:

  • 400 — дуудагч буруу зүйл илгээсэн ба өөр хүсэлт илгээвэл ажиллана. Тэнцэхгүй бичилт, огноо байхгүй, байхгүй дансны код, мэдэгдэхгүй үүрэг, хоосон эхний үлдэгдэл, буруу форматтай огноо, буруу kind.
  • 409 — хүсэлт зөв байсан ба дэвтрийн төлөв татгалзаж байна. Үе хаагдсан, буцаалт аль хэдийн байгаа, тухайн оны эхний үлдэгдэл өөр тоотой байгаа, код давхардсан, данс бичилттэй тул код нь хөлдсөн, үүрэгт данс зурагдаагүй, хатуу түгжээ ухрахыг хүсэв.

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

Бусад статус: 404 тухайн тенантад ийм бичилт/данс байхгүй; 401 session-гүй; 403 апп суулгаагүй эсвэл эрх дутуу; 500 бусад бүх зүйл (серверийн лог дээр). Бие нь үргэлж {"error": "..."}.


3.10 Office (io.example.office)

Жинхэнэ Word/Excel/PowerPoint — нээлттэй эхийн OnlyOffice Document Server-ийг embed хийсэн. Модуль нь өөрийн тавиуртай (docx/xlsx/pptx) бөгөөд мөн Drive дээрх файлын засварлагч (internal.EditableFileStore чадвар, docs/DESIGN_DRIVE.md §6).

{id} нь тавиурыг ч нэрлэнэ: <uuid> = Office-ийн өөрийн баримт, drive-<uuid> = файл сангийн файл. Доорх бүх зам хоёуланд нь ажиллана (/sign-аас бусад).

Арга Зам Эрх Тайлбар
GET /api/v1/office/ office.read Баримтын жагсаалт (загварууд орохгүй)
POST /api/v1/office/ office.manage {title, kind}docx, xlsx эсвэл pptx. Хоосон файл нь embed хийсэн template-ээс
GET /api/v1/office/templates office.read Загваруудын жагсаалт
POST /api/v1/office/{id}/save-as-template office.manage {title?} — баримтыг хуулж загвар үүсгэнэ (эх нь хэвээр)
POST /api/v1/office/templates/{id}/use office.manage {title?} — загвараас бие даасан шинэ баримт
GET /api/v1/office/{id}/config office.read Засварлагчийн JWT-signed тохиргоо + docserver-ийн хаяг. ?lang=, ?fresh=1 (кэш хордсон үед шинэ document key). drive-<uuid> бол төрлийг файлын нэрээр тодорхойлно; засварлаж болохгүй өргөтгөл бол 400, файл сан суугаагүй бол 501
POST /api/v1/office/{id}/sign office.manage Хадгалагдсан хувилбарыг PDF болгож E-Sign тавиур руу. Илгээхийн өмнө нээлттэй засварыг албадан хадгална. Зөвхөн өөрийн тавиурdrive- бол 501
PUT · DELETE /api/v1/office/{id} office.manage Нэр солих · устгах

Хаалтгүй хоёр зам (document server session барьдаггүй тул):

Арга Зам Эрх мэдэл
GET /api/v1/office/files/{id} URL дотор богино настай HMAC токен (t= tenant, exp=, token=) — тохиргоо гаргах үед үүсгэгдэнэ. Токен нь tenant-ыг өөрөө агуулдаг тул хүсэлт зөв schema-д уягдана
POST /api/v1/office/files/{id}/callback Мөн HMAC токен, дээр нь docserver-ийн JWT co-signature. Хадгалалт бүр шинэ versiondrive- бол Drive-ын шинэ хувилбар (дарж бичихгүй)

Угтвар нь HMAC-ийн субъектын нэг хэсэг: нэг тавиурт зориулж үүсгэсэн токеныг нөгөө дээр нь ашиглах боломжгүй.

Хоёулаа route_policy_test.go-ийн publicRoutes-д шалтгаантайгаа нэрлэгдсэн. ONLYOFFICE_* тохируулаагүй бол /config нь 503 буцаана — файл хадгалагдсаар байна.


3.11 Drive (io.example.drive)

Workspace тутмын файлын сервер: хавтасны мод, ачаалалт, хувилбар, хогийн сав, Range дэмждэг татаж авалт, хуваалцах, од, үйл ажиллагааны лог, хайлт. Дизайн ба урсгалууд: DESIGN_DRIVE.md; хүснэгтүүд: DATABASE_SCHEMA.md (tenant/0006, tenant/0009, tenant/0011).

Эрх нь модулийн өөрийнх. Gov Services-ийн адил Drive нь цөмийн appRequestPermission зурагдалд зориуд ороогүй: платформын автомат зурагдал арга бүрээс нэг эрх гаргадаг ба POST /nodes/{id}/copy (manage) ба POST хэлбэртэй уншилтыг ялгаж чадахгүй. Маршрут бүр drive.read эсвэл drive.manage бүлгийн аль нэгэнд гараар байрлана.

Алдааны зурагдал (fail, drive.go): 404 олдсонгүй; 400 нэр буруу / энэ нь хавтас / энэ нь файл / role буруу; 409 нэр эзэлсэн, мөчлөг, эх хавтас нь хогийн саванд, хогийн саванд байгаагүй; 413 файл хэт том; бусад бүх зүйл 500 + серверийн лог. Бие нь {"error": "..."}.

Node биет (GET-үүдийн буцаадаг хэлбэр): id, parent_id (язгуурт null), kind (folder/file), name, path (материалчлагдсан), depth, size_bytes, mime, version, trashed_at, created_by, created_at, updated_at. Блобын sha нь гадагш гардаггүй.

v1 — мод, байт, хувилбар

Арга Зам Эрх Тайлбар
GET /api/v1/drive/nodes drive.read Хавтасны хүүхдүүд → node массив (дээд тал нь 2000). ?parent= (хоосон эсвэл root = язгуур; байхгүй хавтас → 404), ?sort=name│size│updated (анхдагч: хавтас эхэлж, дараа нь нэр), ?order=asc│desc. Хогийн саванд байгааг оруулахгүй
GET /api/v1/drive/nodes/{id} drive.read Нэг node + өвөг залгамжлагчид: {node, breadcrumb[]}. breadcrumb нь язгуураас эхэлнэ, node өөрөө орохгүй ба parent_id-гаас гаргагдана (path-ыг задлан таамаглахгүй)
POST /api/v1/drive/folders drive.manage {parent_id?, name}201 + node. Ах дүү нэр давхардвал 409
POST /api/v1/drive/files drive.manage multipart/form-data ачаалалт → 201 + node. Талбарууд: parent_id?, name?, file. Дараалал чухал — хэсгүүд урсгалаар уншигдана (ParseMultipartForm биш) тул parent_id ба name нь file-аас өмнө ирнэ; байт эхлэхээс өмнө очих газар нь мэдэгдсэн байх ёстой. name хоосон бол файлын нэрийн сүүлчийн сегмент. Байгаа файлын нэр дээр ачаалах нь орлуулахгүй — шинэ хувилбар нэмнэ; тэр нэрийг хавтас эзэлсэн бол 409. Нэг файлын хязгаар нь DRIVE_MAX_FILE_BYTES (анхдагч 100 MiB) — хэтэрвэл 413. Content-Type-ыг сервер өөрөө үнэрлэж тогтооно, клиентийн зарласныг хэзээ ч дахин ашиглахгүй
GET /api/v1/drive/nodes/{id}/download drive.read Одоогийн контентыг урсгана. Range дэмжигдэнэ (206 + Content-Range, хамрахгүй муж → 416). Үргэлж X-Content-Type-Options: nosniff. Анхдагчаар attachment; ?disposition=inline нь скрипт ажиллуулж чадахгүй төрөлд л олгогдоно (HTML/SVG нь энэ origin дээр inline рендер хийгдэхгүй). Хавтас → 400, хогийн саванд байгаа → 404
PATCH /api/v1/drive/nodes/{id} drive.manage {name} — нэр солих. Дэд модны бүх зам нэг гүйлгээнд дахин бичигдэнэ
POST /api/v1/drive/nodes/{id}/move drive.manage {parent_id?} — зөөх (null/""/"root" = язгуур). Хавтсыг өөрийнх нь дотор зөөвөл 409 (мөчлөг)
POST /api/v1/drive/nodes/{id}/copy drive.manage {parent_id?, name?}201 + шинэ node. Хавтас бол дэд мод нь бүхэлдээ хуулагдана (гүн дээд тал нь 64); блоб нь ref_count-оор хуваалцагдана — диск дээр хоёр дахь хуулбар үүсэхгүй
DELETE /api/v1/drive/nodes/{id} drive.manage Хогийн сав руу (soft). Дэд мод нь хамт орж, түүн доторх бүх амьд холбоос нэг статементэд цуцлагдана — хаясан файл нь URL нь хэн нэгний гарт байсан ч татагдахгүй болно
POST /api/v1/drive/nodes/{id}/restore drive.manage Хогийн савнаас буцаана. Эх хавтас нь өөрөө хогийн саванд эсвэл байхгүй бол 409; нэр нь чөлөөлөгдөж, өөр зүйл эзэлсэн бол мөн 409
DELETE /api/v1/drive/nodes/{id}/purge drive.manage Бүрмөсөн устгах → 204. Зөвхөн хогийн саванд байгаа зүйлд ажиллана (409) — нэг ч хүсэлт шууд устгаж чадахгүй
POST /api/v1/drive/nodes/{id}/sign drive.manage Хадгалагдсан PDF-ийг гарын үсгийн модульд шууд өгнө (татаж, дахин ачаалахгүй) → {status, esign_document_id, node_id}. Байтууд урсгалаар дамжина; хэмжээний хязгаар нь хүлээн авагчийнх. PDF биш → 400; хогийн саванд эсвэл хавтас → 404; гарын үсгийн модуль суулгаагүй бол 501; хүлээн авагч татгалзвал 502. Эх файл өөрчлөгдөхгүй, хувилбар нэмэгдэхгүй
GET /api/v1/drive/trash drive.read Хогийн сав → node массив. Хаясан язгуурууд л жагсана: хавтасны агуулга түүнтэй хамт орсон тул тэднийг хажууд нь харуулбал нэг устгал зуу мэт харагдана
GET /api/v1/drive/nodes/{id}/versions drive.read Файлын түүх, шинэ нь эхэнд: [{id, node_id, version, size_bytes, comment?, created_by?, created_at}]. Хавтас → 400
POST /api/v1/drive/nodes/{id}/versions/{version}/restore drive.manage Хуучин хувилбарыг шинэ хувилбар болгож буцаана (дарж бичихгүй). {version} эерэг бүхэл байх ёстой, эс бөгөөс 400
GET /api/v1/drive/usage drive.read {total_bytes, stored_bytes, file_count, folder_count, trashed_count}. total_bytes нь хүн тоолох ёсоор нийлбэр; stored_bytes нь диск бодитоор эзэлж буй хэмжээ — хоёр node эсвэл хоёр хувилбар контентоо хуваалцах бүрд бага байна
GET /api/v1/drive/search drive.read Шүүлттэй хайлт — доорх «Хайлт» хэсгийг үз

v2 — хуваалцах, од, лог

Арга Зам Эрх Тайлбар
GET /api/v1/drive/nodes/{id}/shares drive.read Нэг node-ийн хуваалцлын самбар нэг хүсэлтээр: {members[], links[], my_role}. members нь энэ node дээр шууд нэрлэгдсэн грантууд (өвгөөс өвлөсөн нь орохгүй — энэ бол цуцалж чадах зүйлсийн жагсаалт); links нь токенгүй холбоосын мөрүүд; my_role нь дуудагчийн өвлөлтийг тооцсон бодит эрх ("", viewer, editor)
POST /api/v1/drive/nodes/{id}/shares drive.read + node дээрх эрх {subject_id, role}role нь viewer эсвэл editor (400). Дахин хуваалцах нь role-ыг солино, хоёр дахь мөр үүсгэхгүй. 201 + share биет. Хогийн саванд байгаа node-ыг хуваалцахгүй (404)
DELETE /api/v1/drive/nodes/{id}/shares/{shareID} drive.read + node дээрх эрх Гишүүний грантыг авах → 204
POST /api/v1/drive/nodes/{id}/links drive.read + node дээрх эрх Холбоос үүсгэх — доорх «Холбоосын токен» хэсгийг үз
DELETE /api/v1/drive/nodes/{id}/links/{linkID} drive.read + node дээрх эрх Холбоосыг цуцлах → 204. Мөр нь revoked_at-тай үлдэнэ (устахгүй)
GET /api/v1/drive/shared-with-me drive.read Дуудагчийг шууд нэрлэсэн node-ууд, шинэ нь эхэнд (дээд тал нь 500). Дэд модыг задлахгүй: «надтай хуваалцсан» дэлгэцэд хуваалцсан хавтас нь харагдах ёстой, түүний арван мянган удам биш
GET /api/v1/drive/starred drive.read Дуудагчийн од тавьсан зүйлс, шинэ нь эхэнд (дээд тал нь 500). Хогийн саванд байгааг оруулахгүй
PUT /api/v1/drive/nodes/{id}/star drive.read Од тавих → 204. Идемпотент — хоёр удаа тавих нь нэг л факт. Байхгүй эсвэл хогийн саванд байгаа node → 404
DELETE /api/v1/drive/nodes/{id}/star drive.read Од авах → 204. Байхгүй одыг авах нь алдаа биш: дуудагч төлөв хүссэн, төлөв нь биеллээ
GET /api/v1/drive/nodes/{id}/activity drive.read Нэг node-ийн түүх, шинэ нь эхэнд (дээд тал нь 200). Node оршиж байхыг шаардахгүй — тэр нь энэ хүснэгтийн бүх учир
GET /api/v1/drive/activity drive.read Workspace-ийн урсгал, шинэ нь эхэнд (дээд тал нь 200)

Үйл ажиллагааны бичлэг: {id, node_id, action, node_name, node_path, actor_id?, detail, created_at}. action нь хаалттай олонлог: created, uploaded, renamed, moved, copied, trashed, restored, purged, shared, unshared, version_restored. Дэлгэц бүрийг хоёр хэл дээр зурдаг тул чөлөөт текст байж болохгүй. node_name/node_path нь эвэнтийн агшны snapshot — тэдгээр нь маргааш байхгүй байж болох зүйлийг тодорхойлно. Од нь бичигддэггүй: «хэн нэгэн од тавилаа»-гаар дүүрсэн лог нь файл алга болоход хэн ч уншихгүй лог.

Хуваалцлыг өөрчлөх эрх нь маршрутын биш, node-ийнх. Дээрх бичдэг таван зам нь drive.read бүлэгт байрлана: drive.read нь хүсэлтийг энэ хүртэл авчирдаг, харин бичиж болох эсэхийг node тутамд шийднэ — workspace админ, эсвэл drive.manage эзэмшигч, эсвэл тухайн node дээр (эсвэл дээрх хавтас дээр) editor грант авсан хүн. Татгалзал нь 403 (404 биш): дуудагч аль хэдийн node-ыг харж чадаж байгаа тул нуух нь түүнд шинэ юу ч хэлэхгүй, эрхийн асуудлыг байхгүй файл мэт харагдуулах байв. editor ба viewer хоёрын ялгаа яг энэ — үүнгүй бол схем нь хадгалдаг, юу ч хэрэгжүүлдэггүй ялгаа болох байв.

Хайлт

GET /api/v1/drive/searchdrive.read. Хариулт нь node биет дээр starred (boolean) нэмсэн массив, updated_at DESC-оор эрэмбэлэгдэж дээд тал нь 200 мөр. Буруу шүүлт нь чимээгүй үл тоомсорлогдохгүй, 400 болно: ажиллаагүй хэмжээний шүүлт нь хүн хүлээснээс олон файлтай диск мэт харагдана.

Параметр Утга
q Нэрээр хайх. Хоёр нөхцөл нэг дор: дэд мөрийн ILIKE ба simple tsvector дээрх plainto_tsquery — нэрийн дундаас гурван үсэг ч, бүтэн үг ч олдоно
kind file эсвэл folder; өөр утга → 400
mime Яг тэнцүү харьцуулалт
min_size, max_size Байтын тоо. Байхгүй нь тэгтэй адилгүй — тэг байттай файл байдаг ба «дор хаяж 0 байт» нь юу ч өөрчлөхгүй шүүлт
from, to updated_at муж. RFC 3339 бүтэн тамга эсвэл энгийн огноо (2026-08-19)
starred=true Зөвхөн дуудагчийн од тавьсан
shared=me Зөвхөн дуудагчтай хуваалцсан (хуваалцсан хавтасны дэд мод хамт)

Хогийн саванд байгаа болон өөр тенантын зүйл хэзээ ч буцахгүй; сүүлийнх нь хоёр давхар — schema нь dbguard-аар тогтоогдоно, дээр нь predicate өөрөө tenant_id авч явна.

Байрлуулалт нь pg_trgm-тэй эсэхээс үл хамааран асуулга ижил тул хариулт ч ижил — зөвхөн гүйцэтгэлийн план өөр (DATABASE_SCHEMA.md, tenant/0009).

Холбоосын токен — үүсгэх (session шаардана)

POST /api/v1/drive/nodes/{id}/linksdrive.read + node дээрх эрх. Бие: {password?, expires_at?}. expires_at нь RFC 3339 ба ирээдүйд байх ёстой (эс бөгөөс 400); нууц үг нь дээд тал нь 72 байт (bcrypt-ийн өөрийн хязгаар — үүнээс цааш авбал хүний бодсоноос богино нууц үг болно).

201-ийн бие:

{
  "link": { "id": "...", "node_id": "...", "has_password": true,
            "expires_at": "...", "download_count": 0, "created_at": "..." },
  "token": "<tenant-uuid>.<secret>",
  "path": "/api/v1/drive/links/<tenant-uuid>.<secret>"
}

Энэ хариулт бол токен оршин байх цорын ганц агшин. Хүснэгтэд зөвхөн түүний хэш хадгалагдана — токеныг дахин уншуулж чадах endpoint байхгүй, байх ч ёсгүй. Алдвал хийх зүйл нь «дахин харуул» биш, цуцлаад шинийг үүсгэх; энэ нь цорын ганц шударга хариулт бөгөөд цуцлагдсаны мөр үлдээдэг хариулт.

path нь зам, бүтэн URL биш: префикс бүхий прокигийн ард сервер нь хөтөч юу бичсэнийг найдвартай мэдэхгүй, таасан хостоос бүтээсэн холбоос нь юу ч нээхгүй холбоос. Хостыг клиент өөрөө залгана.

Холбоосын хоёр зам — нээлттэй, session-гүй

Энэ модулийн 30 замаас зөвхөн энэ хоёр нь танилтгүй. Хуваалцсан холбоос гэдэг нь тодорхойлолтоороо session-гүй хүнд өгсөн уншилтын эрх тул tenant хаалга тэдний өмнө зогсож чадахгүй.

Арга Зам Эрх мэдэл
POST /api/v1/drive/links/{token} Зөвхөн токен. Бие нь сонголттой — нууц үггүй холбоосыг хоосон POST нээнэ: {password?, node_id?}
GET /api/v1/drive/links/{token}/download Зөвхөн токен. Нууц үг нь X-Drive-Link-Password толгойд — query параметр нь түүнийг лог болгон Referer бүрд оруулах байв. ?node= нь дэд модны файлыг сонгоно

Tenant нь токен дотор явна. Токен нь <tenant-uuid>.<secret> хэлбэртэй ба эхний хагас нь схемийн нэр болох гэж байгаа тул uuid эсэхийг шалгана. Хүсэлт бизнесийн хүснэгт хөндөхөөс өмнө tenant.WithTenantID-ээр гараар уягдана — schema-per-tenant дор платформын замаар хийсэн асуулга public-ыг шийддэг ба 00049-ээс хойш тэнд бизнес хүснэгт байхгүй. Хоёулаа internal/platform/route_policy_test.go-ийн publicRoutes-д нэрлэгдсэн — нээлттэй зам нэмэх нь review дээр харагддаг болох цэг.

Татгалзал нь зориуд нэг ижил. Дараах бүх тохиолдол яг ижил хариулт өгнө — 401 + {"error":"invalid link or password"}:

  • хэзээ ч байгаагүй токен;
  • хугацаа нь дууссан холбоос;
  • цуцлагдсан холбоос;
  • node нь хогийн саванд орсон холбоос;
  • зөв токен дээрх буруу нууц үг;
  • грантын гадна байгаа node_id/?node=.

Энэ бол алдаа биш, шийдвэр. Ялгаж хэлбэл endpoint нь токен амьд эсэхийг шалгах хэрэгсэл болно: «буруу нууц үг»-ийг «ийм холбоос байхгүй»-ээс салгаж чадсан хүн аль токен бодит болохыг тандаж чадна, «хугацаа дууссан»-ыг «байгаагүй»-ээс салгаж чадсан хүн энэ workspace энд ямар нэг зүйл хуваалцаж байсныг мэдэж авна. Үнэ нь «энэ холбоосын хугацаа дууссан» гэж хэлж чадахгүй дэлгэц, тэр үнийг зориуд төлсөн. Цаг хугацаа ч хариултын нэг хэсэг: олдоогүй холбоос нь хаях зориулалтын хэш дээр bcrypt ажиллуулж, буруу нууц үгтэй ижил хугацаа зарцуулна.

Тодруулах хүсэлт (POST) нь {node, root, children, expires_at, has_password, download_path} буцаана — хавтасны холбоос нь дэд модыг нээх тул children нь тухайн хавтасны амьд хүүхдүүд. Татаж авалт нь үргэлж attachment: session-ийн ард байгаа хүн өөрийн workspace-ийн зургийг inline харж болно, харин энд хүсэлт нь хаана ч тавигдсан байж болох URL-аас ирдэг ба inline рендер нь энэ origin дээр болно. Амжилттай татаж авалт бүр download_count, last_used_at-ыг ахиулна.


4. Гүнзгий мэдээлэл

  • Session токен: 32 байт crypto/rand, DB-д зөвхөн SHA-256 хэш. TTL 12 цаг.
  • Cookie: HttpOnly; SameSite=Strict; Secure (production үед). Хөгжүүлэлтийн split-origin орчинд (3000↔︎8080) cookie дамждаггүй тул frontend Bearer токеныг давхар илгээдэг.
  • OAuth2 access токен: hydra_at_<64 hex> opaque, TTL 1 цаг, introspection-ээр шалгагдана. Authorization code урсгалаар олгогдсон бол хүн ба workspace-ыг хоёуланг нь заана (sub, tenant). id_token нь эсрэгээрээ RS256 JWT — JWKS-ээр шалгагдана, TTL 10 минут (нэвтэрсэн тухай мэдэгдэл болохоос хадгалж явах эрх биш). Authorization code 60 секунд, refresh 30 хоног. Session-ийн адил DB-д зөвхөн SHA-256 хэш (oauth2_tokens) хадгалагдана; цуцлалт revoked_at-аар тэмдэглэгдэж бүх replica дээр хүчинтэй болно. Цаг тутмын janitor хугацаа дууссанаас хойш 24 цагийн дараа мөрүүдийг устгана.
  • JSON body хэмжээний хязгаар: AI chat/stt/translate 1 MiB, TTS 16 KiB, prompt 32 KiB, knowledge 256 KiB.